diff --git a/README.md b/README.md index fee42bb..58cc62a 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,8 @@ Press `Ctrl+6` (or your configured key) to enter lookback mode: When you exit lookback mode, any cached output is processed and the current state is displayed. +**Auto-lookback**: After 5 seconds of idle (no new renders), the full history is automatically displayed so you can scroll back without pressing any keys. + ## Configuration Create `~/.config/claude-chill.toml`: diff --git a/crates/claude-chill/src/bin.rs b/crates/claude-chill/src/bin.rs index 02b82e4..8a6c405 100644 --- a/crates/claude-chill/src/bin.rs +++ b/crates/claude-chill/src/bin.rs @@ -42,10 +42,15 @@ fn main() -> ExitCode { } }; + let auto_lookback_timeout_ms = cli + .auto_lookback_timeout + .unwrap_or(config.auto_lookback_timeout_ms); + let proxy_config = ProxyConfig { max_history_lines: history_lines, lookback_key, lookback_sequence, + auto_lookback_timeout_ms, }; let cmd_args: Vec<&str> = cli.args.iter().map(|s| s.as_str()).collect(); diff --git a/crates/claude-chill/src/cli.rs b/crates/claude-chill/src/cli.rs index 9615ece..bbacde5 100644 --- a/crates/claude-chill/src/cli.rs +++ b/crates/claude-chill/src/cli.rs @@ -13,4 +13,7 @@ pub struct Cli { #[arg(short = 'k', long = "lookback-key")] pub lookback_key: Option, + + #[arg(long = "auto-lookback-timeout")] + pub auto_lookback_timeout: Option, } diff --git a/crates/claude-chill/src/config.rs b/crates/claude-chill/src/config.rs index 1955b0b..ca0605b 100644 --- a/crates/claude-chill/src/config.rs +++ b/crates/claude-chill/src/config.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; const DEFAULT_LOOKBACK_KEY: &str = "[ctrl][6]"; const DEFAULT_REFRESH_RATE: u64 = 20; +const DEFAULT_AUTO_LOOKBACK_TIMEOUT_MS: u64 = 5000; #[derive(Debug, Clone, Deserialize)] #[serde(default)] @@ -12,6 +13,7 @@ pub struct Config { pub history_lines: usize, pub lookback_key: String, pub refresh_rate: u64, + pub auto_lookback_timeout_ms: u64, } impl Default for Config { @@ -20,6 +22,7 @@ impl Default for Config { history_lines: 100_000, lookback_key: DEFAULT_LOOKBACK_KEY.to_string(), refresh_rate: DEFAULT_REFRESH_RATE, + auto_lookback_timeout_ms: DEFAULT_AUTO_LOOKBACK_TIMEOUT_MS, } } } @@ -97,6 +100,7 @@ mod tests { assert_eq!(config.lookback_key, "[ctrl][6]"); assert_eq!(config.refresh_rate, 20); assert_eq!(config.redraw_throttle_ms(), 50); + assert_eq!(config.auto_lookback_timeout_ms, 5000); } #[test] diff --git a/crates/claude-chill/src/proxy.rs b/crates/claude-chill/src/proxy.rs index 8cecc92..7e405f7 100644 --- a/crates/claude-chill/src/proxy.rs +++ b/crates/claude-chill/src/proxy.rs @@ -48,6 +48,7 @@ pub struct ProxyConfig { pub max_history_lines: usize, pub lookback_key: String, pub lookback_sequence: Vec, + pub auto_lookback_timeout_ms: u64, } impl Default for ProxyConfig { @@ -56,6 +57,7 @@ impl Default for ProxyConfig { max_history_lines: 100_000, lookback_key: "[ctrl][6]".to_string(), lookback_sequence: vec![0x1E], + auto_lookback_timeout_ms: 5000, } } } @@ -95,6 +97,8 @@ pub struct Proxy { vt_parser: vt100::Parser, vt_prev_screen: Option, last_output_time: Option, + last_render_time: Option, + auto_lookback_timeout: Duration, sync_buffer: Vec, in_sync_block: bool, in_lookback_mode: bool, @@ -161,6 +165,8 @@ impl Proxy { history.push_bytes(CLEAR_SCREEN); history.push_bytes(CURSOR_HOME); + let auto_lookback_timeout = Duration::from_millis(config.auto_lookback_timeout_ms); + debug!("Proxy::spawn: command={} args={:?}", command, args); Ok(Self { @@ -172,6 +178,8 @@ impl Proxy { vt_parser, vt_prev_screen: None, last_output_time: None, + last_render_time: None, + auto_lookback_timeout, sync_buffer: Vec::with_capacity(SYNC_BUFFER_CAPACITY), in_sync_block: false, in_lookback_mode: false, @@ -224,6 +232,7 @@ impl Proxy { match poll(&mut poll_fds, PollTimeout::from(poll_timeout_ms)) { Ok(0) => { self.flush_pending_vt_render(&stdout_fd)?; + self.check_auto_lookback(&stdout_fd)?; continue; } Ok(_) => {} @@ -551,6 +560,43 @@ impl Proxy { // Store current screen for next diff self.vt_prev_screen = Some(self.vt_parser.screen().clone()); self.vt_render_pending = false; + self.last_render_time = Some(Instant::now()); + Ok(()) + } + + fn check_auto_lookback(&mut self, stdout_fd: &F) -> Result<()> { + if self.auto_lookback_timeout.is_zero() { + return Ok(()); + } + if self.in_lookback_mode || self.in_alternate_screen { + return Ok(()); + } + let Some(render_time) = self.last_render_time else { + return Ok(()); + }; + if render_time.elapsed() < self.auto_lookback_timeout { + return Ok(()); + } + self.dump_history(stdout_fd)?; + self.last_render_time = None; + Ok(()) + } + + fn dump_history(&mut self, stdout_fd: &F) -> Result<()> { + debug!( + "dump_history: history_bytes={} lines={}", + self.history.total_bytes(), + self.history.line_count() + ); + self.output_buffer.clear(); + self.history.append_all(&mut self.output_buffer); + + write_all(stdout_fd, CLEAR_SCREEN)?; + write_all(stdout_fd, CURSOR_HOME)?; + write_all(stdout_fd, &self.output_buffer)?; + + // Force full VT render on next output since terminal now shows history + self.vt_prev_screen = None; Ok(()) }