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