diff --git a/Cargo.lock b/Cargo.lock index 9d6a921842..c18a3d7907 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,6 +83,7 @@ dependencies = [ "clap_complete", "cli-table", "crossbeam-channel", + "crossterm", "directories", "eyre", "fs-err", @@ -93,7 +94,6 @@ dependencies = [ "pretty_env_logger", "serde", "serde_json", - "termion", "tokio", "tracing-subscriber", "tui", @@ -509,6 +509,31 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crossterm" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2102ea4f781910f8a5b98dd061f4c2023f479ce7bb1236330099ceb5a93cf17" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot 0.12.0", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-common" version = "0.1.3" @@ -1346,12 +1371,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" -[[package]] -name = "numtoa" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" - [[package]] name = "once_cell" version = "1.10.0" @@ -1641,15 +1660,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "redox_termios" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f" -dependencies = [ - "redox_syscall", -] - [[package]] name = "redox_users" version = "0.4.3" @@ -1959,6 +1969,27 @@ dependencies = [ "dirs-next", ] +[[package]] +name = "signal-hook" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -2198,18 +2229,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "termion" -version = "1.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e" -dependencies = [ - "libc", - "numtoa", - "redox_syscall", - "redox_termios", -] - [[package]] name = "textwrap" version = "0.15.0" @@ -2482,13 +2501,13 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "tui" -version = "0.16.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39c8ce4e27049eed97cfa363a5048b09d995e209994634a0efc26a14ab6c0c23" +checksum = "96fe69244ec2af261bced1d9046a6fee6c8c2a6b0228e59e5ba39bc8ba4ed729" dependencies = [ "bitflags", "cassowary", - "termion", + "crossterm", "unicode-segmentation", "unicode-width", ] diff --git a/Cargo.toml b/Cargo.toml index 7bce89e8f8..00d639f298 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,8 +50,8 @@ directories = "4" indicatif = "0.16.2" serde = { version = "1.0.126", features = ["derive"] } serde_json = "1.0.75" -tui = "0.16" -termion = "1.5" +tui = "0.18" +crossterm = "0.23" unicode-width = "0.1" itertools = "0.10.3" tokio = { version = "1", features = ["full"] } diff --git a/src/command/client.rs b/src/command/client.rs index 4858e2ba17..5593a84ddf 100644 --- a/src/command/client.rs +++ b/src/command/client.rs @@ -11,7 +11,6 @@ use atuin_common::utils::uuid_v4; #[cfg(feature = "sync")] mod sync; -mod event; mod history; mod import; mod init; diff --git a/src/command/client/event.rs b/src/command/client/event.rs deleted file mode 100644 index f09752d6c8..0000000000 --- a/src/command/client/event.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::thread; -use std::time::Duration; - -use crossbeam_channel::unbounded; -use termion::event::Key; -use termion::input::TermRead; - -pub enum Event { - Input(I), - Tick, -} - -/// A small event handler that wrap termion input and tick events. Each event -/// type is handled in its own thread and returned to a common `Receiver` -pub struct Events { - rx: crossbeam_channel::Receiver>, -} - -#[derive(Debug, Clone, Copy)] -pub struct Config { - pub exit_key: Key, - pub tick_rate: Duration, -} - -impl Default for Config { - fn default() -> Config { - Config { - exit_key: Key::Char('q'), - tick_rate: Duration::from_millis(250), - } - } -} - -impl Events { - pub fn new() -> Events { - Events::with_config(Config::default()) - } - - pub fn with_config(config: Config) -> Events { - let (tx, rx) = unbounded(); - - { - let tx = tx.clone(); - thread::spawn(move || { - let tty = termion::get_tty().expect("Could not find tty"); - for key in tty.keys().flatten() { - if let Err(err) = tx.send(Event::Input(key)) { - eprintln!("{}", err); - return; - } - } - }) - }; - - thread::spawn(move || loop { - if tx.send(Event::Tick).is_err() { - break; - } - thread::sleep(config.tick_rate); - }); - - Events { rx } - } - - pub fn next(&self) -> Result, crossbeam_channel::RecvError> { - self.rx.recv() - } -} diff --git a/src/command/client/search.rs b/src/command/client/search.rs index 45b1f978bc..f42e06f77a 100644 --- a/src/command/client/search.rs +++ b/src/command/client/search.rs @@ -1,11 +1,14 @@ use chrono::Utc; use clap::Parser; +use crossterm::{ + event::{self, KeyCode, KeyEvent, KeyModifiers}, + execute, terminal, +}; use eyre::Result; -use std::env; -use std::{io::stdout, ops::Sub, time::Duration}; -use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; +use std::{env, io::Write, ops::Sub, time::Duration}; + use tui::{ - backend::{Backend, TermionBackend}, + backend::{Backend, CrosstermBackend}, layout::{Alignment, Constraint, Corner, Direction, Layout}, style::{Color, Modifier, Style}, text::{Span, Spans, Text}, @@ -22,7 +25,6 @@ use atuin_client::{ settings::{FilterMode, SearchMode, Settings}, }; -use super::event::{Event, Events}; use super::history::ListMode; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -275,14 +277,17 @@ async fn query_results( } async fn key_handler( - input: Key, + input: KeyEvent, search_mode: SearchMode, db: &mut (impl Database + Send + Sync), app: &mut State, ) -> Option { - match input { - Key::Esc | Key::Ctrl('c' | 'd' | 'g') => return Some(String::from("")), - Key::Char('\n') => { + match input.code { + KeyCode::Esc => return Some(String::from("")), + KeyCode::Char('c' | 'd' | 'g') if input.modifiers.contains(KeyModifiers::CONTROL) => { + return Some(String::from("")) + } + KeyCode::Enter => { let i = app.results_state.selected().unwrap_or(0); return Some( @@ -291,7 +296,11 @@ async fn key_handler( .map_or(app.input.clone(), |h| h.command.clone()), ); } - Key::Alt(c) if ('1'..='9').contains(&c) => { + KeyCode::Down => app.down(), + KeyCode::Char('n') if input.modifiers.contains(KeyModifiers::CONTROL) => app.down(), + KeyCode::Up => app.up(), + KeyCode::Char('p') if input.modifiers.contains(KeyModifiers::CONTROL) => app.up(), + KeyCode::Char(c @ '1'..='9') if input.modifiers.contains(KeyModifiers::ALT) => { let c = c.to_digit(10)? as usize; let i = app.results_state.selected()? + c; @@ -301,16 +310,12 @@ async fn key_handler( .map_or(app.input.clone(), |h| h.command.clone()), ); } - Key::Char(c) => { - app.input.push(c); - query_results(app, search_mode, db).await.unwrap(); - } - Key::Backspace => { + KeyCode::Backspace => { app.input.pop(); query_results(app, search_mode, db).await.unwrap(); } // \u{7f} is escape sequence for backspace - Key::Alt('\u{7f}') => { + KeyCode::Char('\u{7f}') if input.modifiers.contains(KeyModifiers::ALT) => { let words: Vec<&str> = app.input.split(' ').collect(); if words.is_empty() { return None; @@ -322,11 +327,11 @@ async fn key_handler( } query_results(app, search_mode, db).await.unwrap(); } - Key::Ctrl('u') => { + KeyCode::Char('u') if input.modifiers.contains(KeyModifiers::CONTROL) => { app.input = String::from(""); query_results(app, search_mode, db).await.unwrap(); } - Key::Ctrl('r') => { + KeyCode::Char('r') if input.modifiers.contains(KeyModifiers::CONTROL) => { app.filter_mode = match app.filter_mode { FilterMode::Global => FilterMode::Host, FilterMode::Host => FilterMode::Session, @@ -336,31 +341,9 @@ async fn key_handler( query_results(app, search_mode, db).await.unwrap(); } - Key::Down | Key::Ctrl('n') => { - let i = match app.results_state.selected() { - Some(i) => { - if i == 0 { - 0 - } else { - i - 1 - } - } - None => 0, - }; - app.results_state.select(Some(i)); - } - Key::Up | Key::Ctrl('p') => { - let i = match app.results_state.selected() { - Some(i) => { - if i >= app.results.len() - 1 { - app.results.len() - 1 - } else { - i + 1 - } - } - None => 0, - }; - app.results_state.select(Some(i)); + KeyCode::Char(c) => { + app.input.push(c); + query_results(app, search_mode, db).await.unwrap(); } _ => {} }; @@ -368,6 +351,23 @@ async fn key_handler( None } +impl State { + fn down(&mut self) { + let i = match self.results_state.selected() { + Some(i) => i.saturating_sub(1), + None => 0, + }; + self.results_state.select(Some(i)); + } + fn up(&mut self) { + let i = match self.results_state.selected() { + Some(i) => (i + 1).min(self.results.len() - 1), + None => 0, + }; + self.results_state.select(Some(i)); + } +} + #[allow(clippy::cast_possible_truncation)] fn draw(f: &mut Frame<'_, T>, history_count: i64, app: &mut State) { let chunks = Layout::default() @@ -521,6 +521,41 @@ fn draw_compact(f: &mut Frame<'_, T>, history_count: i64, app: &mut ); } +struct Stdout { + stdout: std::fs::File, +} + +impl Stdout { + pub fn new() -> std::io::Result { + terminal::enable_raw_mode()?; + // let mut stdout = stdout(); + let mut stdout = std::fs::OpenOptions::new() + .read(true) + .write(true) + .open("/dev/tty")?; + execute!(stdout, terminal::EnterAlternateScreen)?; + // execute!(stdout, event::EnableMouseCapture)?; + Ok(Self { stdout }) + } +} + +impl Drop for Stdout { + fn drop(&mut self) { + execute!(self.stdout, terminal::LeaveAlternateScreen).unwrap(); + terminal::disable_raw_mode().unwrap(); + } +} + +impl Write for Stdout { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.stdout.write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.stdout.flush() + } +} + // this is a big blob of horrible! clean it up! // for now, it works. But it'd be great if it were more easily readable, and // modular. I'd like to add some more stats and stuff at some point @@ -532,15 +567,10 @@ async fn select_history( style: atuin_client::settings::Style, db: &mut (impl Database + Send + Sync), ) -> Result { - let stdout = stdout().into_raw_mode()?; - let stdout = MouseTerminal::from(stdout); - let stdout = AlternateScreen::from(stdout); - let backend = TermionBackend::new(stdout); + let stdout = Stdout::new()?; + let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; - // Setup event handlers - let events = Events::new(); - let mut app = State { input: query.join(" "), results: Vec::new(), @@ -554,9 +584,11 @@ async fn select_history( loop { let history_count = db.history_count().await?; // Handle input - if let Event::Input(input) = events.next()? { - if let Some(output) = key_handler(input, search_mode, db, &mut app).await { - return Ok(output); + if event::poll(Duration::from_millis(250))? { + if let event::Event::Key(input) = event::read()? { + if let Some(output) = key_handler(input, search_mode, db, &mut app).await { + return Ok(output); + } } }