mirror of
https://github.com/openai/codex.git
synced 2026-02-01 22:47:52 +00:00
Compare commits
2 Commits
capture-sh
...
jif/fix-cr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
683ccaceb8 | ||
|
|
5ba25217f0 |
@@ -1,3 +1,4 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt;
|
||||
use std::io::IsTerminal;
|
||||
use std::io::Result;
|
||||
@@ -41,10 +42,13 @@ use crate::custom_terminal::Terminal as CustomTerminal;
|
||||
use crate::tui::job_control::SUSPEND_KEY;
|
||||
#[cfg(unix)]
|
||||
use crate::tui::job_control::SuspendContext;
|
||||
use focus_sequence::FocusSequenceBuffer;
|
||||
|
||||
#[cfg(unix)]
|
||||
mod job_control;
|
||||
|
||||
mod focus_sequence;
|
||||
|
||||
/// A type alias for the terminal type used in this application
|
||||
pub type Terminal = CustomTerminal<CrosstermBackend<Stdout>>;
|
||||
|
||||
@@ -147,7 +151,7 @@ fn set_panic_hook() {
|
||||
}));
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum TuiEvent {
|
||||
Key(KeyEvent),
|
||||
Paste(String),
|
||||
@@ -245,6 +249,8 @@ impl Tui {
|
||||
pub fn event_stream(&self) -> Pin<Box<dyn Stream<Item = TuiEvent> + Send + 'static>> {
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
const FOCUS_SEQUENCE_TIMEOUT: Duration = Duration::from_millis(30);
|
||||
|
||||
let mut crossterm_events = crossterm::event::EventStream::new();
|
||||
let mut draw_rx = self.draw_tx.subscribe();
|
||||
|
||||
@@ -255,9 +261,24 @@ impl Tui {
|
||||
let alt_screen_active = self.alt_screen_active.clone();
|
||||
|
||||
let terminal_focused = self.terminal_focused.clone();
|
||||
let mut focus_buffer = FocusSequenceBuffer::new(FOCUS_SEQUENCE_TIMEOUT);
|
||||
let mut buffered_events: VecDeque<TuiEvent> = VecDeque::new();
|
||||
let event_stream = async_stream::stream! {
|
||||
loop {
|
||||
if let Some(buffered) = buffered_events.pop_front() {
|
||||
yield buffered;
|
||||
continue;
|
||||
}
|
||||
|
||||
let deadline = focus_buffer.deadline();
|
||||
select! {
|
||||
_ = async {
|
||||
if let Some(wait_until) = deadline {
|
||||
tokio::time::sleep_until(wait_until).await;
|
||||
}
|
||||
}, if deadline.is_some() => {
|
||||
focus_buffer.flush_as_keys(&mut buffered_events);
|
||||
}
|
||||
Some(Ok(event)) = crossterm_events.next() => {
|
||||
match event {
|
||||
Event::Key(key_event) => {
|
||||
@@ -268,20 +289,31 @@ impl Tui {
|
||||
yield TuiEvent::Draw;
|
||||
continue;
|
||||
}
|
||||
yield TuiEvent::Key(key_event);
|
||||
if focus_buffer.handle_key_event(
|
||||
key_event,
|
||||
&mut buffered_events,
|
||||
&terminal_focused,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
buffered_events.push_back(TuiEvent::Key(key_event));
|
||||
}
|
||||
Event::Resize(_, _) => {
|
||||
yield TuiEvent::Draw;
|
||||
focus_buffer.flush_as_keys(&mut buffered_events);
|
||||
buffered_events.push_back(TuiEvent::Draw);
|
||||
}
|
||||
Event::Paste(pasted) => {
|
||||
yield TuiEvent::Paste(pasted);
|
||||
focus_buffer.flush_as_keys(&mut buffered_events);
|
||||
buffered_events.push_back(TuiEvent::Paste(pasted));
|
||||
}
|
||||
Event::FocusGained => {
|
||||
terminal_focused.store(true, Ordering::Relaxed);
|
||||
crate::terminal_palette::requery_default_colors();
|
||||
yield TuiEvent::Draw;
|
||||
focus_buffer.flush_as_keys(&mut buffered_events);
|
||||
buffered_events.push_back(TuiEvent::Draw);
|
||||
}
|
||||
Event::FocusLost => {
|
||||
focus_buffer.flush_as_keys(&mut buffered_events);
|
||||
terminal_focused.store(false, Ordering::Relaxed);
|
||||
}
|
||||
_ => {}
|
||||
@@ -290,11 +322,13 @@ impl Tui {
|
||||
result = draw_rx.recv() => {
|
||||
match result {
|
||||
Ok(_) => {
|
||||
yield TuiEvent::Draw;
|
||||
focus_buffer.flush_as_keys(&mut buffered_events);
|
||||
buffered_events.push_back(TuiEvent::Draw);
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
|
||||
// We dropped one or more draw notifications; coalesce to a single draw.
|
||||
yield TuiEvent::Draw;
|
||||
focus_buffer.flush_as_keys(&mut buffered_events);
|
||||
buffered_events.push_back(TuiEvent::Draw);
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
|
||||
// Sender dropped; stop emitting draws from this source.
|
||||
|
||||
200
codex-rs/tui/src/tui/focus_sequence.rs
Normal file
200
codex-rs/tui/src/tui/focus_sequence.rs
Normal file
@@ -0,0 +1,200 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyEventKind;
|
||||
use tokio::time::Instant as TokioInstant;
|
||||
|
||||
use super::TuiEvent;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub(super) enum FocusSequenceState {
|
||||
#[default]
|
||||
None,
|
||||
Esc(KeyEvent),
|
||||
EscBracket(KeyEvent, KeyEvent),
|
||||
}
|
||||
|
||||
/// Coalesces split focus change escape sequences so they cannot masquerade as key input.
|
||||
#[derive(Debug)]
|
||||
pub(super) struct FocusSequenceBuffer {
|
||||
state: FocusSequenceState,
|
||||
deadline: Option<TokioInstant>,
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
impl FocusSequenceBuffer {
|
||||
pub(super) fn new(timeout: Duration) -> Self {
|
||||
Self {
|
||||
state: FocusSequenceState::None,
|
||||
deadline: None,
|
||||
timeout,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn deadline(&self) -> Option<TokioInstant> {
|
||||
self.deadline
|
||||
}
|
||||
|
||||
pub(super) fn handle_key_event(
|
||||
&mut self,
|
||||
key_event: KeyEvent,
|
||||
queue: &mut VecDeque<TuiEvent>,
|
||||
terminal_focused: &Arc<AtomicBool>,
|
||||
) -> bool {
|
||||
match &self.state {
|
||||
FocusSequenceState::None => {
|
||||
if Self::is_plain_esc(&key_event) {
|
||||
self.state = FocusSequenceState::Esc(key_event);
|
||||
self.start_deadline();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
FocusSequenceState::Esc(esc) => {
|
||||
if Self::is_left_bracket(&key_event) {
|
||||
self.state = FocusSequenceState::EscBracket(*esc, key_event);
|
||||
self.start_deadline();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
FocusSequenceState::EscBracket(_, _) => {
|
||||
if Self::is_focus_tail(&key_event) {
|
||||
self.apply_focus_event(key_event, queue, terminal_focused);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !matches!(self.state, FocusSequenceState::None) {
|
||||
self.flush_as_keys(queue);
|
||||
if Self::is_plain_esc(&key_event) {
|
||||
self.state = FocusSequenceState::Esc(key_event);
|
||||
self.start_deadline();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub(super) fn flush_as_keys(&mut self, queue: &mut VecDeque<TuiEvent>) {
|
||||
match std::mem::take(&mut self.state) {
|
||||
FocusSequenceState::Esc(esc) => queue.push_back(TuiEvent::Key(esc)),
|
||||
FocusSequenceState::EscBracket(esc, bracket) => {
|
||||
queue.push_back(TuiEvent::Key(esc));
|
||||
queue.push_back(TuiEvent::Key(bracket));
|
||||
}
|
||||
FocusSequenceState::None => {}
|
||||
}
|
||||
self.deadline = None;
|
||||
}
|
||||
|
||||
fn start_deadline(&mut self) {
|
||||
self.deadline = Some(TokioInstant::now() + self.timeout);
|
||||
}
|
||||
|
||||
fn apply_focus_event(
|
||||
&mut self,
|
||||
key_event: KeyEvent,
|
||||
queue: &mut VecDeque<TuiEvent>,
|
||||
terminal_focused: &Arc<AtomicBool>,
|
||||
) {
|
||||
let focus_gained = matches!(key_event.code, KeyCode::Char('I'));
|
||||
terminal_focused.store(focus_gained, Ordering::Relaxed);
|
||||
if focus_gained {
|
||||
crate::terminal_palette::requery_default_colors();
|
||||
queue.push_back(TuiEvent::Draw);
|
||||
}
|
||||
self.state = FocusSequenceState::None;
|
||||
self.deadline = None;
|
||||
}
|
||||
|
||||
fn is_plain_esc(key_event: &KeyEvent) -> bool {
|
||||
key_event.code == KeyCode::Esc
|
||||
&& key_event.modifiers.is_empty()
|
||||
&& matches!(key_event.kind, KeyEventKind::Press | KeyEventKind::Repeat)
|
||||
}
|
||||
|
||||
fn is_left_bracket(key_event: &KeyEvent) -> bool {
|
||||
Self::is_char(key_event, '[')
|
||||
}
|
||||
|
||||
fn is_focus_tail(key_event: &KeyEvent) -> bool {
|
||||
Self::is_char(key_event, 'I') || Self::is_char(key_event, 'O')
|
||||
}
|
||||
|
||||
fn is_char(key_event: &KeyEvent, expected: char) -> bool {
|
||||
matches!(key_event.code, KeyCode::Char(c) if c == expected)
|
||||
&& key_event.modifiers.is_empty()
|
||||
&& matches!(key_event.kind, KeyEventKind::Press | KeyEventKind::Repeat)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::FocusSequenceBuffer;
|
||||
use super::FocusSequenceState;
|
||||
use super::TuiEvent;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
fn key(code: KeyCode) -> KeyEvent {
|
||||
KeyEvent::new(code, KeyModifiers::NONE)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn focus_in_sequence_coalesces_to_draw() {
|
||||
let mut buffer = FocusSequenceBuffer::new(Duration::from_millis(30));
|
||||
let mut queue = VecDeque::new();
|
||||
let focused = Arc::new(AtomicBool::new(false));
|
||||
|
||||
assert!(buffer.handle_key_event(key(KeyCode::Esc), &mut queue, &focused));
|
||||
assert!(buffer.handle_key_event(key(KeyCode::Char('[')), &mut queue, &focused));
|
||||
assert!(buffer.handle_key_event(key(KeyCode::Char('I')), &mut queue, &focused));
|
||||
|
||||
assert_eq!(focused.load(Ordering::Relaxed), true);
|
||||
assert_eq!(queue.pop_front(), Some(TuiEvent::Draw));
|
||||
assert!(queue.is_empty());
|
||||
assert!(matches!(buffer.state, FocusSequenceState::None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn focus_out_sequence_is_absorbed_without_leaking_keys() {
|
||||
let mut buffer = FocusSequenceBuffer::new(Duration::from_millis(30));
|
||||
let mut queue = VecDeque::new();
|
||||
let focused = Arc::new(AtomicBool::new(true));
|
||||
|
||||
assert!(buffer.handle_key_event(key(KeyCode::Esc), &mut queue, &focused));
|
||||
assert!(buffer.handle_key_event(key(KeyCode::Char('[')), &mut queue, &focused));
|
||||
assert!(buffer.handle_key_event(key(KeyCode::Char('O')), &mut queue, &focused));
|
||||
|
||||
assert_eq!(focused.load(Ordering::Relaxed), false);
|
||||
assert!(queue.is_empty());
|
||||
assert!(matches!(buffer.state, FocusSequenceState::None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mismatched_sequence_flushes_pending_escape() {
|
||||
let mut buffer = FocusSequenceBuffer::new(Duration::from_millis(30));
|
||||
let mut queue = VecDeque::new();
|
||||
let focused = Arc::new(AtomicBool::new(false));
|
||||
|
||||
assert!(buffer.handle_key_event(key(KeyCode::Esc), &mut queue, &focused));
|
||||
assert!(!buffer.handle_key_event(key(KeyCode::Char('X')), &mut queue, &focused));
|
||||
|
||||
assert_eq!(queue.pop_front(), Some(TuiEvent::Key(key(KeyCode::Esc))));
|
||||
assert!(queue.is_empty());
|
||||
assert!(matches!(buffer.state, FocusSequenceState::None));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user