Fix/key sequences (#2)
* Change default hotkey to Ctrl+6 and fix ctrl+punctuation handling * Disable lookback mode when alternate screen is active
This commit is contained in:
@@ -52,7 +52,7 @@ claude-chill -l 50 -- claude # Set max lines to 50
|
||||
|
||||
## Lookback Mode
|
||||
|
||||
Press `Ctrl+Shift+J` (or your configured key) to enter lookback mode:
|
||||
Press `Ctrl+6` (or your configured key) to enter lookback mode:
|
||||
|
||||
1. **Claude pauses** - Output from Claude is cached, input is blocked
|
||||
2. **History dumps** - The full history buffer is written to your terminal
|
||||
@@ -68,7 +68,7 @@ Create `~/.config/claude-chill.toml`:
|
||||
```toml
|
||||
max_lines = 100 # Lines shown per sync block
|
||||
history_lines = 100000 # Lines stored for lookback
|
||||
lookback_key = "[ctrl][shift][j]"
|
||||
lookback_key = "[ctrl][6]"
|
||||
```
|
||||
|
||||
### Key Format
|
||||
@@ -79,6 +79,10 @@ Modifiers: `[ctrl]`, `[shift]`, `[alt]`
|
||||
|
||||
Keys: `[a]`-`[z]`, `[f1]`-`[f12]`, `[pageup]`, `[pagedown]`, `[home]`, `[end]`, `[enter]`, `[tab]`, `[space]`, `[esc]`
|
||||
|
||||
### Why Ctrl+6?
|
||||
|
||||
`Ctrl+6` sends 0x1E, a control character not frequently used by terminals, signals, or shells. Avoid `Ctrl+letter` hotkeys - terminals can't distinguish `Ctrl+J` from `Ctrl+Shift+J`.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This tool was developed for personal convenience on Debian Linux. It works for me, but it hasn't been extensively tested across different terminals, operating systems, or edge cases. Don't use it to send anyone to space, perform surgery, or run critical infrastructure. If it breaks, you get to keep both pieces.
|
||||
|
||||
@@ -3,7 +3,7 @@ use serde::Deserialize;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const DEFAULT_LOOKBACK_KEY: &str = "[ctrl][shift][j]";
|
||||
const DEFAULT_LOOKBACK_KEY: &str = "[ctrl][6]";
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(default)]
|
||||
@@ -89,12 +89,12 @@ mod tests {
|
||||
let config = Config::default();
|
||||
assert_eq!(config.max_lines, 100);
|
||||
assert_eq!(config.history_lines, 100_000);
|
||||
assert_eq!(config.lookback_key, "[ctrl][shift][j]");
|
||||
assert_eq!(config.lookback_key, "[ctrl][6]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_lookback_sequence() {
|
||||
let config = Config::default();
|
||||
assert_eq!(config.lookback_sequence(), vec![0x0A]);
|
||||
assert_eq!(config.lookback_sequence(), vec![0x1E]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,11 @@ pub const CLEAR_SCREEN: &[u8] = b"\x1b[2J";
|
||||
pub const CLEAR_SCROLLBACK: &[u8] = b"\x1b[3J";
|
||||
pub const CURSOR_HOME: &[u8] = b"\x1b[H";
|
||||
|
||||
pub const ALT_SCREEN_ENTER: &[u8] = b"\x1b[?1049h";
|
||||
pub const ALT_SCREEN_EXIT: &[u8] = b"\x1b[?1049l";
|
||||
pub const ALT_SCREEN_ENTER_LEGACY: &[u8] = b"\x1b[?47h";
|
||||
pub const ALT_SCREEN_EXIT_LEGACY: &[u8] = b"\x1b[?47l";
|
||||
|
||||
pub const SYNC_BUFFER_CAPACITY: usize = 1024 * 1024;
|
||||
pub const OUTPUT_BUFFER_CAPACITY: usize = 32768;
|
||||
pub const INPUT_BUFFER_CAPACITY: usize = 64;
|
||||
|
||||
@@ -241,23 +241,43 @@ fn key_to_escape_sequence(code: &KeyCode, modifiers: &Modifiers) -> Vec<u8> {
|
||||
b" ".to_vec()
|
||||
}
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
if modifiers.ctrl && c.is_ascii_alphabetic() {
|
||||
let ctrl_char = (c.to_ascii_uppercase() as u8) - b'A' + 1;
|
||||
if modifiers.alt {
|
||||
vec![0x1b, ctrl_char]
|
||||
} else {
|
||||
vec![ctrl_char]
|
||||
}
|
||||
} else if modifiers.alt {
|
||||
vec![0x1b, *c as u8]
|
||||
} else if modifiers.shift {
|
||||
vec![c.to_ascii_uppercase() as u8]
|
||||
KeyCode::Char(c) => char_to_escape_sequence(*c, modifiers),
|
||||
}
|
||||
}
|
||||
|
||||
fn char_to_escape_sequence(c: char, modifiers: &Modifiers) -> Vec<u8> {
|
||||
if modifiers.ctrl {
|
||||
let ctrl_byte = match c {
|
||||
'a'..='z' => Some((c.to_ascii_uppercase() as u8) - b'A' + 1),
|
||||
'A'..='Z' => Some((c as u8) - b'A' + 1),
|
||||
'@' => Some(0x00),
|
||||
'[' => Some(0x1B),
|
||||
'\\' => Some(0x1C),
|
||||
']' => Some(0x1D),
|
||||
'^' | '6' => Some(0x1E),
|
||||
'_' | '7' => Some(0x1F),
|
||||
'2' => Some(0x00),
|
||||
'3' => Some(0x1B),
|
||||
'4' => Some(0x1C),
|
||||
'5' => Some(0x1D),
|
||||
'8' => Some(0x7F),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(byte) = ctrl_byte {
|
||||
return if modifiers.alt {
|
||||
vec![0x1b, byte]
|
||||
} else {
|
||||
vec![*c as u8]
|
||||
}
|
||||
vec![byte]
|
||||
};
|
||||
}
|
||||
}
|
||||
if modifiers.alt {
|
||||
vec![0x1b, c as u8]
|
||||
} else if modifiers.shift {
|
||||
vec![c.to_ascii_uppercase() as u8]
|
||||
} else {
|
||||
vec![c as u8]
|
||||
}
|
||||
}
|
||||
|
||||
fn modified_key(base: &[u8], modifier: u8) -> Vec<u8> {
|
||||
@@ -383,4 +403,28 @@ mod tests {
|
||||
let result = parse("[ctrl][shift]");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ctrl_caret() {
|
||||
let key = parse("[ctrl][^]").unwrap();
|
||||
assert_eq!(key.to_escape_sequence(), vec![0x1E]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ctrl_6_same_as_ctrl_caret() {
|
||||
let key = parse("[ctrl][6]").unwrap();
|
||||
assert_eq!(key.to_escape_sequence(), vec![0x1E]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ctrl_bracket() {
|
||||
let key = parse("[ctrl][[]").unwrap();
|
||||
assert_eq!(key.to_escape_sequence(), vec![0x1B]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ctrl_backslash() {
|
||||
let key = parse("[ctrl][\\]").unwrap();
|
||||
assert_eq!(key.to_escape_sequence(), vec![0x1C]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::escape_sequences::{
|
||||
ALT_SCREEN_ENTER, ALT_SCREEN_ENTER_LEGACY, ALT_SCREEN_EXIT, ALT_SCREEN_EXIT_LEGACY,
|
||||
CLEAR_SCREEN, CLEAR_SCROLLBACK, CURSOR_HOME, INPUT_BUFFER_CAPACITY, OUTPUT_BUFFER_CAPACITY,
|
||||
SYNC_BUFFER_CAPACITY, SYNC_END, SYNC_START,
|
||||
};
|
||||
@@ -43,8 +44,8 @@ impl Default for ProxyConfig {
|
||||
Self {
|
||||
max_output_lines: 100,
|
||||
max_history_lines: 100_000,
|
||||
lookback_key: "[ctrl][shift][j]".to_string(),
|
||||
lookback_sequence: vec![0x0A],
|
||||
lookback_key: "[ctrl][6]".to_string(),
|
||||
lookback_sequence: vec![0x1E],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,6 +59,7 @@ pub struct Proxy {
|
||||
sync_buffer: Vec<u8>,
|
||||
in_sync_block: bool,
|
||||
in_lookback_mode: bool,
|
||||
in_alternate_screen: bool,
|
||||
lookback_cache: Vec<u8>,
|
||||
input_buffer: Vec<u8>,
|
||||
output_buffer: Vec<u8>,
|
||||
@@ -65,6 +67,10 @@ pub struct Proxy {
|
||||
sync_end_finder: memmem::Finder<'static>,
|
||||
clear_screen_finder: memmem::Finder<'static>,
|
||||
cursor_home_finder: memmem::Finder<'static>,
|
||||
alt_screen_enter_finder: memmem::Finder<'static>,
|
||||
alt_screen_exit_finder: memmem::Finder<'static>,
|
||||
alt_screen_enter_legacy_finder: memmem::Finder<'static>,
|
||||
alt_screen_exit_legacy_finder: memmem::Finder<'static>,
|
||||
}
|
||||
|
||||
impl Proxy {
|
||||
@@ -117,6 +123,7 @@ impl Proxy {
|
||||
sync_buffer: Vec::with_capacity(SYNC_BUFFER_CAPACITY),
|
||||
in_sync_block: false,
|
||||
in_lookback_mode: false,
|
||||
in_alternate_screen: false,
|
||||
lookback_cache: Vec::new(),
|
||||
input_buffer: Vec::with_capacity(INPUT_BUFFER_CAPACITY),
|
||||
output_buffer: Vec::with_capacity(OUTPUT_BUFFER_CAPACITY),
|
||||
@@ -124,6 +131,10 @@ impl Proxy {
|
||||
sync_end_finder: memmem::Finder::new(SYNC_END),
|
||||
clear_screen_finder: memmem::Finder::new(CLEAR_SCREEN),
|
||||
cursor_home_finder: memmem::Finder::new(CURSOR_HOME),
|
||||
alt_screen_enter_finder: memmem::Finder::new(ALT_SCREEN_ENTER),
|
||||
alt_screen_exit_finder: memmem::Finder::new(ALT_SCREEN_EXIT),
|
||||
alt_screen_enter_legacy_finder: memmem::Finder::new(ALT_SCREEN_ENTER_LEGACY),
|
||||
alt_screen_exit_legacy_finder: memmem::Finder::new(ALT_SCREEN_EXIT_LEGACY),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -201,6 +212,10 @@ impl Proxy {
|
||||
}
|
||||
|
||||
fn process_output(&mut self, data: &[u8], stdout_fd: i32) -> Result<()> {
|
||||
if self.in_alternate_screen {
|
||||
return self.process_output_alt_screen(data, stdout_fd);
|
||||
}
|
||||
|
||||
if self.in_lookback_mode {
|
||||
self.lookback_cache.extend_from_slice(data);
|
||||
return Ok(());
|
||||
@@ -209,6 +224,22 @@ impl Proxy {
|
||||
let mut pos = 0;
|
||||
|
||||
while pos < data.len() {
|
||||
if let Some(alt_pos) = self.find_alt_screen_enter(&data[pos..]) {
|
||||
if self.in_sync_block {
|
||||
self.sync_buffer
|
||||
.extend_from_slice(&data[pos..pos + alt_pos]);
|
||||
write_all_raw(stdout_fd, &self.sync_buffer)?;
|
||||
self.sync_buffer.clear();
|
||||
self.in_sync_block = false;
|
||||
} else if alt_pos > 0 {
|
||||
write_all_raw(stdout_fd, &data[pos..pos + alt_pos])?;
|
||||
}
|
||||
self.in_alternate_screen = true;
|
||||
let seq_len = self.alt_screen_enter_len(&data[pos + alt_pos..]);
|
||||
write_all_raw(stdout_fd, &data[pos + alt_pos..pos + alt_pos + seq_len])?;
|
||||
return self.process_output_alt_screen(&data[pos + alt_pos + seq_len..], stdout_fd);
|
||||
}
|
||||
|
||||
if self.in_sync_block {
|
||||
if let Some(idx) = self.sync_end_finder.find(&data[pos..]) {
|
||||
self.sync_buffer.extend_from_slice(&data[pos..pos + idx]);
|
||||
@@ -237,6 +268,55 @@ impl Proxy {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_output_alt_screen(&mut self, data: &[u8], stdout_fd: i32) -> Result<()> {
|
||||
if let Some(exit_pos) = self.find_alt_screen_exit(data) {
|
||||
write_all_raw(stdout_fd, &data[..exit_pos])?;
|
||||
let seq_len = self.alt_screen_exit_len(&data[exit_pos..]);
|
||||
write_all_raw(stdout_fd, &data[exit_pos..exit_pos + seq_len])?;
|
||||
self.in_alternate_screen = false;
|
||||
return self.process_output(&data[exit_pos + seq_len..], stdout_fd);
|
||||
}
|
||||
write_all_raw(stdout_fd, data)
|
||||
}
|
||||
|
||||
fn find_alt_screen_enter(&self, data: &[u8]) -> Option<usize> {
|
||||
let pos1 = self.alt_screen_enter_finder.find(data);
|
||||
let pos2 = self.alt_screen_enter_legacy_finder.find(data);
|
||||
match (pos1, pos2) {
|
||||
(Some(a), Some(b)) => Some(a.min(b)),
|
||||
(Some(a), None) => Some(a),
|
||||
(None, Some(b)) => Some(b),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn find_alt_screen_exit(&self, data: &[u8]) -> Option<usize> {
|
||||
let pos1 = self.alt_screen_exit_finder.find(data);
|
||||
let pos2 = self.alt_screen_exit_legacy_finder.find(data);
|
||||
match (pos1, pos2) {
|
||||
(Some(a), Some(b)) => Some(a.min(b)),
|
||||
(Some(a), None) => Some(a),
|
||||
(None, Some(b)) => Some(b),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn alt_screen_enter_len(&self, data: &[u8]) -> usize {
|
||||
if data.starts_with(ALT_SCREEN_ENTER) {
|
||||
ALT_SCREEN_ENTER.len()
|
||||
} else {
|
||||
ALT_SCREEN_ENTER_LEGACY.len()
|
||||
}
|
||||
}
|
||||
|
||||
fn alt_screen_exit_len(&self, data: &[u8]) -> usize {
|
||||
if data.starts_with(ALT_SCREEN_EXIT) {
|
||||
ALT_SCREEN_EXIT.len()
|
||||
} else {
|
||||
ALT_SCREEN_EXIT_LEGACY.len()
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_sync_block(&mut self, stdout_fd: i32) -> Result<()> {
|
||||
let has_clear_screen = self.clear_screen_finder.find(&self.sync_buffer).is_some();
|
||||
let has_cursor_home = self.cursor_home_finder.find(&self.sync_buffer).is_some();
|
||||
@@ -271,6 +351,10 @@ impl Proxy {
|
||||
fn process_input(&mut self, data: &[u8], stdout_fd: i32) -> Result<()> {
|
||||
let master_fd = self.pty_master.as_raw_fd();
|
||||
|
||||
if self.in_alternate_screen {
|
||||
return write_all_raw(master_fd, data);
|
||||
}
|
||||
|
||||
for &byte in data {
|
||||
if self.in_lookback_mode && byte == 0x03 {
|
||||
self.input_buffer.clear();
|
||||
|
||||
Reference in New Issue
Block a user