feat: make it possible to toggle mouse mode in the Rust TUI (#971)
I did a bit of research to understand why I could not use my mouse to drag to select text to copy to the clipboard in iTerm. Apparently https://github.com/openai/codex/pull/641 to enable mousewheel scrolling broke this functionality. It seems that, unless we put in a bit of effort, we can have drag-to-select or scrolling, but not both. Though if you know the trick to hold down `Option` will dragging with the mouse in iTerm, you can probably get by with this. (I did not know about this option prior to researching this issue.) Nevertheless, users may still prefer to disable mouse capture altogether, so this PR introduces: * the ability to set `tui.disable_mouse_capture = true` in `config.toml` to disable mouse capture * a new command, `/toggle-mouse-mode` to toggle mouse capture
This commit is contained in:
@@ -3,6 +3,7 @@ use crate::app_event_sender::AppEventSender;
|
||||
use crate::chatwidget::ChatWidget;
|
||||
use crate::git_warning_screen::GitWarningOutcome;
|
||||
use crate::git_warning_screen::GitWarningScreen;
|
||||
use crate::mouse_capture::MouseCapture;
|
||||
use crate::scroll_event_helper::ScrollEventHelper;
|
||||
use crate::slash_command::SlashCommand;
|
||||
use crate::tui;
|
||||
@@ -122,7 +123,11 @@ impl App<'_> {
|
||||
self.app_event_tx.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn run(&mut self, terminal: &mut tui::Tui) -> Result<()> {
|
||||
pub(crate) fn run(
|
||||
&mut self,
|
||||
terminal: &mut tui::Tui,
|
||||
mouse_capture: &mut MouseCapture,
|
||||
) -> Result<()> {
|
||||
// Insert an event to trigger the first render.
|
||||
let app_event_tx = self.app_event_tx.clone();
|
||||
app_event_tx.send(AppEvent::Redraw);
|
||||
@@ -176,6 +181,11 @@ impl App<'_> {
|
||||
SlashCommand::Clear => {
|
||||
self.chat_widget.clear_conversation_history();
|
||||
}
|
||||
SlashCommand::ToggleMouseMode => {
|
||||
if let Err(e) = mouse_capture.toggle() {
|
||||
tracing::error!("Failed to toggle mouse mode: {e}");
|
||||
}
|
||||
}
|
||||
SlashCommand::Quit => {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ use crate::slash_command::SlashCommand;
|
||||
use crate::slash_command::built_in_slash_commands;
|
||||
|
||||
const MAX_POPUP_ROWS: usize = 5;
|
||||
/// Ideally this is enough to show the longest command name.
|
||||
const FIRST_COLUMN_WIDTH: u16 = 20;
|
||||
|
||||
use ratatui::style::Modifier;
|
||||
|
||||
@@ -176,15 +178,18 @@ impl WidgetRef for CommandPopup {
|
||||
|
||||
use ratatui::layout::Constraint;
|
||||
|
||||
let table = Table::new(rows, [Constraint::Length(15), Constraint::Min(10)])
|
||||
.style(style)
|
||||
.column_spacing(1)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.style(style),
|
||||
);
|
||||
let table = Table::new(
|
||||
rows,
|
||||
[Constraint::Length(FIRST_COLUMN_WIDTH), Constraint::Min(10)],
|
||||
)
|
||||
.style(style)
|
||||
.column_spacing(1)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.style(style),
|
||||
);
|
||||
|
||||
table.render(area, buf);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ mod git_warning_screen;
|
||||
mod history_cell;
|
||||
mod log_layer;
|
||||
mod markdown;
|
||||
mod mouse_capture;
|
||||
mod scroll_event_helper;
|
||||
mod slash_command;
|
||||
mod status_indicator_widget;
|
||||
@@ -152,7 +153,7 @@ fn run_ratatui_app(
|
||||
std::panic::set_hook(Box::new(|info| {
|
||||
tracing::error!("panic: {info}");
|
||||
}));
|
||||
let mut terminal = tui::init()?;
|
||||
let (mut terminal, mut mouse_capture) = tui::init(&config)?;
|
||||
terminal.clear()?;
|
||||
|
||||
let Cli { prompt, images, .. } = cli;
|
||||
@@ -168,7 +169,7 @@ fn run_ratatui_app(
|
||||
});
|
||||
}
|
||||
|
||||
let app_result = app.run(&mut terminal);
|
||||
let app_result = app.run(&mut terminal, &mut mouse_capture);
|
||||
|
||||
restore();
|
||||
app_result
|
||||
|
||||
69
codex-rs/tui/src/mouse_capture.rs
Normal file
69
codex-rs/tui/src/mouse_capture.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use crossterm::event::DisableMouseCapture;
|
||||
use crossterm::event::EnableMouseCapture;
|
||||
use ratatui::crossterm::execute;
|
||||
use std::io::Result;
|
||||
use std::io::stdout;
|
||||
|
||||
pub(crate) struct MouseCapture {
|
||||
mouse_capture_is_active: bool,
|
||||
}
|
||||
|
||||
impl MouseCapture {
|
||||
pub(crate) fn new_with_capture(mouse_capture_is_active: bool) -> Result<Self> {
|
||||
if mouse_capture_is_active {
|
||||
enable_capture()?;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
mouse_capture_is_active,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl MouseCapture {
|
||||
/// Idempotent method to set the mouse capture state.
|
||||
pub fn set_active(&mut self, is_active: bool) -> Result<()> {
|
||||
match (self.mouse_capture_is_active, is_active) {
|
||||
(true, true) => {}
|
||||
(false, false) => {}
|
||||
(true, false) => {
|
||||
disable_capture()?;
|
||||
self.mouse_capture_is_active = false;
|
||||
}
|
||||
(false, true) => {
|
||||
enable_capture()?;
|
||||
self.mouse_capture_is_active = true;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn toggle(&mut self) -> Result<()> {
|
||||
self.set_active(!self.mouse_capture_is_active)
|
||||
}
|
||||
|
||||
pub(crate) fn disable(&mut self) -> Result<()> {
|
||||
if self.mouse_capture_is_active {
|
||||
disable_capture()?;
|
||||
self.mouse_capture_is_active = false;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MouseCapture {
|
||||
fn drop(&mut self) {
|
||||
if self.disable().is_err() {
|
||||
// The user is likely shutting down, so ignore any errors so the
|
||||
// shutdown process can complete.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn enable_capture() -> Result<()> {
|
||||
execute!(stdout(), EnableMouseCapture)
|
||||
}
|
||||
|
||||
fn disable_capture() -> Result<()> {
|
||||
execute!(stdout(), DisableMouseCapture)
|
||||
}
|
||||
@@ -13,6 +13,7 @@ use strum_macros::IntoStaticStr;
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
pub enum SlashCommand {
|
||||
Clear,
|
||||
ToggleMouseMode,
|
||||
Quit,
|
||||
}
|
||||
|
||||
@@ -21,6 +22,9 @@ impl SlashCommand {
|
||||
pub fn description(self) -> &'static str {
|
||||
match self {
|
||||
SlashCommand::Clear => "Clear the chat history.",
|
||||
SlashCommand::ToggleMouseMode => {
|
||||
"Toggle mouse mode (enable for scrolling, disable for text selection)"
|
||||
}
|
||||
SlashCommand::Quit => "Exit the application.",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::io::Result;
|
||||
use std::io::Stdout;
|
||||
use std::io::stdout;
|
||||
use std::io::{self};
|
||||
|
||||
use codex_core::config::Config;
|
||||
use crossterm::event::DisableBracketedPaste;
|
||||
use crossterm::event::DisableMouseCapture;
|
||||
use crossterm::event::EnableBracketedPaste;
|
||||
use crossterm::event::EnableMouseCapture;
|
||||
use ratatui::Terminal;
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
use ratatui::crossterm::execute;
|
||||
@@ -14,17 +14,21 @@ use ratatui::crossterm::terminal::LeaveAlternateScreen;
|
||||
use ratatui::crossterm::terminal::disable_raw_mode;
|
||||
use ratatui::crossterm::terminal::enable_raw_mode;
|
||||
|
||||
use crate::mouse_capture::MouseCapture;
|
||||
|
||||
/// A type alias for the terminal type used in this application
|
||||
pub type Tui = Terminal<CrosstermBackend<Stdout>>;
|
||||
|
||||
/// Initialize the terminal
|
||||
pub fn init() -> io::Result<Tui> {
|
||||
pub fn init(config: &Config) -> Result<(Tui, MouseCapture)> {
|
||||
execute!(stdout(), EnterAlternateScreen)?;
|
||||
execute!(stdout(), EnableMouseCapture)?;
|
||||
execute!(stdout(), EnableBracketedPaste)?;
|
||||
let mouse_capture = MouseCapture::new_with_capture(!config.tui.disable_mouse_capture)?;
|
||||
|
||||
enable_raw_mode()?;
|
||||
set_panic_hook();
|
||||
Terminal::new(CrosstermBackend::new(stdout()))
|
||||
let tui = Terminal::new(CrosstermBackend::new(stdout()))?;
|
||||
Ok((tui, mouse_capture))
|
||||
}
|
||||
|
||||
fn set_panic_hook() {
|
||||
@@ -36,8 +40,13 @@ fn set_panic_hook() {
|
||||
}
|
||||
|
||||
/// Restore the terminal to its original state
|
||||
pub fn restore() -> io::Result<()> {
|
||||
execute!(stdout(), DisableMouseCapture)?;
|
||||
pub fn restore() -> Result<()> {
|
||||
// We are shutting down, and we cannot reference the `MouseCapture`, so we
|
||||
// categorically disable mouse capture just to be safe.
|
||||
if execute!(stdout(), DisableMouseCapture).is_err() {
|
||||
// It is possible that `DisableMouseCapture` is written more than once
|
||||
// on shutdown, so ignore the error in this case.
|
||||
}
|
||||
execute!(stdout(), DisableBracketedPaste)?;
|
||||
execute!(stdout(), LeaveAlternateScreen)?;
|
||||
disable_raw_mode()?;
|
||||
|
||||
Reference in New Issue
Block a user