diff options
Diffstat (limited to '')
| -rw-r--r-- | src/command/event.rs | 8 | ||||
| -rw-r--r-- | src/command/history.rs | 112 | ||||
| -rw-r--r-- | src/command/import.rs | 12 | ||||
| -rw-r--r-- | src/command/mod.rs | 42 | ||||
| -rw-r--r-- | src/command/search.rs | 252 | ||||
| -rw-r--r-- | src/command/stats.rs | 10 | ||||
| -rw-r--r-- | src/command/sync.rs | 8 |
7 files changed, 299 insertions, 145 deletions
diff --git a/src/command/event.rs b/src/command/event.rs index b205be70..f09752d6 100644 --- a/src/command/event.rs +++ b/src/command/event.rs @@ -1,7 +1,7 @@ -use std::sync::mpsc; use std::thread; use std::time::Duration; +use crossbeam_channel::unbounded; use termion::event::Key; use termion::input::TermRead; @@ -13,7 +13,7 @@ pub enum Event<I> { /// 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: mpsc::Receiver<Event<Key>>, + rx: crossbeam_channel::Receiver<Event<Key>>, } #[derive(Debug, Clone, Copy)] @@ -37,7 +37,7 @@ impl Events { } pub fn with_config(config: Config) -> Events { - let (tx, rx) = mpsc::channel(); + let (tx, rx) = unbounded(); { let tx = tx.clone(); @@ -62,7 +62,7 @@ impl Events { Events { rx } } - pub fn next(&self) -> Result<Event<Key>, mpsc::RecvError> { + pub fn next(&self) -> Result<Event<Key>, crossbeam_channel::RecvError> { self.rx.recv() } } diff --git a/src/command/history.rs b/src/command/history.rs index a88aeae2..7542496c 100644 --- a/src/command/history.rs +++ b/src/command/history.rs @@ -1,7 +1,10 @@ use std::env; +use std::io::Write; +use std::time::Duration; use eyre::Result; use structopt::StructOpt; +use tabwriter::TabWriter; use atuin_client::database::Database; use atuin_client::history::History; @@ -36,29 +39,65 @@ pub enum Cmd { #[structopt(long, short)] session: bool, - }, - #[structopt( - about="search for a command", - aliases=&["se", "sea", "sear", "searc"], - )] - Search { query: Vec<String> }, + #[structopt(long, short)] + human: bool, + }, #[structopt( about="get the last command ran", aliases=&["la", "las"], )] - Last {}, + Last { + #[structopt(long, short)] + human: bool, + }, } -fn print_list(h: &[History]) { - for i in h { - println!("{}", i.command); +#[allow(clippy::clippy::cast_sign_loss)] +pub fn print_list(h: &[History], human: bool) { + let mut writer = TabWriter::new(std::io::stdout()).padding(2); + + let lines = h.iter().map(|h| { + if human { + let duration = humantime::format_duration(Duration::from_nanos(std::cmp::max( + h.duration, 0, + ) as u64)) + .to_string(); + let duration: Vec<&str> = duration.split(' ').collect(); + let duration = duration[0]; + + format!( + "{}\t{}\t{}\n", + h.timestamp.format("%Y-%m-%d %H:%M:%S"), + h.command.trim(), + duration, + ) + } else { + format!( + "{}\t{}\t{}\n", + h.timestamp.timestamp_nanos(), + h.command.trim(), + h.duration + ) + } + }); + + for i in lines.rev() { + writer + .write_all(i.as_bytes()) + .expect("failed to write to tab writer"); } + + writer.flush().expect("failed to flush tab writer"); } impl Cmd { - pub async fn run(&self, settings: &Settings, db: &mut (impl Database + Send)) -> Result<()> { + pub async fn run( + &self, + settings: &Settings, + db: &mut (impl Database + Send + Sync), + ) -> Result<()> { match self { Self::Start { command: words } => { let command = words.join(" "); @@ -69,7 +108,7 @@ impl Cmd { // print the ID // we use this as the key for calling end println!("{}", h.id); - db.save(&h)?; + db.save(&h).await?; Ok(()) } @@ -78,7 +117,7 @@ impl Cmd { return Ok(()); } - let mut h = db.load(id)?; + let mut h = db.load(id).await?; if h.duration > 0 { debug!("cannot end history - already has duration"); @@ -90,7 +129,7 @@ impl Cmd { h.exit = *exit; h.duration = chrono::Utc::now().timestamp_nanos() - h.timestamp.timestamp_nanos(); - db.update(&h)?; + db.update(&h).await?; if settings.should_sync()? { debug!("running periodic background sync"); @@ -102,41 +141,38 @@ impl Cmd { Ok(()) } - Self::List { session, cwd, .. } => { - const QUERY_SESSION: &str = "select * from history where session = ?;"; - const QUERY_DIR: &str = "select * from history where cwd = ?;"; - const QUERY_SESSION_DIR: &str = - "select * from history where cwd = ?1 and session = ?2;"; - + Self::List { + session, + cwd, + human, + } => { let params = (session, cwd); - let cwd = env::current_dir()?.display().to_string(); let session = env::var("ATUIN_SESSION")?; - let history = match params { - (false, false) => db.list(None, false)?, - (true, false) => db.query(QUERY_SESSION, &[session.as_str()])?, - (false, true) => db.query(QUERY_DIR, &[cwd.as_str()])?, - (true, true) => { - db.query(QUERY_SESSION_DIR, &[cwd.as_str(), session.as_str()])? - } - }; + let query_session = format!("select * from history where session = {};", session); - print_list(&history); + let query_dir = format!("select * from history where cwd = {};", cwd); + let query_session_dir = format!( + "select * from history where cwd = {} and session = {};", + cwd, session + ); - Ok(()) - } + let history = match params { + (false, false) => db.list(None, false).await?, + (true, false) => db.query_history(query_session.as_str()).await?, + (false, true) => db.query_history(query_dir.as_str()).await?, + (true, true) => db.query_history(query_session_dir.as_str()).await?, + }; - Self::Search { query } => { - let history = db.prefix_search(&query.join(""))?; - print_list(&history); + print_list(&history, *human); Ok(()) } - Self::Last {} => { - let last = db.last()?; - print_list(&[last]); + Self::Last { human } => { + let last = db.last().await?; + print_list(&[last], *human); Ok(()) } diff --git a/src/command/import.rs b/src/command/import.rs index 56fb30a7..931e7af4 100644 --- a/src/command/import.rs +++ b/src/command/import.rs @@ -26,7 +26,7 @@ pub enum Cmd { } impl Cmd { - pub fn run(&self, db: &mut impl Database) -> Result<()> { + pub async fn run(&self, db: &mut (impl Database + Send + Sync)) -> Result<()> { println!(" A'Tuin "); println!("======================"); println!(" \u{1f30d} "); @@ -41,19 +41,19 @@ impl Cmd { if shell.ends_with("/zsh") { println!("Detected ZSH"); - import_zsh(db) + import_zsh(db).await } else { println!("cannot import {} history", shell); Ok(()) } } - Self::Zsh => import_zsh(db), + Self::Zsh => import_zsh(db).await, } } } -fn import_zsh(db: &mut impl Database) -> Result<()> { +async fn import_zsh(db: &mut (impl Database + Send + Sync)) -> Result<()> { // oh-my-zsh sets HISTFILE=~/.zhistory // zsh has no default value for this var, but uses ~/.zhistory. // we could maybe be smarter about this in the future :) @@ -103,7 +103,7 @@ fn import_zsh(db: &mut impl Database) -> Result<()> { buf.push(i); if buf.len() == buf_size { - db.save_bulk(&buf)?; + db.save_bulk(&buf).await?; progress.inc(buf.len() as u64); buf.clear(); @@ -111,7 +111,7 @@ fn import_zsh(db: &mut impl Database) -> Result<()> { } if !buf.is_empty() { - db.save_bulk(&buf)?; + db.save_bulk(&buf).await?; progress.inc(buf.len() as u64); } diff --git a/src/command/mod.rs b/src/command/mod.rs index 805ad9f0..78e6402e 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -47,12 +47,27 @@ pub enum AtuinCmd { #[structopt(long, short, about = "filter search result by directory")] cwd: Option<String>, + #[structopt(long = "exclude-cwd", about = "exclude directory from results")] + exclude_cwd: Option<String>, + #[structopt(long, short, about = "filter search result by exit code")] exit: Option<i64>, + #[structopt(long = "exclude-exit", about = "exclude results with this exit code")] + exclude_exit: Option<i64>, + + #[structopt(long, short, about = "only include results added before this date")] + before: Option<String>, + + #[structopt(long, about = "only include results after this date")] + after: Option<String>, + #[structopt(long, short, about = "open interactive search UI")] interactive: bool, + #[structopt(long, short, about = "use human-readable formatting for time")] + human: bool, + query: Vec<String>, }, @@ -79,20 +94,39 @@ impl AtuinCmd { let db_path = PathBuf::from(client_settings.db_path.as_str()); - let mut db = Sqlite::new(db_path)?; + let mut db = Sqlite::new(db_path).await?; match self { Self::History(history) => history.run(&client_settings, &mut db).await, - Self::Import(import) => import.run(&mut db), + Self::Import(import) => import.run(&mut db).await, Self::Server(server) => server.run(&server_settings).await, - Self::Stats(stats) => stats.run(&mut db, &client_settings), + Self::Stats(stats) => stats.run(&mut db, &client_settings).await, Self::Init => init::init(), Self::Search { cwd, exit, interactive, + human, + exclude_exit, + exclude_cwd, + before, + after, query, - } => search::run(cwd, exit, interactive, &query, &mut db), + } => { + search::run( + cwd, + exit, + interactive, + human, + exclude_exit, + exclude_cwd, + before, + after, + &query, + &mut db, + ) + .await + } Self::Sync { force } => sync::run(&client_settings, force, &mut db).await, Self::Login(l) => l.run(&client_settings), diff --git a/src/command/search.rs b/src/command/search.rs index b074371e..76a6a42c 100644 --- a/src/command/search.rs +++ b/src/command/search.rs @@ -1,15 +1,16 @@ +use chrono::Utc; use eyre::Result; use std::time::Duration; use std::{io::stdout, ops::Sub}; use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use tui::{ - backend::TermionBackend, + backend::{Backend, TermionBackend}, layout::{Alignment, Constraint, Corner, Direction, Layout}, style::{Color, Modifier, Style}, text::{Span, Spans, Text}, widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, - Terminal, + Frame, Terminal, }; use unicode_width::UnicodeWidthStr; @@ -28,8 +29,8 @@ struct State { results_state: ListState, } -#[allow(clippy::clippy::cast_sign_loss)] impl State { + #[allow(clippy::clippy::cast_sign_loss)] fn durations(&self) -> Vec<(String, String)> { self.results .iter() @@ -129,24 +130,28 @@ impl State { } } -fn query_results(app: &mut State, db: &mut impl Database) { +async fn query_results(app: &mut State, db: &mut (impl Database + Send + Sync)) -> Result<()> { let results = match app.input.as_str() { - "" => db.list(Some(200), true), - i => db.prefix_search(i), + "" => db.list(Some(200), true).await?, + i => db.search(Some(200), i).await?, }; - if let Ok(results) = results { - app.results = results; - } + app.results = results; if app.results.is_empty() { app.results_state.select(None); } else { app.results_state.select(Some(0)); } + + Ok(()) } -fn key_handler(input: Key, db: &mut impl Database, app: &mut State) -> Option<String> { +async fn key_handler( + input: Key, + db: &mut (impl Database + Send + Sync), + app: &mut State, +) -> Option<String> { match input { Key::Esc => return Some(String::from("")), Key::Char('\n') => { @@ -160,11 +165,11 @@ fn key_handler(input: Key, db: &mut impl Database, app: &mut State) -> Option<St } Key::Char(c) => { app.input.push(c); - query_results(app, db); + query_results(app, db).await.unwrap(); } Key::Backspace => { app.input.pop(); - query_results(app, db); + query_results(app, db).await.unwrap(); } Key::Down => { let i = match app.results_state.selected() { @@ -198,11 +203,82 @@ fn key_handler(input: Key, db: &mut impl Database, app: &mut State) -> Option<St None } +#[allow(clippy::clippy::cast_possible_truncation)] +fn draw<T: Backend>(f: &mut Frame<'_, T>, history_count: i64, app: &mut State) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .margin(1) + .constraints( + [ + Constraint::Length(2), + Constraint::Min(1), + Constraint::Length(3), + ] + .as_ref(), + ) + .split(f.size()); + + let top_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(chunks[0]); + + let top_left_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(1), Constraint::Length(1)].as_ref()) + .split(top_chunks[0]); + + let top_right_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(1), Constraint::Length(1)].as_ref()) + .split(top_chunks[1]); + + let title = Paragraph::new(Text::from(Span::styled( + format!("A'tuin v{}", VERSION), + Style::default().add_modifier(Modifier::BOLD), + ))); + + let help = vec![ + Span::raw("Press "), + Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)), + Span::raw(" to exit."), + ]; + + let help = Text::from(Spans::from(help)); + let help = Paragraph::new(help); + + let input = Paragraph::new(app.input.clone()) + .block(Block::default().borders(Borders::ALL).title("Query")); + + let stats = Paragraph::new(Text::from(Span::raw(format!( + "history count: {}", + history_count, + )))) + .alignment(Alignment::Right); + + f.render_widget(title, top_left_chunks[0]); + f.render_widget(help, top_left_chunks[1]); + + app.render_results(f, chunks[1]); + f.render_widget(stats, top_right_chunks[0]); + f.render_widget(input, chunks[2]); + + f.set_cursor( + // Put cursor past the end of the input text + chunks[2].x + app.input.width() as u16 + 1, + // Move one line down, from the border to the input line + chunks[2].y + 1, + ); +} + // 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 #[allow(clippy::clippy::cast_possible_truncation)] -fn select_history(query: &[String], db: &mut impl Database) -> Result<String> { +async fn select_history( + query: &[String], + db: &mut (impl Database + Send + Sync), +) -> Result<String> { let stdout = stdout().into_raw_mode()?; let stdout = MouseTerminal::from(stdout); let stdout = AlternateScreen::from(stdout); @@ -218,91 +294,35 @@ fn select_history(query: &[String], db: &mut impl Database) -> Result<String> { results_state: ListState::default(), }; - query_results(&mut app, db); + query_results(&mut app, db).await?; loop { + let history_count = db.history_count().await?; // Handle input if let Event::Input(input) = events.next()? { - if let Some(output) = key_handler(input, db, &mut app) { + if let Some(output) = key_handler(input, db, &mut app).await { return Ok(output); } } - terminal.draw(|f| { - let chunks = Layout::default() - .direction(Direction::Vertical) - .margin(1) - .constraints( - [ - Constraint::Length(2), - Constraint::Min(1), - Constraint::Length(3), - ] - .as_ref(), - ) - .split(f.size()); - - let top_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(chunks[0]); - - let top_left_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(1), Constraint::Length(1)].as_ref()) - .split(top_chunks[0]); - - let top_right_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(1), Constraint::Length(1)].as_ref()) - .split(top_chunks[1]); - - let title = Paragraph::new(Text::from(Span::styled( - format!("A'tuin v{}", VERSION), - Style::default().add_modifier(Modifier::BOLD), - ))); - - let help = vec![ - Span::raw("Press "), - Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)), - Span::raw(" to exit."), - ]; - - let help = Text::from(Spans::from(help)); - let help = Paragraph::new(help); - - let input = Paragraph::new(app.input.clone()) - .block(Block::default().borders(Borders::ALL).title("Query")); - - let stats = Paragraph::new(Text::from(Span::raw(format!( - "history count: {}", - db.history_count().unwrap() - )))) - .alignment(Alignment::Right); - - f.render_widget(title, top_left_chunks[0]); - f.render_widget(help, top_left_chunks[1]); - - app.render_results(f, chunks[1]); - f.render_widget(stats, top_right_chunks[0]); - f.render_widget(input, chunks[2]); - - f.set_cursor( - // Put cursor past the end of the input text - chunks[2].x + app.input.width() as u16 + 1, - // Move one line down, from the border to the input line - chunks[2].y + 1, - ); - })?; + terminal.draw(|f| draw(f, history_count, &mut app))?; } } -pub fn run( +// This is supposed to more-or-less mirror the command line version, so ofc +// it is going to have a lot of args +#[allow(clippy::clippy::clippy::too_many_arguments)] +pub async fn run( cwd: Option<String>, exit: Option<i64>, interactive: bool, + human: bool, + exclude_exit: Option<i64>, + exclude_cwd: Option<String>, + before: Option<String>, + after: Option<String>, query: &[String], - db: &mut impl Database, + db: &mut (impl Database + Send + Sync), ) -> Result<()> { let dir = if let Some(cwd) = cwd { if cwd == "." { @@ -319,14 +339,70 @@ pub fn run( }; if interactive { - let item = select_history(query, db)?; + let item = select_history(query, db).await?; eprintln!("{}", item); } else { - let results = db.search(dir, exit, query.join(" ").as_str())?; + let results = db.search(None, query.join(" ").as_str()).await?; - for i in &results { - println!("{}", i.command); - } + // TODO: This filtering would be better done in the SQL query, I just + // need a nice way of building queries. + let results: Vec<History> = results + .iter() + .filter(|h| { + if let Some(exit) = exit { + if h.exit != exit { + return false; + } + } + + if let Some(exit) = exclude_exit { + if h.exit == exit { + return false; + } + } + + if let Some(cwd) = &exclude_cwd { + if h.cwd.as_str() == cwd.as_str() { + return false; + } + } + + if let Some(cwd) = &dir { + if h.cwd.as_str() != cwd.as_str() { + return false; + } + } + + if let Some(before) = &before { + let before = chrono_english::parse_date_string( + before.as_str(), + Utc::now(), + chrono_english::Dialect::Uk, + ); + + if before.is_err() || h.timestamp.gt(&before.unwrap()) { + return false; + } + } + + if let Some(after) = &after { + let after = chrono_english::parse_date_string( + after.as_str(), + Utc::now(), + chrono_english::Dialect::Uk, + ); + + if after.is_err() || h.timestamp.lt(&after.unwrap()) { + return false; + } + } + + true + }) + .map(std::borrow::ToOwned::to_owned) + .collect(); + + super::history::print_list(&results, human); } Ok(()) diff --git a/src/command/stats.rs b/src/command/stats.rs index 5c9a9dbb..6aa54a2c 100644 --- a/src/command/stats.rs +++ b/src/command/stats.rs @@ -71,7 +71,11 @@ fn compute_stats(history: &[History]) -> Result<()> { } impl Cmd { - pub fn run(&self, db: &mut impl Database, settings: &Settings) -> Result<()> { + pub async fn run( + &self, + db: &mut (impl Database + Send + Sync), + settings: &Settings, + ) -> Result<()> { match self { Self::Day { words } => { let words = if words.is_empty() { @@ -86,7 +90,7 @@ impl Cmd { }; let end = start + Duration::days(1); - let history = db.range(start.into(), end.into())?; + let history = db.range(start.into(), end.into()).await?; compute_stats(&history)?; @@ -94,7 +98,7 @@ impl Cmd { } Self::All => { - let history = db.list(None, false)?; + let history = db.list(None, false).await?; compute_stats(&history)?; diff --git a/src/command/sync.rs b/src/command/sync.rs index d70b554f..f8bfd5e2 100644 --- a/src/command/sync.rs +++ b/src/command/sync.rs @@ -4,11 +4,15 @@ use atuin_client::database::Database; use atuin_client::settings::Settings; use atuin_client::sync; -pub async fn run(settings: &Settings, force: bool, db: &mut (impl Database + Send)) -> Result<()> { +pub async fn run( + settings: &Settings, + force: bool, + db: &mut (impl Database + Send + Sync), +) -> Result<()> { sync::sync(settings, force, db).await?; println!( "Sync complete! {} items in database, force: {}", - db.history_count()?, + db.history_count().await?, force ); Ok(()) |
