From 3d4c647216a007dd6adf91447a6b8f95c2c0eb1b Mon Sep 17 00:00:00 2001 From: "firat.sertgoz" Date: Sat, 21 Feb 2026 08:46:15 +0400 Subject: [PATCH] fix: map Esc to interrupt and Ctrl+C to graceful quit (#267) * fix: map Esc to interrupt and Ctrl+C to graceful quit * Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Co-authored-by: Illia Polosukhin Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- Cargo.toml | 2 +- src/channels/repl.rs | 45 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3bd8e947..80f07f51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,7 +82,7 @@ clap = { version = "4", features = ["derive", "env"] } # Terminal crossterm = "0.28" -rustyline = { version = "17", features = ["derive", "with-file-history"] } +rustyline = { version = "17", features = ["custom-bindings", "derive", "with-file-history"] } termimad = "0.34" # Channel integrations diff --git a/src/channels/repl.rs b/src/channels/repl.rs index 812eaa52..15500a40 100644 --- a/src/channels/repl.rs +++ b/src/channels/repl.rs @@ -15,6 +15,7 @@ //! - `/compact` - Compact the context //! - `/new` - Start a new thread //! - `yes`/`no`/`always` - Respond to tool approval prompts +//! - `Esc` - Interrupt current operation use std::borrow::Cow; use std::io::{self, Write}; @@ -28,7 +29,10 @@ use rustyline::error::ReadlineError; use rustyline::highlight::Highlighter; use rustyline::hint::Hinter; use rustyline::validate::Validator; -use rustyline::{CompletionType, Editor, Helper}; +use rustyline::{ + Cmd as ReadlineCmd, CompletionType, ConditionalEventHandler, Editor, Event, EventContext, + EventHandler, Helper, KeyCode, KeyEvent, Modifiers, RepeatCount, +}; use termimad::MadSkin; use tokio::sync::mpsc; use tokio_stream::wrappers::ReceiverStream; @@ -121,6 +125,23 @@ impl Highlighter for ReplHelper { impl Validator for ReplHelper {} impl Helper for ReplHelper {} +struct EscInterruptHandler { + triggered: Arc, +} + +impl ConditionalEventHandler for EscInterruptHandler { + fn handle( + &self, + _evt: &Event, + _n: RepeatCount, + _positive: bool, + _ctx: &EventContext, + ) -> Option { + self.triggered.store(true, Ordering::Relaxed); + Some(ReadlineCmd::Interrupt) + } +} + /// Build a termimad skin with our color scheme. fn make_skin() -> MadSkin { let mut skin = MadSkin::default(); @@ -247,6 +268,7 @@ fn print_help() { println!(" {c}/compact{r} {d}compact context window{r}"); println!(" {c}/new{r} {d}new conversation thread{r}"); println!(" {c}/interrupt{r} {d}stop current operation{r}"); + println!(" {c}esc{r} {d}stop current operation{r}"); println!(); println!(" {h}Approval responses{r}"); println!(" {c}yes{r} ({c}y{r}) {d}approve tool execution{r}"); @@ -274,6 +296,7 @@ impl Channel for ReplChannel { let single_message = self.single_message.clone(); let debug_mode = Arc::clone(&self.debug_mode); let suppress_banner = Arc::clone(&self.suppress_banner); + let esc_interrupt_triggered_for_thread = Arc::new(AtomicBool::new(false)); std::thread::spawn(move || { // Single message mode: send it and return @@ -301,6 +324,13 @@ impl Channel for ReplChannel { rl.set_helper(Some(ReplHelper)); + rl.bind_sequence( + KeyEvent(KeyCode::Esc, Modifiers::NONE), + EventHandler::Conditional(Box::new(EscInterruptHandler { + triggered: Arc::clone(&esc_interrupt_triggered_for_thread), + })), + ); + // Load history let hist_path = history_path(); if let Some(parent) = hist_path.parent() { @@ -360,9 +390,16 @@ impl Channel for ReplChannel { } } Err(ReadlineError::Interrupted) => { - // Ctrl+C: send /interrupt - let msg = IncomingMessage::new("repl", "default", "/interrupt"); - if tx.blocking_send(msg).is_err() { + if esc_interrupt_triggered_for_thread.swap(false, Ordering::Relaxed) { + // Esc: interrupt current operation and keep REPL open. + let msg = IncomingMessage::new("repl", "default", "/interrupt"); + if tx.blocking_send(msg).is_err() { + break; + } + } else { + // Ctrl+C (VINTR): request graceful shutdown. + let msg = IncomingMessage::new("repl", "default", "/quit"); + let _ = tx.blocking_send(msg); break; } }