Skip to content

feat: headless mode#47

Merged
Pertempto merged 36 commits intomainfrom
feat/headless-mode
Jan 12, 2026
Merged

feat: headless mode#47
Pertempto merged 36 commits intomainfrom
feat/headless-mode

Conversation

@Pertempto
Copy link
Contributor

@Pertempto Pertempto commented Jan 8, 2026

Implements spec from #46

Testing

  • just build-docker
  • just run-docker headless --bind 0.0.0.0
    • You need the --bind because the headless server runs inside the docker container
  • Try accessing localhost:8484/render/latest in your browser
  • It should show main screen of the TUI
  • just input key "?"
  • Try accessing localhost:8484/render/latest in your browser
  • It should show help screen of the TUI

Key changes:
- HTTP server on port 8080 instead of stdin/stdout JSON
- POST /input returns both render_url and raw ANSI
- GET /render/latest.png for easy browser viewing
- GET /render/{timestamp}.png for specific renders
- Larger default terminal size (120x30)
- just recipes for starting test server
'headless' better describes running the TUI without a terminal,
which is the standard term (like headless browsers).
- /render/latest now redirects (302) to timestamped URL
- /state includes render_url and ansi in response
Update run-docker to bind port 8080 for headless subcommand instead
of adding new recipes.
Examples:
- just input key j
- just input type "hello world"
- just input resize 40 160
- Fix terminal size references to 160x40
- Add task for error responses on invalid actions
- Clarify initial render happens on startup
- Clarify input recipe wraps curl
- Bind to localhost (127.0.0.1) by default
- Add --bind flag to optionally expose to network
- Document lack of authentication and CORS
- Note that server is for local dev/testing only
- Add --max-renders flag (default: 100) to prevent unbounded memory growth
- Implement FIFO eviction when limit reached
- Clarify that ansi field contains raw View() output with all escape sequences
Reduced verbosity while keeping all essential information:
- Problem section removed (context is clear)
- Usage examples consolidated
- Design decisions summarized
- Task list unchanged
- Add backticks around all endpoint references
- Unify GET /state and POST /input response formats
- Update creation date to January 8
- Must use FiraCode Nerd Font for powerline symbol support
- Add task for configuring bubbletea ANSI output to non-tty
Address clarifying questions from implementation:
- Key format uses tea.KeyMsg.String() (enter, esc, ctrl+c, etc.)
- Mode values from CurrentMode.Name (list, start, help, stats)
- Timestamp format with all dashes for URL safety
- Link to Ghostty source for color palette reference
- Add full 16-color palette table with hex values
- Specify pure black (#000000) as default/background color
- Remove external Ghostty reference
@github-actions
Copy link

github-actions bot commented Jan 8, 2026

cmd/headless/input.go — ParseKeyMsg: the ctrl branch uses tea.KeyCtrlA + tea.KeyType(baseKey[0]-'a'). This is brittle and unsafe (indexing baseKey[0] can panic for empty or multi-byte input, and arithmetic on key constants is fragile).

Please:

  • Validate len(baseKey) >= 1 and restrict to a-z (ASCII) before mapping.
  • Replace the arithmetic with an explicit mapping from letters to the corresponding tea.KeyCtrlX constants (or a small switch/table). This is safer and clearer.

@github-actions
Copy link

github-actions bot commented Jan 8, 2026

In cmd/headless/render.go:

  • loadFonts() returns fontErr but callers don't check for regularFont/boldFont being nil before using them. Consider returning a clear error message if embedded fonts are missing and avoid dereferencing nil fonts in drawChar.

  • parseANSI advances curY on newline without bounds checking; writing beyond rend.height may occur. Add bounds checks when incrementing or writing to grid to prevent panics.

  • decodeUTF8 and parseANSI use manual UTF-8 decoding; consider using utf8.DecodeRuneInString from the standard library to handle edge cases and invalid sequences safely.

  • drawChar recreates a truetype Face per cell and defers face.Close() each time; that's expensive. Cache font.Face objects per font/size in Renderer to avoid allocating per-glyph.

These changes will improve robustness and performance of the renderer.

@github-actions
Copy link

github-actions bot commented Jan 8, 2026

Changes Requested

  • Fix unsafe ctrl key mapping in cmd/headless/input.go to validate input and use explicit mapping.
  • Avoid calling s.model.Update while holding s.mu in cmd/headless/headless.go; release lock before model update and re-acquire when updating render storage.
  • Add bounds checks in cmd/headless/render.go parseANSI for newline handling and use utf8.DecodeRuneInString. Cache truetype faces instead of recreating per-cell.

Summary of Changes

  • Added new headless subcommand and HTTP server with endpoints /input, /state, /render/latest, and /render/{ts}.png.
  • Implemented ANSI-to-PNG renderer with embedded fonts and tests for parsing and rendering.
  • Implemented input parsing (ParseKeyMsg, ParseTypeToKeyMsgs) and request handling with FIFO render storage.
  • Updated docs (README.md, AGENTS.md, specs/007-headless-mode.md) and added just recipes for headless usage.

Overall Feedback

  • The feature is a substantial and useful addition, enabling programmatic interaction with the TUI and providing helpful tooling for agents and CI. However, there are some safety and performance issues that need addressing before merging:
    • Negative: Several unsafe assumptions (indexing strings, holding locks across model updates, manual UTF-8 decoding, per-cell font allocation) can lead to panics, deadlocks, or high CPU/memory use. These need fixes.
    • Positive: The tests are comprehensive for the new package, documentation and tooling were updated, and the design (render FIFO, endpoints, JSON responses) is solid. Great work on the spec implementation! 🎉

If you want, I can provide small code snippets for the ctrl mapping, utf8 usage, and face caching to speed up fixes. 🙂

@Pertempto
Copy link
Contributor Author

@bambam955 something really cool you can do with this if you have jq installed to parse the output JSON from the POST /input:

just input key "tab" | jq -r ".ansi"

image

Base automatically changed from spec/007-test-subcommand-http-redesign to main January 12, 2026 19:55
@Pertempto
Copy link
Contributor Author

Code review bot feedback should be addressed in 7252225 and cdaae04

@Pertempto Pertempto marked this pull request as ready for review January 12, 2026 20:01
@Pertempto Pertempto self-assigned this Jan 12, 2026
@Pertempto
Copy link
Contributor Author

I ran all test steps on my end and it seemed to work well.

@Pertempto Pertempto merged commit 17ffa74 into main Jan 12, 2026
1 check passed
@Pertempto Pertempto deleted the feat/headless-mode branch January 12, 2026 20:03
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