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>
This commit is contained in:
parent
9e05d747ea
commit
24388033a5
2 changed files with 139 additions and 117 deletions
|
@ -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")?
|
||||
|
|
|
@ -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 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
|
||||
}
|
||||
|
||||
let extra_width = UnicodeWidthStr::width(self.input.substring());
|
||||
|
||||
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;
|
||||
|
|
Loading…
Reference in a new issue