Skip to content

Conversation

@akesson
Copy link
Member

@akesson akesson commented Nov 15, 2025

Summary

Adds terminal information detection to enable handlers to adapt their output based on the client's terminal capabilities.

Breaking Changes

⚠️ CommandHandler trait signature changed - all handler implementations must be updated to accept the new terminal_info parameter.

See CHANGELOG.md for migration guide.

Features

  • Terminal detection: width, height, TTY status, color support (4 levels), theme (light/dark)
  • Client-side detection: happens before each command
  • Graceful fallback: individual fields optional when detection fails
  • Fast: 10-25ms typical (Terminal.app, Zed, Warp), <1ms on unsupported terminals
  • Non-blocking: theme detection wrapped in spawn_blocking

Dependencies

  • termbg 0.6 - theme detection
  • terminal_size 0.4 - width/height
  • supports-color 3.0 - color levels

Migration

Add terminal_info parameter to your handler:

async fn handle(
    &self,
    command: &str,
    terminal_info: TerminalInfo,  // NEW
    output: impl AsyncWrite + Send + Unpin,
    cancel_token: CancellationToken,
) -> Result<i32>

Test Plan

  • All tests updated and passing
  • Cargo check passes
  • Theme detection benchmarked on multiple terminals
  • CHANGELOG updated

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Breaking Changes

    • Updated CommandHandler trait to include a terminal information parameter.
    • Modified command message structure to carry terminal metadata alongside commands.
  • New Features

    • Added terminal detection capabilities: width, height, TTY status, color support, and theme detection.
    • Introduced new public types: TerminalInfo, ColorSupport, and Theme for accessing terminal environment information.
  • Chores

    • Added new dependencies for terminal environment detection.

akesson and others added 3 commits November 15, 2025 11:05
Enables handlers to receive terminal information from clients for
adaptive output formatting. Detects width, height, TTY status, color
support, and theme (light/dark) using termbg.

Breaking Changes:
- CommandHandler::handle() now takes terminal_info parameter
- Updated to v0.4.0

Features:
- Terminal width/height detection (optional, can fail)
- TTY detection (always available)
- Color support levels: None, Basic16, Colors256, Truecolor
- Theme detection (Light/Dark) via termbg with 100ms timeout
- Detection happens client-side before each command
- Graceful fallback when detection fails

Dependencies:
- termbg 0.6 - theme detection
- terminal_size 0.4 - width/height detection
- supports-color 3.0 - color capability detection

Performance:
- Theme detection: 10-25ms typical (validated on Terminal.app, Zed, Warp)
- Fast failure: <1ms on unsupported terminals
- Non-blocking: wrapped in spawn_blocking to protect async runtime

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Nov 15, 2025

Walkthrough

This release introduces terminal capability detection to the daemon CLI architecture. A new src/terminal.rs module defines TerminalInfo, ColorSupport, and Theme types. The CommandHandler::handle() method now requires a terminal_info parameter, and SocketMessage::Command is restructured to carry both command and terminal metadata. The client detects terminal info before sending commands; the server propagates it through the handler invocation.

Changes

Cohort / File(s) Summary
Public API and trait definitions
src/lib.rs, CHANGELOG.md
New terminal module re-exported with TerminalInfo, ColorSupport, Theme types. Updated CommandHandler trait signature to include terminal_info: TerminalInfo parameter. Version bumped to 0.4.0 with breaking-change documentation and migration guide.
Terminal detection module
src/terminal.rs
New module implementing TerminalInfo, ColorSupport, and Theme types. TerminalInfo::detect() async method gathers terminal width, height, TTY status, color support, and theme (via non-blocking and blocking operations with 100ms timeout). Includes test coverage for detection logic.
Message transport layer
src/transport.rs
SocketMessage::Command variant restructured from Command(String) to struct-like Command { command: String, terminal_info: TerminalInfo }. Updated imports to include TerminalInfo.
Client-side terminal integration
src/client.rs
Added TerminalInfo::detect() call during command execution. Client now sends SocketMessage::Command { command, terminal_info } instead of SocketMessage::Command(command). Includes logging of detected terminal info.
Server-side terminal handling
src/server.rs
Deserialization of SocketMessage::Command now yields (command, terminal_info) tuple. Handler invocation updated to pass terminal_info as second argument. Enhanced logging displays terminal width, height, TTY status, color support, and theme.
Example implementations
examples/common/mod.rs, examples/concurrent.rs
Updated CommandHandler::handle() implementations to include unused _terminal_info: TerminalInfo parameter. No behavioral changes; example commands remain functionally identical.
Test implementations
src/tests.rs, tests/integration_tests.rs, tests/version_tests.rs
All test CommandHandler implementations (SimpleHandler, EchoHandler, ChunkedHandler, CancellableHandler, ErrorHandler, ConcurrentTrackingHandler) updated with _terminal_info: TerminalInfo parameter. Test cases constructing SocketMessage::Command updated to provide terminal_info field.
Dependencies
Cargo.toml
Version bumped to 0.4.0. Added three new dependencies: termbg (theme detection), terminal_size (dimension detection), supports-color (color capability detection).

Sequence Diagram

sequenceDiagram
    participant Client
    participant TerminalModule as Terminal Detection
    participant Transport
    participant Server
    participant Handler

    Client->>TerminalModule: TerminalInfo::detect()
    activate TerminalModule
    TerminalModule->>TerminalModule: Detect width, height, TTY
    TerminalModule->>TerminalModule: Detect color support
    TerminalModule->>TerminalModule: Detect theme (blocking)
    TerminalModule-->>Client: TerminalInfo
    deactivate TerminalModule

    Client->>Transport: Send Command { command, terminal_info }
    activate Transport
    Transport->>Transport: Serialize
    Transport->>Server: SocketMessage
    deactivate Transport

    activate Server
    Server->>Server: Deserialize → (command, terminal_info)
    Server->>Server: Log terminal info
    Server->>Handler: handle(command, terminal_info, output, cancel)
    deactivate Server

    activate Handler
    Handler->>Handler: Process command
    Handler-->>Server: Result
    deactivate Handler
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Terminal detection logic (src/terminal.rs): Review blocking-to-async bridging via tokio::task::spawn_blocking with 100ms timeout; verify error handling and graceful fallbacks for detection failures
  • Message protocol changes (src/transport.rs): Verify serialization/deserialization of struct-like Command variant does not break wire compatibility or introduce unexpected behavior
  • Trait signature propagation: All CommandHandler implementations across examples, tests, and server must correctly receive the new parameter; verify consistent parameter ordering across ~10 implementation sites
  • Client terminal detection integration (src/client.rs): Ensure TerminalInfo::detect() is called at the right point in the command execution flow and that the result is properly propagated

Possibly related PRs

  • human-solutions/daemon-cli#4: Modifies CommandHandler trait signature and SocketMessage variants, directly intersecting with the public IPC protocol changes in this release.

Poem

🐰 Terminal whispers now heard, width and height in flight!
Colors bloom, themes take shape, TTY knows the light.
Through the daemon's pathways, metadata flows true—
A breaking change, a brighter stage, for 0.4.0! 🌈✨

Pre-merge checks and finishing touches

✅ 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 'Add terminal info support with theme detection (v0.4.0)' directly and clearly summarizes the main change: adding terminal information detection with theme support in version 0.4.0.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch terminfo

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

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.

Actionable comments posted: 0

🧹 Nitpick comments (8)
CHANGELOG.md (2)

15-18: Document graceful fallback behavior for optional fields.

The PR objectives mention "provides graceful fallbacks (fields optional when detection fails)," which is an important usability feature. Consider clarifying in the Added section that TerminalInfo fields are optional when detection fails, so handlers can implement defensive fallbacks gracefully.

- Terminal info detection: width, height, TTY status, color support, theme (light/dark)
  - Fields are optional and gracefully omitted if detection fails
  - Handlers can safely unwrap or use defaults for missing fields
- New types: `TerminalInfo`, `ColorSupport`, `Theme`

20-31: Expand migration guide with practical usage examples.

The migration guide shows only the new function signature but doesn't explain how to work with TerminalInfo or what capabilities it provides. Consider adding a brief example showing how handlers can access terminal metadata (e.g., color support, theme) to adapt output.

### Migration

Add `terminal_info` parameter to your handler (prefix with `_` if unused):
```rust
async fn handle(
    &self,
    command: &str,
    terminal_info: TerminalInfo,  // NEW - provides width, height, colors, theme
    output: impl AsyncWrite + Send + Unpin,
    cancel_token: CancellationToken,
) -> Result<i32>

Example: Use terminal capabilities to adapt output:

async fn handle(
    &self,
    command: &str,
    terminal_info: TerminalInfo,
    output: impl AsyncWrite + Send + Unpin,
    cancel_token: CancellationToken,
) -> Result<i32> {
    if let Some(colors) = terminal_info.color_support {
        // Use color codes based on support level
    }
    if let Some(theme) = terminal_info.theme {
        // Adapt colors/styling to light or dark theme
    }
    Ok(0)
}

</blockquote></details>
<details>
<summary>examples/common/mod.rs (1)</summary><blockquote>

`29-32`: **Example handler signature updated correctly for TerminalInfo**

The `CommandHandler` impl now takes `_terminal_info: TerminalInfo`, which keeps the example compiling against the new API while explicitly marking the parameter as unused; the unknown-command `Ok(127)` branch remains unchanged behavior-wise and is a reasonable choice.


If you want to showcase the new terminal features, consider a follow‑up that uses `terminal_info` here (e.g., printing detected width/theme) to give users a concrete example.


Also applies to: 117-120

</blockquote></details>
<details>
<summary>examples/concurrent.rs (1)</summary><blockquote>

`60-66`: **Concurrent example updated cleanly to the new CommandHandler API**

Adding `_terminal_info: TerminalInfo` to `TaskQueueHandler::handle` aligns this example with the new trait without altering behavior, and the unknown-command `Ok(127)` path remains appropriate.


You might later extend this example to actually use `terminal_info` (e.g., adjusting formatting when not a TTY) to demonstrate terminal-aware concurrency scenarios.


Also applies to: 195-213

</blockquote></details>
<details>
<summary>src/lib.rs (1)</summary><blockquote>

`35-41`: **Doc examples correctly updated for `terminal_info`**

The CommandHandler examples now include the `terminal_info: TerminalInfo` parameter in the right position and match the trait signature. In the second (no_run) example you already use `_terminal_info`; if you want doctest users to avoid unused-variable warnings, you could optionally mirror that `_terminal_info` naming in the first and third examples as well.



Also applies to: 214-222

</blockquote></details>
<details>
<summary>src/tests.rs (1)</summary><blockquote>

`122-133`: **TerminalInfo wiring in handler tests is correct; consider small dedup**

Both async tests now construct appropriate `TerminalInfo` instances and pass them into `handle`, aligning with the new trait signature and server behavior. If you find yourself adding more tests like this, you could factor a tiny helper (e.g., `fn basic_tty_info(...)`) to cut repetition, but it’s not strictly necessary at this size.



Also applies to: 166-172, 177-179

</blockquote></details>
<details>
<summary>src/terminal.rs (1)</summary><blockquote>

`88-104`: **Color detection and tests are minimal but effective; consider future extensibility**

`detect_color_support()` correctly collapses the supports‑color levels into your four enums, and the tests ensure both it and `TerminalInfo::detect()` execute without panicking across environments.

If you anticipate adding more color levels or themes later, you might consider marking the enums `#[non_exhaustive]` upfront to avoid breaking changes, but that’s a forward‑compatibility nicety rather than a requirement.



Also applies to: 110-137

</blockquote></details>
<details>
<summary>tests/integration_tests.rs (1)</summary><blockquote>

`271-283`: **Sequential-commands test correctly adapts to new connect API; optional helper extraction**

Each iteration clones `daemon_exe` and calls `DaemonClient::connect_with_name_and_timestamp` with the same `build_timestamp` as the server, then asserts a `0` exit code. The logic is sound.

If you ever want to trim repetition, consider a small helper that returns a connected `DaemonClient` for a given `(daemon_name, root_path, daemon_exe, build_timestamp)`, but this is optional for tests.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used**: CodeRabbit UI

**Review profile**: CHILL

**Plan**: Pro

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between cf6414cc74b91bee6f44c74118294e2d34159a91 and 45981fa160e1b86f1281162bb225f3f9a9a14dbf.

</details>

<details>
<summary>📒 Files selected for processing (12)</summary>

* `CHANGELOG.md` (1 hunks)
* `Cargo.toml` (2 hunks)
* `examples/common/mod.rs` (2 hunks)
* `examples/concurrent.rs` (2 hunks)
* `src/client.rs` (3 hunks)
* `src/lib.rs` (7 hunks)
* `src/server.rs` (3 hunks)
* `src/terminal.rs` (1 hunks)
* `src/tests.rs` (5 hunks)
* `src/transport.rs` (2 hunks)
* `tests/integration_tests.rs` (16 hunks)
* `tests/version_tests.rs` (9 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code graph analysis (3)</summary>

<details>
<summary>src/client.rs (2)</summary><blockquote>

<details>
<summary>src/transport.rs (2)</summary>

* `socket_path` (36-40)
* `socket_path` (97-99)

</details>
<details>
<summary>src/terminal.rs (1)</summary>

* `detect` (51-85)

</details>

</blockquote></details>
<details>
<summary>tests/version_tests.rs (1)</summary><blockquote>

<details>
<summary>src/server.rs (1)</summary>

* `new_with_name_and_timestamp` (140-162)

</details>

</blockquote></details>
<details>
<summary>tests/integration_tests.rs (2)</summary><blockquote>

<details>
<summary>src/server.rs (1)</summary>

* `new_with_name_and_timestamp` (140-162)

</details>
<details>
<summary>src/client.rs (1)</summary>

* `connect_with_name_and_timestamp` (78-188)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (29)</summary><blockquote>

<details>
<summary>CHANGELOG.md (1)</summary><blockquote>

`8-9`: **Verify release date is intentional.**

The version header lists `2025-01-15` as the release date, but the PR was created on `2025-11-15`. Confirm whether this is a placeholder that should be updated to today's date or if an earlier release date is intended.

</blockquote></details>
<details>
<summary>src/transport.rs (1)</summary><blockquote>

`1-10`: **SocketMessage::Command payload change is consistent with new TerminalInfo flow**

Switching `Command` to a struct variant with `{ command, terminal_info }` and importing `TerminalInfo` here matches the new client behavior and handler signatures; the framing and (de)serialization path remain unchanged.


Please just confirm that `TerminalInfo` in `src/terminal.rs` derives `Serialize`/`Deserialize` (or otherwise implements them) and that server-side matching is updated to:

```rust
SocketMessage::Command { command, terminal_info }

everywhere, to avoid subtle protocol mismatches.

Also applies to: 174-177

src/client.rs (2)

2-4: Client imports and daemon spawn call remain straightforward

Importing TerminalInfo alongside socket_path, SocketClient, and SocketMessage is consistent with the new protocol usage, and the minor reflow of the spawn_and_wait_for_ready call doesn’t change behavior.

Just ensure all call sites use the same spawn_and_wait_for_ready signature and that no downstream code still expects the old SocketMessage::Command shape.

Also applies to: 112-118


310-319: TerminalInfo detection and propagation before command execution looks correct

Detecting terminal info in execute_command, logging it, and then sending SocketMessage::Command { command, terminal_info } cleanly wires the new metadata into the existing streaming loop without altering output semantics.

Two points to sanity‑check:

  1. Detection costTerminalInfo::detect().await runs per command. The design doc says 10–25 ms typical; if you ever see slower terminals, you could cache TerminalInfo in DaemonClient and refresh it only on demand.
  2. Protocol expectations – Verify the server SocketMessage match arm is now:
SocketMessage::Command { command, terminal_info }

and that TerminalInfo is Serialize + Deserialize and Debug, matching the logging and serialization here.

Also applies to: 321-327

Cargo.toml (1)

3-3: Verification complete: no security advisories found

No security advisories were found for termbg 0.6 or terminal_size 0.4. The supports-color Rust crate (3.0) has no RustSec advisory (the npm package incident is unrelated). The version bump and dependency additions are safe to merge.

Regarding default features: all three crates use defaults (no explicit feature restrictions). This is a valid design choice, though if binary size is a concern, you could selectively disable features for individual crates—but this is optional optimization, not required.

src/lib.rs (2)

109-116: Public re‑exports of terminal types look consistent

Adding mod terminal; plus pub use terminal::{ColorSupport, TerminalInfo, Theme}; and wiring them through prelude keeps the public API coherent and ergonomic for downstream users.

Also applies to: 124-127


258-262: CommandHandler trait extension with terminal_info is sound

The new documentation block for terminal_info clearly explains semantics and failure modes, and the updated signature keeps argument order intuitive (&str then TerminalInfo then output and cancel_token). This matches how the server and tests are invoking it elsewhere.

Also applies to: 268-275

src/tests.rs (2)

17-29: Test handler trait implementation updated correctly

TestHandler’s handle now accepts _terminal_info: TerminalInfo in the correct position and ignores it, which keeps the compilation check meaningful without changing test behavior.


55-65: Good coverage of SocketMessage::Command with TerminalInfo

The serialization test now constructs a non‑trivial TerminalInfo and asserts every field after round‑trip JSON (including color_support and theme), which is a solid regression guard for the transport change.

Also applies to: 69-78

tests/version_tests.rs (3)

17-29: SimpleHandler updated to new CommandHandler signature

The added _terminal_info: TerminalInfo argument matches the library trait and is safely ignored for these version‑handshake tests.


38-45: Server construction formatting‑only changes

The reflowed DaemonServer::new_with_name_and_timestamp calls are purely stylistic and don’t affect behavior; argument order and values remain consistent.

Also applies to: 83-90, 135-141, 185-191, 244-250, 293-299


211-223: Command tests now correctly include terminal metadata

test_version_handshake_before_command and test_command_without_handshake_fails now send SocketMessage::Command { command, terminal_info } with realistic TerminalInfo instances. This keeps the protocol tests aligned with the new transport shape and ensures the server path is exercised with both TTY and non‑TTY configurations.

Also applies to: 261-272

src/server.rs (2)

45-53: Server‑side CommandHandler example kept in sync

The example implementation now includes _terminal_info: TerminalInfo in the right slot, matching the public trait and making it clear to users where terminal context is injected.


290-305: TerminalInfo propagation from transport to handler looks correct

The server now:

  • Deserializes SocketMessage::Command { command, terminal_info },
  • Logs terminal characteristics at debug level, and
  • Invokes handler.handle(&command, terminal_info, output_writer, cancel_token_clone).

This is a clean, single‑ownership flow for TerminalInfo and integrates smoothly with the existing streaming/cancellation logic.

Also applies to: 316-319

src/terminal.rs (1)

5-18: TerminalInfo data model and detection strategy are well‑structured

The TerminalInfo / ColorSupport / Theme types map cleanly onto terminal capabilities, derive the right traits for transport (Serialize/Deserialize, Clone, Eq), and TerminalInfo::detect() composes:

  • TTY and size checks,
  • Color level via supports_color, and
  • Theme via a bounded spawn_blocking call to termbg (gated on is_tty),

with graceful fallbacks when any step fails. This gives handlers a robust, low‑friction snapshot of the client terminal.

Also applies to: 21-38, 40-85

tests/integration_tests.rs (14)

28-34: Server helper uses new connection-limit-aware constructor correctly

start_test_daemon now calls DaemonServer::new_with_name_and_timestamp with the expected (daemon_name, root_path, build_timestamp, handler, 100) parameter order and a generous connection limit for tests. This matches the server constructor signature and keeps existing test behavior intact.


53-59: Custom connection-limit helper is wired correctly

start_test_daemon_with_limit forwards the max_connections argument directly into DaemonServer::new_with_name_and_timestamp, which is exactly what the connection-limit tests rely on. The parameter ordering and types look correct.


81-89: EchoHandler updated to accept TerminalInfo without behavioral changes

Adding _terminal_info: TerminalInfo after command: &str keeps the signature aligned with the updated CommandHandler trait while leaving handler behavior unchanged. Using a leading underscore for the unused parameter is idiomatic.


101-109: ChunkedHandler signature now matches CommandHandler

The inserted _terminal_info: TerminalInfo parameter is in the correct position and type, and the chunked output loop remains unchanged, so the test still exercises streaming behavior as before.


124-143: CancellableHandler updated for TerminalInfo; cancellation semantics preserved

The new _terminal_info: TerminalInfo argument is correctly added, and the cancellation loop logic is untouched. The handler still returns an error on cancellation as expected by the tests.


149-161: ErrorHandler signature updated cleanly for TerminalInfo

The _terminal_info: TerminalInfo parameter is correctly placed between _command and output, matching the trait. Error propagation (Err(anyhow!("Test error"))) is preserved.


169-190: Basic streaming test migrated to new client connect API and stronger exit-code check

DaemonClient::connect_with_name_and_timestamp(&daemon_name, &root_path, daemon_exe, build_timestamp) matches the client constructor’s signature and uses the same build_timestamp as the server, ensuring the version handshake passes. The added assert_eq!(result.unwrap(), 0) makes the test stricter without changing intent.


203-220: Chunked-output test updated to use versioned connect and explicit success code

The connect_with_name_and_timestamp call passes consistent daemon metadata, and the new assert_eq!(result.unwrap(), 0) clearly asserts a successful exit code in addition to is_ok(). This is a solid tightening of the test.


233-245: Error-reporting test uses new connect API consistently with server setup

connect_with_name_and_timestamp receives the same build_timestamp and path used to launch the server, so the version handshake will succeed before exercising the error path. The core assertions on error propagation remain unchanged.


304-312: Connection-close test now uses the versioned connect API appropriately

Creating a single mutable client with connect_with_name_and_timestamp and then moving it into the spawned task preserves the long-running command scenario while respecting the new version handshake semantics.


352-386: ConcurrentTrackingHandler updated to accept TerminalInfo while preserving concurrency tracking

The _terminal_info: TerminalInfo parameter is correctly added to the signature, and the active/max concurrency bookkeeping remains unchanged. This keeps the handler compatible with the new trait without impacting the concurrency assertions.


401-429: Concurrent-clients test uses new connect API per task and tightens exit-code validation

Each spawned task calls connect_with_name_and_timestamp with cloned daemon metadata and the shared build_timestamp, and the nested assertions (result.is_ok(), exit_code.is_ok(), and exit_code.unwrap() == 0) clearly separate task panics, client errors, and non-zero exit codes. This is a good, explicit check.


461-472: Stress test migrated to new connect API with intact success/error accounting

All 15 concurrent clients now connect via connect_with_name_and_timestamp using the same timestamp as the server. The subsequent match on Ok(exit_code) vs. Err(_) still correctly distinguishes successful runs from expected errors in the stress scenario.


529-543: Connection-limit test correctly targets limited server via new client connect API

Each of the 6 clients uses connect_with_name_and_timestamp with consistent daemon metadata, which is crucial for exercising the non-blocking semaphore behavior introduced in the server. The rest of the test logic continues to validate that only up to 3 executions succeed concurrently.

@akesson akesson merged commit 0361a7f into main Nov 15, 2025
1 check passed
@akesson akesson deleted the terminfo branch November 15, 2025 11:21
@akesson akesson restored the terminfo branch November 15, 2025 11:21
@akesson akesson deleted the terminfo branch November 15, 2025 11:21
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