aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/command/event.rs8
-rw-r--r--src/command/history.rs112
-rw-r--r--src/command/import.rs12
-rw-r--r--src/command/mod.rs42
-rw-r--r--src/command/search.rs252
-rw-r--r--src/command/stats.rs10
-rw-r--r--src/command/sync.rs8
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(())