diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index 718bee05..230cbd2b 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -21,7 +21,7 @@ codex-ansi-escape = { path = "../ansi-escape" } codex-core = { path = "../core" } codex-common = { path = "../common", features = ["cli", "elapsed"] } color-eyre = "0.6.3" -crossterm = "0.28.1" +crossterm = { version = "0.28.1", features = ["bracketed-paste"] } mcp-types = { path = "../mcp-types" } ratatui = { version = "0.29.0", features = [ "unstable-widget-ref", diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index e85e0b85..3a9c4648 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -47,29 +47,52 @@ impl App<'_> { let app_event_tx = app_event_tx.clone(); std::thread::spawn(move || { while let Ok(event) = crossterm::event::read() { - let app_event = match event { - crossterm::event::Event::Key(key_event) => AppEvent::KeyEvent(key_event), - crossterm::event::Event::Resize(_, _) => AppEvent::Redraw, + match event { + crossterm::event::Event::Key(key_event) => { + if let Err(e) = app_event_tx.send(AppEvent::KeyEvent(key_event)) { + tracing::error!("failed to send key event: {e}"); + } + } + crossterm::event::Event::Resize(_, _) => { + if let Err(e) = app_event_tx.send(AppEvent::Redraw) { + tracing::error!("failed to send resize event: {e}"); + } + } crossterm::event::Event::Mouse(MouseEvent { kind: MouseEventKind::ScrollUp, .. }) => { scroll_event_helper.scroll_up(); - continue; } crossterm::event::Event::Mouse(MouseEvent { kind: MouseEventKind::ScrollDown, .. }) => { scroll_event_helper.scroll_down(); - continue; + } + crossterm::event::Event::Paste(pasted) => { + use crossterm::event::KeyModifiers; + + for ch in pasted.chars() { + let key_event = match ch { + '\n' | '\r' => { + // Represent newline as so that the bottom + // pane treats it as a literal newline instead of a submit + // action (submission is only triggered on Enter *without* + // any modifiers). + KeyEvent::new(KeyCode::Enter, KeyModifiers::SHIFT) + } + _ => KeyEvent::new(KeyCode::Char(ch), KeyModifiers::empty()), + }; + if let Err(e) = app_event_tx.send(AppEvent::KeyEvent(key_event)) { + tracing::error!("failed to send pasted key event: {e}"); + break; + } + } } _ => { - continue; + // Ignore any other events. } - }; - if let Err(e) = app_event_tx.send(app_event) { - tracing::error!("failed to send event: {e}"); } } }); diff --git a/codex-rs/tui/src/tui.rs b/codex-rs/tui/src/tui.rs index 934cf94e..6bbb7e25 100644 --- a/codex-rs/tui/src/tui.rs +++ b/codex-rs/tui/src/tui.rs @@ -2,7 +2,9 @@ use std::io::Stdout; use std::io::stdout; use std::io::{self}; +use crossterm::event::DisableBracketedPaste; use crossterm::event::DisableMouseCapture; +use crossterm::event::EnableBracketedPaste; use crossterm::event::EnableMouseCapture; use ratatui::Terminal; use ratatui::backend::CrosstermBackend; @@ -19,6 +21,7 @@ pub type Tui = Terminal>; pub fn init() -> io::Result { execute!(stdout(), EnterAlternateScreen)?; execute!(stdout(), EnableMouseCapture)?; + execute!(stdout(), EnableBracketedPaste)?; enable_raw_mode()?; set_panic_hook(); Terminal::new(CrosstermBackend::new(stdout())) @@ -35,6 +38,7 @@ fn set_panic_hook() { /// Restore the terminal to its original state pub fn restore() -> io::Result<()> { execute!(stdout(), DisableMouseCapture)?; + execute!(stdout(), DisableBracketedPaste)?; execute!(stdout(), LeaveAlternateScreen)?; disable_raw_mode()?; Ok(())