feat: Allow pasting newlines (#866)

Noticed that when pasting multi-line blocks, each newline was treated
like a new submission.
Update tui to handle Paste directly and map newlines to shift+enter.

# Test

Copied this into clipboard:
```
Do nothing.
Explain this repo to me.
```

Pasted in and saw multi-line input. Hitting Enter then submitted the
full block.
This commit is contained in:
jcoens-openai
2025-05-09 11:33:46 -07:00
committed by GitHub
parent 93817643ee
commit 78843c3940
3 changed files with 37 additions and 10 deletions

View File

@@ -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",

View File

@@ -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 <Shift+Enter> 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}");
}
}
});

View File

@@ -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<CrosstermBackend<Stdout>>;
pub fn init() -> io::Result<Tui> {
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(())