Skip to content

Add viewport VT export action#10

Open
austinywang wants to merge 1 commit intomainfrom
issue-1073-ansi-read-screen
Open

Add viewport VT export action#10
austinywang wants to merge 1 commit intomainfrom
issue-1073-ansi-read-screen

Conversation

@austinywang
Copy link

@austinywang austinywang commented Mar 10, 2026

Summary

  • add a viewport-specific write-screen binding action that can emit VT/ANSI text
  • keep the existing full-screen and scrollback export paths unchanged
  • unblock cmux read-screen/capture-pane ANSI preservation for viewport-only reads

Testing

  • built GhosttyKit.xcframework locally with zig build -Demit-xcframework=true -Dxcframework-target=universal -Doptimize=ReleaseFast -Dversion-string=1.3.0-dev

Summary by cubic

Adds a viewport-only export action that can emit plain text, HTML, or VT/ANSI to a temp file, enabling ANSI preservation for visible-viewport reads. Addresses Linear issue ghostty-org#1073.

  • New Features
    • Added write_viewport_file binding and command entries to export only the visible viewport; supports .copy, .paste, and .open.
    • Supports emit formats: plain text, HTML, and VT/ANSI.
    • Extended writeScreenFile with a .viewport selection using visible bounds; full-screen and scrollback exports are unchanged.

Written for commit 3058878. Summary will update on new commits.

Summary by CodeRabbit

  • New Features
    • Added ability to export the currently visible viewport to a file, separate from full screen history dumps
    • Viewport export supports multiple formats (HTML, ANSI) with copy, paste, and open actions

@coderabbitai
Copy link

coderabbitai bot commented Mar 10, 2026

📝 Walkthrough

Walkthrough

The changes introduce a new viewport dumping capability to the application by adding a write_viewport_file action variant across the binding, command, and surface layers. A new viewport variant was added to the WriteScreenLoc enum, enabling writing visible viewport contents to files alongside existing screen/history/selection operations.

Changes

Cohort / File(s) Summary
Action Definition
src/input/Binding.zig
Added write_viewport_file: WriteScreen action variant and updated Scope handling to include it as an app/surface-scoped action.
Surface Processing
src/Surface.zig
Extended WriteScreenLoc enum with viewport variant and implemented binding action handler that delegates to writeScreenFile(.viewport, v).
Command Configuration
src/input/command.zig
Added command definitions for write_viewport_file with HTML and ANSI VT sequence variants, each supporting copy/paste/open outcomes with associated descriptions.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 A viewport's view now finds its way,
To files that store the light of day,
From binding's call to surface true,
The visible world is captured too!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a viewport-specific export action with VT/ANSI support, which is the primary objective of this PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch issue-1073-ansi-read-screen

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link

greptile-apps bot commented Mar 10, 2026

Greptile Summary

This PR adds a write_viewport_file binding action that captures only the currently visible viewport (as opposed to the full screen or scrollback history) and writes it to a temporary file, unblocking VT/ANSI-preserving read-screen workflows in cmux. The implementation is clean and closely mirrors the existing write_screen_file and write_scrollback_file paths.

Key changes:

  • src/Surface.zig: Adds viewport to the WriteScreenLoc enum and a corresponding branch in writeScreenFile that constructs a terminal.Selection spanning getTopLeft(.viewport)getBottomRight(.viewport).
  • src/input/Binding.zig: Declares write_viewport_file: WriteScreen in the Action union and adds it to the surface-action list; no alternate-screen guard is needed (unlike history, the viewport always exists).
  • src/input/command.zig: Registers all 9 command-palette entries (copy / paste / open × plain / html / vt), consistent with the existing screen and scrollback commands.

Minor observations:

  • The orelse break :viewport null guard in writeScreenFile is unreachable — getBottomRight(.viewport) never returns null (it uses an internal .? assert). This is harmless but could benefit from a clarifying comment.
  • No parsing tests were added for write_viewport_file, unlike the existing coverage for write_screen_file in Binding.zig around line 4503.

Confidence Score: 4/5

  • This PR is safe to merge; it adds a well-scoped new action with no changes to existing code paths.
  • The implementation closely mirrors established patterns, the new code path is additive and isolated, and no existing behaviour is modified. Score is 4 rather than 5 only because parsing tests for the new action are absent.
  • No files require special attention; all changes are additive and self-contained.

Important Files Changed

Filename Overview
src/Surface.zig Adds .viewport to WriteScreenLoc and a corresponding branch in writeScreenFile. The implementation correctly delegates to pages.getTopLeft(.viewport) / getBottomRight(.viewport). The orelse break :viewport null guard will never fire (viewport always exists), which is a minor dead-code observation but not a bug.
src/input/Binding.zig Adds write_viewport_file: WriteScreen to the Action union and registers it correctly in the surface-action list. No parsing tests were added for the new action, unlike the coverage provided for write_screen_file.
src/input/command.zig Adds all 9 expected command palette entries for write_viewport_file (copy/paste/open × plain/html/vt). Titles and descriptions are accurate and consistent with existing write_screen_file entries.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Key binding triggers\nwrite_viewport_file action] --> B[performBindingAction\nin Surface.zig]
    B --> C[writeScreenFile\nloc = .viewport]
    C --> D[Acquire renderer_state mutex]
    D --> E[pages.getTopLeft .viewport]
    D --> F[pages.getBottomRight .viewport]
    E --> G[terminal.Selection.init\ntopLeft, bottomRight, false]
    F --> G
    G -->|sel is null\nnever happens for viewport| H[Cleanup tmp_dir\nreturn early]
    G -->|sel is non-null| I[ScreenFormatter\nemit = plain / vt / html]
    I --> J[Write to tmp file\nviewport.txt or viewport.html]
    J --> K{WriteScreen.Action}
    K -->|copy| L[Copy file path\nto clipboard]
    K -->|paste| M[Paste file path\ninto terminal]
    K -->|open| N[Open file in\ndefault OS editor]
Loading

Last reviewed commit: 3058878

Comment on lines +6108 to +6115
.viewport => viewport: {
break :viewport terminal.Selection.init(
pages.getTopLeft(.viewport),
pages.getBottomRight(.viewport) orelse
break :viewport null,
false,
);
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

orelse branch is dead code for .viewport

getBottomRight(.viewport) is guaranteed to return a non-null Pin — its implementation unconditionally computes a pin via br.down(self.rows - 1).? (which itself asserts non-null internally) and then returns it. This means orelse break :viewport null will never be taken.

This is consistent with how .screen is handled (also using orelse defensively), so it's not a correctness issue, but it is worth noting that unlike .history — where the null return is possible when the history region is empty — the viewport region always exists in a live terminal. A comment clarifying this behavior (similar to the one on .history about alternate screens) would improve readability.

.jump_to_prompt,
.write_scrollback_file,
.write_screen_file,
.write_viewport_file,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No parsing test added for write_viewport_file

The sibling actions write_screen_file and write_scrollback_file both have dedicated parsing tests (around line 4503) that exercise both the shorthand form (e.g. write_screen_file:copy) and the extended form (e.g. write_screen_file:copy,html). There are no corresponding tests for write_viewport_file.

Since parsing is shared via the WriteScreen type, the behaviour is almost certainly correct, but adding test coverage would be consistent with the existing test patterns and would guard against future regressions, e.g.:

{
    const binding = try parseSingle("a=write_viewport_file:copy");
    try testing.expect(binding.action == .write_viewport_file);
    try testing.expectEqual(Action.WriteScreen.copy, binding.action.write_viewport_file);
}

{
    const binding = try parseSingle("a=write_viewport_file:copy,vt");
    try testing.expect(binding.action == .write_viewport_file);
    try testing.expectEqual(Action.WriteScreen{
        .action = .copy,
        .emit = .vt,
    }, binding.action.write_viewport_file);
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 3 files

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/input/command.zig (1)

342-408: Add a focused regression test for the new viewport export commands.

This matrix is all hand-written, and the current command defaults test only smoke-tests the aggregate arrays. A small assertion around actionCommands(.write_viewport_file) for the plain/HTML/VT variants would make copy/paste drift between these export tables much easier to catch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/input/command.zig` around lines 342 - 408, Add a focused regression test
that asserts the specific entries returned by
actionCommands(.write_viewport_file) include the nine expected variants (three
actions: copy/paste/open × three emits: default/plain, .html, .vt) and their
titles/descriptions (or at least their .action/.emit discriminants) to prevent
silent drift; update the existing "command defaults" test suite by adding a new
test case that calls actionCommands(.write_viewport_file) and verifies each
returned command has the correct .action (.copy/.paste/.open) and .emit
(absent/default, .html, .vt) combinations in the same order as the matrix shown.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/input/command.zig`:
- Around line 342-408: Add a focused regression test that asserts the specific
entries returned by actionCommands(.write_viewport_file) include the nine
expected variants (three actions: copy/paste/open × three emits: default/plain,
.html, .vt) and their titles/descriptions (or at least their .action/.emit
discriminants) to prevent silent drift; update the existing "command defaults"
test suite by adding a new test case that calls
actionCommands(.write_viewport_file) and verifies each returned command has the
correct .action (.copy/.paste/.open) and .emit (absent/default, .html, .vt)
combinations in the same order as the matrix shown.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 610d9614-648f-4a62-b609-11352ead97ff

📥 Commits

Reviewing files that changed from the base of the PR and between 1b008f5 and 3058878.

📒 Files selected for processing (3)
  • src/Surface.zig
  • src/input/Binding.zig
  • src/input/command.zig

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant