From d53ad84e5727f7cad09eee09a2444da81a88b643 Mon Sep 17 00:00:00 2001 From: P T Weir Date: Mon, 20 Oct 2025 21:02:40 +0100 Subject: feat: Interactive Inspector (#2319) ### What does this PR do? Adds simple navigation to the inspector, to explore a session starting from a single command. This creates a new user flow, where a user can find a history entry in the interactive view (in, say, Global mode), and hit Ctrl+o to navigate back and forward through that command's session. IMAGINED USE-CASE: I remembered that I did a sequence of git steps but I can't remember the order and forgot to document it. I remember that `reflog` was involved and want to see the actual sequence, and only those commands. IMAGINED USE-CASE: I used a curl command to get my IP address for greenlisting before I connected to the bastion server `abc.xyz` over SSH - I could easily find the SSH command with abc.xyz, and go back one step in the session, but without this change, scrolling through all my curl commands ever run to find a forgotten URL/domain would be too much work. Since this gives the inspector tab a broader purpose than viewing analytics, it needs to function even when there are not enough screen rows for charts -- hence, this PR also introduces an ultracompact mode for the inspector that _just_ shows the neighbouring history commands (as simple scrolling three-entry list, with no panes) if there are fewer than `auto_hide_height` rows (default: 8). Otherwise, the inspector behaves as normal, except that Up / Down will change the focused command by navigating through the session. That means there is no "compact" mode for the inspector - when the interactive search is compact (but not ultracompact), the inspector shows its usual chart view. The UX for this could be improved - to keep this PR as lean as it realistically can be, I have tried to keep the flow very minimal, but a follow-up PR could introduce some tooltips, nicer ultracompact formatting, etc. A minor QoL improvement that comes with this - since I had to deal with bold text and would otherwise have need a theming exception, I took the opportunity to ensure the theme engine sets styles completely (so a theme can have bold), not just colours. To limit scope creep, I do not add TOML syntax so (for now) you can only customize colours from config files, but it means that default-bold text (etc.) can now use the theming engine if the code-defined default Meaning is bolded. Key changes: * introduces a simplified inspector tab, with only previous-current-next commands as rows, in compact mode * allows navigation through session history within the inspector (so compact inspector view is still useful) It also (see comments below): * makes `compact` into `compactness`, an enum (to better standardize across inspector/interactive) * makes the inspector _only_ change layout for ultracompact mode, which is still compact+(height<8) * clippy's complexity limit wanted draw split up a little, so not sure if this is a reasonable minimal way to do so for now * adds a `(none)` theme to the theming to enable output testing without styling * ~~additional tests, although keen for input on how best to do these~~ one functional test, as a starting point * ~~documentation~~ [minor doc changes only](https://github.com/atuinsh/docs/pull/72), as I am not sure there is much to say _Was stacked on #2357, which is now in `main`_ ## Checks - [x] I am happy for maintainers to push small adjustments to this PR, to speed up the review cycle - [x] I have checked that there are no existing pull requests for the same thing --- crates/atuin-client/src/theme.rs | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) (limited to 'crates/atuin-client/src/theme.rs') diff --git a/crates/atuin-client/src/theme.rs b/crates/atuin-client/src/theme.rs index fc15bfd8..76ddbb22 100644 --- a/crates/atuin-client/src/theme.rs +++ b/crates/atuin-client/src/theme.rs @@ -51,7 +51,7 @@ pub struct ThemeDefinitionConfigBlock { pub parent: Option, } -use crossterm::style::{Color, ContentStyle}; +use crossterm::style::{Attribute, Attributes, Color, ContentStyle}; // For now, a theme is loaded as a mapping of meanings to colors, but it may be desirable to // expand that in the future to general styles, so we populate a Meaning->ContentStyle hashmap. @@ -228,6 +228,14 @@ impl StyleFactory { ..ContentStyle::default() } } + + fn from_fg_color_and_attributes(color: Color, attributes: Attributes) -> ContentStyle { + ContentStyle { + foreground_color: Some(color), + attributes, + ..ContentStyle::default() + } + } } // Built-in themes. Rather than having extra files added before any theming @@ -275,7 +283,10 @@ lazy_static! { ), ( Meaning::Important, - StyleFactory::from_fg_color(Color::White), + StyleFactory::from_fg_color_and_attributes( + Color::White, + Attributes::from(Attribute::Bold), + ), ), (Meaning::Muted, StyleFactory::from_fg_color(Color::Grey)), (Meaning::Base, ContentStyle::default()), @@ -285,6 +296,19 @@ lazy_static! { static ref BUILTIN_THEMES: HashMap<&'static str, Theme> = { HashMap::from([ ("default", HashMap::new()), + ( + "(none)", + HashMap::from([ + (Meaning::AlertError, ContentStyle::default()), + (Meaning::AlertWarn, ContentStyle::default()), + (Meaning::AlertInfo, ContentStyle::default()), + (Meaning::Annotation, ContentStyle::default()), + (Meaning::Guidance, ContentStyle::default()), + (Meaning::Important, ContentStyle::default()), + (Meaning::Muted, ContentStyle::default()), + (Meaning::Base, ContentStyle::default()), + ]), + ), ( "autumn", HashMap::from([ @@ -430,7 +454,7 @@ impl ThemeManager { } Some(self.load_theme(parent_name.as_str(), Some(max_depth - 1))) } - None => None, + None => Some(self.load_theme("default", Some(max_depth - 1))), }; if debug && name != theme_config.theme.name { @@ -461,7 +485,7 @@ impl ThemeManager { Ok(theme) => theme, Err(err) => { log::warn!("Could not load theme {name}: {err}"); - built_ins.get("default").unwrap() + built_ins.get("(none)").unwrap() } }, } @@ -669,7 +693,8 @@ mod theme_tests { testing_logger::validate(|captured_logs| assert_eq!(captured_logs.len(), 0)); - // If the parent is not found, we end up with the base theme colors + // If the parent is not found, we end up with the no theme colors or styling + // as this is considered a (soft) error state. let nunsolarized = Config::builder() .add_source(ConfigFile::from_str( " @@ -692,7 +717,7 @@ mod theme_tests { nunsolarized_theme .as_style(Meaning::Guidance) .foreground_color, - Some(Color::DarkBlue) + None ); testing_logger::validate(|captured_logs| { -- cgit v1.3.1