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:
David Beesley
2026-01-18 06:17:14 +13:00
committed by GitHub
parent 76b65add1d
commit 91e0516fe9
5 changed files with 158 additions and 21 deletions

View File

@@ -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]);
}
}

View File

@@ -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;

View File

@@ -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]);
}
}

View File

@@ -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();