From e8c841527880ac6ca47fd514ac70aad81764dae8 Mon Sep 17 00:00:00 2001 From: Conrad Ludgate Date: Mon, 12 Sep 2022 20:19:22 +0100 Subject: [PATCH] update default layout (#523) * update layouts * add other duration changes * fmt :( --- Cargo.lock | 9 +- Cargo.toml | 3 +- src/command/client/history.rs | 14 +- src/command/client/search.rs | 2 + src/command/client/search/duration.rs | 50 +++++ src/command/client/search/interactive.rs | 222 +++++++++-------------- 6 files changed, 143 insertions(+), 157 deletions(-) create mode 100644 src/command/client/search/duration.rs diff --git a/Cargo.lock b/Cargo.lock index 3747553..6a25cb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,7 +86,6 @@ dependencies = [ "directories", "eyre", "fs-err", - "humantime 2.1.0", "indicatif", "itertools", "log", @@ -568,7 +567,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" dependencies = [ "atty", - "humantime 1.3.0", + "humantime", "log", "regex", "termcolor", @@ -837,12 +836,6 @@ dependencies = [ "quick-error", ] -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" version = "0.14.19" diff --git a/Cargo.toml b/Cargo.toml index 05c7964..d925c36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "atuin" version = "0.10.0" authors = ["Ellie Huxtable "] -edition = "2018" +edition = "2021" rust-version = "1.59" license = "MIT" description = "atuin - magical shell history" @@ -65,7 +65,6 @@ async-trait = "0.1.49" chrono-english = "0.1.4" cli-table = { version = "0.4", default-features = false } base64 = "0.13.0" -humantime = "2.1.0" crossbeam-channel = "0.5.1" clap = { version = "3.1.18", features = ["derive"] } clap_complete = "3.1.4" diff --git a/src/command/client/history.rs b/src/command/client/history.rs index 805fe4c..fe5bfcb 100644 --- a/src/command/client/history.rs +++ b/src/command/client/history.rs @@ -16,6 +16,8 @@ use atuin_client::{ #[cfg(feature = "sync")] use atuin_client::sync; +use super::search::format_duration; + #[derive(Subcommand)] #[clap(infer_subcommands = true)] pub enum Cmd { @@ -92,11 +94,7 @@ pub fn print_list(h: &[History], list_mode: ListMode) { #[allow(clippy::cast_sign_loss)] pub fn print_human_list(w: &mut StdoutLock, h: &[History]) { for h in h.iter().rev() { - 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]; + let duration = format_duration(Duration::from_nanos(std::cmp::max(h.duration, 0) as u64)); let time = h.timestamp.format("%Y-%m-%d %H:%M:%S"); let cmd = h.command.trim(); @@ -108,11 +106,7 @@ pub fn print_human_list(w: &mut StdoutLock, h: &[History]) { #[allow(clippy::cast_sign_loss)] pub fn print_regular(w: &mut StdoutLock, h: &[History]) { for h in h.iter().rev() { - 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]; + let duration = format_duration(Duration::from_nanos(std::cmp::max(h.duration, 0) as u64)); let time = h.timestamp.format("%Y-%m-%d %H:%M:%S"); let cmd = h.command.trim(); diff --git a/src/command/client/search.rs b/src/command/client/search.rs index 7b84d41..915589a 100644 --- a/src/command/client/search.rs +++ b/src/command/client/search.rs @@ -9,8 +9,10 @@ use atuin_client::{ use super::history::ListMode; mod cursor; +mod duration; mod event; mod interactive; +pub use duration::format_duration; #[derive(Parser)] pub struct Cmd { diff --git a/src/command/client/search/duration.rs b/src/command/client/search/duration.rs new file mode 100644 index 0000000..3cdd4e8 --- /dev/null +++ b/src/command/client/search/duration.rs @@ -0,0 +1,50 @@ +use std::{ops::ControlFlow, time::Duration}; + +#[allow(clippy::module_name_repetitions)] +pub fn format_duration(f: Duration) -> String { + fn item(name: &str, value: u64) -> ControlFlow { + if value > 0 { + ControlFlow::Break(format!("{}{}", value, name)) + } else { + ControlFlow::Continue(()) + } + } + + // impl taken and modified from + // https://github.com/tailhook/humantime/blob/master/src/duration.rs#L295-L331 + // Copyright (c) 2016 The humantime Developers + fn fmt(f: Duration) -> ControlFlow { + let secs = f.as_secs(); + let nanos = f.subsec_nanos(); + + let years = secs / 31_557_600; // 365.25d + let year_days = secs % 31_557_600; + let months = year_days / 2_630_016; // 30.44d + let month_days = year_days % 2_630_016; + let days = month_days / 86400; + let day_secs = month_days % 86400; + let hours = day_secs / 3600; + let minutes = day_secs % 3600 / 60; + let seconds = day_secs % 60; + + let millis = nanos / 1_000_000; + + // a difference from our impl than the original is that + // we only care about the most-significant segment of the duration. + // If the item call returns `Break`, then the `?` will early-return. + // This allows for a very consise impl + item("y", years)?; + item("mo", months)?; + item("d", days)?; + item("h", hours)?; + item("m", minutes)?; + item("s", seconds)?; + item("ms", u64::from(millis))?; + ControlFlow::Continue(()) + } + + match fmt(f) { + ControlFlow::Break(b) => b, + ControlFlow::Continue(()) => String::from("0s"), + } +} diff --git a/src/command/client/search/interactive.rs b/src/command/client/search/interactive.rs index e355b3f..10a440a 100644 --- a/src/command/client/search/interactive.rs +++ b/src/command/client/search/interactive.rs @@ -10,7 +10,7 @@ use tui::{ layout::{Alignment, Constraint, Corner, Direction, Layout}, style::{Color, Modifier, Style}, text::{Span, Spans, Text}, - widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, + widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph}, Frame, Terminal, }; use unicode_width::UnicodeWidthStr; @@ -26,6 +26,7 @@ use atuin_client::{ use super::{ cursor::Cursor, event::{Event, Events}, + format_duration, }; use crate::VERSION; @@ -41,120 +42,62 @@ struct State { context: Context, } +fn duration(h: &History) -> String { + let duration = Duration::from_nanos(u64::try_from(h.duration).unwrap_or(0)); + format_duration(duration) +} + +fn ago(h: &History) -> String { + let ago = chrono::Utc::now().sub(h.timestamp); + + // Account for the chance that h.timestamp is "in the future" + // This would mean that "ago" is negative, and the unwrap here + // would fail. + // If the timestamp would otherwise be in the future, display + // the time ago as 0. + let ago = ago.to_std().unwrap_or_default(); + format_duration(ago) + " ago" +} + impl State { - #[allow(clippy::cast_sign_loss)] - fn durations(&self) -> Vec<(String, String)> { - self.results - .iter() - .map(|h| { - let duration = - Duration::from_millis(std::cmp::max(h.duration, 0) as u64 / 1_000_000); - let duration = humantime::format_duration(duration).to_string(); - let duration: Vec<&str> = duration.split(' ').collect(); - - let ago = chrono::Utc::now().sub(h.timestamp); - - // Account for the chance that h.timestamp is "in the future" - // This would mean that "ago" is negative, and the unwrap here - // would fail. - // If the timestamp would otherwise be in the future, display - // the time ago as 0. - let ago = humantime::format_duration( - ago.to_std().unwrap_or_else(|_| Duration::new(0, 0)), - ) - .to_string(); - let ago: Vec<&str> = ago.split(' ').collect(); - - ( - duration[0] - .to_string() - .replace("days", "d") - .replace("day", "d") - .replace("weeks", "w") - .replace("week", "w") - .replace("months", "mo") - .replace("month", "mo") - .replace("years", "y") - .replace("year", "y"), - ago[0] - .to_string() - .replace("days", "d") - .replace("day", "d") - .replace("weeks", "w") - .replace("week", "w") - .replace("months", "mo") - .replace("month", "mo") - .replace("years", "y") - .replace("year", "y") - + " ago", - ) - }) - .collect() - } - fn render_results( &mut self, f: &mut tui::Frame, r: tui::layout::Rect, b: tui::widgets::Block, ) { - let durations = self.durations(); - let max_length = durations.iter().fold(0, |largest, i| { - std::cmp::max(largest, i.0.len() + i.1.len()) - }); + let max_length = 12; // '123ms' + '59s ago' let results: Vec = self .results .iter() .enumerate() .map(|(i, m)| { - let command = m.command.to_string().replace('\n', " ").replace('\t', " "); + // these encode the slices of `" > "`, `" {n} "`, or `" "` in a compact form. + // Yes, this is a hack, but it makes me feel happy + let slices = " > 1 2 3 4 5 6 7 8 9 "; + let index = self.results_state.selected().and_then(|s| i.checked_sub(s)); + let slice_index = index.unwrap_or(10).min(10) * 2; - let mut command = Span::raw(command); - - let (duration, mut ago) = durations[i].clone(); - - while (duration.len() + ago.len()) < max_length { - ago = format!(" {}", ago); - } - - let selected_index = match self.results_state.selected() { - None => Span::raw(" "), - Some(selected) => match i.checked_sub(selected) { - None => Span::raw(" "), - Some(diff) => { - if 0 < diff && diff < 10 { - Span::raw(format!(" {} ", diff)) - } else { - Span::raw(" ") - } - } - }, + let status_colour = if m.success() { + Color::Green + } else { + Color::Red }; + let ago = ago(m); + let duration = format!("{:width$}", duration(m), width = max_length - ago.len()); - let duration = Span::styled( - duration, - Style::default().fg(if m.success() { - Color::Green - } else { - Color::Red - }), - ); - - let ago = Span::styled(ago, Style::default().fg(Color::Blue)); - - if let Some(selected) = self.results_state.selected() { - if selected == i { - command.style = - Style::default().fg(Color::Red).add_modifier(Modifier::BOLD); - } + let command = m.command.replace(['\n', '\t'], " "); + let mut command = Span::raw(command); + if slice_index == 0 { + command.style = Style::default().fg(Color::Red).add_modifier(Modifier::BOLD); } let spans = Spans::from(vec![ - selected_index, - duration, + Span::raw(&slices[slice_index..slice_index + 3]), + Span::styled(duration, Style::default().fg(status_colour)), Span::raw(" "), - ago, + Span::styled(ago, Style::default().fg(Color::Blue)), Span::raw(" "), command, ]); @@ -163,10 +106,7 @@ impl State { }) .collect(); - let results = List::new(results) - .block(b) - .start_corner(Corner::BottomLeft) - .highlight_symbol(">> "); + let results = List::new(results).block(b).start_corner(Corner::BottomLeft); f.render_stateful_widget(results, r, &mut self.results_state); } @@ -280,73 +220,78 @@ impl State { fn draw(&mut self, f: &mut Frame<'_, T>, history_count: i64) { let chunks = Layout::default() .direction(Direction::Vertical) - .margin(1) - .constraints( - [ - Constraint::Length(2), - Constraint::Min(1), - Constraint::Length(3), - ] - .as_ref(), - ) + .margin(0) + .constraints([ + Constraint::Length(3), + Constraint::Min(1), + Constraint::Length(3), + ]) .split(f.size()); let top_chunks = Layout::default() .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .constraints([Constraint::Percentage(50); 2]) .split(chunks[0]); let top_left_chunks = Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Length(1), Constraint::Length(1)].as_ref()) + .constraints([Constraint::Length(1); 3]) .split(top_chunks[0]); let top_right_chunks = Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Length(1), Constraint::Length(1)].as_ref()) + .constraints([Constraint::Length(1); 3]) .split(top_chunks[1]); let title = Paragraph::new(Text::from(Span::styled( - format!("Atuin v{}", VERSION), + format!(" Atuin v{VERSION}"), Style::default().add_modifier(Modifier::BOLD), ))); let help = vec![ - Span::raw("Press "), + 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(self.input.as_str().to_owned()).block( - Block::default() - .borders(Borders::ALL) - .title(self.filter_mode.as_str()), - ); - + let help = Paragraph::new(Text::from(Spans::from(help))); let stats = Paragraph::new(Text::from(Span::raw(format!( - "history count: {}", - history_count, - )))) - .alignment(Alignment::Right); + "history count: {history_count} ", + )))); - f.render_widget(title, top_left_chunks[0]); - f.render_widget(help, top_left_chunks[1]); - f.render_widget(stats, top_right_chunks[0]); + f.render_widget(title, top_left_chunks[1]); + f.render_widget(help, top_left_chunks[2]); + f.render_widget(stats.alignment(Alignment::Right), top_right_chunks[1]); self.render_results( f, chunks[1], - Block::default().borders(Borders::ALL).title("History"), + Block::default() + .borders(Borders::TOP | Borders::LEFT | Borders::RIGHT) + .border_type(BorderType::Rounded), // .title("History"), + ); + + let input = format!( + "[{:^14}] {}", + self.filter_mode.as_str(), + self.input.as_str(), + ); + let input = Paragraph::new(input).block( + Block::default() + .borders(Borders::BOTTOM | Borders::LEFT | Borders::RIGHT) + .border_type(BorderType::Rounded) + .title(format!( + "{:─>width$}", + "", + width = chunks[2].width as usize - 2 + )), ); f.render_widget(input, chunks[2]); let width = UnicodeWidthStr::width(self.input.substring()); f.set_cursor( // Put cursor past the end of the input text - chunks[2].x + width as u16 + 1, + chunks[2].x + width as u16 + 18, // Move one line down, from the border to the input line chunks[2].y + 1, ); @@ -399,22 +344,25 @@ impl State { .style(Style::default().fg(Color::DarkGray)) .alignment(Alignment::Right); - let filter_mode = self.filter_mode.as_str(); - let input = Paragraph::new(format!("{}] {}", filter_mode, self.input.as_str())) - .block(Block::default()); - f.render_widget(title, header_chunks[0]); f.render_widget(help, header_chunks[1]); f.render_widget(stats, header_chunks[2]); self.render_results(f, chunks[1], Block::default()); + + let input = format!( + "[{:^14}] {}", + self.filter_mode.as_str(), + self.input.as_str(), + ); + let input = Paragraph::new(input).block(Block::default()); f.render_widget(input, chunks[2]); - let extra_width = UnicodeWidthStr::width(self.input.substring()) + filter_mode.len(); + let extra_width = UnicodeWidthStr::width(self.input.substring()); f.set_cursor( // Put cursor past the end of the input text - chunks[2].x + extra_width as u16 + 2, + chunks[2].x + extra_width as u16 + 17, // Move one line down, from the border to the input line chunks[2].y + 1, );