Background rect elements were being inserted inside text elements,
causing invalid SVG structure. Now collect all background rects first,
then render them before the text element for each row.
Track column count separately from character count to properly
handle wide characters (CJK, emoji) that occupy 2 terminal columns
but have a single character + empty placeholder in pyte buffer.
- Created svg_exporter.py with direct pyte-to-SVG rendering
- Eliminates Rich's export_svg() quirks (clip path count mismatch)
- Added 63 comprehensive tests for SVG exporter
- Removed Rich imports from local_server.py, terminal_session.py,
app_session.py, and cli.py
- Replaced RichHandler with standard logging.basicConfig
- Replaced @rich.repr.auto with standard __repr__ methods
- Rich is no longer directly imported (still transitive via textual-serve)
Bump version to 0.3.0
- Remove separate force_redraw on WebSocket connect
- Integrate size toggle into set_terminal_size for single redraw
- Update test to expect two executor calls (toggle pattern)
Remove width/height query params from screenshot endpoint.
New sessions created by screenshot use DEFAULT_TERMINAL_SIZE.
Existing sessions keep their current size.
- Track last known terminal size in TerminalSession
- Add force_redraw() method that re-sends SIGWINCH to trigger redraw
- Call force_redraw() when WebSocket reconnects to existing session
- Helps tmux and similar apps restore proper display after disconnect
- Fix Ctrl-C to exit immediately by setting exit_event before cleanup
- Filter Docker containers by compose project name to match correct stack
- Derive compose project from manifest directory (matches docker-compose default)
- Improve Docker socket availability check to test actual connectivity
- Add DOCKER_HOST env var support for alternate socket paths
- Better error logging for socket permission issues
Bump version to 0.2.5
- Remove excessive debug logging
- Add single warning when no containers found (with hint about socket mount)
- Use HTTP/1.0 to avoid chunked encoding complexity
- Simplify response parsing
- Refactor into separate methods for cleaner code
- Better handling of chunked transfer encoding
- Add more debug logging to diagnose parsing failures
- Log body preview when JSON not found
Helps diagnose empty sparklines by logging:
- Container discovery results and matches
- Stats request failures
- CPU calculation results
- What containers/services were found vs expected
Server-side:
- Limit SSE notifications to max once per second per route
- Track last notification time per route
Client-side:
- Debounce screenshot refreshes with 2s minimum interval per tile
- Pending refreshes are scheduled if events arrive during debounce window
When terminal resizes, old screenshot content is stale until
the app (tmux etc) re-renders at new dimensions. Clear cache
to force re-capture after resize.
- Pass service names (not slugs) to DockerStatsCollector
- Create slug->name mapping for sparkline lookups
- Stats are stored by service name, looked up by slug
- Add debug logging when no containers found
- New /events SSE endpoint pushes activity notifications to browsers
- Dashboard subscribes to SSE stream instead of polling
- Screenshots refresh instantly when terminal activity occurs
- Sparklines still poll every 30s (appropriate for 30min history)
- SSE includes keepalive every 30s and auto-reconnect on error
- Removes inefficient 5s polling; updates only on actual changes
Sparklines:
- Poll interval: 2s -> 10s
- History size: 30 -> 180 readings
- Now shows 30 minutes of CPU history
Screenshots:
- Dashboard refresh interval: 15s -> 5s
- Combined with dirty tracking, updates on activity with 5s cap
- New docker_stats.py module reads container stats from Docker socket
using only asyncio + stdlib (no new dependencies)
- Calculates CPU % from delta of cpu_usage and system_cpu_usage
- Maintains ring buffer of last 30 CPU readings per container
- render_sparkline_svg() generates mini SVG chart from history
- DockerStatsCollector polls containers every 2 seconds
- New /cpu-sparkline.svg endpoint serves sparkline for a container
- Dashboard shows sparkline in tile header next to container name
- Only active in compose mode (--compose-manifest flag)
- Graceful degradation if Docker socket unavailable
Bump version to 0.1.17
- get_screen_state() now returns has_changes flag indicating if screen changed
- pyte's dirty set tracks which rows have been modified since last read
- Screenshot handler returns cached SVG immediately when no changes detected
- Removed _screenshot_last_rendered_activity tracking (replaced by dirty flag)
- Added test for dirty flag behavior
Bump version to 0.1.16
Use tile slug as window name in window.open() so clicking the same
tile twice focuses the existing tab instead of opening a new one.
Changed: window.open(url, '_blank') -> window.open(url, 'webterm-{slug}')
1. Lock pyte screen initialization in open() to prevent races with
concurrent _update_screen() calls
2. Reorder session registration: call open() BEFORE adding to
sessions/routes dicts, so sessions are fully initialized before
other code can access them
3. Add clarifying comment that PTY resize completes before pyte resize
These fixes prevent dimension mismatches between PTY and pyte screen
that could cause content wrapping in screenshots.
When creating a new session for screenshot:
1. Use width/height query params instead of hardcoded DISCONNECT_RESIZE
2. Add a small delay (0.5s) after creating session to allow initial output
This ensures new sessions are created with the correct dimensions
matching what the screenshot expects.
The screenshot was creating a new pyte screen with arbitrary dimensions
from query params, but the replay buffer contains ANSI sequences meant
for the session's actual terminal size. This mismatch caused wrapping.
Now we use get_screen_state() which returns the actual screen buffer
from the terminal session's pyte screen, with the correct dimensions.
This ensures the screenshot matches exactly what the terminal rendered.
Pyte uses 'brightblack', 'brightred', etc. but Rich expects
'bright_black', 'bright_red' with underscores. Added PYTE_TO_RICH_COLOR
mapping to translate color names in screenshot rendering.
Test fixes:
- Fix app_session.py to use 'textual-webterm' package name (not 'textual-web')
- Fix CLI version test to not hardcode version number
- Fix static path test to not use removed Path._flavour attribute
Removed unused dependencies:
- xdg
- msgpack
- httpx
All 209 tests pass with 86% coverage.
Screenshots now properly preserve terminal colors:
1. Replay buffer provides raw ANSI data with color codes
2. pyte interprets escape sequences for accurate screen state
3. Rich renders the pyte buffer with colors to SVG
This gives us both accurate terminal state (no creeping/wrapping)
and proper color preservation in screenshots.
Bumps version to 0.1.12.
Instead of trying to replay a truncated byte buffer through pyte, this
change maintains a pyte Screen object within TerminalSession that gets
updated as terminal data flows through. This provides accurate terminal
state for screenshots without issues from buffer truncation.
Key changes:
- Add pyte Screen and Stream to TerminalSession
- Update screen state as data arrives via _update_screen()
- Add get_screen_lines() to return current screen state
- Resize pyte screen when terminal size changes
- Update local_server to use get_screen_lines() directly
- Remove _apply_carriage_returns() workaround
This properly fixes the tmux status bar 'creeping up' issue by ensuring
the screenshot always reflects the actual terminal state.
Convert f-string logging to lazy %-style interpolation throughout:
- session_manager.py
- cli.py
- terminal_session.py
This follows Python logging best practices for performance (lazy
evaluation) and consistency across the codebase.
Addresses REFACTORING.md item about normalizing logging style.
Consolidate nested helper functions and reduce complexity:
- Inline header extraction with first_header helper
- Use rpartition for cleaner host:port splitting
- Simplify control flow with loop over header candidates
Addresses REFACTORING.md item about simplifying _get_ws_url_from_request.
Replace try/finally with contextlib.AsyncExitStack for cleaner
structured cleanup of the aiohttp runner. This ensures proper
resource cleanup even in complex shutdown scenarios.
Addresses REFACTORING.md item about TaskGroup/cleanup context.
Replace bare Exception catch with specific exception types:
- json.JSONDecodeError for invalid JSON
- TypeError, KeyError, ValueError for malformed messages
- OSError for I/O errors
Addresses REFACTORING.md item about narrowing WebSocket error handling.
Replaces simple carriage return handling with pyte terminal emulator
to properly interpret all ANSI escape sequences including cursor
positioning. This fixes the tmux status bar 'creeping up' issue in
screenshots.
Adds pyte dependency to pyproject.toml.
Resolves TODO item #2.