From 176eae02f7f855ef40a4461caca61d530486113f Mon Sep 17 00:00:00 2001 From: "Helmut K. C. Tessarek" Date: Thu, 18 Apr 2024 11:16:54 -0400 Subject: [PATCH] feat: show preview auto (#1804) * feat: show preview auto * refactor: preview_auto --- atuin-client/config.toml | 5 + atuin-client/src/settings.rs | 2 + .../src/command/client/search/interactive.rs | 208 ++++++++++++++++-- 3 files changed, 194 insertions(+), 21 deletions(-) diff --git a/atuin-client/config.toml b/atuin-client/config.toml index 254586a5..415fd441 100644 --- a/atuin-client/config.toml +++ b/atuin-client/config.toml @@ -73,6 +73,11 @@ ## useful when the command is longer than the terminal width and is cut off # show_preview = false +## enable or disable automatic preview. It shows a preview, if the command is +## longer than the width of the terminal. It respects max_preview_height. +## (default: true) +# show_preview_auto = true + ## what to do when the escape key is pressed when searching ## possible values: return-original, return-query # exit_mode = "return-original" diff --git a/atuin-client/src/settings.rs b/atuin-client/src/settings.rs index 035af321..daf8fe34 100644 --- a/atuin-client/src/settings.rs +++ b/atuin-client/src/settings.rs @@ -358,6 +358,7 @@ pub struct Settings { pub inline_height: u16, pub invert: bool, pub show_preview: bool, + pub show_preview_auto: bool, pub max_preview_height: u16, pub show_help: bool, pub show_tabs: bool, @@ -615,6 +616,7 @@ impl Settings { .set_default("style", "auto")? .set_default("inline_height", 0)? .set_default("show_preview", false)? + .set_default("show_preview_auto", true)? .set_default("max_preview_height", 4)? .set_default("show_help", true)? .set_default("show_tabs", true)? diff --git a/atuin/src/command/client/search/interactive.rs b/atuin/src/command/client/search/interactive.rs index d2a314ef..7a3a834b 100644 --- a/atuin/src/command/client/search/interactive.rs +++ b/atuin/src/command/client/search/interactive.rs @@ -533,6 +533,52 @@ impl State { self.results_state.select(i.min(self.results_len - 1)); } + #[allow(clippy::cast_possible_truncation)] + #[allow(clippy::bool_to_int_with_if)] + fn calc_preview_height( + settings: &Settings, + results: &[History], + selected: usize, + tab_index: usize, + compact: bool, + border_size: u16, + preview_width: u16, + ) -> u16 { + if settings.show_preview_auto && tab_index == 0 && !results.is_empty() { + let length_current_cmd = results[selected].command.len() as u16; + // The '- 19' takes the characters before the command (duration and time) into account + if length_current_cmd > preview_width - 19 { + std::cmp::min( + settings.max_preview_height, + (length_current_cmd + preview_width - 1 - border_size) + / (preview_width - border_size), + ) + border_size * 2 + } else { + 1 + } + } else if settings.show_preview && !settings.show_preview_auto && tab_index == 0 { + let longest_command = results + .iter() + .max_by(|h1, h2| h1.command.len().cmp(&h2.command.len())); + longest_command.map_or(0, |v| { + std::cmp::min( + settings.max_preview_height, + v.command + .split('\n') + .map(|line| { + (line.len() as u16 + preview_width - 1 - border_size) + / (preview_width - border_size) + }) + .sum(), + ) + }) + border_size * 2 + } else if compact || tab_index == 1 { + 0 + } else { + 1 + } + } + #[allow(clippy::cast_possible_truncation)] #[allow(clippy::bool_to_int_with_if)] #[allow(clippy::too_many_lines)] @@ -551,27 +597,15 @@ impl State { let invert = settings.invert; let border_size = if compact { 0 } else { 1 }; let preview_width = f.size().width - 2; - let preview_height = if settings.show_preview && self.tab_index == 0 { - let longest_command = results - .iter() - .max_by(|h1, h2| h1.command.len().cmp(&h2.command.len())); - longest_command.map_or(0, |v| { - std::cmp::min( - settings.max_preview_height, - v.command - .split('\n') - .map(|line| { - (line.len() as u16 + preview_width - 1 - border_size) - / (preview_width - border_size) - }) - .sum(), - ) - }) + border_size * 2 - } else if compact || self.tab_index == 1 { - 0 - } else { - 1 - }; + let preview_height = Self::calc_preview_height( + settings, + results, + self.results_state.selected(), + self.tab_index, + compact, + border_size, + preview_width, + ); let show_help = settings.show_help && (!compact || f.size().height > 1); let show_tabs = settings.show_tabs; let chunks = Layout::default() @@ -1141,3 +1175,135 @@ fn set_clipboard(s: String) { any(target_os = "windows", target_os = "macos", target_os = "linux") )))] fn set_clipboard(_s: String) {} + +#[cfg(test)] +mod tests { + use atuin_client::history::History; + use atuin_client::settings::Settings; + + use super::State; + + #[test] + fn calc_preview_height_test() { + let settings_preview_auto = Settings { + show_preview_auto: true, + ..Settings::utc() + }; + + let settings_preview_auto_h2 = Settings { + show_preview_auto: true, + max_preview_height: 2, + ..Settings::utc() + }; + + let settings_preview_h4 = Settings { + show_preview_auto: false, + show_preview: true, + max_preview_height: 4, + ..Settings::utc() + }; + + let cmd_60: History = History::capture() + .timestamp(time::OffsetDateTime::now_utc()) + .command("for i in $(seq -w 10); do echo \"item number $i - abcd\"; done") + .cwd("/") + .build() + .into(); + + let cmd_124: History = History::capture() + .timestamp(time::OffsetDateTime::now_utc()) + .command("echo 'Aurea prima sata est aetas, quae vindice nullo, sponte sua, sine lege fidem rectumque colebat. Poena metusque aberant'") + .cwd("/") + .build() + .into(); + + let cmd_200: History = History::capture() + .timestamp(time::OffsetDateTime::now_utc()) + .command("CREATE USER atuin WITH ENCRYPTED PASSWORD 'supersecretpassword'; CREATE DATABASE atuin WITH OWNER = atuin; \\c atuin; REVOKE ALL PRIVILEGES ON SCHEMA public FROM PUBLIC; echo 'All done. 200 characters'") + .cwd("/") + .build() + .into(); + + let results: Vec = vec![cmd_60, cmd_124, cmd_200]; + + // the selected command does not require a preview + let no_preview = State::calc_preview_height( + &settings_preview_auto, + &results, + 0 as usize, + 0 as usize, + false, + 1, + 80, + ); + // the selected command requires 2 lines + let preview_h2 = State::calc_preview_height( + &settings_preview_auto, + &results, + 1 as usize, + 0 as usize, + false, + 1, + 80, + ); + // the selected command requires 3 lines + let preview_h3 = State::calc_preview_height( + &settings_preview_auto, + &results, + 2 as usize, + 0 as usize, + false, + 1, + 80, + ); + // the selected command requires a preview of 1 line (happens when the command is between preview_width-19 and preview_width) + let preview_one_line = State::calc_preview_height( + &settings_preview_auto, + &results, + 0 as usize, + 0 as usize, + false, + 1, + 66, + ); + // the selected command requires 3 lines, but we have a max preview height limit of 2 + let preview_limit_at_2 = State::calc_preview_height( + &settings_preview_auto_h2, + &results, + 2 as usize, + 0 as usize, + false, + 1, + 80, + ); + // the longest command requires 3 lines + let preview_static_h3 = State::calc_preview_height( + &settings_preview_h4, + &results, + 1 as usize, + 0 as usize, + false, + 1, + 80, + ); + // the longest command requires 10 lines, but we have a max preview height limit of 4 + let preview_static_limit_at_4 = State::calc_preview_height( + &settings_preview_h4, + &results, + 1 as usize, + 0 as usize, + false, + 1, + 20, + ); + + assert_eq!(no_preview, 1); + // 1*2 is the space for the border + assert_eq!(preview_h2, 2 + 1 * 2); + assert_eq!(preview_h3, 3 + 1 * 2); + assert_eq!(preview_one_line, 1 + 1 * 2); + assert_eq!(preview_limit_at_2, 2 + 1 * 2); + assert_eq!(preview_static_h3, 3 + 1 * 2); + assert_eq!(preview_static_limit_at_4, 4 + 1 * 2); + } +}