13 Commits

Author SHA1 Message Date
overcuriousity
50ffeb1b6e Merge pull request #16 from overcuriousity/fix-keyboard-shortcut
adjust keyboard shortcut
2025-12-14 14:46:48 +01:00
overcuriousity
d6b8231bae adjust keyboard shortcut 2025-12-14 14:46:11 +01:00
overcuriousity
8b13cfc37b Merge pull request #15 from overcuriousity/claude/fix-unknown-signer-y3bWG
Fix GPG signature verification always showing "Unknown signer"
2025-12-14 14:38:48 +01:00
Claude
62fa781350 Fix GPG signature verification always showing "Unknown signer"
The verify_signature() function had a logic bug where the break statement
was executed even when quote parsing failed, preventing fallback to the
key ID extraction.

The break was positioned to execute whenever "Good signature from" appeared,
regardless of whether the signer name was successfully extracted from quotes.
This left signer_info as "Unknown signer" when quote parsing failed.

Fix: Move break inside the successful parsing block (if len(parts) >= 2)
so it only breaks after successfully extracting the signer name. If quote
parsing fails, the loop continues and can fall back to extracting the key
ID from the "using" line.

Testing: Created comprehensive test suite verifying correct extraction of
signer information and proper handling of edge cases.
2025-12-14 13:32:21 +00:00
overcuriousity
f4f276160a Merge pull request #14 from overcuriousity/claude/enhance-cli-interface-nYnNL
Claude/enhance cli interface n yn nl
2025-12-14 14:18:30 +01:00
Claude
33cad5bd5f Document new CLI commands in README
Added comprehensive CLI Command Reference section documenting:
- Context management commands (--show-context, --list, --switch-*)
- Case/evidence creation commands (--new-case, --new-evidence)
- Advanced note-taking features (--stdin, --case, --evidence overrides)
- Export commands

Placed section prominently after the hot logging feature overview
to maintain focus on the primary use case while documenting the
full CLI surface area.
2025-12-14 13:17:42 +00:00
Claude
4fad8a3561 Enhance CLI interface with comprehensive context management and rapid note-taking features
Added CLI features for rapid observation logging during live forensic analysis:

Context Management:
- --show-context: Display active case and evidence
- --list: List all cases and evidence in hierarchy
- --switch-case: Change active case (by ID or case number)
- --switch-evidence: Change active evidence (by ID or name)

Case/Evidence Creation:
- --new-case: Create new case with optional --name and --investigator
- --new-evidence: Create new evidence with optional --description

Enhanced Note-Taking:
- --stdin: Read note content from stdin for piping command output
- --case: Add note to specific case without changing active context
- --evidence: Add note to specific evidence without changing active context

All context switching commands support both UUIDs and human-friendly identifiers
(case numbers for cases, names for evidence). Creating new items automatically
sets them as active. Override flags allow temporary context changes without
modifying the active state.

Updated CLAUDE.md with comprehensive CLI command examples organized by workflow.
2025-12-14 13:14:25 +00:00
overcuriousity
48525fe505 Merge pull request #13 from overcuriousity/claude/fix-setup-wizard-display-IroVi
Always prompt for GPG key selection for transparency
2025-12-14 13:50:20 +01:00
Claude
085c9e9aa8 Always prompt for GPG key selection for transparency
Remove auto-selection when only one key exists. Users should always
be explicitly aware of which key is being used and have the option
to choose the default key (option 0) instead.

trace/gpg_wizard.py:79-95
2025-12-14 12:36:12 +00:00
overcuriousity
06548df373 Merge pull request #12 from overcuriousity/claude/fix-setup-wizard-display-IroVi
Fix setup wizard not showing on first run
2025-12-14 13:22:11 +01:00
Claude
dff27ac7e4 Fix setup wizard not showing on first run
The wizard was checking if 'pgp_enabled' key existed in settings dict,
but StateManager.get_settings() always returns this key (as a default
value when settings.json doesn't exist). This caused the wizard to
think it had already run, even on first launch.

Fix: Check if settings file exists instead of checking for key presence.

trace/gpg_wizard.py:120
2025-12-14 12:21:00 +00:00
overcuriousity
a1f95548fd Merge pull request #11 from overcuriousity/claude/oWjkR
Fix visual overlap in case detail view when no evidence items exist
2025-12-14 13:14:20 +01:00
Claude
425a169217 Fix visual overlap in case detail view when no evidence items exist
Fixed rendering issue where the Case Notes section would overlap with the
"No evidence items" message. The problem occurred because the y_pos variable
was not updated after drawing the 2-line message, causing subsequent content
to render at the wrong vertical position.

Changes:
- Adjusted "No evidence items" message to start at y_pos (not y_pos + 1)
  for consistency with evidence item rendering
- Added y_pos += 2 after the message to account for the 2 lines used
- Fixed boundary check from y_pos + 2 to y_pos + 1

Also verified all other views (case list, tags, IOCs) handle empty states
correctly and don't have similar overlap issues.

Resolves visual bug reported in case detail view.
2025-12-14 12:00:08 +00:00
6 changed files with 464 additions and 73 deletions

View File

@@ -9,20 +9,75 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Development Commands ## Development Commands
### Running the Application ### Running the Application
#### Launching TUI
```bash ```bash
# Run directly from source # Launch TUI (default behavior)
python3 main.py python3 main.py
# Quick CLI note addition (requires active case/evidence set in TUI)
python3 main.py "Your note content here"
# Export to markdown
python3 main.py --export --output report.md
# Open TUI directly at active case/evidence # Open TUI directly at active case/evidence
python3 main.py --open python3 main.py --open
``` ```
#### Context Management
```bash
# Show current active case and evidence
python3 main.py --show-context
# List all cases and evidence in hierarchy
python3 main.py --list
# Switch active case (by case number or UUID)
python3 main.py --switch-case 2024-001
# Switch active evidence (by name or UUID, within active case)
python3 main.py --switch-evidence "disk-image-1"
```
#### Creating Cases and Evidence
```bash
# Create new case (automatically sets as active)
python3 main.py --new-case 2024-001
# Create case with metadata
python3 main.py --new-case 2024-001 --name "Ransomware Investigation" --investigator "John Doe"
# Create evidence in active case (automatically sets as active)
python3 main.py --new-evidence "Laptop HDD"
# Create evidence with description
python3 main.py --new-evidence "Server Logs" --description "Apache access logs from compromised server"
```
#### Adding Notes
```bash
# Quick note to active context
python3 main.py "Observed suspicious process at 14:32"
# Read note from stdin (for piping command output)
echo "Network spike detected" | python3 main.py --stdin
ps aux | grep malware | python3 main.py --stdin
tail -f logfile.txt | grep error | python3 main.py --stdin
# Add note to specific case without changing active context
python3 main.py --case 2024-002 "Found malware in temp folder"
# Add note to specific evidence without changing active context
python3 main.py --evidence "disk-image-2" "Bad sectors detected"
# Add note to specific case and evidence (both overrides)
python3 main.py --case 2024-001 --evidence "Laptop HDD" "Recovered deleted files"
```
#### Export
```bash
# Export all data to markdown
python3 main.py --export --output report.md
# Export with default filename (trace_export.md)
python3 main.py --export
```
### Building Binary ### Building Binary
```bash ```bash
# Install dependencies first # Install dependencies first

View File

@@ -22,6 +22,76 @@ trace "Observed outbound connection to 192.168.1.55 on port 80. #suspicious #net
**System Integrity Chain:** Each command-line note is immediately stamped, concatenated with its content, and hashed using SHA256 before storage. This ensures a non-repudiable log entry. **System Integrity Chain:** Each command-line note is immediately stamped, concatenated with its content, and hashed using SHA256 before storage. This ensures a non-repudiable log entry.
## CLI Command Reference
### Context Management
View and switch between cases and evidence without opening the TUI:
```bash
# Show current active case and evidence
trace --show-context
# List all cases and evidence in hierarchy
trace --list
# Switch active case (by case number or UUID)
trace --switch-case 2024-001
# Switch active evidence (by name or UUID, within active case)
trace --switch-evidence "disk-image-1"
```
### Case and Evidence Creation
Create new cases and evidence directly from the command line:
```bash
# Create new case (automatically becomes active)
trace --new-case 2024-001
# Create case with full metadata
trace --new-case 2024-001 --name "Ransomware Investigation" --investigator "Jane Doe"
# Create evidence in active case (automatically becomes active)
trace --new-evidence "Laptop HDD"
# Create evidence with description
trace --new-evidence "Server Logs" --description "Apache logs from compromised server"
```
### Advanced Note-Taking
Beyond basic hot logging, trace supports stdin piping and context overrides:
```bash
# Pipe command output directly into notes
ps aux | grep malware | trace --stdin
tail -f /var/log/auth.log | grep "Failed password" | trace --stdin
netstat -an | trace --stdin
# Add note to specific case without changing active context
trace --case 2024-002 "Found malware in temp folder"
# Add note to specific evidence without changing active context
trace --evidence "Memory Dump" "Suspicious process identified"
# Override both case and evidence for a single note
trace --case 2024-001 --evidence "Disk Image" "Recovered deleted files"
```
**Identifiers:** All commands accept both human-friendly identifiers (case numbers like `2024-001`, evidence names like `Laptop HDD`) and UUIDs. Use `--list` to see available identifiers.
### Export
```bash
# Export all data to markdown (GPG-signed if enabled)
trace --export --output investigation-report.md
# Export with default filename (trace_export.md)
trace --export
```
## Installation & Deployment ## Installation & Deployment
### Quick Install from Latest Release ### Quick Install from Latest Release

View File

@@ -1,11 +1,204 @@
import argparse import argparse
import sys import sys
import time import time
from .models import Note, Case from typing import Optional, Tuple
from .models import Note, Case, Evidence
from .storage import Storage, StateManager from .storage import Storage, StateManager
from .crypto import Crypto from .crypto import Crypto
def quick_add_note(content: str): def find_case(storage: Storage, identifier: str) -> Optional[Case]:
"""Find a case by case_id (UUID) or case_number."""
for case in storage.cases:
if case.case_id == identifier or case.case_number == identifier:
return case
return None
def find_evidence(case: Case, identifier: str) -> Optional[Evidence]:
"""Find evidence by evidence_id (UUID) or name within a case."""
for evidence in case.evidence:
if evidence.evidence_id == identifier or evidence.name == identifier:
return evidence
return None
def show_context():
"""Display the current active context."""
state_manager = StateManager()
storage = Storage()
state = state_manager.get_active()
case_id = state.get("case_id")
evidence_id = state.get("evidence_id")
if not case_id:
print("No active context set.")
print("Use --switch-case to set an active case, or open the TUI to select one.")
return
case = storage.get_case(case_id)
if not case:
print("Error: Active case not found in storage.")
return
print(f"Active context:")
print(f" Case: {case.case_number}", end="")
if case.name:
print(f" - {case.name}", end="")
print(f" [{case.case_id[:8]}...]")
if evidence_id:
evidence = find_evidence(case, evidence_id)
if evidence:
print(f" Evidence: {evidence.name}", end="")
if evidence.description:
print(f" - {evidence.description}", end="")
print(f" [{evidence.evidence_id[:8]}...]")
else:
print(f" Evidence: [not found - stale reference]")
else:
print(f" Evidence: [none - notes will attach to case]")
def list_contexts():
"""List all cases and their evidence in a hierarchical format."""
storage = Storage()
if not storage.cases:
print("No cases found.")
print("Use --new-case to create one, or open the TUI.")
return
print("Cases and Evidence:")
for case in storage.cases:
# Show case
print(f" [{case.case_id[:8]}...] {case.case_number}", end="")
if case.name:
print(f" - {case.name}", end="")
if case.investigator:
print(f" (Investigator: {case.investigator})", end="")
print()
# Show evidence under this case
for evidence in case.evidence:
print(f" [{evidence.evidence_id[:8]}...] {evidence.name}", end="")
if evidence.description:
print(f" - {evidence.description}", end="")
print()
# Add blank line between cases for readability
if storage.cases[-1] != case:
print()
def create_case(case_number: str, name: Optional[str] = None, investigator: Optional[str] = None):
"""Create a new case and set it as active."""
storage = Storage()
state_manager = StateManager()
# Check if case number already exists
existing = find_case(storage, case_number)
if existing:
print(f"Error: Case with number '{case_number}' already exists.", file=sys.stderr)
sys.exit(1)
# Create new case
case = Case(case_number=case_number, name=name, investigator=investigator)
storage.cases.append(case)
storage.save_data()
# Set as active case
state_manager.set_active(case.case_id, None)
print(f"✓ Created case '{case_number}' [{case.case_id[:8]}...]")
if name:
print(f" Name: {name}")
if investigator:
print(f" Investigator: {investigator}")
print(f"✓ Set as active case")
def create_evidence(name: str, description: Optional[str] = None):
"""Create new evidence and attach to active case."""
storage = Storage()
state_manager = StateManager()
state = state_manager.get_active()
case_id = state.get("case_id")
if not case_id:
print("Error: No active case set. Use --switch-case or --new-case first.", file=sys.stderr)
sys.exit(1)
case = storage.get_case(case_id)
if not case:
print("Error: Active case not found in storage.", file=sys.stderr)
sys.exit(1)
# Check if evidence with this name already exists in the case
existing = find_evidence(case, name)
if existing:
print(f"Error: Evidence named '{name}' already exists in case '{case.case_number}'.", file=sys.stderr)
sys.exit(1)
# Create new evidence
evidence = Evidence(name=name, description=description)
case.evidence.append(evidence)
storage.save_data()
# Set as active evidence
state_manager.set_active(case.case_id, evidence.evidence_id)
print(f"✓ Created evidence '{name}' [{evidence.evidence_id[:8]}...]")
if description:
print(f" Description: {description}")
print(f"✓ Added to case '{case.case_number}'")
print(f"✓ Set as active evidence")
def switch_case(identifier: str):
"""Switch active case context."""
storage = Storage()
state_manager = StateManager()
case = find_case(storage, identifier)
if not case:
print(f"Error: Case '{identifier}' not found.", file=sys.stderr)
print("Use --list to see available cases.", file=sys.stderr)
sys.exit(1)
# Set as active case, clear evidence
state_manager.set_active(case.case_id, None)
print(f"✓ Switched to case '{case.case_number}' [{case.case_id[:8]}...]")
if case.name:
print(f" {case.name}")
def switch_evidence(identifier: str):
"""Switch active evidence context within the active case."""
storage = Storage()
state_manager = StateManager()
state = state_manager.get_active()
case_id = state.get("case_id")
if not case_id:
print("Error: No active case set. Use --switch-case first.", file=sys.stderr)
sys.exit(1)
case = storage.get_case(case_id)
if not case:
print("Error: Active case not found in storage.", file=sys.stderr)
sys.exit(1)
evidence = find_evidence(case, identifier)
if not evidence:
print(f"Error: Evidence '{identifier}' not found in case '{case.case_number}'.", file=sys.stderr)
print("Use --list to see available evidence.", file=sys.stderr)
sys.exit(1)
# Set as active evidence
state_manager.set_active(case.case_id, evidence.evidence_id)
print(f"✓ Switched to evidence '{evidence.name}' [{evidence.evidence_id[:8]}...]")
if evidence.description:
print(f" {evidence.description}")
def quick_add_note(content: str, case_override: Optional[str] = None, evidence_override: Optional[str] = None):
storage = Storage() storage = Storage()
state_manager = StateManager() state_manager = StateManager()
@@ -17,31 +210,43 @@ def quick_add_note(content: str):
state = state_manager.get_active() state = state_manager.get_active()
settings = state_manager.get_settings() settings = state_manager.get_settings()
case_id = state.get("case_id") # Handle case override or use active case
evidence_id = state.get("evidence_id") if case_override:
case = find_case(storage, case_override)
if not case:
print(f"Error: Case '{case_override}' not found.", file=sys.stderr)
print("Use --list to see available cases.", file=sys.stderr)
sys.exit(1)
else:
case_id = state.get("case_id")
if not case_id:
print("Error: No active case set. Use --switch-case, --new-case, or open the TUI to select a case.", file=sys.stderr)
sys.exit(1)
if not case_id: case = storage.get_case(case_id)
print("Error: No active case set. Open the TUI to select a case first.", file=sys.stderr) if not case:
sys.exit(1) print("Error: Active case not found in storage.", file=sys.stderr)
sys.exit(1)
case = storage.get_case(case_id)
if not case:
print("Error: Active case not found in storage. Ensure you have set an active case in the TUI.", file=sys.stderr)
sys.exit(1)
# Handle evidence override or use active evidence
target_evidence = None target_evidence = None
if evidence_id: if evidence_override:
# Find and validate evidence belongs to active case target_evidence = find_evidence(case, evidence_override)
for ev in case.evidence:
if ev.evidence_id == evidence_id:
target_evidence = ev
break
if not target_evidence: if not target_evidence:
# Evidence ID is set but doesn't exist in case - clear it print(f"Error: Evidence '{evidence_override}' not found in case '{case.case_number}'.", file=sys.stderr)
print(f"Warning: Active evidence not found in case. Clearing to case level.", file=sys.stderr) print("Use --list to see available evidence.", file=sys.stderr)
state_manager.set_active(case_id, None) sys.exit(1)
elif not case_override: # Only use active evidence if not overriding case
evidence_id = state.get("evidence_id")
if evidence_id:
# Find and validate evidence belongs to active case
target_evidence = find_evidence(case, evidence_id)
if not target_evidence:
# Evidence ID is set but doesn't exist in case - clear it
print(f"Warning: Active evidence not found in case. Clearing to case level.", file=sys.stderr)
state_manager.set_active(case.case_id, None)
# Create note # Create note
note = Note(content=content) note = Note(content=content)
@@ -67,10 +272,6 @@ def quick_add_note(content: str):
if target_evidence: if target_evidence:
target_evidence.notes.append(note) target_evidence.notes.append(note)
print(f"✓ Note added to evidence '{target_evidence.name}'") print(f"✓ Note added to evidence '{target_evidence.name}'")
elif evidence_id:
print("Warning: Active evidence not found. Adding to case instead.")
case.notes.append(note)
print(f"✓ Note added to case '{case.case_number}'")
else: else:
case.notes.append(note) case.notes.append(note)
print(f"✓ Note added to case '{case.case_number}'") print(f"✓ Note added to case '{case.case_number}'")
@@ -182,25 +383,93 @@ def format_note_for_export(note: Note) -> str:
return "".join(lines) return "".join(lines)
def main(): def main():
parser = argparse.ArgumentParser(description="trace: Forensic Note Taking Tool") parser = argparse.ArgumentParser(
parser.add_argument("note", nargs="?", help="Quick note content to add to active context") description="trace: Forensic Note Taking Tool",
parser.add_argument("--export", help="Export all data to Markdown file", action="store_true") epilog="Examples:\n"
parser.add_argument("--output", help="Output file for export", default="trace_export.md") " trace 'Found suspicious process' Add note to active context\n"
parser.add_argument("--open", "-o", help="Open TUI directly at active case/evidence", action="store_true") " trace --stdin < output.txt Add file contents as note\n"
" trace --list List all cases and evidence\n"
" trace --new-case 2024-001 Create new case\n"
" trace --switch-case 2024-001 Switch active case\n",
formatter_class=argparse.RawDescriptionHelpFormatter
)
# We will import TUI only if needed to keep start time fast # Note content (positional or stdin)
parser.add_argument("note", nargs="?", help="Quick note content to add to active context")
parser.add_argument("--stdin", action="store_true", help="Read note content from stdin")
# Context management
parser.add_argument("--show-context", action="store_true", help="Show active case and evidence")
parser.add_argument("--list", action="store_true", help="List all cases and evidence")
parser.add_argument("--switch-case", metavar="IDENTIFIER", help="Switch active case (by ID or case number)")
parser.add_argument("--switch-evidence", metavar="IDENTIFIER", help="Switch active evidence (by ID or name)")
# Temporary overrides for note addition
parser.add_argument("--case", metavar="IDENTIFIER", help="Use specific case for this note (doesn't change active)")
parser.add_argument("--evidence", metavar="IDENTIFIER", help="Use specific evidence for this note (doesn't change active)")
# Case and evidence creation
parser.add_argument("--new-case", metavar="CASE_NUMBER", help="Create new case")
parser.add_argument("--name", metavar="NAME", help="Name for new case")
parser.add_argument("--investigator", metavar="INVESTIGATOR", help="Investigator name for new case")
parser.add_argument("--new-evidence", metavar="EVIDENCE_NAME", help="Create new evidence in active case")
parser.add_argument("--description", metavar="DESC", help="Description for new evidence")
# Export
parser.add_argument("--export", action="store_true", help="Export all data to Markdown file")
parser.add_argument("--output", metavar="FILE", default="trace_export.md", help="Output file for export")
# TUI
parser.add_argument("--open", "-o", action="store_true", help="Open TUI directly at active case/evidence")
args = parser.parse_args() args = parser.parse_args()
# Handle context management commands
if args.show_context:
show_context()
return
if args.list:
list_contexts()
return
if args.switch_case:
switch_case(args.switch_case)
return
if args.switch_evidence:
switch_evidence(args.switch_evidence)
return
# Handle case/evidence creation
if args.new_case:
create_case(args.new_case, name=args.name, investigator=args.investigator)
return
if args.new_evidence:
create_evidence(args.new_evidence, description=args.description)
return
# Handle export
if args.export: if args.export:
export_markdown(args.output) export_markdown(args.output)
return return
if args.note: # Handle note addition
quick_add_note(args.note) if args.stdin:
# Read from stdin
content = sys.stdin.read().strip()
if not content:
print("Error: No content provided from stdin.", file=sys.stderr)
sys.exit(1)
quick_add_note(content, case_override=args.case, evidence_override=args.evidence)
return return
# Check for first run and run GPG wizard if needed if args.note:
quick_add_note(args.note, case_override=args.case, evidence_override=args.evidence)
return
# No arguments - check for first run and launch TUI
from .gpg_wizard import check_and_run_wizard from .gpg_wizard import check_and_run_wizard
check_and_run_wizard() check_and_run_wizard()

View File

@@ -61,9 +61,9 @@ class Crypto:
parts = line.split('"') parts = line.split('"')
if len(parts) >= 2: if len(parts) >= 2:
signer_info = parts[1] signer_info = parts[1]
break break # Only break after successfully extracting signer info
elif "using" in line: elif "using" in line:
# Try to get key ID # Try to get key ID as fallback
if "key" in line.lower(): if "key" in line.lower():
signer_info = line.strip() signer_info = line.strip()

View File

@@ -76,27 +76,23 @@ def run_gpg_wizard():
# Let user select a key # Let user select a key
selected_key = None selected_key = None
if len(keys) == 1: while True:
print(f"Only one key found. Using: {keys[0][1]}") try:
selected_key = keys[0][0] choice = input(f"Select a key (1-{len(keys)}, or 0 to use default key): ").strip()
else: choice_num = int(choice)
while True:
try:
choice = input(f"Select a key (1-{len(keys)}, or 0 to use default key): ").strip()
choice_num = int(choice)
if choice_num == 0: if choice_num == 0:
print("Using GPG default key (no specific key ID)") print("Using GPG default key (no specific key ID)")
selected_key = None selected_key = None
break break
elif 1 <= choice_num <= len(keys): elif 1 <= choice_num <= len(keys):
selected_key = keys[choice_num - 1][0] selected_key = keys[choice_num - 1][0]
print(f"Selected: {keys[choice_num - 1][1]}") print(f"Selected: {keys[choice_num - 1][1]}")
break break
else: else:
print(f"Please enter a number between 0 and {len(keys)}") print(f"Please enter a number between 0 and {len(keys)}")
except ValueError: except ValueError:
print("Please enter a valid number") print("Please enter a valid number")
print("\n✓ GPG signing enabled!") print("\n✓ GPG signing enabled!")
if selected_key: if selected_key:
@@ -115,10 +111,9 @@ def check_and_run_wizard():
Returns True if wizard was run, False otherwise. Returns True if wizard was run, False otherwise.
""" """
state_manager = StateManager() state_manager = StateManager()
settings = state_manager.get_settings()
# Check if wizard has already been run (presence of any GPG setting indicates setup was done) # Check if settings file exists - if it does, wizard has already been run
if "pgp_enabled" in settings: if state_manager.settings_file.exists():
return False return False
# First run - run wizard # First run - run wizard

View File

@@ -753,11 +753,12 @@ class TUI:
if not evidence_list: if not evidence_list:
# Check if we have space to display the message # Check if we have space to display the message
if y_pos + 2 < self.height - 2: if y_pos + 1 < self.height - 2:
self.stdscr.attron(curses.color_pair(3)) self.stdscr.attron(curses.color_pair(3))
self.stdscr.addstr(y_pos + 1, 4, "┌─ No evidence items") self.stdscr.addstr(y_pos, 4, "┌─ No evidence items")
self.stdscr.addstr(y_pos + 2, 4, "└─ Press 'N' to add evidence") self.stdscr.addstr(y_pos + 1, 4, "└─ Press 'N' to add evidence")
self.stdscr.attroff(curses.color_pair(3)) self.stdscr.attroff(curses.color_pair(3))
y_pos += 2 # Account for the 2 lines used by the message
else: else:
# Scrolling for evidence list # Scrolling for evidence list
# Calculate remaining space # Calculate remaining space
@@ -1260,7 +1261,7 @@ class TUI:
self.stdscr.addstr(current_y, 2, "Signature: ? Unsigned", curses.color_pair(3)) self.stdscr.addstr(current_y, 2, "Signature: ? Unsigned", curses.color_pair(3))
current_y += 1 current_y += 1
self.stdscr.addstr(self.height - 3, 2, "[d] Delete [b] Back [v] Verify", curses.color_pair(3)) self.stdscr.addstr(self.height - 3, 2, "[d] Delete [b] Back [V] Verify", curses.color_pair(3))
def draw_help(self): def draw_help(self):
"""Draw the help screen with keyboard shortcuts and features""" """Draw the help screen with keyboard shortcuts and features"""
@@ -1357,7 +1358,7 @@ class TUI:
help_lines.append((" Layer 2: Export Entire export document GPG-signed", curses.A_NORMAL)) help_lines.append((" Layer 2: Export Entire export document GPG-signed", curses.A_NORMAL))
help_lines.append((" Dual verification: individual + document level", curses.A_DIM)) help_lines.append((" Dual verification: individual + document level", curses.A_DIM))
help_lines.append((" Verification ✓=verified ✗=failed ?=unsigned", curses.A_NORMAL)) help_lines.append((" Verification ✓=verified ✗=failed ?=unsigned", curses.A_NORMAL))
help_lines.append((" Press 'v' on note detail for verification info", curses.A_DIM)) help_lines.append((" Press 'V' on note detail for verification info", curses.A_DIM))
help_lines.append((" GPG Settings Press 's' to toggle signing & select GPG key", curses.A_NORMAL)) help_lines.append((" GPG Settings Press 's' to toggle signing & select GPG key", curses.A_NORMAL))
help_lines.append((" External Verify gpg --verify exported-file.md", curses.A_DIM)) help_lines.append((" External Verify gpg --verify exported-file.md", curses.A_DIM))
help_lines.append(("", curses.A_NORMAL)) help_lines.append(("", curses.A_NORMAL))
@@ -1748,7 +1749,8 @@ class TUI:
self.view_evidence_notes() self.view_evidence_notes()
else: else:
self.view_evidence_notes() self.view_evidence_notes()
elif self.current_view == "note_detail": elif key == ord('V'):
if self.current_view == "note_detail":
# Verify signature in note detail view # Verify signature in note detail view
if self.current_note: if self.current_note:
self.verify_note_signature() self.verify_note_signature()