summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPatrick Decat <pdecat@gmail.com>2023-03-05 19:36:35 +0100
committerGitHub <noreply@github.com>2023-03-05 18:36:35 +0000
commit24388033a594e664cc1718fda8ba802399b0d038 (patch)
tree7d24aaa677d30fe56e608c3084f7496d642a707f
parent9e05d747eaf87905d0a67b23479dd374541f9180 (diff)
Show preview of selected command (#643)
* Only show help if terminal strictly has more than one line * There is no border around the input line in compact mode * Add command preview * Dynamic preview height * Avoid extra allocations Co-authored-by: Conrad Ludgate <oon@conradludgate.com> * Address clippy error * Merge normal and compact views code * Add show_preview setting, disabled by default * Convert `bool` to `int` with `if` for legibility --------- Co-authored-by: Conrad Ludgate <oon@conradludgate.com>
-rw-r--r--atuin-client/src/settings.rs2
-rw-r--r--src/command/client/search/interactive.rs254
2 files changed, 139 insertions, 117 deletions
diff --git a/atuin-client/src/settings.rs b/atuin-client/src/settings.rs
index 2975edba..95b01c86 100644
--- a/atuin-client/src/settings.rs
+++ b/atuin-client/src/settings.rs
@@ -112,6 +112,7 @@ pub struct Settings {
pub filter_mode: FilterMode,
pub filter_mode_shell_up_key_binding: FilterMode,
pub shell_up_key_binding: bool,
+ pub show_preview: bool,
pub exit_mode: ExitMode,
#[serde(with = "serde_regex", default = "RegexSet::empty")]
pub history_filter: RegexSet,
@@ -296,6 +297,7 @@ impl Settings {
.set_default("filter_mode", "global")?
.set_default("filter_mode_shell_up_key_binding", "global")?
.set_default("shell_up_key_binding", false)?
+ .set_default("show_preview", false)?
.set_default("exit_mode", "return-original")?
.set_default("session_token", "")?
.set_default("style", "auto")?
diff --git a/src/command/client/search/interactive.rs b/src/command/client/search/interactive.rs
index 903c3362..9e7d3409 100644
--- a/src/command/client/search/interactive.rs
+++ b/src/command/client/search/interactive.rs
@@ -175,107 +175,43 @@ impl State {
}
#[allow(clippy::cast_possible_truncation)]
- fn draw<T: Backend>(&mut self, f: &mut Frame<'_, T>, results: &[History]) {
- let chunks = Layout::default()
- .direction(Direction::Vertical)
- .margin(0)
- .constraints([
- Constraint::Length(3),
- Constraint::Min(1),
- Constraint::Length(3),
- ])
- .split(f.size());
-
- let top_chunks = Layout::default()
- .direction(Direction::Horizontal)
- .constraints([Constraint::Percentage(50); 2])
- .split(chunks[0]);
-
- let top_left_chunks = Layout::default()
- .direction(Direction::Vertical)
- .constraints([Constraint::Length(1); 3])
- .split(top_chunks[0]);
-
- let top_right_chunks = Layout::default()
- .direction(Direction::Vertical)
- .constraints([Constraint::Length(1); 3])
- .split(top_chunks[1]);
-
- let title = if self.update_needed.is_some() {
- let version = self.update_needed.clone().unwrap();
-
- Paragraph::new(Text::from(Span::styled(
- format!(" Atuin v{VERSION} - UPDATE AVAILABLE {version}"),
- Style::default().add_modifier(Modifier::BOLD).fg(Color::Red),
- )))
+ #[allow(clippy::bool_to_int_with_if)]
+ fn draw<T: Backend>(
+ &mut self,
+ f: &mut Frame<'_, T>,
+ results: &[History],
+ compact: bool,
+ show_preview: bool,
+ ) {
+ let border_size = if compact { 0 } else { 1 };
+ let preview_width = f.size().width - 2;
+ let preview_height = if show_preview {
+ 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(
+ 4,
+ (v.command.len() as u16 + preview_width - 1 - border_size)
+ / (preview_width - border_size),
+ )
+ }) + border_size * 2
+ } else if compact {
+ 0
} else {
- Paragraph::new(Text::from(Span::styled(
- format!(" Atuin v{VERSION}"),
- Style::default().add_modifier(Modifier::BOLD),
- )))
+ 1
};
-
- let help = vec![
- Span::raw(" Press "),
- Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
- Span::raw(" to exit."),
- ];
-
- let help = Paragraph::new(Text::from(Spans::from(help)));
- let stats = Paragraph::new(Text::from(Span::raw(format!(
- "history count: {} ",
- self.history_count
- ))));
-
- f.render_widget(title, top_left_chunks[1]);
- f.render_widget(help, top_left_chunks[2]);
- f.render_widget(stats.alignment(Alignment::Right), top_right_chunks[1]);
-
- let results = HistoryList::new(results).block(
- Block::default()
- .borders(Borders::TOP | Borders::LEFT | Borders::RIGHT)
- .border_type(BorderType::Rounded),
- );
-
- f.render_stateful_widget(results, chunks[1], &mut self.results_state);
-
- 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]);
-
- let width = UnicodeWidthStr::width(self.input.substring());
- f.set_cursor(
- // Put cursor past the end of the input text
- chunks[2].x + width as u16 + PREFIX_LENGTH + 2,
- // Move one line down, from the border to the input line
- chunks[2].y + 1,
- );
- }
-
- #[allow(clippy::cast_possible_truncation)]
- fn draw_compact<T: Backend>(&mut self, f: &mut Frame<'_, T>, results: &[History]) {
+ let show_help = !compact || f.size().height > 1;
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(0)
.horizontal_margin(1)
.constraints(
[
- Constraint::Length(1),
+ Constraint::Length(if show_help { 1 } else { 0 }),
Constraint::Min(1),
- Constraint::Length(1),
+ Constraint::Length(1 + border_size),
+ Constraint::Length(preview_height),
]
.as_ref(),
)
@@ -293,48 +229,136 @@ impl State {
)
.split(chunks[0]);
- let title = Paragraph::new(Text::from(Span::styled(
- format!("Atuin v{VERSION}"),
- Style::default().fg(Color::DarkGray),
- )));
+ let title = self.build_title();
+ f.render_widget(title, header_chunks[0]);
+
+ let help = self.build_help();
+ f.render_widget(help, header_chunks[1]);
+
+ let stats = self.build_stats();
+ f.render_widget(stats, header_chunks[2]);
+
+ let results_list = Self::build_results_list(compact, results);
+ f.render_stateful_widget(results_list, chunks[1], &mut self.results_state);
+
+ let input = self.build_input(compact, chunks[2].width.into());
+ f.render_widget(input, chunks[2]);
+
+ let preview = self.build_preview(results, compact, preview_width, chunks[3].width.into());
+ f.render_widget(preview, chunks[3]);
+
+ let extra_width = UnicodeWidthStr::width(self.input.substring());
+
+ let cursor_offset = if compact { 0 } else { 1 };
+ f.set_cursor(
+ // Put cursor past the end of the input text
+ chunks[2].x + extra_width as u16 + PREFIX_LENGTH + 1 + cursor_offset,
+ chunks[2].y + cursor_offset,
+ );
+ }
+
+ fn build_title(&mut self) -> Paragraph {
+ let title = if self.update_needed.is_some() {
+ let version = self.update_needed.clone().unwrap();
+
+ Paragraph::new(Text::from(Span::styled(
+ format!(" Atuin v{VERSION} - UPDATE AVAILABLE {version}"),
+ Style::default().add_modifier(Modifier::BOLD).fg(Color::Red),
+ )))
+ } else {
+ Paragraph::new(Text::from(Span::styled(
+ format!(" Atuin v{VERSION}"),
+ Style::default().add_modifier(Modifier::BOLD),
+ )))
+ };
+ title
+ }
+ #[allow(clippy::unused_self)]
+ fn build_help(&mut self) -> Paragraph {
let help = Paragraph::new(Text::from(Spans::from(vec![
Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
Span::raw(" to exit"),
])))
.style(Style::default().fg(Color::DarkGray))
.alignment(Alignment::Center);
+ help
+ }
+ fn build_stats(&mut self) -> Paragraph {
let stats = Paragraph::new(Text::from(Span::raw(format!(
"history count: {}",
self.history_count,
))))
.style(Style::default().fg(Color::DarkGray))
.alignment(Alignment::Right);
+ stats
+ }
- f.render_widget(title, header_chunks[0]);
- f.render_widget(help, header_chunks[1]);
- f.render_widget(stats, header_chunks[2]);
-
- let results = HistoryList::new(results);
- f.render_stateful_widget(results, chunks[1], &mut self.results_state);
+ fn build_results_list(compact: bool, results: &[History]) -> HistoryList {
+ let results_list = if compact {
+ HistoryList::new(results)
+ } else {
+ HistoryList::new(results).block(
+ Block::default()
+ .borders(Borders::TOP | Borders::LEFT | Borders::RIGHT)
+ .border_type(BorderType::Rounded),
+ )
+ };
+ results_list
+ }
+ fn build_input(&mut self, compact: bool, chunk_width: usize) -> Paragraph {
let input = format!(
"[{:^14}] {}",
self.filter_mode.as_str(),
self.input.as_str(),
);
- let input = Paragraph::new(input);
- f.render_widget(input, chunks[2]);
-
- let extra_width = UnicodeWidthStr::width(self.input.substring());
+ let input = if compact {
+ Paragraph::new(input)
+ } else {
+ Paragraph::new(input).block(
+ Block::default()
+ .borders(Borders::LEFT | Borders::RIGHT)
+ .border_type(BorderType::Rounded)
+ .title(format!("{:─>width$}", "", width = chunk_width - 2)),
+ )
+ };
+ input
+ }
- f.set_cursor(
- // Put cursor past the end of the input text
- chunks[2].x + extra_width as u16 + PREFIX_LENGTH + 1,
- // Move one line down, from the border to the input line
- chunks[2].y + 1,
- );
+ fn build_preview(
+ &mut self,
+ results: &[History],
+ compact: bool,
+ preview_width: u16,
+ chunk_width: usize,
+ ) -> Paragraph {
+ let selected = self.results_state.selected();
+ let command = if results.is_empty() {
+ String::new()
+ } else {
+ use itertools::Itertools as _;
+ let s = &results[selected].command;
+ s.char_indices()
+ .step_by(preview_width.into())
+ .map(|(i, _)| i)
+ .chain(Some(s.len()))
+ .tuple_windows()
+ .map(|(a, b)| &s[a..b])
+ .join("\n")
+ };
+ let preview = if compact {
+ Paragraph::new(command).style(Style::default().fg(Color::DarkGray))
+ } else {
+ Paragraph::new(command).block(
+ Block::default()
+ .borders(Borders::BOTTOM | Borders::LEFT | Borders::RIGHT)
+ .border_type(BorderType::Rounded)
+ .title(format!("{:─>width$}", "", width = chunk_width - 2)),
+ )
+ };
+ preview
}
}
@@ -420,11 +444,7 @@ pub async fn history(
atuin_client::settings::Style::Compact => true,
atuin_client::settings::Style::Full => false,
};
- if compact {
- terminal.draw(|f| app.draw_compact(f, &results))?;
- } else {
- terminal.draw(|f| app.draw(f, &results))?;
- }
+ terminal.draw(|f| app.draw(f, &results, compact, settings.show_preview))?;
let initial_input = app.input.as_str().to_owned();
let initial_filter_mode = app.filter_mode;