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: FilterMode,
|
||||||
pub filter_mode_shell_up_key_binding: FilterMode,
|
pub filter_mode_shell_up_key_binding: FilterMode,
|
||||||
pub shell_up_key_binding: bool,
|
pub shell_up_key_binding: bool,
|
||||||
|
pub show_preview: bool,
|
||||||
pub exit_mode: ExitMode,
|
pub exit_mode: ExitMode,
|
||||||
#[serde(with = "serde_regex", default = "RegexSet::empty")]
|
#[serde(with = "serde_regex", default = "RegexSet::empty")]
|
||||||
pub history_filter: RegexSet,
|
pub history_filter: RegexSet,
|
||||||
|
@ -296,6 +297,7 @@ impl Settings {
|
||||||
.set_default("filter_mode", "global")?
|
.set_default("filter_mode", "global")?
|
||||||
.set_default("filter_mode_shell_up_key_binding", "global")?
|
.set_default("filter_mode_shell_up_key_binding", "global")?
|
||||||
.set_default("shell_up_key_binding", false)?
|
.set_default("shell_up_key_binding", false)?
|
||||||
|
.set_default("show_preview", false)?
|
||||||
.set_default("exit_mode", "return-original")?
|
.set_default("exit_mode", "return-original")?
|
||||||
.set_default("session_token", "")?
|
.set_default("session_token", "")?
|
||||||
.set_default("style", "auto")?
|
.set_default("style", "auto")?
|
||||||
|
|
|
@ -175,107 +175,43 @@ impl State {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::cast_possible_truncation)]
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
fn draw<T: Backend>(&mut self, f: &mut Frame<'_, T>, results: &[History]) {
|
#[allow(clippy::bool_to_int_with_if)]
|
||||||
let chunks = Layout::default()
|
fn draw<T: Backend>(
|
||||||
.direction(Direction::Vertical)
|
&mut self,
|
||||||
.margin(0)
|
f: &mut Frame<'_, T>,
|
||||||
.constraints([
|
results: &[History],
|
||||||
Constraint::Length(3),
|
compact: bool,
|
||||||
Constraint::Min(1),
|
show_preview: bool,
|
||||||
Constraint::Length(3),
|
) {
|
||||||
])
|
let border_size = if compact { 0 } else { 1 };
|
||||||
.split(f.size());
|
let preview_width = f.size().width - 2;
|
||||||
|
let preview_height = if show_preview {
|
||||||
let top_chunks = Layout::default()
|
let longest_command = results
|
||||||
.direction(Direction::Horizontal)
|
.iter()
|
||||||
.constraints([Constraint::Percentage(50); 2])
|
.max_by(|h1, h2| h1.command.len().cmp(&h2.command.len()));
|
||||||
.split(chunks[0]);
|
longest_command.map_or(0, |v| {
|
||||||
|
std::cmp::min(
|
||||||
let top_left_chunks = Layout::default()
|
4,
|
||||||
.direction(Direction::Vertical)
|
(v.command.len() as u16 + preview_width - 1 - border_size)
|
||||||
.constraints([Constraint::Length(1); 3])
|
/ (preview_width - border_size),
|
||||||
.split(top_chunks[0]);
|
)
|
||||||
|
}) + border_size * 2
|
||||||
let top_right_chunks = Layout::default()
|
} else if compact {
|
||||||
.direction(Direction::Vertical)
|
0
|
||||||
.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),
|
|
||||||
)))
|
|
||||||
} else {
|
} else {
|
||||||
Paragraph::new(Text::from(Span::styled(
|
1
|
||||||
format!(" Atuin v{VERSION}"),
|
|
||||||
Style::default().add_modifier(Modifier::BOLD),
|
|
||||||
)))
|
|
||||||
};
|
};
|
||||||
|
let show_help = !compact || f.size().height > 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 chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.margin(0)
|
.margin(0)
|
||||||
.horizontal_margin(1)
|
.horizontal_margin(1)
|
||||||
.constraints(
|
.constraints(
|
||||||
[
|
[
|
||||||
Constraint::Length(1),
|
Constraint::Length(if show_help { 1 } else { 0 }),
|
||||||
Constraint::Min(1),
|
Constraint::Min(1),
|
||||||
Constraint::Length(1),
|
Constraint::Length(1 + border_size),
|
||||||
|
Constraint::Length(preview_height),
|
||||||
]
|
]
|
||||||
.as_ref(),
|
.as_ref(),
|
||||||
)
|
)
|
||||||
|
@ -293,48 +229,136 @@ impl State {
|
||||||
)
|
)
|
||||||
.split(chunks[0]);
|
.split(chunks[0]);
|
||||||
|
|
||||||
let title = Paragraph::new(Text::from(Span::styled(
|
let title = self.build_title();
|
||||||
format!("Atuin v{VERSION}"),
|
f.render_widget(title, header_chunks[0]);
|
||||||
Style::default().fg(Color::DarkGray),
|
|
||||||
)));
|
|
||||||
|
|
||||||
|
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![
|
let help = Paragraph::new(Text::from(Spans::from(vec![
|
||||||
Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
|
Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
Span::raw(" to exit"),
|
Span::raw(" to exit"),
|
||||||
])))
|
])))
|
||||||
.style(Style::default().fg(Color::DarkGray))
|
.style(Style::default().fg(Color::DarkGray))
|
||||||
.alignment(Alignment::Center);
|
.alignment(Alignment::Center);
|
||||||
|
help
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_stats(&mut self) -> Paragraph {
|
||||||
let stats = Paragraph::new(Text::from(Span::raw(format!(
|
let stats = Paragraph::new(Text::from(Span::raw(format!(
|
||||||
"history count: {}",
|
"history count: {}",
|
||||||
self.history_count,
|
self.history_count,
|
||||||
))))
|
))))
|
||||||
.style(Style::default().fg(Color::DarkGray))
|
.style(Style::default().fg(Color::DarkGray))
|
||||||
.alignment(Alignment::Right);
|
.alignment(Alignment::Right);
|
||||||
|
stats
|
||||||
|
}
|
||||||
|
|
||||||
f.render_widget(title, header_chunks[0]);
|
fn build_results_list(compact: bool, results: &[History]) -> HistoryList {
|
||||||
f.render_widget(help, header_chunks[1]);
|
let results_list = if compact {
|
||||||
f.render_widget(stats, header_chunks[2]);
|
HistoryList::new(results)
|
||||||
|
} else {
|
||||||
let results = HistoryList::new(results);
|
HistoryList::new(results).block(
|
||||||
f.render_stateful_widget(results, chunks[1], &mut self.results_state);
|
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!(
|
let input = format!(
|
||||||
"[{:^14}] {}",
|
"[{:^14}] {}",
|
||||||
self.filter_mode.as_str(),
|
self.filter_mode.as_str(),
|
||||||
self.input.as_str(),
|
self.input.as_str(),
|
||||||
);
|
);
|
||||||
let input = Paragraph::new(input);
|
let input = if compact {
|
||||||
f.render_widget(input, chunks[2]);
|
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());
|
fn build_preview(
|
||||||
|
&mut self,
|
||||||
f.set_cursor(
|
results: &[History],
|
||||||
// Put cursor past the end of the input text
|
compact: bool,
|
||||||
chunks[2].x + extra_width as u16 + PREFIX_LENGTH + 1,
|
preview_width: u16,
|
||||||
// Move one line down, from the border to the input line
|
chunk_width: usize,
|
||||||
chunks[2].y + 1,
|
) -> 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::Compact => true,
|
||||||
atuin_client::settings::Style::Full => false,
|
atuin_client::settings::Style::Full => false,
|
||||||
};
|
};
|
||||||
if compact {
|
terminal.draw(|f| app.draw(f, &results, compact, settings.show_preview))?;
|
||||||
terminal.draw(|f| app.draw_compact(f, &results))?;
|
|
||||||
} else {
|
|
||||||
terminal.draw(|f| app.draw(f, &results))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let initial_input = app.input.as_str().to_owned();
|
let initial_input = app.input.as_str().to_owned();
|
||||||
let initial_filter_mode = app.filter_mode;
|
let initial_filter_mode = app.filter_mode;
|
||||||
|
|
Loading…
Reference in a new issue