update default layout (#523)

* update layouts

* add other duration changes

* fmt :(
This commit is contained in:
Conrad Ludgate 2022-09-12 20:19:22 +01:00 committed by GitHub
parent 702a644f68
commit e8c8415278
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 143 additions and 157 deletions

9
Cargo.lock generated
View file

@ -86,7 +86,6 @@ dependencies = [
"directories", "directories",
"eyre", "eyre",
"fs-err", "fs-err",
"humantime 2.1.0",
"indicatif", "indicatif",
"itertools", "itertools",
"log", "log",
@ -568,7 +567,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36"
dependencies = [ dependencies = [
"atty", "atty",
"humantime 1.3.0", "humantime",
"log", "log",
"regex", "regex",
"termcolor", "termcolor",
@ -837,12 +836,6 @@ dependencies = [
"quick-error", "quick-error",
] ]
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "0.14.19" version = "0.14.19"

View file

@ -2,7 +2,7 @@
name = "atuin" name = "atuin"
version = "0.10.0" version = "0.10.0"
authors = ["Ellie Huxtable <ellie@elliehuxtable.com>"] authors = ["Ellie Huxtable <ellie@elliehuxtable.com>"]
edition = "2018" edition = "2021"
rust-version = "1.59" rust-version = "1.59"
license = "MIT" license = "MIT"
description = "atuin - magical shell history" description = "atuin - magical shell history"
@ -65,7 +65,6 @@ async-trait = "0.1.49"
chrono-english = "0.1.4" chrono-english = "0.1.4"
cli-table = { version = "0.4", default-features = false } cli-table = { version = "0.4", default-features = false }
base64 = "0.13.0" base64 = "0.13.0"
humantime = "2.1.0"
crossbeam-channel = "0.5.1" crossbeam-channel = "0.5.1"
clap = { version = "3.1.18", features = ["derive"] } clap = { version = "3.1.18", features = ["derive"] }
clap_complete = "3.1.4" clap_complete = "3.1.4"

View file

@ -16,6 +16,8 @@ use atuin_client::{
#[cfg(feature = "sync")] #[cfg(feature = "sync")]
use atuin_client::sync; use atuin_client::sync;
use super::search::format_duration;
#[derive(Subcommand)] #[derive(Subcommand)]
#[clap(infer_subcommands = true)] #[clap(infer_subcommands = true)]
pub enum Cmd { pub enum Cmd {
@ -92,11 +94,7 @@ pub fn print_list(h: &[History], list_mode: ListMode) {
#[allow(clippy::cast_sign_loss)] #[allow(clippy::cast_sign_loss)]
pub fn print_human_list(w: &mut StdoutLock, h: &[History]) { pub fn print_human_list(w: &mut StdoutLock, h: &[History]) {
for h in h.iter().rev() { for h in h.iter().rev() {
let duration = let duration = format_duration(Duration::from_nanos(std::cmp::max(h.duration, 0) as u64));
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 time = h.timestamp.format("%Y-%m-%d %H:%M:%S"); let time = h.timestamp.format("%Y-%m-%d %H:%M:%S");
let cmd = h.command.trim(); let cmd = h.command.trim();
@ -108,11 +106,7 @@ pub fn print_human_list(w: &mut StdoutLock, h: &[History]) {
#[allow(clippy::cast_sign_loss)] #[allow(clippy::cast_sign_loss)]
pub fn print_regular(w: &mut StdoutLock, h: &[History]) { pub fn print_regular(w: &mut StdoutLock, h: &[History]) {
for h in h.iter().rev() { for h in h.iter().rev() {
let duration = let duration = format_duration(Duration::from_nanos(std::cmp::max(h.duration, 0) as u64));
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 time = h.timestamp.format("%Y-%m-%d %H:%M:%S"); let time = h.timestamp.format("%Y-%m-%d %H:%M:%S");
let cmd = h.command.trim(); let cmd = h.command.trim();

View file

@ -9,8 +9,10 @@ use atuin_client::{
use super::history::ListMode; use super::history::ListMode;
mod cursor; mod cursor;
mod duration;
mod event; mod event;
mod interactive; mod interactive;
pub use duration::format_duration;
#[derive(Parser)] #[derive(Parser)]
pub struct Cmd { pub struct Cmd {

View file

@ -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<String> {
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<String, ()> {
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"),
}
}

View file

@ -10,7 +10,7 @@ use tui::{
layout::{Alignment, Constraint, Corner, Direction, Layout}, layout::{Alignment, Constraint, Corner, Direction, Layout},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
text::{Span, Spans, Text}, text::{Span, Spans, Text},
widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph},
Frame, Terminal, Frame, Terminal,
}; };
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
@ -26,6 +26,7 @@ use atuin_client::{
use super::{ use super::{
cursor::Cursor, cursor::Cursor,
event::{Event, Events}, event::{Event, Events},
format_duration,
}; };
use crate::VERSION; use crate::VERSION;
@ -41,17 +42,12 @@ struct State {
context: Context, context: Context,
} }
impl State { fn duration(h: &History) -> String {
#[allow(clippy::cast_sign_loss)] let duration = Duration::from_nanos(u64::try_from(h.duration).unwrap_or(0));
fn durations(&self) -> Vec<(String, String)> { format_duration(duration)
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();
fn ago(h: &History) -> String {
let ago = chrono::Utc::now().sub(h.timestamp); let ago = chrono::Utc::now().sub(h.timestamp);
// Account for the chance that h.timestamp is "in the future" // Account for the chance that h.timestamp is "in the future"
@ -59,102 +55,49 @@ impl State {
// would fail. // would fail.
// If the timestamp would otherwise be in the future, display // If the timestamp would otherwise be in the future, display
// the time ago as 0. // the time ago as 0.
let ago = humantime::format_duration( let ago = ago.to_std().unwrap_or_default();
ago.to_std().unwrap_or_else(|_| Duration::new(0, 0)), format_duration(ago) + " ago"
) }
.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()
}
impl State {
fn render_results<T: tui::backend::Backend>( fn render_results<T: tui::backend::Backend>(
&mut self, &mut self,
f: &mut tui::Frame<T>, f: &mut tui::Frame<T>,
r: tui::layout::Rect, r: tui::layout::Rect,
b: tui::widgets::Block, b: tui::widgets::Block,
) { ) {
let durations = self.durations(); let max_length = 12; // '123ms' + '59s ago'
let max_length = durations.iter().fold(0, |largest, i| {
std::cmp::max(largest, i.0.len() + i.1.len())
});
let results: Vec<ListItem> = self let results: Vec<ListItem> = self
.results .results
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, m)| { .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 status_colour = if m.success() {
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 duration = Span::styled(
duration,
Style::default().fg(if m.success() {
Color::Green Color::Green
} else { } else {
Color::Red Color::Red
}), };
); let ago = ago(m);
let duration = format!("{:width$}", duration(m), width = max_length - ago.len());
let ago = Span::styled(ago, Style::default().fg(Color::Blue)); let command = m.command.replace(['\n', '\t'], " ");
let mut command = Span::raw(command);
if let Some(selected) = self.results_state.selected() { if slice_index == 0 {
if selected == i { command.style = Style::default().fg(Color::Red).add_modifier(Modifier::BOLD);
command.style =
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD);
}
} }
let spans = Spans::from(vec![ let spans = Spans::from(vec![
selected_index, Span::raw(&slices[slice_index..slice_index + 3]),
duration, Span::styled(duration, Style::default().fg(status_colour)),
Span::raw(" "), Span::raw(" "),
ago, Span::styled(ago, Style::default().fg(Color::Blue)),
Span::raw(" "), Span::raw(" "),
command, command,
]); ]);
@ -163,10 +106,7 @@ impl State {
}) })
.collect(); .collect();
let results = List::new(results) let results = List::new(results).block(b).start_corner(Corner::BottomLeft);
.block(b)
.start_corner(Corner::BottomLeft)
.highlight_symbol(">> ");
f.render_stateful_widget(results, r, &mut self.results_state); f.render_stateful_widget(results, r, &mut self.results_state);
} }
@ -280,73 +220,78 @@ impl State {
fn draw<T: Backend>(&mut self, f: &mut Frame<'_, T>, history_count: i64) { fn draw<T: Backend>(&mut self, f: &mut Frame<'_, T>, history_count: i64) {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.margin(1) .margin(0)
.constraints( .constraints([
[ Constraint::Length(3),
Constraint::Length(2),
Constraint::Min(1), Constraint::Min(1),
Constraint::Length(3), Constraint::Length(3),
] ])
.as_ref(),
)
.split(f.size()); .split(f.size());
let top_chunks = Layout::default() let top_chunks = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .constraints([Constraint::Percentage(50); 2])
.split(chunks[0]); .split(chunks[0]);
let top_left_chunks = Layout::default() let top_left_chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Length(1)].as_ref()) .constraints([Constraint::Length(1); 3])
.split(top_chunks[0]); .split(top_chunks[0]);
let top_right_chunks = Layout::default() let top_right_chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Length(1)].as_ref()) .constraints([Constraint::Length(1); 3])
.split(top_chunks[1]); .split(top_chunks[1]);
let title = Paragraph::new(Text::from(Span::styled( let title = Paragraph::new(Text::from(Span::styled(
format!("Atuin v{}", VERSION), format!(" Atuin v{VERSION}"),
Style::default().add_modifier(Modifier::BOLD), Style::default().add_modifier(Modifier::BOLD),
))); )));
let help = vec![ let help = vec![
Span::raw("Press "), Span::raw(" Press "),
Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)), Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to exit."), Span::raw(" to exit."),
]; ];
let help = Text::from(Spans::from(help)); let help = Paragraph::new(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 stats = Paragraph::new(Text::from(Span::raw(format!( let stats = Paragraph::new(Text::from(Span::raw(format!(
"history count: {}", "history count: {history_count} ",
history_count, ))));
))))
.alignment(Alignment::Right);
f.render_widget(title, top_left_chunks[0]); f.render_widget(title, top_left_chunks[1]);
f.render_widget(help, top_left_chunks[1]); f.render_widget(help, top_left_chunks[2]);
f.render_widget(stats, top_right_chunks[0]); f.render_widget(stats.alignment(Alignment::Right), top_right_chunks[1]);
self.render_results( self.render_results(
f, f,
chunks[1], 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]); f.render_widget(input, chunks[2]);
let width = UnicodeWidthStr::width(self.input.substring()); let width = UnicodeWidthStr::width(self.input.substring());
f.set_cursor( f.set_cursor(
// Put cursor past the end of the input text // 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 // Move one line down, from the border to the input line
chunks[2].y + 1, chunks[2].y + 1,
); );
@ -399,22 +344,25 @@ impl State {
.style(Style::default().fg(Color::DarkGray)) .style(Style::default().fg(Color::DarkGray))
.alignment(Alignment::Right); .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(title, header_chunks[0]);
f.render_widget(help, header_chunks[1]); f.render_widget(help, header_chunks[1]);
f.render_widget(stats, header_chunks[2]); f.render_widget(stats, header_chunks[2]);
self.render_results(f, chunks[1], Block::default()); 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]); 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( f.set_cursor(
// Put cursor past the end of the input text // 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 // Move one line down, from the border to the input line
chunks[2].y + 1, chunks[2].y + 1,
); );