feat: add auto lookback mode (#7)

* feat: add auto lookback mode

Automatically shows full terminal history after idle timeout,
returns to truncated view when new output arrives.

- Add auto_lookback_timeout_ms config option (default: 1000ms)
- Add --auto-lookback-timeout CLI flag
- Set to 0 to disable the feature

* intermediate

* auto lookback

---------

Co-authored-by: Ilya Sinkin <ilyasinkin2002@yandex.kz>
This commit is contained in:
David Beesley
2026-01-19 18:33:24 +13:00
committed by GitHub
parent a824eaea77
commit e0c04ae8dd
5 changed files with 60 additions and 0 deletions

View File

@@ -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. 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 ## Configuration
Create `~/.config/claude-chill.toml`: Create `~/.config/claude-chill.toml`:

View File

@@ -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 { let proxy_config = ProxyConfig {
max_history_lines: history_lines, max_history_lines: history_lines,
lookback_key, lookback_key,
lookback_sequence, lookback_sequence,
auto_lookback_timeout_ms,
}; };
let cmd_args: Vec<&str> = cli.args.iter().map(|s| s.as_str()).collect(); let cmd_args: Vec<&str> = cli.args.iter().map(|s| s.as_str()).collect();

View File

@@ -13,4 +13,7 @@ pub struct Cli {
#[arg(short = 'k', long = "lookback-key")] #[arg(short = 'k', long = "lookback-key")]
pub lookback_key: Option<String>, pub lookback_key: Option<String>,
#[arg(long = "auto-lookback-timeout")]
pub auto_lookback_timeout: Option<u64>,
} }

View File

@@ -5,6 +5,7 @@ use std::path::PathBuf;
const DEFAULT_LOOKBACK_KEY: &str = "[ctrl][6]"; const DEFAULT_LOOKBACK_KEY: &str = "[ctrl][6]";
const DEFAULT_REFRESH_RATE: u64 = 20; const DEFAULT_REFRESH_RATE: u64 = 20;
const DEFAULT_AUTO_LOOKBACK_TIMEOUT_MS: u64 = 5000;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
#[serde(default)] #[serde(default)]
@@ -12,6 +13,7 @@ pub struct Config {
pub history_lines: usize, pub history_lines: usize,
pub lookback_key: String, pub lookback_key: String,
pub refresh_rate: u64, pub refresh_rate: u64,
pub auto_lookback_timeout_ms: u64,
} }
impl Default for Config { impl Default for Config {
@@ -20,6 +22,7 @@ impl Default for Config {
history_lines: 100_000, history_lines: 100_000,
lookback_key: DEFAULT_LOOKBACK_KEY.to_string(), lookback_key: DEFAULT_LOOKBACK_KEY.to_string(),
refresh_rate: DEFAULT_REFRESH_RATE, 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.lookback_key, "[ctrl][6]");
assert_eq!(config.refresh_rate, 20); assert_eq!(config.refresh_rate, 20);
assert_eq!(config.redraw_throttle_ms(), 50); assert_eq!(config.redraw_throttle_ms(), 50);
assert_eq!(config.auto_lookback_timeout_ms, 5000);
} }
#[test] #[test]

View File

@@ -48,6 +48,7 @@ pub struct ProxyConfig {
pub max_history_lines: usize, pub max_history_lines: usize,
pub lookback_key: String, pub lookback_key: String,
pub lookback_sequence: Vec<u8>, pub lookback_sequence: Vec<u8>,
pub auto_lookback_timeout_ms: u64,
} }
impl Default for ProxyConfig { impl Default for ProxyConfig {
@@ -56,6 +57,7 @@ impl Default for ProxyConfig {
max_history_lines: 100_000, max_history_lines: 100_000,
lookback_key: "[ctrl][6]".to_string(), lookback_key: "[ctrl][6]".to_string(),
lookback_sequence: vec![0x1E], lookback_sequence: vec![0x1E],
auto_lookback_timeout_ms: 5000,
} }
} }
} }
@@ -95,6 +97,8 @@ pub struct Proxy {
vt_parser: vt100::Parser, vt_parser: vt100::Parser,
vt_prev_screen: Option<vt100::Screen>, vt_prev_screen: Option<vt100::Screen>,
last_output_time: Option<Instant>, last_output_time: Option<Instant>,
last_render_time: Option<Instant>,
auto_lookback_timeout: Duration,
sync_buffer: Vec<u8>, sync_buffer: Vec<u8>,
in_sync_block: bool, in_sync_block: bool,
in_lookback_mode: bool, in_lookback_mode: bool,
@@ -161,6 +165,8 @@ impl Proxy {
history.push_bytes(CLEAR_SCREEN); history.push_bytes(CLEAR_SCREEN);
history.push_bytes(CURSOR_HOME); history.push_bytes(CURSOR_HOME);
let auto_lookback_timeout = Duration::from_millis(config.auto_lookback_timeout_ms);
debug!("Proxy::spawn: command={} args={:?}", command, args); debug!("Proxy::spawn: command={} args={:?}", command, args);
Ok(Self { Ok(Self {
@@ -172,6 +178,8 @@ impl Proxy {
vt_parser, vt_parser,
vt_prev_screen: None, vt_prev_screen: None,
last_output_time: None, last_output_time: None,
last_render_time: None,
auto_lookback_timeout,
sync_buffer: Vec::with_capacity(SYNC_BUFFER_CAPACITY), sync_buffer: Vec::with_capacity(SYNC_BUFFER_CAPACITY),
in_sync_block: false, in_sync_block: false,
in_lookback_mode: false, in_lookback_mode: false,
@@ -224,6 +232,7 @@ impl Proxy {
match poll(&mut poll_fds, PollTimeout::from(poll_timeout_ms)) { match poll(&mut poll_fds, PollTimeout::from(poll_timeout_ms)) {
Ok(0) => { Ok(0) => {
self.flush_pending_vt_render(&stdout_fd)?; self.flush_pending_vt_render(&stdout_fd)?;
self.check_auto_lookback(&stdout_fd)?;
continue; continue;
} }
Ok(_) => {} Ok(_) => {}
@@ -551,6 +560,43 @@ impl Proxy {
// Store current screen for next diff // Store current screen for next diff
self.vt_prev_screen = Some(self.vt_parser.screen().clone()); self.vt_prev_screen = Some(self.vt_parser.screen().clone());
self.vt_render_pending = false; self.vt_render_pending = false;
self.last_render_time = Some(Instant::now());
Ok(())
}
fn check_auto_lookback<F: AsFd>(&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<F: AsFd>(&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(()) Ok(())
} }