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:
Ellie Huxtable 2023-05-21 17:42:44 +01:00 committed by GitHub
parent d2240e1163
commit 5b5e4eaa86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 109 additions and 49 deletions

View File

@ -147,6 +147,7 @@ pub struct Settings {
pub filter_mode_shell_up_key_binding: Option<FilterMode>,
pub shell_up_key_binding: bool,
pub inline_height: u16,
pub invert: bool,
pub show_preview: bool,
pub exit_mode: ExitMode,
pub word_jump_mode: WordJumpMode,
@ -336,6 +337,7 @@ impl Settings {
.set_default("style", "auto")?
.set_default("inline_height", 0)?
.set_default("show_preview", false)?
.set_default("invert", false)?
.set_default("exit_mode", "return-original")?
.set_default("word_jump_mode", "emacs")?
.set_default(

View File

@ -13,6 +13,7 @@ use super::format_duration;
pub struct HistoryList<'a> {
history: &'a [History],
block: Option<Block<'a>>,
inverted: bool,
}
#[derive(Default)]
@ -61,6 +62,7 @@ impl<'a> StatefulWidget for HistoryList<'a> {
x: 0,
y: 0,
state,
inverted: self.inverted,
};
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> {
pub fn new(history: &'a [History]) -> Self {
pub fn new(history: &'a [History], inverted: bool) -> Self {
Self {
history,
block: None,
inverted,
}
}
@ -110,6 +113,7 @@ struct DrawState<'a> {
x: u16,
y: u16,
state: &'a ListState,
inverted: bool,
}
// longest line prefix I could come up with
@ -176,7 +180,13 @@ impl DrawState<'_> {
fn draw(&mut self, s: &str, style: Style) {
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;
self.x += self.buf.set_stringn(cx, cy, s, w, style).0 - cx;
}

View File

@ -47,6 +47,13 @@ struct State {
engine: Box<dyn SearchEngine>,
}
#[derive(Clone, Copy)]
struct StyleState {
compact: bool,
invert: bool,
inner_width: usize,
}
impl State {
async fn query_results(&mut self, db: &mut dyn Database) -> Result<Vec<History>> {
let results = self.engine.query(&self.search, db).await?;
@ -99,6 +106,7 @@ impl State {
let ctrl = input.modifiers.contains(KeyModifiers::CONTROL);
let alt = input.modifiers.contains(KeyModifiers::ALT);
// reset the state, will be set to true later if user really did change it
self.switched_search_mode = false;
match input.code {
@ -190,13 +198,23 @@ impl State {
self.search_mode = self.search_mode.next(settings);
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 {
ExitMode::ReturnOriginal => RETURN_ORIGINAL,
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);
self.results_state.select(i);
}
@ -204,7 +222,11 @@ impl State {
let i = self.results_state.selected().saturating_sub(1);
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;
self.results_state.select(i.min(len - 1));
}
@ -231,16 +253,16 @@ impl State {
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::bool_to_int_with_if)]
fn draw<T: Backend>(
&mut self,
f: &mut Frame<'_, T>,
results: &[History],
compact: bool,
show_preview: bool,
) {
fn draw<T: Backend>(&mut self, f: &mut Frame<'_, T>, results: &[History], settings: &Settings) {
let compact = match settings.style {
atuin_client::settings::Style::Auto => f.size().height < 14,
atuin_client::settings::Style::Compact => true,
atuin_client::settings::Style::Full => false,
};
let invert = settings.invert;
let border_size = if compact { 0 } else { 1 };
let preview_width = f.size().width - 2;
let preview_height = if show_preview {
let preview_height = if settings.show_preview {
let longest_command = results
.iter()
.max_by(|h1, h2| h1.command.len().cmp(&h2.command.len()));
@ -262,15 +284,34 @@ impl State {
.margin(0)
.horizontal_margin(1)
.constraints(
[
Constraint::Length(if show_help { 1 } else { 0 }),
Constraint::Min(1),
Constraint::Length(1 + border_size),
Constraint::Length(preview_height),
]
if invert {
[
Constraint::Length(1 + border_size), // input
Constraint::Min(1), // results list
Constraint::Length(preview_height), // preview
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(),
)
.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()
.direction(Direction::Horizontal)
@ -282,7 +323,7 @@ impl State {
]
.as_ref(),
)
.split(chunks[0]);
.split(header_chunk);
let title = self.build_title();
f.render_widget(title, header_chunks[0]);
@ -293,22 +334,23 @@ impl State {
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 results_list = Self::build_results_list(style, results);
f.render_stateful_widget(results_list, results_list_chunk, &mut self.results_state);
let input = self.build_input(compact, chunks[2].width.into());
f.render_widget(input, chunks[2]);
let input = self.build_input(style);
f.render_widget(input, input_chunk);
let preview = self.build_preview(results, compact, preview_width, chunks[3].width.into());
f.render_widget(preview, chunks[3]);
let preview =
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 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,
input_chunk.x + extra_width as u16 + PREFIX_LENGTH + 1 + cursor_offset,
input_chunk.y + cursor_offset,
);
}
@ -350,20 +392,27 @@ impl State {
stats
}
fn build_results_list(compact: bool, results: &[History]) -> HistoryList {
let results_list = if compact {
HistoryList::new(results)
fn build_results_list(style: StyleState, results: &[History]) -> HistoryList {
let results_list = HistoryList::new(results, style.invert);
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 {
HistoryList::new(results).block(
results_list.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 {
fn build_input(&mut self, style: StyleState) -> Paragraph {
/// Max width of the UI box showing current mode
const MAX_WIDTH: usize = 14;
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
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 = if compact {
Paragraph::new(input)
let 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 {
Paragraph::new(input).block(
input.block(
Block::default()
.borders(Borders::LEFT | Borders::RIGHT)
.border_type(BorderType::Rounded)
.title(format!("{:─>width$}", "", width = chunk_width - 2)),
.title(format!("{:─>width$}", "", width = style.inner_width - 2)),
)
};
input
}
}
fn build_preview(
@ -529,14 +584,7 @@ pub async fn history(
let mut results = app.query_results(&mut db).await?;
let index = 'render: loop {
let compact = match settings.style {
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))?;
terminal.draw(|f| app.draw(f, &results, settings))?;
let initial_input = app.search.input.as_str().to_owned();
let initial_filter_mode = app.search.filter_mode;