From 85ca483a1d4d52942b99d6fc0efdbff4fe441db8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Dec 2025 19:33:52 +0000 Subject: [PATCH] Display raw PGP signature in verification dialog Modified the verify_note_signature() function to show the complete raw PGP signature text when users press 'V' in the note details view. This allows users to copy/paste signatures into external tools like Kleopatra for independent verification. Changes: - Added raw signature display after verification status info - Implemented scrollable dialog with arrow keys and Page Up/Down support - Added clear separator and label for the signature section - Shows signature for verified, failed, and unsigned notes (when present) Users can now easily copy PGP signatures for external verification workflows. --- trace/tui_app.py | 90 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 69 insertions(+), 21 deletions(-) diff --git a/trace/tui_app.py b/trace/tui_app.py index 45e5a30..17015d9 100644 --- a/trace/tui_app.py +++ b/trace/tui_app.py @@ -119,7 +119,7 @@ class TUI: self.flash_time = time.time() def verify_note_signature(self): - """Show detailed verification dialog for current note""" + """Show detailed verification dialog for current note with raw signature""" if not self.current_note: return @@ -134,6 +134,7 @@ class TUI: "To sign notes, enable GPG signing in settings", "and ensure you have a GPG key configured." ] + signature_lines = [] elif verified: title = "✓ Signature Verified" message = [ @@ -141,8 +142,13 @@ class TUI: "", f"Signed by: {info}", "", - "This note has not been tampered with since signing." + "This note has not been tampered with since signing.", + "", + "─" * 60, + "RAW PGP SIGNATURE (copy/paste for external verification):", + "─" * 60, ] + signature_lines = self.current_note.signature.split('\n') else: title = "✗ Signature Verification Failed" message = [ @@ -153,37 +159,79 @@ class TUI: "Possible causes:", "- Public key not in keyring", "- Note content was modified after signing", - "- Signature is corrupted" + "- Signature is corrupted", + "", + "─" * 60, + "RAW PGP SIGNATURE (copy/paste for external verification):", + "─" * 60, ] + signature_lines = self.current_note.signature.split('\n') - # Display dialog (reuse pattern from other dialogs) + # Combine message and signature + all_lines = message + signature_lines + + # Display scrollable dialog h, w = self.stdscr.getmaxyx() - dialog_h = min(len(message) + 6, h - 4) - dialog_w = min(max(len(line) for line in [title] + message) + 6, w - 4) + dialog_h = min(h - 4, 40) # Max height for dialog + dialog_w = min(w - 4, 100) # Max width for dialog start_y = (h - dialog_h) // 2 start_x = (w - dialog_w) // 2 # Create dialog window dialog = curses.newwin(dialog_h, dialog_w, start_y, start_x) - dialog.box() - # Title - dialog.attron(curses.A_BOLD) - title_x = (dialog_w - len(title)) // 2 - dialog.addstr(1, title_x, title) - dialog.attroff(curses.A_BOLD) + scroll_offset = 0 + max_scroll = max(0, len(all_lines) - (dialog_h - 6)) - # Message - for i, line in enumerate(message): - dialog.addstr(3 + i, 2, line) + while True: + dialog.clear() + dialog.box() - # Footer - footer = "Press any key to close" - footer_x = (dialog_w - len(footer)) // 2 - dialog.addstr(dialog_h - 2, footer_x, footer, curses.color_pair(3)) + # Title + dialog.attron(curses.A_BOLD) + title_x = (dialog_w - len(title)) // 2 + try: + dialog.addstr(1, title_x, title) + except curses.error: + pass + dialog.attroff(curses.A_BOLD) - dialog.refresh() - dialog.getch() + # Display visible lines + visible_lines = all_lines[scroll_offset:scroll_offset + dialog_h - 6] + for i, line in enumerate(visible_lines): + try: + # Truncate line if too long for dialog width + truncated_line = line[:dialog_w - 4] + dialog.addstr(3 + i, 2, truncated_line) + except curses.error: + pass + + # Footer with scroll indicators + if max_scroll > 0: + footer = f"↑/↓ Scroll ({scroll_offset + 1}/{len(all_lines)}) Any other key to close" + else: + footer = "Press any key to close" + footer_x = max(2, (dialog_w - len(footer)) // 2) + try: + dialog.addstr(dialog_h - 2, footer_x, footer[:dialog_w - 4], curses.color_pair(3)) + except curses.error: + pass + + dialog.refresh() + + # Handle input + key = dialog.getch() + if key == curses.KEY_UP and scroll_offset > 0: + scroll_offset -= 1 + elif key == curses.KEY_DOWN and scroll_offset < max_scroll: + scroll_offset += 1 + elif key == curses.KEY_PPAGE: # Page Up + scroll_offset = max(0, scroll_offset - (dialog_h - 6)) + elif key == curses.KEY_NPAGE: # Page Down + scroll_offset = min(max_scroll, scroll_offset + (dialog_h - 6)) + else: + # Any other key closes the dialog + break def _save_nav_position(self): """Save current navigation position before changing views"""