Changed _is_all_horizontal_box_drawing to _is_mostly_horizontal_box_drawing
with 80% threshold. This handles cases where terminal data has occasional
corrupted characters (like U+FFFD replacement chars) mixed in with
horizontal line characters.
Version bump to 0.3.9
Horizontal line characters (─━═) render narrower than the intended
character width in most fonts, causing gaps when followed by other
characters. Now using textLength + lengthAdjust='spacing' to force
horizontal box-drawing spans to occupy their correct width.
- Added _is_all_horizontal_box_drawing() helper
- Added textLength attribute for horizontal line spans > 1 char
- Added comprehensive tests for new functionality
- svg_exporter.py now has 100% test coverage
Version bump to 0.3.8
- Added dominant-baseline: text-before-edge for proper vertical text positioning
- Added text-rendering: optimizeLegibility for crisper text
- Simplified y-position calculation (top-aligned with baseline)
- Added tests for box drawing character detection helpers
- Added test for CSS properties
- Removed unreachable dead code paths (empty span checks)
- svg_exporter.py now has 100% test coverage
Version bump to 0.3.7
Horizontal lines (─━═) can merge since they form continuous lines.
Vertical lines (│┃║) and corners/junctions need separate x positioning
to align properly with adjacent characters.
Same box-drawing characters (like ───) can now merge into a single
tspan, but different box-drawing characters (like │╯) are kept
separate for precise positioning. This reduces SVG size while
maintaining alignment at character transitions.
Box-drawing and block element characters (U+2500-U+259F, U+25A0-U+25FF)
are now rendered as individual tspans with their own x positions to
prevent visual misalignment caused by font rendering variations.
Use SVG textLength and lengthAdjust='spacingAndGlyphs' to enforce
exact character spacing, preventing gaps between box-drawing and
other characters that may render at slightly different widths.
Also include whitespace spans in output for proper alignment.
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.