Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ultracompact Mode for Inspector (was RfC: Ultracompact Mode) #2319

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 30 additions & 5 deletions crates/atuin-client/src/theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -233,6 +233,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
Expand Down Expand Up @@ -280,7 +288,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()),
Expand All @@ -290,6 +301,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([
Expand Down Expand Up @@ -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()
}
},
}
Expand Down Expand Up @@ -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(
"
Expand All @@ -692,7 +717,7 @@ mod theme_tests {
nunsolarized_theme
.as_style(Meaning::Guidance)
.foreground_color,
Some(Color::DarkBlue)
None
);

testing_logger::validate(|captured_logs| {
Expand Down
187 changes: 165 additions & 22 deletions crates/atuin/src/command/client/search/inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ use ratatui::{
layout::Rect,
prelude::{Constraint, Direction, Layout},
style::Style,
text::{Span, Text},
widgets::{Bar, BarChart, BarGroup, Block, Borders, Padding, Paragraph, Row, Table},
Frame,
};

use super::duration::format_duration;

use super::super::theme::{Meaning, Theme};
use super::interactive::{InputAction, State};
use super::interactive::{to_compactness, Compactness, InputAction, State};

#[allow(clippy::cast_sign_loss)]
fn u64_or_zero(num: i64) -> u64 {
Expand All @@ -33,52 +34,81 @@ 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("No previous command".to_string(), |prev| prev.command),
.map_or("[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))
});

let next = Paragraph::new(
stats
.next
.clone()
.map_or("No next command".to_string(), |next| next.command),
.map_or("[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]);
Expand Down Expand Up @@ -251,6 +281,32 @@ fn draw_stats_charts(f: &mut Frame<'_>, parent: Rect, stats: &HistoryStats, them
}

pub fn draw(
f: &mut Frame<'_>,
chunk: Rect,
history: &History,
stats: &HistoryStats,
settings: &Settings,
theme: &Theme,
) {
let compactness = to_compactness(f, settings);

match compactness {
Compactness::Ultracompact => draw_ultracompact(f, chunk, history, stats, theme),
_ => draw_full(f, chunk, history, stats, theme),
}
}

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,
Expand All @@ -267,15 +323,15 @@ 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, stats, theme);
draw_stats_charts(f, stats_layout[1], stats, theme);
}

// 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,
Expand All @@ -284,6 +340,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);
}
}
Loading
Loading