aboutsummaryrefslogtreecommitdiffstats
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/atuin-client/src/theme.rs37
-rw-r--r--crates/atuin/src/command/client/search/inspector.rs190
-rw-r--r--crates/atuin/src/command/client/search/interactive.rs389
3 files changed, 476 insertions, 140 deletions
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<String>,
}
-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()),
@@ -286,6 +297,19 @@ lazy_static! {
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| {
diff --git a/crates/atuin/src/command/client/search/inspector.rs b/crates/atuin/src/command/client/search/inspector.rs
index a39e63a8..34d22eba 100644
--- a/crates/atuin/src/command/client/search/inspector.rs
+++ b/crates/atuin/src/command/client/search/inspector.rs
@@ -11,13 +11,14 @@ use ratatui::{
layout::Rect,
prelude::{Constraint, Direction, Layout},
style::Style,
+ text::{Span, Text},
widgets::{Bar, BarChart, BarGroup, Block, Borders, Padding, Paragraph, Row, Table},
};
use super::duration::format_duration;
use super::super::theme::{Meaning, Theme};
-use super::interactive::{InputAction, State};
+use super::interactive::{Compactness, InputAction, State, to_compactness};
#[allow(clippy::cast_sign_loss)]
fn u64_or_zero(num: i64) -> u64 {
@@ -29,52 +30,83 @@ pub fn draw_commands(
parent: Rect,
history: &History,
stats: &HistoryStats,
+ compact: bool,
theme: &Theme,
) {
let commands = Layout::default()
- .direction(Direction::Horizontal)
- .constraints([
- Constraint::Ratio(1, 4),
- Constraint::Ratio(1, 2),
- Constraint::Ratio(1, 4),
- ])
+ .direction(if compact {
+ Direction::Vertical
+ } else {
+ Direction::Horizontal
+ })
+ .constraints(if compact {
+ [
+ Constraint::Length(1),
+ Constraint::Length(1),
+ Constraint::Min(0),
+ ]
+ } else {
+ [
+ Constraint::Ratio(1, 4),
+ Constraint::Ratio(1, 2),
+ Constraint::Ratio(1, 4),
+ ]
+ })
.split(parent);
- let command = Paragraph::new(history.command.clone()).block(
+ let command = Paragraph::new(Text::from(Span::styled(
+ history.command.clone(),
+ theme.as_style(Meaning::Important),
+ )))
+ .block(if compact {
+ Block::new()
+ .borders(Borders::NONE)
+ .style(theme.as_style(Meaning::Base))
+ } else {
Block::new()
.borders(Borders::ALL)
- .title("Command")
.style(theme.as_style(Meaning::Base))
- .padding(Padding::horizontal(1)),
- );
+ .title("Command")
+ .padding(Padding::horizontal(1))
+ });
let previous = Paragraph::new(
stats
.previous
.clone()
- .map_or_else(|| "No previous command".to_string(), |prev| prev.command),
+ .map_or_else(|| "[No previous command]".to_string(), |prev| prev.command),
)
- .block(
+ .block(if compact {
+ Block::new()
+ .borders(Borders::NONE)
+ .style(theme.as_style(Meaning::Annotation))
+ } else {
Block::new()
.borders(Borders::ALL)
- .title("Previous command")
.style(theme.as_style(Meaning::Annotation))
- .padding(Padding::horizontal(1)),
- );
+ .title("Previous command")
+ .padding(Padding::horizontal(1))
+ });
+ // Add [] around blank text, as when this is shown in a list
+ // compacted, it makes it more obviously control text.
let next = Paragraph::new(
stats
.next
.clone()
- .map_or_else(|| "No next command".to_string(), |next| next.command),
+ .map_or_else(|| "[No next command]".to_string(), |next| next.command),
)
- .block(
+ .block(if compact {
+ Block::new()
+ .borders(Borders::NONE)
+ .style(theme.as_style(Meaning::Annotation))
+ } else {
Block::new()
.borders(Borders::ALL)
.title("Next command")
+ .padding(Padding::horizontal(1))
.style(theme.as_style(Meaning::Annotation))
- .padding(Padding::horizontal(1)),
- );
+ });
f.render_widget(previous, commands[0]);
f.render_widget(command, commands[1]);
@@ -258,6 +290,33 @@ pub fn draw(
chunk: Rect,
history: &History,
stats: &HistoryStats,
+ settings: &Settings,
+ theme: &Theme,
+ tz: Timezone,
+) {
+ let compactness = to_compactness(f, settings);
+
+ match compactness {
+ Compactness::Ultracompact => draw_ultracompact(f, chunk, history, stats, theme),
+ _ => draw_full(f, chunk, history, stats, theme, tz),
+ }
+}
+
+pub fn draw_ultracompact(
+ f: &mut Frame<'_>,
+ chunk: Rect,
+ history: &History,
+ stats: &HistoryStats,
+ theme: &Theme,
+) {
+ draw_commands(f, chunk, history, stats, true, theme);
+}
+
+pub fn draw_full(
+ f: &mut Frame<'_>,
+ chunk: Rect,
+ history: &History,
+ stats: &HistoryStats,
theme: &Theme,
tz: Timezone,
) {
@@ -271,7 +330,7 @@ pub fn draw(
.constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)])
.split(vert_layout[1]);
- draw_commands(f, vert_layout[0], history, stats, theme);
+ draw_commands(f, vert_layout[0], history, stats, false, theme);
draw_stats_table(f, stats_layout[0], history, tz, stats, theme);
draw_stats_charts(f, stats_layout[1], stats, theme);
}
@@ -279,7 +338,7 @@ pub fn draw(
// I'm going to break this out more, but just starting to move things around before changing
// structure and making it nicer.
pub fn input(
- _state: &mut State,
+ state: &mut State,
_settings: &Settings,
selected: usize,
input: &KeyEvent,
@@ -288,6 +347,93 @@ pub fn input(
match input.code {
KeyCode::Char('d') if ctrl => InputAction::Delete(selected),
+ KeyCode::Up => {
+ state.inspecting_state.move_to_previous();
+ InputAction::Redraw
+ }
+ KeyCode::Down => {
+ state.inspecting_state.move_to_next();
+ InputAction::Redraw
+ }
_ => InputAction::Continue,
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::draw_ultracompact;
+ use atuin_client::{
+ history::{History, HistoryId, HistoryStats},
+ theme::ThemeManager,
+ };
+ use ratatui::{backend::TestBackend, prelude::*};
+ use time::OffsetDateTime;
+
+ fn mock_history_stats() -> (History, HistoryStats) {
+ let history = History {
+ id: HistoryId::from("test1".to_string()),
+ timestamp: OffsetDateTime::now_utc(),
+ duration: 3,
+ exit: 0,
+ command: "/bin/cmd".to_string(),
+ cwd: "/toot".to_string(),
+ session: "sesh1".to_string(),
+ hostname: "hostn".to_string(),
+ deleted_at: None,
+ };
+ let next = History {
+ id: HistoryId::from("test2".to_string()),
+ timestamp: OffsetDateTime::now_utc(),
+ duration: 2,
+ exit: 0,
+ command: "/bin/cmd -os".to_string(),
+ cwd: "/toot".to_string(),
+ session: "sesh1".to_string(),
+ hostname: "hostn".to_string(),
+ deleted_at: None,
+ };
+ let prev = History {
+ id: HistoryId::from("test3".to_string()),
+ timestamp: OffsetDateTime::now_utc(),
+ duration: 1,
+ exit: 0,
+ command: "/bin/cmd -a".to_string(),
+ cwd: "/toot".to_string(),
+ session: "sesh1".to_string(),
+ hostname: "hostn".to_string(),
+ deleted_at: None,
+ };
+ let stats = HistoryStats {
+ next: Some(next.clone()),
+ previous: Some(prev.clone()),
+ total: 2,
+ average_duration: 3,
+ exits: Vec::new(),
+ day_of_week: Vec::new(),
+ duration_over_time: Vec::new(),
+ };
+ (history, stats)
+ }
+
+ #[test]
+ fn test_output_looks_correct_for_ultracompact() {
+ let backend = TestBackend::new(22, 5);
+ let mut terminal = Terminal::new(backend).expect("Could not create terminal");
+ let chunk = Rect::new(0, 0, 22, 5);
+ let (history, stats) = mock_history_stats();
+ let prev = stats.previous.clone().unwrap();
+ let next = stats.next.clone().unwrap();
+
+ let mut manager = ThemeManager::new(Some(true), Some("".to_string()));
+ let theme = manager.load_theme("(none)", None);
+ let _ = terminal.draw(|f| draw_ultracompact(f, chunk, &history, &stats, &theme));
+ let mut lines = [" "; 5].map(|l| Line::from(l));
+ for (n, entry) in [prev, history, next].iter().enumerate() {
+ let mut l = lines[n].to_string();
+ l.replace_range(0..entry.command.len(), &entry.command);
+ lines[n] = Line::from(l);
+ }
+
+ terminal.backend().assert_buffer_lines(lines);
+ }
+}
diff --git a/crates/atuin/src/command/client/search/interactive.rs b/crates/atuin/src/command/client/search/interactive.rs
index f2471879..f50568e7 100644
--- a/crates/atuin/src/command/client/search/interactive.rs
+++ b/crates/atuin/src/command/client/search/interactive.rs
@@ -17,7 +17,7 @@ use super::{
};
use atuin_client::{
database::{Database, current_context},
- history::{History, HistoryStats, store::HistoryStore},
+ history::{History, HistoryId, HistoryStats, store::HistoryStore},
settings::{
CursorStyle, ExitMode, FilterMode, KeymapMode, PreviewStrategy, SearchMode, Settings,
},
@@ -54,6 +54,7 @@ const TAB_TITLES: [&str; 2] = ["Search", "Inspect"];
pub enum InputAction {
Accept(usize),
+ AcceptInspecting,
Copy(usize),
Delete(usize),
ReturnOriginal,
@@ -62,6 +63,49 @@ pub enum InputAction {
Redraw,
}
+#[derive(Clone)]
+pub struct InspectingState {
+ current: Option<HistoryId>,
+ next: Option<HistoryId>,
+ previous: Option<HistoryId>,
+}
+
+impl InspectingState {
+ pub fn move_to_previous(&mut self) {
+ let previous = self.previous.clone();
+ self.reset();
+ self.current = previous;
+ }
+
+ pub fn move_to_next(&mut self) {
+ let next = self.next.clone();
+ self.reset();
+ self.current = next;
+ }
+
+ pub fn reset(&mut self) {
+ self.current = None;
+ self.next = None;
+ self.previous = None;
+ }
+}
+
+pub fn to_compactness(f: &Frame, settings: &Settings) -> Compactness {
+ if match settings.style {
+ atuin_client::settings::Style::Auto => f.area().height < 14,
+ atuin_client::settings::Style::Compact => true,
+ atuin_client::settings::Style::Full => false,
+ } {
+ if settings.auto_hide_height != 0 && f.area().height <= settings.auto_hide_height {
+ Compactness::Ultracompact
+ } else {
+ Compactness::Compact
+ }
+ } else {
+ Compactness::Full
+ }
+}
+
#[allow(clippy::struct_field_names)]
pub struct State {
history_count: i64,
@@ -76,14 +120,23 @@ pub struct State {
current_cursor: Option<CursorStyle>,
tab_index: usize,
+ pub inspecting_state: InspectingState,
+
search: SearchState,
engine: Box<dyn SearchEngine>,
now: Box<dyn Fn() -> OffsetDateTime + Send>,
}
#[derive(Clone, Copy)]
+pub enum Compactness {
+ Ultracompact,
+ Compact,
+ Full,
+}
+
+#[derive(Clone, Copy)]
struct StyleState {
- compact: bool,
+ compactness: Compactness,
invert: bool,
inner_width: usize,
}
@@ -96,6 +149,11 @@ impl State {
) -> Result<Vec<History>> {
let results = self.engine.query(&self.search, db).await?;
+ self.inspecting_state = InspectingState {
+ current: None,
+ next: None,
+ previous: None,
+ };
self.results_state.select(0);
self.results_len = results.len();
@@ -227,7 +285,13 @@ impl State {
KeyCode::Char('c' | 'g') if ctrl => Some(InputAction::ReturnOriginal),
KeyCode::Esc if esc_allow_exit => Some(Self::handle_key_exit(settings)),
KeyCode::Char('[') if ctrl && esc_allow_exit => Some(Self::handle_key_exit(settings)),
- KeyCode::Tab => Some(InputAction::Accept(self.results_state.selected())),
+ KeyCode::Tab => match self.tab_index {
+ 0 => Some(InputAction::Accept(self.results_state.selected())),
+
+ 1 => Some(InputAction::AcceptInspecting),
+
+ _ => panic!("invalid tab index on input"),
+ },
KeyCode::Right if cursor_at_end_of_line && settings.keys.accept_past_line_end => {
Some(InputAction::Accept(self.results_state.selected()))
}
@@ -532,6 +596,7 @@ impl State {
fn scroll_down(&mut self, scroll_len: usize) {
let i = self.results_state.selected().saturating_sub(scroll_len);
+ self.inspecting_state.reset();
self.results_state.select(i);
}
@@ -539,6 +604,7 @@ impl State {
let i = self.results_state.selected() + scroll_len;
self.results_state
.select(i.min(self.results_len.saturating_sub(1)));
+ self.inspecting_state.reset();
}
#[allow(clippy::cast_possible_truncation)]
@@ -548,7 +614,7 @@ impl State {
results: &[History],
selected: usize,
tab_index: usize,
- compact: bool,
+ compactness: Compactness,
border_size: u16,
preview_width: u16,
) -> u16 {
@@ -608,14 +674,13 @@ impl State {
}) + border_size * 2
} else if settings.show_preview && settings.preview.strategy == PreviewStrategy::Fixed {
settings.max_preview_height + border_size * 2
- } else if compact || tab_index == 1 {
+ } else if !matches!(compactness, Compactness::Full) || tab_index == 1 {
0
} else {
1
}
}
- #[allow(clippy::cast_possible_truncation)]
#[allow(clippy::bool_to_int_with_if)]
#[allow(clippy::too_many_lines)]
fn draw(
@@ -623,33 +688,31 @@ impl State {
f: &mut Frame,
results: &[History],
stats: Option<HistoryStats>,
+ inspecting: Option<&History>,
settings: &Settings,
theme: &Theme,
) {
- let compact = match settings.style {
- atuin_client::settings::Style::Auto => f.area().height < 14,
- atuin_client::settings::Style::Compact => true,
- atuin_client::settings::Style::Full => false,
- };
+ let compactness = to_compactness(f, settings);
let invert = settings.invert;
- let border_size = if compact { 0 } else { 1 };
+ let border_size = match compactness {
+ Compactness::Full => 1,
+ _ => 0,
+ };
let preview_width = f.area().width - 2;
let preview_height = Self::calc_preview_height(
settings,
results,
self.results_state.selected(),
self.tab_index,
- compact,
+ compactness,
border_size,
preview_width,
);
- let show_help = settings.show_help && (!compact || f.area().height > 1);
+ let show_help =
+ settings.show_help && (matches!(compactness, Compactness::Full) || f.area().height > 1);
// This is an OR, as it seems more likely for someone to wish to override
// tabs unexpectedly being missed, than unexpectedly present.
- let hide_extra = settings.auto_hide_height != 0
- && compact
- && f.area().height <= settings.auto_hide_height;
- let show_tabs = settings.show_tabs && !hide_extra;
+ let show_tabs = settings.show_tabs && !matches!(compactness, Compactness::Ultracompact);
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(0)
@@ -663,22 +726,23 @@ impl State {
Constraint::Length(if show_tabs { 1 } else { 0 }), // tabs
Constraint::Length(if show_help { 1 } else { 0 }), // header (sic)
]
- } else if hide_extra {
- [
- Constraint::Length(if show_help { 1 } else { 0 }), // header
- Constraint::Length(0), // tabs
- Constraint::Min(1), // results list
- Constraint::Length(0),
- Constraint::Length(0),
- ]
} else {
- [
- Constraint::Length(if show_help { 1 } else { 0 }), // header
- Constraint::Length(if show_tabs { 1 } else { 0 }), // tabs
- Constraint::Min(1), // results list
- Constraint::Length(1 + border_size), // input
- Constraint::Length(preview_height), // preview
- ]
+ match compactness {
+ Compactness::Ultracompact => [
+ Constraint::Length(if show_help { 1 } else { 0 }), // header
+ Constraint::Length(0), // tabs
+ Constraint::Min(1), // results list
+ Constraint::Length(0),
+ Constraint::Length(0),
+ ],
+ _ => [
+ Constraint::Length(if show_help { 1 } else { 0 }), // header
+ Constraint::Length(if show_tabs { 1 } else { 0 }), // tabs
+ Constraint::Min(1), // results list
+ Constraint::Length(1 + border_size), // input
+ Constraint::Length(preview_height), // preview
+ ],
+ }
}
.as_ref(),
)
@@ -700,13 +764,13 @@ impl State {
.block(Block::default().borders(Borders::NONE))
.select(self.tab_index)
.style(Style::default())
- .highlight_style(Style::default().bold().white().on_black());
+ .highlight_style(theme.as_style(Meaning::Important));
f.render_widget(tabs, tabs_chunk);
}
let style = StyleState {
- compact,
+ compactness,
invert,
inner_width: input_chunk.width.into(),
};
@@ -732,15 +796,18 @@ impl State {
let stats_tab = self.build_stats(theme);
f.render_widget(stats_tab, header_chunks[2]);
- let indicator: String = if !hide_extra {
- " > ".to_string()
- } else if self.switched_search_mode {
- format!("S{}>", self.search_mode.as_str().chars().next().unwrap())
- } else {
- format!(
- "{}> ",
- self.search.filter_mode.as_str().chars().next().unwrap()
- )
+ let indicator: String = match compactness {
+ Compactness::Ultracompact => {
+ if self.switched_search_mode {
+ format!("S{}>", self.search_mode.as_str().chars().next().unwrap())
+ } else {
+ format!(
+ "{}> ",
+ self.search.filter_mode.as_str().chars().next().unwrap()
+ )
+ }
+ }
+ _ => " > ".to_string(),
};
match self.tab_index {
@@ -775,11 +842,16 @@ impl State {
.alignment(Alignment::Center);
f.render_widget(message, results_list_chunk);
} else {
+ let inspecting = match inspecting {
+ Some(inspecting) => inspecting,
+ None => &results[self.results_state.selected()],
+ };
super::inspector::draw(
f,
results_list_chunk,
- &results[self.results_state.selected()],
+ inspecting,
&stats.expect("Drawing inspector, but no stats"),
+ settings,
theme,
settings.timezone,
);
@@ -800,33 +872,48 @@ impl State {
}
}
- if !hide_extra {
- let input = self.build_input(style);
- f.render_widget(input, input_chunk);
-
- let preview_width = if compact {
- preview_width
- } else {
- preview_width - 2
+ if !matches!(compactness, Compactness::Ultracompact) {
+ let preview_width = match compactness {
+ Compactness::Full => preview_width - 2,
+ _ => preview_width,
};
let preview = self.build_preview(
results,
- compact,
+ compactness,
preview_width,
preview_chunk.width.into(),
theme,
);
- f.render_widget(preview, preview_chunk);
+ self.draw_preview(f, style, input_chunk, compactness, preview_chunk, preview);
+ }
+ }
- let extra_width = UnicodeWidthStr::width(self.search.input.substring());
+ #[allow(clippy::cast_possible_truncation)]
+ fn draw_preview(
+ &self,
+ f: &mut Frame,
+ style: StyleState,
+ input_chunk: Rect,
+ compactness: Compactness,
+ preview_chunk: Rect,
+ preview: Paragraph,
+ ) {
+ let input = self.build_input(style);
+ f.render_widget(input, input_chunk);
- let cursor_offset = if compact { 0 } else { 1 };
- f.set_cursor_position((
- // Put cursor past the end of the input text
- input_chunk.x + extra_width as u16 + PREFIX_LENGTH + 1 + cursor_offset,
- input_chunk.y + cursor_offset,
- ));
- }
+ f.render_widget(preview, preview_chunk);
+
+ let extra_width = UnicodeWidthStr::width(self.search.input.substring());
+
+ let cursor_offset = match compactness {
+ Compactness::Full => 1,
+ _ => 0,
+ };
+ f.set_cursor_position((
+ // Put cursor past the end of the input text
+ input_chunk.x + extra_width as u16 + PREFIX_LENGTH + 1 + cursor_offset,
+ input_chunk.y + cursor_offset,
+ ));
}
fn build_title(&self, theme: &Theme) -> Paragraph<'_> {
@@ -916,21 +1003,24 @@ impl State {
show_numeric_shortcuts,
);
- if style.compact {
- results_list
- } else if style.invert {
- results_list.block(
- Block::default()
- .borders(Borders::LEFT | Borders::RIGHT)
- .border_type(BorderType::Rounded)
- .title(format!("{:─>width$}", "", width = style.inner_width - 2)),
- )
- } else {
- results_list.block(
- Block::default()
- .borders(Borders::TOP | Borders::LEFT | Borders::RIGHT)
- .border_type(BorderType::Rounded),
- )
+ match style.compactness {
+ Compactness::Full => {
+ if style.invert {
+ results_list.block(
+ Block::default()
+ .borders(Borders::LEFT | Borders::RIGHT)
+ .border_type(BorderType::Rounded)
+ .title(format!("{:─>width$}", "", width = style.inner_width - 2)),
+ )
+ } else {
+ results_list.block(
+ Block::default()
+ .borders(Borders::TOP | Borders::LEFT | Borders::RIGHT)
+ .border_type(BorderType::Rounded),
+ )
+ }
+ }
+ _ => results_list,
}
}
@@ -947,28 +1037,31 @@ impl State {
debug_assert!(mode_width >= mode.len(), "mode name '{mode}' is too long!");
let input = format!("[{pref}{mode:^mode_width$}] {}", self.search.input.as_str(),);
let input = Paragraph::new(input);
- if style.compact {
- input
- } else if style.invert {
- input.block(
- Block::default()
- .borders(Borders::LEFT | Borders::RIGHT | Borders::TOP)
- .border_type(BorderType::Rounded),
- )
- } else {
- input.block(
- Block::default()
- .borders(Borders::LEFT | Borders::RIGHT)
- .border_type(BorderType::Rounded)
- .title(format!("{:─>width$}", "", width = style.inner_width - 2)),
- )
+ match style.compactness {
+ Compactness::Full => {
+ if style.invert {
+ input.block(
+ Block::default()
+ .borders(Borders::LEFT | Borders::RIGHT | Borders::TOP)
+ .border_type(BorderType::Rounded),
+ )
+ } else {
+ input.block(
+ Block::default()
+ .borders(Borders::LEFT | Borders::RIGHT)
+ .border_type(BorderType::Rounded)
+ .title(format!("{:─>width$}", "", width = style.inner_width - 2)),
+ )
+ }
+ }
+ _ => input,
}
}
fn build_preview(
&self,
results: &[History],
- compact: bool,
+ compactness: Compactness,
preview_width: u16,
chunk_width: usize,
theme: &Theme,
@@ -991,15 +1084,14 @@ impl State {
.join("\n")
};
- if compact {
- Paragraph::new(command).style(theme.as_style(Meaning::Annotation))
- } else {
- Paragraph::new(command).block(
+ match compactness {
+ Compactness::Full => Paragraph::new(command).block(
Block::default()
.borders(Borders::BOTTOM | Borders::LEFT | Borders::RIGHT)
.border_type(BorderType::Rounded)
.title(format!("{:─>width$}", "", width = chunk_width - 2)),
- )
+ ),
+ _ => Paragraph::new(command).style(theme.as_style(Meaning::Annotation)),
}
}
}
@@ -1158,6 +1250,11 @@ pub async fn history(
switched_search_mode: false,
search_mode,
tab_index: 0,
+ inspecting_state: InspectingState {
+ current: None,
+ next: None,
+ previous: None,
+ },
search: SearchState {
input,
filter_mode: settings
@@ -1194,9 +1291,19 @@ pub async fn history(
}
let mut stats: Option<HistoryStats> = None;
+ let mut inspecting: Option<History> = None;
let accept;
let result = 'render: loop {
- terminal.draw(|f| app.draw(f, &results, stats.clone(), settings, theme))?;
+ terminal.draw(|f| {
+ app.draw(
+ f,
+ &results,
+ stats.clone(),
+ inspecting.as_ref(),
+ settings,
+ theme,
+ );
+ })?;
let initial_input = app.search.input.as_str().to_owned();
let initial_filter_mode = app.search.filter_mode;
@@ -1217,6 +1324,7 @@ pub async fn history(
app.results_len -= 1;
let selected = app.results_state.selected();
if selected == app.results_len {
+ app.inspecting_state.reset();
app.results_state.select(selected - 1);
}
@@ -1233,7 +1341,7 @@ pub async fn history(
},
InputAction::Redraw => {
terminal.clear()?;
- terminal.draw(|f| app.draw(f, &results, stats.clone(), settings, theme))?;
+ terminal.draw(|f| app.draw(f, &results, stats.clone(), inspecting.as_ref(), settings, theme))?;
},
r => {
accept = app.accept;
@@ -1258,11 +1366,39 @@ pub async fn history(
results = app.query_results(&mut db, settings.smart_sort).await?;
}
+ let inspecting_id = app.inspecting_state.clone().current;
+ // If inspecting ID is not the current inspecting History, update it.
+ match inspecting_id {
+ Some(inspecting_id) => {
+ if inspecting.is_none() || inspecting_id != inspecting.clone().unwrap().id {
+ inspecting = db.load(inspecting_id.0.as_str()).await?;
+ }
+ }
+ _ => {
+ inspecting = None;
+ }
+ }
+
stats = if app.tab_index == 0 {
None
} else if !results.is_empty() {
- let selected = results[app.results_state.selected()].clone();
- Some(db.stats(&selected).await?)
+ // If we have stats, then we can indicate next available IDs. This avoids passing
+ // around a database object, or a full stats object.
+ let selected = match inspecting.clone() {
+ Some(insp) => insp,
+ None => results[app.results_state.selected()].clone(),
+ };
+ let stats = db.stats(&selected).await?;
+ app.inspecting_state.current = Some(selected.id);
+ app.inspecting_state.previous = match stats.previous.clone() {
+ Some(p) => Some(p.id),
+ _ => None,
+ };
+ app.inspecting_state.next = match stats.next.clone() {
+ Some(p) => Some(p.id),
+ _ => None,
+ };
+ Some(stats)
} else {
None
};
@@ -1275,6 +1411,25 @@ pub async fn history(
}
match result {
+ InputAction::AcceptInspecting => {
+ match inspecting {
+ Some(result) => {
+ let mut command = result.command;
+ if accept
+ && matches!(
+ Shell::from_env(),
+ Shell::Zsh | Shell::Fish | Shell::Bash | Shell::Xonsh
+ )
+ {
+ command = String::from("__atuin_accept__:") + &command;
+ }
+
+ // index is in bounds so we return that entry
+ Ok(command)
+ }
+ None => Ok(String::new()),
+ }
+ }
InputAction::Accept(index) if index < results.len() => {
let mut command = results.swap_remove(index).command;
@@ -1341,7 +1496,7 @@ mod tests {
use crate::command::client::search::engines::{self, SearchState};
use crate::command::client::search::history_list::ListState;
- use super::State;
+ use super::{Compactness, InspectingState, State};
#[test]
#[allow(clippy::too_many_lines)]
@@ -1410,7 +1565,7 @@ mod tests {
&results,
0_usize,
0_usize,
- false,
+ Compactness::Full,
1,
80,
);
@@ -1420,7 +1575,7 @@ mod tests {
&results,
1_usize,
0_usize,
- false,
+ Compactness::Full,
1,
80,
);
@@ -1430,7 +1585,7 @@ mod tests {
&results,
2_usize,
0_usize,
- false,
+ Compactness::Full,
1,
80,
);
@@ -1440,7 +1595,7 @@ mod tests {
&results,
0_usize,
0_usize,
- false,
+ Compactness::Full,
1,
66,
);
@@ -1450,7 +1605,7 @@ mod tests {
&results,
2_usize,
0_usize,
- false,
+ Compactness::Full,
1,
80,
);
@@ -1460,7 +1615,7 @@ mod tests {
&results,
1_usize,
0_usize,
- false,
+ Compactness::Full,
1,
80,
);
@@ -1470,7 +1625,7 @@ mod tests {
&results,
1_usize,
0_usize,
- false,
+ Compactness::Full,
1,
20,
);
@@ -1480,7 +1635,7 @@ mod tests {
&results,
1_usize,
0_usize,
- false,
+ Compactness::Full,
1,
20,
);
@@ -1512,6 +1667,11 @@ mod tests {
prefix: false,
current_cursor: None,
tab_index: 0,
+ inspecting_state: InspectingState {
+ current: None,
+ next: None,
+ previous: None,
+ },
search: SearchState {
input: String::new().into(),
filter_mode: FilterMode::Directory,
@@ -1558,6 +1718,11 @@ mod tests {
prefix: false,
current_cursor: None,
tab_index: 0,
+ inspecting_state: InspectingState {
+ current: None,
+ next: None,
+ previous: None,
+ },
search: SearchState {
input: String::new().into(),
filter_mode: FilterMode::Global,