Skip to content

feat(tui): copy log lines from the log viewer #274

@johntmyers

Description

@johntmyers

Problem Statement

Users need to copy log content out of the TUI log viewer — for pasting into bug reports, Slack messages, scripts, or further analysis. Currently there is no clipboard integration at all; the only option is native terminal mouse selection, which is cumbersome when mouse capture is active (for scroll support) and impossible for copying filtered/structured data cleanly.

Technical Context

The TUI log viewer (crates/navigator-tui/src/ui/sandbox_logs.rs) renders structured log lines with timestamp, source, level, message, and key=value fields. It supports vim-style scrolling (j/k/g/G), autoscroll/follow mode, source filtering, and a detail popup for individual lines. The viewer already tracks a cursor position (log_cursor) and scroll offset (sandbox_log_scroll) over a filtered view of Vec<LogLine>. No clipboard or selection code exists today.

Affected Components

Component Key Files Role
App state crates/navigator-tui/src/app.rs LogLine struct (L51-59), log state fields (L347-359), key handler handle_logs_key (L887-962)
Log renderer crates/navigator-tui/src/ui/sandbox_logs.rs draw() (L11-104), render_log_line() (L215-250), detail popup (L110-188)
Event system crates/navigator-tui/src/event.rs Event enum (L12-35) — may need new variant for copy feedback
Nav bar / chrome crates/navigator-tui/src/ui/mod.rs Key hints for SandboxLogs focus (L226-264)
Main loop crates/navigator-tui/src/lib.rs Mouse event handling (L224-230), log stream management

Technical Investigation

Current Log Viewer Architecture

Data model: Vec<LogLine> stored on App, filtered by LogSourceFilter via filtered_log_lines() (app.rs:472-481). Each LogLine has: timestamp_ms, level, source, target, message, fields: HashMap<String, String>.

Viewport model: sandbox_log_scroll (first visible line index in filtered list) + log_cursor (offset from scroll position). Absolute selected index = scroll + cursor. log_viewport_height is set each frame by the draw pass (area.height - 2 for borders).

Rendering: Each line is rendered as styled spans: HH:MM:SS {source:7} {level:5} {message} {key=value ...}, truncated to viewport width with . The detail popup shows the full untruncated content.

Key bindings in use: j/k (navigate), g/G (top/bottom), f (follow), s (source filter), Enter (detail popup), Esc (back), q (quit), r (rules), p (policy). Available keys: y, v, Y, c, C, Shift+arrows, Ctrl+O, Ctrl+S are all unbound.

Clipboard Mechanism: Manual OSC 52

Write \x1b]52;c;{base64}\x07 directly to stdout. The terminal emulator intercepts this and copies to the system clipboard.

  • Works over SSH/tmux/mosh — the escape sequence forwards through the connection to the local terminal
  • Zero new dependencies — only needs base64 encoding
  • Supported by: iTerm2, Alacritty, Kitty, WezTerm, Windows Terminal, Konsole, foot, xterm
  • Limitation: Write-only (no paste), fire-and-forget (no success/failure feedback), some terminals disable by default
  • Decision: Manual implementation (~5 lines), not upgrading crossterm from 0.28 to 0.29 for built-in support

Decisions Made

Decision Resolution
Clipboard format Match display format as-is: HH:MM:SS {source:7} {level:5} {message} {key=value ...}
Copy feedback No feedback toast/status message — silent fire-and-forget
crossterm upgrade Manual OSC 52 implementation, stay on crossterm 0.28

Copy UX Features

Feature 1: Copy Current Line (y)

Single keypress copies the line at the cursor.

  • Key: y (vim yank convention)
  • What's copied: Plain text rendering of the log line matching display format, ANSI codes stripped
  • Implementation: Read filtered_log_lines()[scroll + cursor], format as plain text, write via OSC 52
  • Complexity: Low — ~30 lines of new code

Feature 2: Copy Viewport (Y)

Copy all lines currently visible in the viewport.

  • Key: Y (capital Y — "yank all visible")
  • What's copied: All visible lines joined with newlines, plain text
  • Implementation: Slice filtered_log_lines()[scroll..scroll+viewport_height], format each, join with \n
  • Complexity: Low — reuses the single-line formatter

Feature 3: Visual Selection Mode (v + j/k + y)

Vim-style visual mode for selecting a range of lines.

  • Enter: v to enter visual/selection mode
  • Extend: j/k/Up/Down/g/G move the cursor while anchor stays fixed
  • Copy: y yanks the selected range
  • Cancel: Esc exits visual mode without copying
  • Visual indicator: Selected lines get a distinct background highlight (new Theme field, e.g., selection style)
  • Status bar: Show "VISUAL" mode indicator plus line count "3 lines selected"

State model additions:

selection_mode: bool
selection_anchor: Option<usize>  // absolute index in filtered list where 'v' was pressed

Selected range = min(anchor, cursor)..=max(anchor, cursor) in the filtered log lines.

  • Complexity: Medium — new mode, modified key dispatch, selection rendering, ~100-150 lines

Feature 4: Shift+Arrow Range Select (Alternative to v-mode)

  • Key: Shift+Up / Shift+Down extends selection from current cursor
  • Copy: y yanks, Esc clears selection
  • Note: Some terminals don't distinguish Shift+Up from Up — less reliable than v-mode
  • Complexity: Low incremental if v-mode exists — additional entry point into selection state

Feature 5: Export to File (Ctrl+S / :export)

For large log volumes where clipboard is impractical.

  • Key: Ctrl+S (k9s screendump convention) or :export command
  • What's exported: All filtered log lines to a timestamped file
  • Path: $TMPDIR/openshell-logs-{sandbox}-{timestamp}.log
  • Complexity: Low-Medium — file I/O, path formatting, ~50 lines

Code References

Location Description
app.rs:51-59 LogLine struct — needs a to_plain_text() or Display impl for clipboard formatting
app.rs:347-359 Log state fields — needs selection_mode, selection_anchor additions
app.rs:887-962 handle_logs_key() — add y/Y/v key arms, modify j/k to extend selection in visual mode
sandbox_logs.rs:60-76 Line rendering loop — needs selection highlight logic (distinct from cursor highlight)
sandbox_logs.rs:79-97 Status bar — needs mode indicator ("VISUAL") and selection count
sandbox_logs.rs:215-250 render_log_line() — reuse for plain-text clipboard formatting (strip styles)
ui/mod.rs:226-264 Nav bar hints — add [y] Copy [v] Select [Y] Copy View hints
theme.rs Need a selection style for highlighted selected lines (distinct from log_cursor)

Patterns to Follow

  • k9s autoscroll pattern is already implemented — selection mode should auto-pause autoscroll
  • Detail popup pattern (Enter to open, Esc to close) — copy could work from within the popup too
  • Nav bar key hints are context-sensitive — selection mode should show different hints
  • InputMode enum already exists for command mode — visual/selection mode is analogous
  • Status feedback pattern: status_text on App is used for transient messages

Proposed Approach

Implement in phases: (1) y to copy current line and Y to copy viewport using manual OSC 52 — covers the most common use case with minimal code. (2) v-mode visual selection with j/k to extend and y to yank — covers multi-line selection. (3) Optionally Shift+Up/Down as alternative selection entry and Ctrl+S / :export for file export. Clipboard format matches the display rendering. No new crate dependencies. No copy feedback UI.

Scope Assessment

  • Complexity: Medium (Phase 1 is Low, Phase 2 is Medium)
  • Confidence: High — clear path, well-understood patterns, no architectural unknowns
  • Estimated files to change: 5 (app.rs, sandbox_logs.rs, ui/mod.rs, theme.rs, event.rs)
  • Issue type: feat

Risks & Open Questions

  • OSC 52 terminal coverage: Not all terminals support it (notably older macOS Terminal.app versions, some Linux TTYs). Silent failure — the sequence is just ignored. Users on unsupported terminals still have native Shift+click selection as fallback.
  • Large clipboard payloads: Some terminals truncate OSC 52 payloads (e.g., xterm limits to ~100KB base64). File export (Ctrl+S) is the right mechanism for large volumes.
  • Mouse capture interaction: When mouse capture is on, native terminal selection is blocked. Adding v-mode is the right answer. Shift+click bypasses mouse capture in most terminals as a fallback.
  • Selection mode + autoscroll conflict: Visual selection mode must auto-pause autoscroll to prevent the selection from shifting under the user.
  • Shift+arrow reliability: Some terminals don't report Shift+Up distinctly from Up. v-mode is the primary mechanism; shift+arrow is best-effort.

Test Considerations

  • Unit tests for plain text formatting: Test LogLine::to_plain_text() output for various line shapes (with/without fields, long messages, special characters)
  • Unit tests for selection range math: Test selection_anchor + cursor → range calculation, edge cases (anchor == cursor, anchor > cursor, empty log list)
  • Integration: Manual testing is primary — clipboard operations are inherently side-effectful. Test across iTerm2, Alacritty, and at least one Linux terminal. Test over SSH.
  • No existing test infrastructure for the TUI crate — tests would be the first in this crate

Created by spike investigation. Use build-from-issue to plan and implement.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions