Input bar at the top if we are in inline mode (#866)
* Put input chunk at the top in inline mode * Invert the search results if bar is at top * fix styling on reversed rendering * add setting * settings --------- Co-authored-by: Patrick Decat <pdecat@gmail.com> Co-authored-by: Conrad Ludgate <conrad.ludgate@truelayer.com>
This commit is contained in:
parent
d2240e1163
commit
5b5e4eaa86
3 changed files with 109 additions and 49 deletions
|
@ -147,6 +147,7 @@ pub struct Settings {
|
||||||
pub filter_mode_shell_up_key_binding: Option<FilterMode>,
|
pub filter_mode_shell_up_key_binding: Option<FilterMode>,
|
||||||
pub shell_up_key_binding: bool,
|
pub shell_up_key_binding: bool,
|
||||||
pub inline_height: u16,
|
pub inline_height: u16,
|
||||||
|
pub invert: bool,
|
||||||
pub show_preview: bool,
|
pub show_preview: bool,
|
||||||
pub exit_mode: ExitMode,
|
pub exit_mode: ExitMode,
|
||||||
pub word_jump_mode: WordJumpMode,
|
pub word_jump_mode: WordJumpMode,
|
||||||
|
@ -336,6 +337,7 @@ impl Settings {
|
||||||
.set_default("style", "auto")?
|
.set_default("style", "auto")?
|
||||||
.set_default("inline_height", 0)?
|
.set_default("inline_height", 0)?
|
||||||
.set_default("show_preview", false)?
|
.set_default("show_preview", false)?
|
||||||
|
.set_default("invert", false)?
|
||||||
.set_default("exit_mode", "return-original")?
|
.set_default("exit_mode", "return-original")?
|
||||||
.set_default("word_jump_mode", "emacs")?
|
.set_default("word_jump_mode", "emacs")?
|
||||||
.set_default(
|
.set_default(
|
||||||
|
|
|
@ -13,6 +13,7 @@ use super::format_duration;
|
||||||
pub struct HistoryList<'a> {
|
pub struct HistoryList<'a> {
|
||||||
history: &'a [History],
|
history: &'a [History],
|
||||||
block: Option<Block<'a>>,
|
block: Option<Block<'a>>,
|
||||||
|
inverted: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -61,6 +62,7 @@ impl<'a> StatefulWidget for HistoryList<'a> {
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
state,
|
state,
|
||||||
|
inverted: self.inverted,
|
||||||
};
|
};
|
||||||
|
|
||||||
for item in self.history.iter().skip(state.offset).take(end - start) {
|
for item in self.history.iter().skip(state.offset).take(end - start) {
|
||||||
|
@ -77,10 +79,11 @@ impl<'a> StatefulWidget for HistoryList<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> HistoryList<'a> {
|
impl<'a> HistoryList<'a> {
|
||||||
pub fn new(history: &'a [History]) -> Self {
|
pub fn new(history: &'a [History], inverted: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
history,
|
history,
|
||||||
block: None,
|
block: None,
|
||||||
|
inverted,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,6 +113,7 @@ struct DrawState<'a> {
|
||||||
x: u16,
|
x: u16,
|
||||||
y: u16,
|
y: u16,
|
||||||
state: &'a ListState,
|
state: &'a ListState,
|
||||||
|
inverted: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
// longest line prefix I could come up with
|
// longest line prefix I could come up with
|
||||||
|
@ -176,7 +180,13 @@ impl DrawState<'_> {
|
||||||
|
|
||||||
fn draw(&mut self, s: &str, style: Style) {
|
fn draw(&mut self, s: &str, style: Style) {
|
||||||
let cx = self.list_area.left() + self.x;
|
let cx = self.list_area.left() + self.x;
|
||||||
let cy = self.list_area.bottom() - self.y - 1;
|
|
||||||
|
let cy = if self.inverted {
|
||||||
|
self.list_area.top() + self.y
|
||||||
|
} else {
|
||||||
|
self.list_area.bottom() - self.y - 1
|
||||||
|
};
|
||||||
|
|
||||||
let w = (self.list_area.width - self.x) as usize;
|
let w = (self.list_area.width - self.x) as usize;
|
||||||
self.x += self.buf.set_stringn(cx, cy, s, w, style).0 - cx;
|
self.x += self.buf.set_stringn(cx, cy, s, w, style).0 - cx;
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,13 @@ struct State {
|
||||||
engine: Box<dyn SearchEngine>,
|
engine: Box<dyn SearchEngine>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct StyleState {
|
||||||
|
compact: bool,
|
||||||
|
invert: bool,
|
||||||
|
inner_width: usize,
|
||||||
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
async fn query_results(&mut self, db: &mut dyn Database) -> Result<Vec<History>> {
|
async fn query_results(&mut self, db: &mut dyn Database) -> Result<Vec<History>> {
|
||||||
let results = self.engine.query(&self.search, db).await?;
|
let results = self.engine.query(&self.search, db).await?;
|
||||||
|
@ -99,6 +106,7 @@ impl State {
|
||||||
|
|
||||||
let ctrl = input.modifiers.contains(KeyModifiers::CONTROL);
|
let ctrl = input.modifiers.contains(KeyModifiers::CONTROL);
|
||||||
let alt = input.modifiers.contains(KeyModifiers::ALT);
|
let alt = input.modifiers.contains(KeyModifiers::ALT);
|
||||||
|
|
||||||
// reset the state, will be set to true later if user really did change it
|
// reset the state, will be set to true later if user really did change it
|
||||||
self.switched_search_mode = false;
|
self.switched_search_mode = false;
|
||||||
match input.code {
|
match input.code {
|
||||||
|
@ -190,13 +198,23 @@ impl State {
|
||||||
self.search_mode = self.search_mode.next(settings);
|
self.search_mode = self.search_mode.next(settings);
|
||||||
self.engine = engines::engine(self.search_mode);
|
self.engine = engines::engine(self.search_mode);
|
||||||
}
|
}
|
||||||
KeyCode::Down if self.results_state.selected() == 0 => {
|
KeyCode::Down if !settings.invert && self.results_state.selected() == 0 => {
|
||||||
return Some(match settings.exit_mode {
|
return Some(match settings.exit_mode {
|
||||||
ExitMode::ReturnOriginal => RETURN_ORIGINAL,
|
ExitMode::ReturnOriginal => RETURN_ORIGINAL,
|
||||||
ExitMode::ReturnQuery => RETURN_QUERY,
|
ExitMode::ReturnQuery => RETURN_QUERY,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
KeyCode::Down => {
|
KeyCode::Up if settings.invert && self.results_state.selected() == 0 => {
|
||||||
|
return Some(match settings.exit_mode {
|
||||||
|
ExitMode::ReturnOriginal => RETURN_ORIGINAL,
|
||||||
|
ExitMode::ReturnQuery => RETURN_QUERY,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
KeyCode::Down if !settings.invert => {
|
||||||
|
let i = self.results_state.selected().saturating_sub(1);
|
||||||
|
self.results_state.select(i);
|
||||||
|
}
|
||||||
|
KeyCode::Up if settings.invert => {
|
||||||
let i = self.results_state.selected().saturating_sub(1);
|
let i = self.results_state.selected().saturating_sub(1);
|
||||||
self.results_state.select(i);
|
self.results_state.select(i);
|
||||||
}
|
}
|
||||||
|
@ -204,7 +222,11 @@ impl State {
|
||||||
let i = self.results_state.selected().saturating_sub(1);
|
let i = self.results_state.selected().saturating_sub(1);
|
||||||
self.results_state.select(i);
|
self.results_state.select(i);
|
||||||
}
|
}
|
||||||
KeyCode::Up => {
|
KeyCode::Up if !settings.invert => {
|
||||||
|
let i = self.results_state.selected() + 1;
|
||||||
|
self.results_state.select(i.min(len - 1));
|
||||||
|
}
|
||||||
|
KeyCode::Down if settings.invert => {
|
||||||
let i = self.results_state.selected() + 1;
|
let i = self.results_state.selected() + 1;
|
||||||
self.results_state.select(i.min(len - 1));
|
self.results_state.select(i.min(len - 1));
|
||||||
}
|
}
|
||||||
|
@ -231,16 +253,16 @@ impl State {
|
||||||
|
|
||||||
#[allow(clippy::cast_possible_truncation)]
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
#[allow(clippy::bool_to_int_with_if)]
|
#[allow(clippy::bool_to_int_with_if)]
|
||||||
fn draw<T: Backend>(
|
fn draw<T: Backend>(&mut self, f: &mut Frame<'_, T>, results: &[History], settings: &Settings) {
|
||||||
&mut self,
|
let compact = match settings.style {
|
||||||
f: &mut Frame<'_, T>,
|
atuin_client::settings::Style::Auto => f.size().height < 14,
|
||||||
results: &[History],
|
atuin_client::settings::Style::Compact => true,
|
||||||
compact: bool,
|
atuin_client::settings::Style::Full => false,
|
||||||
show_preview: bool,
|
};
|
||||||
) {
|
let invert = settings.invert;
|
||||||
let border_size = if compact { 0 } else { 1 };
|
let border_size = if compact { 0 } else { 1 };
|
||||||
let preview_width = f.size().width - 2;
|
let preview_width = f.size().width - 2;
|
||||||
let preview_height = if show_preview {
|
let preview_height = if settings.show_preview {
|
||||||
let longest_command = results
|
let longest_command = results
|
||||||
.iter()
|
.iter()
|
||||||
.max_by(|h1, h2| h1.command.len().cmp(&h2.command.len()));
|
.max_by(|h1, h2| h1.command.len().cmp(&h2.command.len()));
|
||||||
|
@ -262,15 +284,34 @@ impl State {
|
||||||
.margin(0)
|
.margin(0)
|
||||||
.horizontal_margin(1)
|
.horizontal_margin(1)
|
||||||
.constraints(
|
.constraints(
|
||||||
|
if invert {
|
||||||
[
|
[
|
||||||
Constraint::Length(if show_help { 1 } else { 0 }),
|
Constraint::Length(1 + border_size), // input
|
||||||
Constraint::Min(1),
|
Constraint::Min(1), // results list
|
||||||
Constraint::Length(1 + border_size),
|
Constraint::Length(preview_height), // preview
|
||||||
Constraint::Length(preview_height),
|
Constraint::Length(if show_help { 1 } else { 0 }), // header (sic)
|
||||||
]
|
]
|
||||||
|
} else {
|
||||||
|
[
|
||||||
|
Constraint::Length(if show_help { 1 } else { 0 }), // header
|
||||||
|
Constraint::Min(1), // results list
|
||||||
|
Constraint::Length(1 + border_size), // input
|
||||||
|
Constraint::Length(preview_height), // preview
|
||||||
|
]
|
||||||
|
}
|
||||||
.as_ref(),
|
.as_ref(),
|
||||||
)
|
)
|
||||||
.split(f.size());
|
.split(f.size());
|
||||||
|
let input_chunk = if invert { chunks[0] } else { chunks[2] };
|
||||||
|
let results_list_chunk = chunks[1];
|
||||||
|
let preview_chunk = if invert { chunks[2] } else { chunks[3] };
|
||||||
|
let header_chunk = if invert { chunks[3] } else { chunks[0] };
|
||||||
|
|
||||||
|
let style = StyleState {
|
||||||
|
compact,
|
||||||
|
invert,
|
||||||
|
inner_width: input_chunk.width.into(),
|
||||||
|
};
|
||||||
|
|
||||||
let header_chunks = Layout::default()
|
let header_chunks = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
|
@ -282,7 +323,7 @@ impl State {
|
||||||
]
|
]
|
||||||
.as_ref(),
|
.as_ref(),
|
||||||
)
|
)
|
||||||
.split(chunks[0]);
|
.split(header_chunk);
|
||||||
|
|
||||||
let title = self.build_title();
|
let title = self.build_title();
|
||||||
f.render_widget(title, header_chunks[0]);
|
f.render_widget(title, header_chunks[0]);
|
||||||
|
@ -293,22 +334,23 @@ impl State {
|
||||||
let stats = self.build_stats();
|
let stats = self.build_stats();
|
||||||
f.render_widget(stats, header_chunks[2]);
|
f.render_widget(stats, header_chunks[2]);
|
||||||
|
|
||||||
let results_list = Self::build_results_list(compact, results);
|
let results_list = Self::build_results_list(style, results);
|
||||||
f.render_stateful_widget(results_list, chunks[1], &mut self.results_state);
|
f.render_stateful_widget(results_list, results_list_chunk, &mut self.results_state);
|
||||||
|
|
||||||
let input = self.build_input(compact, chunks[2].width.into());
|
let input = self.build_input(style);
|
||||||
f.render_widget(input, chunks[2]);
|
f.render_widget(input, input_chunk);
|
||||||
|
|
||||||
let preview = self.build_preview(results, compact, preview_width, chunks[3].width.into());
|
let preview =
|
||||||
f.render_widget(preview, chunks[3]);
|
self.build_preview(results, compact, preview_width, preview_chunk.width.into());
|
||||||
|
f.render_widget(preview, preview_chunk);
|
||||||
|
|
||||||
let extra_width = UnicodeWidthStr::width(self.search.input.substring());
|
let extra_width = UnicodeWidthStr::width(self.search.input.substring());
|
||||||
|
|
||||||
let cursor_offset = if compact { 0 } else { 1 };
|
let cursor_offset = if compact { 0 } else { 1 };
|
||||||
f.set_cursor(
|
f.set_cursor(
|
||||||
// Put cursor past the end of the input text
|
// Put cursor past the end of the input text
|
||||||
chunks[2].x + extra_width as u16 + PREFIX_LENGTH + 1 + cursor_offset,
|
input_chunk.x + extra_width as u16 + PREFIX_LENGTH + 1 + cursor_offset,
|
||||||
chunks[2].y + cursor_offset,
|
input_chunk.y + cursor_offset,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -350,20 +392,27 @@ impl State {
|
||||||
stats
|
stats
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_results_list(compact: bool, results: &[History]) -> HistoryList {
|
fn build_results_list(style: StyleState, results: &[History]) -> HistoryList {
|
||||||
let results_list = if compact {
|
let results_list = HistoryList::new(results, style.invert);
|
||||||
HistoryList::new(results)
|
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 {
|
} else {
|
||||||
HistoryList::new(results).block(
|
results_list.block(
|
||||||
Block::default()
|
Block::default()
|
||||||
.borders(Borders::TOP | Borders::LEFT | Borders::RIGHT)
|
.borders(Borders::TOP | Borders::LEFT | Borders::RIGHT)
|
||||||
.border_type(BorderType::Rounded),
|
.border_type(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
};
|
}
|
||||||
results_list
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_input(&mut self, compact: bool, chunk_width: usize) -> Paragraph {
|
fn build_input(&mut self, style: StyleState) -> Paragraph {
|
||||||
/// Max width of the UI box showing current mode
|
/// Max width of the UI box showing current mode
|
||||||
const MAX_WIDTH: usize = 14;
|
const MAX_WIDTH: usize = 14;
|
||||||
let (pref, mode) = if self.switched_search_mode {
|
let (pref, mode) = if self.switched_search_mode {
|
||||||
|
@ -375,17 +424,23 @@ impl State {
|
||||||
// sanity check to ensure we don't exceed the layout limits
|
// sanity check to ensure we don't exceed the layout limits
|
||||||
debug_assert!(mode_width >= mode.len(), "mode name '{mode}' is too long!");
|
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 = format!("[{pref}{mode:^mode_width$}] {}", self.search.input.as_str(),);
|
||||||
let input = if compact {
|
let input = Paragraph::new(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 {
|
} else {
|
||||||
Paragraph::new(input).block(
|
input.block(
|
||||||
Block::default()
|
Block::default()
|
||||||
.borders(Borders::LEFT | Borders::RIGHT)
|
.borders(Borders::LEFT | Borders::RIGHT)
|
||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
.title(format!("{:─>width$}", "", width = chunk_width - 2)),
|
.title(format!("{:─>width$}", "", width = style.inner_width - 2)),
|
||||||
)
|
)
|
||||||
};
|
}
|
||||||
input
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_preview(
|
fn build_preview(
|
||||||
|
@ -529,14 +584,7 @@ pub async fn history(
|
||||||
let mut results = app.query_results(&mut db).await?;
|
let mut results = app.query_results(&mut db).await?;
|
||||||
|
|
||||||
let index = 'render: loop {
|
let index = 'render: loop {
|
||||||
let compact = match settings.style {
|
terminal.draw(|f| app.draw(f, &results, settings))?;
|
||||||
atuin_client::settings::Style::Auto => {
|
|
||||||
terminal.size().map(|size| size.height < 14).unwrap_or(true)
|
|
||||||
}
|
|
||||||
atuin_client::settings::Style::Compact => true,
|
|
||||||
atuin_client::settings::Style::Full => false,
|
|
||||||
};
|
|
||||||
terminal.draw(|f| app.draw(f, &results, compact, settings.show_preview))?;
|
|
||||||
|
|
||||||
let initial_input = app.search.input.as_str().to_owned();
|
let initial_input = app.search.input.as_str().to_owned();
|
||||||
let initial_filter_mode = app.search.filter_mode;
|
let initial_filter_mode = app.search.filter_mode;
|
||||||
|
|
Loading…
Reference in a new issue