Skip to content

Conversation

@tony
Copy link
Member

@tony tony commented Dec 6, 2025

Adds libtmux.textframe, a fixed-size ASCII frame simulator for testing terminal UI output. Useful for validating capture_pane() output and terminal rendering in tests.

Install: pip install libtmux[textframe]

Features

TextFrame primitive

A dataclass for creating fixed-dimension ASCII frames with overflow detection:

from libtmux.textframe import TextFrame

frame = TextFrame(content_width=10, content_height=2)
frame.set_content(["hello", "world"])
print(frame.render())
# +----------+
# |hello     |
# |world     |
# +----------+
  • Configurable overflow_behavior: "error" (raises with visual diagnostic) or "truncate" (clips silently)
  • Dimension validation via __post_init__

Pane.capture_frame()

High-level method that wraps capture_pane() and returns a TextFrame:

def test_cli_output(pane, textframe_snapshot):
    pane.send_keys("echo 'Hello'", enter=True)

    # Wait for output, then capture as frame
    frame = pane.capture_frame(content_width=40, content_height=10)
    assert frame == textframe_snapshot
  • Defaults to pane dimensions when width/height not specified
  • Uses overflow_behavior="truncate" by default for CI robustness

pytest assertion hook

Rich diff output when comparing TextFrame objects:

TextFrame comparison failed:
  width: 20 != 10
Content diff:
- +----------+
+ +--------------------+

syrupy snapshot extension

TextFrameExtension stores snapshots as .frame files - one file per test for cleaner git diffs:

def test_pane_output(textframe_snapshot):
    frame = TextFrame(content_width=20, content_height=5)
    frame.set_content(["Hello", "World"])
    assert frame == textframe_snapshot

Plugin discovery

Registered via pytest11 entry point - fixtures and hooks are auto-discovered when libtmux[textframe] is installed.

[project.entry-points.pytest11]
libtmux-textframe = "libtmux.textframe.plugin"

[project.optional-dependencies]
textframe = ["syrupy>=4.0.0"]

Files changed

Path Description
src/libtmux/pane.py Add capture_frame() method
src/libtmux/textframe/__init__.py Public API exports
src/libtmux/textframe/core.py TextFrame dataclass, ContentOverflowError
src/libtmux/textframe/plugin.py Syrupy extension, pytest hooks, textframe_snapshot fixture
pyproject.toml Entry point + optional dependency
docs/internals/textframe.md Documentation
tests/textframe/ TextFrame test suite with snapshot baselines
tests/test_pane_capture_frame.py capture_frame() integration tests (12 tests)

See also

@tony tony changed the title Yet another Snapshot PR Yet another Snapshot PR (TextFrame) Dec 6, 2025
@tony tony force-pushed the snapshots-2 branch 2 times, most recently from 6b00c4f to 96d1823 Compare December 7, 2025 08:55
@codecov
Copy link

codecov bot commented Dec 7, 2025

Codecov Report

❌ Patch coverage is 55.40541% with 66 lines in your changes missing coverage. Please review.
✅ Project coverage is 46.28%. Comparing base (e907f81) to head (912288b).

Files with missing lines Patch % Lines
src/libtmux/textframe/core.py 59.81% 37 Missing and 6 partials ⚠️
src/libtmux/textframe/plugin.py 24.13% 21 Missing and 1 partial ⚠️
src/libtmux/pane.py 88.88% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #613      +/-   ##
==========================================
+ Coverage   45.68%   46.28%   +0.59%     
==========================================
  Files          22       25       +3     
  Lines        2250     2398     +148     
  Branches      360      387      +27     
==========================================
+ Hits         1028     1110      +82     
- Misses       1079     1138      +59     
- Partials      143      150       +7     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tony tony marked this pull request as ready for review December 7, 2025 16:18
tony added a commit that referenced this pull request Dec 7, 2025
why: Document new features for release notes.
what:
- TextFrame primitive for terminal UI testing
- pytest assertion hook with rich diff output
- syrupy snapshot extension with .frame files
- Optional install via libtmux[textframe]
@tony tony changed the title Yet another Snapshot PR (TextFrame) TextFrame: Terminal UI testing primitive with pytest/syrupy integration Dec 7, 2025
tony added a commit that referenced this pull request Dec 7, 2025
why: Document the new capture_frame() method for users.
what:
- Add Pane.capture_frame() section under New features
- Include usage example with textframe_snapshot
- Document key features (default dimensions, truncate mode)
tony added a commit that referenced this pull request Dec 7, 2025
why: Document the new capture_pane() parameters for the changelog.

what:
- Add section for Pane.capture_pane() enhanced
- Document all 5 new parameters with flag mappings
- Add code examples for colored output and joined lines
- Note trim_trailing requires tmux 3.4+
tony added a commit that referenced this pull request Dec 7, 2025
why: Document the new capture_pane() parameters for the changelog.

what:
- Add section for Pane.capture_pane() enhanced
- Document all 5 new parameters with flag mappings
- Add code examples for colored output and joined lines
- Note trim_trailing requires tmux 3.4+
tony added a commit that referenced this pull request Dec 7, 2025
why: Document the new capture_pane() parameters for the changelog.

what:
- Add section for Pane.capture_pane() enhanced
- Document all 5 new parameters with flag mappings
- Add code examples for colored output and joined lines
- Note trim_trailing requires tmux 3.4+
tony added a commit that referenced this pull request Dec 7, 2025
why: Document new features for release notes.
what:
- TextFrame primitive for terminal UI testing
- pytest assertion hook with rich diff output
- syrupy snapshot extension with .frame files
- Optional install via libtmux[textframe]
tony added a commit that referenced this pull request Dec 7, 2025
why: Document the new capture_frame() method for users.
what:
- Add Pane.capture_frame() section under New features
- Include usage example with textframe_snapshot
- Document key features (default dimensions, truncate mode)
@tony
Copy link
Member Author

tony commented Dec 7, 2025

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

tony added 12 commits December 7, 2025 17:57
why: Enable snapshot testing for ASCII frame visualization
what:
- Add syrupy to dev dependencies
why: Validate Syrupy snapshot testing for terminal frame visualization
what:
- Add TextFrame dataclass with content overflow detection
- Add ContentOverflowError with Reality vs Mask visual
- Add TextFrameSerializer extending AmberDataSerializer
- Add TextFrameExtension for Syrupy integration
- Add parametrized tests for rendering and nested serialization
why: Store expected ASCII frame output for regression testing
what:
- Add snapshots for basic, empty, and overflow frame rendering
- Add snapshot for nested TextFrame serialization
why: Prevent invalid TextFrame instances from being created with
zero/negative dimensions or multi-character fill strings.
what:
- Add __post_init__ to validate content_width > 0
- Add __post_init__ to validate content_height > 0
- Add __post_init__ to validate fill_char is single character
…rflow

why: Allow flexible handling of oversized content - either error with
visual diagnostic or silently truncate to fit.
what:
- Add OverflowBehavior type alias for "error" | "truncate"
- Add overflow_behavior parameter with default "error" (backward compatible)
- Implement truncate logic to clip width and height
- Update docstrings to reflect new behavior
why: Verify the new overflow_behavior="truncate" mode works correctly
for width, height, and combined overflow scenarios.
what:
- Add overflow_behavior field to Case NamedTuple with default "error"
- Add truncate_width test case (clips horizontal overflow)
- Add truncate_height test case (clips vertical overflow)
- Add truncate_both test case (clips both dimensions)
- Update test to pass overflow_behavior to TextFrame constructor
why: Capture expected output for truncate behavior test cases.
what:
- Add truncate_width snapshot (5x2 frame, clipped "hello")
- Add truncate_height snapshot (10x1 frame, single row)
- Add truncate_both snapshot (5x2 frame, both dimensions clipped)
why: Provide rich assertion output for TextFrame comparisons
without requiring syrupy for basic equality checks.
what:
- Add pytest_assertrepr_compare hook for TextFrame == TextFrame
- Show dimension mismatches (width, height)
- Show content diff using difflib.ndiff
why: Individual .frame files provide cleaner git diffs and
easier review than a single .ambr file with all snapshots.
what:
- Replace AmberSnapshotExtension with SingleFileSnapshotExtension
- Set file extension to .frame
- Simplify serialize() method (removed nested serializer class)
why: Replaced by individual .frame files from SingleFileSnapshotExtension.
what:
- Delete test_core.ambr
why: New snapshot format from SingleFileSnapshotExtension.
what:
- Add test_frame_rendering[basic_success].frame
- Add test_frame_rendering[overflow_width].frame
- Add test_frame_rendering[empty_frame].frame
- Add test_frame_rendering[truncate_width].frame
- Add test_frame_rendering[truncate_height].frame
- Add test_frame_rendering[truncate_both].frame
- Add test_nested_serialization.frame
tony added 16 commits December 7, 2025 17:57
why: Allow opt-in installation of textframe pytest plugin.
what:
- Add [project.optional-dependencies] textframe = ["syrupy>=4.0.0"]
- Add [project.entry-points.pytest11] libtmux-textframe entry point
- Downstream users can now: pip install libtmux[textframe]
why: Document opt-in mechanism for downstream users.
what:
- Update import paths from tests/ to src/libtmux/textframe/
- Add installation section: pip install libtmux[textframe]
- Document auto-discovered fixtures and hooks
- Add Plugin Discovery section explaining pytest11 entry points
- Update file paths table
why: Lockfile reflects new optional dependency.
what:
- Add syrupy to textframe extras in uv.lock
why: Document new features for release notes.
what:
- TextFrame primitive for terminal UI testing
- pytest assertion hook with rich diff output
- syrupy snapshot extension with .frame files
- Optional install via libtmux[textframe]
why: Enable capturing pane content as TextFrame for visualization
and snapshot testing. This bridges capture_pane() with the TextFrame
dataclass for a more ergonomic testing workflow.
what:
- Add capture_frame() method that wraps capture_pane()
- Default to pane dimensions when width/height not specified
- Default to truncate mode for robustness in CI environments
- Add comprehensive docstring with examples
why: Verify capture_frame() works with real tmux panes and integrates
properly with syrupy snapshot testing.
what:
- Add 12 comprehensive tests using NamedTuple parametrization
- Test basic usage, custom dimensions, overflow behavior
- Demonstrate retry_until integration pattern
why: Baseline snapshot for capture_frame() visual regression testing.
what:
- Add test_capture_frame_snapshot.frame baseline
why: Show users how to use capture_frame() for testing terminal output.
what:
- Add Pane.capture_frame() integration section
- Document parameters with table
- Explain design decisions (truncate default, refresh)
- Add retry_until usage example
why: Document the new capture_frame() method for users.
what:
- Add Pane.capture_frame() section under New features
- Include usage example with textframe_snapshot
- Document key features (default dimensions, truncate mode)
why: Comprehensive visual regression testing for all capture_frame() variations.
what:
- Add SnapshotCase NamedTuple for parametrized snapshot testing
- Add 18 snapshot test cases covering:
  - Dimension variations: prompt_only, wide/narrow/tall/short frames
  - start/end parameters: start=0, end=0, end="-", start_end_range
  - Truncation: width and height truncation
  - Special characters and edge cases
- Use retry_until for robust async output handling
why: Baseline snapshots for exhaustive visual regression testing.
what:
- Add 18 .frame snapshot files for parametrized test cases
- Covers dimensions, start/end params, truncation, special chars
why: Show actual frame output to help users understand the feature.
what:
- Add basic usage example with rendered ASCII frame output
- Add multiline output example demonstrating printf capture
- Add truncation example showing long lines clipped to frame width
- Reorganize into sections: Basic, Multiline, Truncation, Snapshot testing
why: Enable doctest verification of capture_frame() output.
what:
- Create new pane with shell='sh' for predictable prompt
- Remove # doctest: +SKIP since output is now deterministic
- Follow established pattern from capture_pane() doctest
why: Allow users to control capture behavior when using capture_frame()
for snapshot testing, such as capturing colored output or joining
wrapped lines.

what:
- Add escape_sequences parameter for ANSI escape sequences
- Add escape_non_printable parameter for octal escapes
- Add join_wrapped parameter for joining wrapped lines
- Add preserve_trailing parameter for trailing spaces
- Add trim_trailing parameter with tmux 3.4+ version check
- Forward all flags to capture_pane() call
why: Verify that capture_frame() correctly forwards all capture_pane()
flags for proper behavior in snapshot testing scenarios.

what:
- Add CaptureFrameFlagCase NamedTuple for parametrized tests
- Add 4 test cases covering key flag behaviors
- Test escape_sequences, join_wrapped, preserve_trailing flags
- Verify flag absence behavior (no_escape_sequences)
why: Document the new capture_frame() parameters for users.

what:
- Add flag forwarding bullet point to capture_frame() feature list
why: Enable interactive exploration of large frame content in terminal
what:
- Add display() method with TTY detection
- Add _curses_display() with scrolling support
- Navigation: arrows, WASD, vim keys (hjkl)
- Page navigation: PgUp/PgDn, Home/End
- Exit: q, Esc, Ctrl-C
@tony tony force-pushed the snapshots-2 branch 2 times, most recently from c043d03 to 94a6f8f Compare December 8, 2025 01:38
tony added 4 commits December 7, 2025 19:39
why: Enable users to discover interactive viewer feature
what:
- Add Interactive Display section with usage example
- Document all keyboard controls in table format
- Note TTY requirement and RuntimeError behavior
why: Include display() in 0.53.x feature list
what:
- Add interactive curses viewer to TextFrame features
why: curses KEY_RESIZE only fires on getch(), missing resize events
     when terminal is resized but no key is pressed
what:
- Replace stdscr.getmaxyx() with shutil.get_terminal_size()
- Remove KEY_RESIZE handling (now redundant)

This follows Rich's approach: query terminal size directly via
ioctl(TIOCGWINSZ) on each loop iteration, which works reliably
in tmux and other terminal multiplexers.
why: Verify display() uses shutil.get_terminal_size() for resize
what:
- Add test_terminal_resize_via_shutil test
- Mock shutil.get_terminal_size to verify it's called
@tony
Copy link
Member Author

tony commented Dec 8, 2025

Code review

Found 1 issue:

  1. ImportError for users without libtmux[textframe] installed: The __init__.py unconditionally imports TextFrameExtension from plugin.py, which requires syrupy. This causes pane.capture_frame() to fail with an ImportError for users who haven't installed the optional dependency.

from libtmux.textframe.core import ContentOverflowError, TextFrame
from libtmux.textframe.plugin import TextFrameExtension
__all__ = ["ContentOverflowError", "TextFrame", "TextFrameExtension"]

The import chain is:

  • pane.capture_frame() imports from libtmux.textframe import TextFrame
  • __init__.py line 6 imports TextFrameExtension from plugin.py
  • plugin.py lines 17-19 unconditionally import pytest and syrupy

Suggested fix: Make the TextFrameExtension import conditional, or move it to a separate module that's only imported when needed.

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

tony added 4 commits December 7, 2025 19:50
why: Users without libtmux[textframe] get ImportError on capture_frame()
what:
- Wrap TextFrameExtension import in try/except ImportError
- Only add to __all__ when syrupy is available
- Core TextFrame functionality works without optional dependency
why: Follow established exception pattern for libtmux exceptions
what:
- Add LibTmuxException as base class alongside ValueError
- Matches pattern of AdjustmentDirectionRequiresAdjustment, etc.
- Enables catching all libtmux exceptions with LibTmuxException
why: Follow CLAUDE.md guideline for stdlib namespace imports
what:
- Change from difflib import ndiff to import difflib
- Use difflib.ndiff() instead of ndiff()
why: Follow pytest best practices from CLAUDE.md guidelines
what:
- Use import unittest.mock namespace style
- Replace patch() context managers with monkeypatch.setattr()
- Document MagicMock necessity for curses window simulation
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.

2 participants