mirror of
https://github.com/openai/codex.git
synced 2026-09-05 15:18:41 +00:00
Defer startup terminal modes until activation
This commit is contained in:
@@ -4,14 +4,11 @@ use std::io::stdin;
|
||||
use std::io::stdout;
|
||||
use std::time::Duration;
|
||||
|
||||
use crossterm::event::DisableBracketedPaste;
|
||||
use crossterm::event::EnableBracketedPaste;
|
||||
use crossterm::event::Event;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyEventKind;
|
||||
use crossterm::event::KeyModifiers;
|
||||
use crossterm::execute;
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
use ratatui::layout::Position;
|
||||
|
||||
@@ -30,11 +27,10 @@ pub(crate) struct PreparedTerminal {
|
||||
impl Drop for PreparedTerminal {
|
||||
fn drop(&mut self) {
|
||||
if self.active {
|
||||
discard_terminal_input();
|
||||
if self.terminal_modes_active {
|
||||
let _ = super::restore_after_exit();
|
||||
} else {
|
||||
let _ = disable_startup_paste_capture();
|
||||
discard_terminal_input();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +40,7 @@ impl Drop for PreparedTerminal {
|
||||
pub(super) struct StartupInputBuffer {
|
||||
text: String,
|
||||
char_count: usize,
|
||||
pending_plain_whitespace: String,
|
||||
}
|
||||
|
||||
impl StartupInputBuffer {
|
||||
@@ -54,11 +51,19 @@ impl StartupInputBuffer {
|
||||
modifiers,
|
||||
kind: KeyEventKind::Press | KeyEventKind::Repeat,
|
||||
..
|
||||
}) if modifiers.difference(KeyModifiers::SHIFT).is_empty() => match code {
|
||||
KeyCode::Char(ch) if !ch.is_control() => self.push_char(ch),
|
||||
KeyCode::Backspace => self.pop_char(),
|
||||
_ => {}
|
||||
},
|
||||
}) => {
|
||||
if modifiers.difference(KeyModifiers::SHIFT).is_empty() {
|
||||
match code {
|
||||
KeyCode::Char(ch) if !ch.is_control() => self.push_plain_char(ch),
|
||||
KeyCode::Backspace => self.pop_char(),
|
||||
KeyCode::Enter => self.push_pending_plain_whitespace('\n'),
|
||||
KeyCode::Tab => self.push_pending_plain_whitespace('\t'),
|
||||
_ => self.pending_plain_whitespace.clear(),
|
||||
}
|
||||
} else {
|
||||
self.pending_plain_whitespace.clear();
|
||||
}
|
||||
}
|
||||
Event::Paste(text) => self.push_text(&text),
|
||||
_ => {}
|
||||
}
|
||||
@@ -71,13 +76,37 @@ impl StartupInputBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
fn push_plain_char(&mut self, ch: char) {
|
||||
self.commit_pending_plain_whitespace();
|
||||
self.push_char(ch);
|
||||
}
|
||||
|
||||
fn push_pending_plain_whitespace(&mut self, ch: char) {
|
||||
if self.char_count + self.pending_plain_whitespace.len() < MAX_STARTUP_INPUT_CHARS {
|
||||
self.pending_plain_whitespace.push(ch);
|
||||
}
|
||||
}
|
||||
|
||||
fn commit_pending_plain_whitespace(&mut self) {
|
||||
let pending = std::mem::take(&mut self.pending_plain_whitespace);
|
||||
for ch in pending.chars() {
|
||||
self.push_char(ch);
|
||||
}
|
||||
}
|
||||
|
||||
fn pop_char(&mut self) {
|
||||
if self.pending_plain_whitespace.pop().is_some() {
|
||||
return;
|
||||
}
|
||||
if self.text.pop().is_some() {
|
||||
self.char_count -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn push_text(&mut self, text: &str) {
|
||||
if !text.is_empty() {
|
||||
self.commit_pending_plain_whitespace();
|
||||
}
|
||||
let mut chars = text.chars().peekable();
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
@@ -95,10 +124,20 @@ impl StartupInputBuffer {
|
||||
}
|
||||
|
||||
pub(super) fn handle_probe_input(&mut self, input: &[u8]) {
|
||||
for ch in String::from_utf8_lossy(input).chars() {
|
||||
let input = String::from_utf8_lossy(input);
|
||||
let mut chars = input.chars().peekable();
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
'\u{8}' | '\u{7f}' => self.pop_char(),
|
||||
ch if !ch.is_control() => self.push_char(ch),
|
||||
'\r' => {
|
||||
if chars.peek() == Some(&'\n') {
|
||||
chars.next();
|
||||
}
|
||||
self.push_pending_plain_whitespace('\n');
|
||||
}
|
||||
'\n' => self.push_pending_plain_whitespace('\n'),
|
||||
'\t' => self.push_pending_plain_whitespace('\t'),
|
||||
ch if !ch.is_control() => self.push_plain_char(ch),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -152,11 +191,6 @@ pub(crate) fn discard_terminal_input() {
|
||||
|
||||
pub(crate) fn abandon_prepared_terminal() {
|
||||
discard_terminal_input();
|
||||
let _ = disable_startup_paste_capture();
|
||||
}
|
||||
|
||||
fn disable_startup_paste_capture() -> Result<()> {
|
||||
execute!(stdout(), DisableBracketedPaste)
|
||||
}
|
||||
|
||||
pub(super) fn capture_startup_input(input: &mut StartupInputBuffer) -> Result<()> {
|
||||
@@ -167,7 +201,7 @@ pub(super) fn capture_startup_input(input: &mut StartupInputBuffer) -> Result<()
|
||||
}
|
||||
|
||||
impl PreparedTerminal {
|
||||
/// Start preserving terminal paste boundaries before slower startup work begins.
|
||||
/// Claim queued terminal input before slower startup work begins.
|
||||
pub(crate) fn prepare() -> Result<Self> {
|
||||
if !stdin().is_terminal() {
|
||||
return Err(std::io::Error::other("stdin is not a terminal"));
|
||||
@@ -177,7 +211,6 @@ impl PreparedTerminal {
|
||||
}
|
||||
super::ensure_virtual_terminal_processing()?;
|
||||
super::set_panic_hook();
|
||||
execute!(stdout(), EnableBracketedPaste)?;
|
||||
Ok(Self {
|
||||
active: true,
|
||||
terminal_modes_active: false,
|
||||
|
||||
@@ -15,10 +15,10 @@ fn startup_input_keeps_text_without_replaying_actions() {
|
||||
Event::Key(KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE)),
|
||||
Event::Key(KeyEvent::new(KeyCode::Char('I'), KeyModifiers::SHIFT)),
|
||||
Event::Key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)),
|
||||
Event::Key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)),
|
||||
Event::Key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE)),
|
||||
Event::Key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE)),
|
||||
Event::Paste("ello\r\n\tworld".to_string()),
|
||||
Event::Key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)),
|
||||
Event::Key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE)),
|
||||
Event::Key(KeyEvent::new_with_kind(
|
||||
KeyCode::Char('!'),
|
||||
KeyModifiers::NONE,
|
||||
@@ -31,6 +31,36 @@ fn startup_input_keeps_text_without_replaying_actions() {
|
||||
assert_eq!(input.into_text(), Some("hello\n\tworld".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_input_preserves_internal_plain_whitespace_but_drops_trailing_actions() {
|
||||
let mut input = StartupInputBuffer::default();
|
||||
for code in [
|
||||
KeyCode::Char('a'),
|
||||
KeyCode::Enter,
|
||||
KeyCode::Char('b'),
|
||||
KeyCode::Tab,
|
||||
KeyCode::Char('c'),
|
||||
KeyCode::Enter,
|
||||
] {
|
||||
input.handle_event(Event::Key(KeyEvent::new(code, KeyModifiers::NONE)));
|
||||
}
|
||||
|
||||
assert_eq!(input.into_text(), Some("a\nb\tc".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_probe_input_preserves_internal_plain_whitespace_across_phases() {
|
||||
let mut input = StartupInputBuffer::default();
|
||||
input.handle_probe_input(b"a\r\n");
|
||||
input.handle_event(Event::Key(KeyEvent::new(
|
||||
KeyCode::Char('b'),
|
||||
KeyModifiers::NONE,
|
||||
)));
|
||||
input.handle_probe_input(b"\t");
|
||||
|
||||
assert_eq!(input.into_text(), Some("a\nb".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_input_is_bounded() {
|
||||
let mut input = StartupInputBuffer::default();
|
||||
|
||||
Reference in New Issue
Block a user