fix(ui): auto-scroll lists when selection moves out of view
Use ratatui's ListState with render_stateful_widget in all list widgets (groups, files, sections, custom lines, search results) so the viewport follows the cursor instead of letting the selected item disappear behind the border. Bump version to 0.1.16.
This commit is contained in:
parent
199fa5e652
commit
2e29267432
6 changed files with 47 additions and 17 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -825,7 +825,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lines"
|
name = "lines"
|
||||||
version = "0.1.0"
|
version = "0.1.16"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "lines"
|
name = "lines"
|
||||||
version = "0.1.15"
|
version = "0.1.16"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["janvete"]
|
authors = ["janvete"]
|
||||||
description = "TUI for running frequently used commands"
|
description = "TUI for running frequently used commands"
|
||||||
|
|
|
||||||
22
src/app.rs
22
src/app.rs
|
|
@ -1,5 +1,7 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use ratatui::widgets::ListState;
|
||||||
|
|
||||||
use crate::config::Terminal;
|
use crate::config::Terminal;
|
||||||
use crate::parser::{load_groups, CommandFile, Group, Section};
|
use crate::parser::{load_groups, CommandFile, Group, Section};
|
||||||
|
|
||||||
|
|
@ -23,6 +25,7 @@ pub struct SearchState {
|
||||||
pub query: String,
|
pub query: String,
|
||||||
pub results: Vec<SearchResult>,
|
pub results: Vec<SearchResult>,
|
||||||
pub cursor: usize,
|
pub cursor: usize,
|
||||||
|
pub list_state: ListState,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -52,6 +55,7 @@ pub struct CustomState {
|
||||||
pub cursor: usize,
|
pub cursor: usize,
|
||||||
pub focus: CustomFocus,
|
pub focus: CustomFocus,
|
||||||
pub command: String,
|
pub command: String,
|
||||||
|
pub list_state: ListState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CustomState {
|
impl CustomState {
|
||||||
|
|
@ -69,12 +73,15 @@ impl CustomState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let selected = vec![false; lines.len()];
|
let selected = vec![false; lines.len()];
|
||||||
|
let mut list_state = ListState::default();
|
||||||
|
list_state.select(Some(0));
|
||||||
CustomState {
|
CustomState {
|
||||||
lines,
|
lines,
|
||||||
selected,
|
selected,
|
||||||
cursor: 0,
|
cursor: 0,
|
||||||
focus: CustomFocus::Lines,
|
focus: CustomFocus::Lines,
|
||||||
command: String::new(),
|
command: String::new(),
|
||||||
|
list_state,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,11 +155,20 @@ pub struct App {
|
||||||
pub search_state: Option<SearchState>,
|
pub search_state: Option<SearchState>,
|
||||||
pub pending_command: Option<PendingCommand>,
|
pub pending_command: Option<PendingCommand>,
|
||||||
pub message: Option<String>,
|
pub message: Option<String>,
|
||||||
|
pub group_list_state: ListState,
|
||||||
|
pub file_list_state: ListState,
|
||||||
|
pub section_list_state: ListState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn new(data_dir: PathBuf, terminal: Terminal, shell: String) -> Self {
|
pub fn new(data_dir: PathBuf, terminal: Terminal, shell: String) -> Self {
|
||||||
let groups = load_groups(&data_dir);
|
let groups = load_groups(&data_dir);
|
||||||
|
let mut group_list_state = ListState::default();
|
||||||
|
let mut file_list_state = ListState::default();
|
||||||
|
let mut section_list_state = ListState::default();
|
||||||
|
group_list_state.select(Some(0));
|
||||||
|
file_list_state.select(Some(0));
|
||||||
|
section_list_state.select(Some(0));
|
||||||
App {
|
App {
|
||||||
data_dir,
|
data_dir,
|
||||||
terminal,
|
terminal,
|
||||||
|
|
@ -167,6 +183,9 @@ impl App {
|
||||||
search_state: None,
|
search_state: None,
|
||||||
pending_command: None,
|
pending_command: None,
|
||||||
message: None,
|
message: None,
|
||||||
|
group_list_state,
|
||||||
|
file_list_state,
|
||||||
|
section_list_state,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -191,10 +210,13 @@ impl App {
|
||||||
|
|
||||||
pub fn enter_search(&mut self) {
|
pub fn enter_search(&mut self) {
|
||||||
let results = build_search_index(&self.groups);
|
let results = build_search_index(&self.groups);
|
||||||
|
let mut list_state = ListState::default();
|
||||||
|
list_state.select(Some(0));
|
||||||
self.search_state = Some(SearchState {
|
self.search_state = Some(SearchState {
|
||||||
query: String::new(),
|
query: String::new(),
|
||||||
results,
|
results,
|
||||||
cursor: 0,
|
cursor: 0,
|
||||||
|
list_state,
|
||||||
});
|
});
|
||||||
self.screen = Screen::Search;
|
self.screen = Screen::Search;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ mod tests {
|
||||||
use crate::app::{CustomLine, CustomState};
|
use crate::app::{CustomLine, CustomState};
|
||||||
|
|
||||||
fn make_state(lines: Vec<&str>, selected: Vec<bool>, command: &str) -> CustomState {
|
fn make_state(lines: Vec<&str>, selected: Vec<bool>, command: &str) -> CustomState {
|
||||||
|
let mut list_state = ratatui::widgets::ListState::default();
|
||||||
|
list_state.select(Some(0));
|
||||||
CustomState {
|
CustomState {
|
||||||
lines: lines
|
lines: lines
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -42,6 +44,7 @@ mod tests {
|
||||||
cursor: 0,
|
cursor: 0,
|
||||||
focus: crate::app::CustomFocus::Lines,
|
focus: crate::app::CustomFocus::Lines,
|
||||||
command: command.to_string(),
|
command: command.to_string(),
|
||||||
|
list_state,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ where
|
||||||
let tick_rate = Duration::from_millis(100);
|
let tick_rate = Duration::from_millis(100);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(|f| ui::draw(f, &app))?;
|
terminal.draw(|f| ui::draw(f, &mut app))?;
|
||||||
|
|
||||||
if let Some(event) = poll_event(tick_rate)?
|
if let Some(event) = poll_event(tick_rate)?
|
||||||
&& !handle_event(&mut app, event)
|
&& !handle_event(&mut app, event)
|
||||||
|
|
|
||||||
33
src/ui.rs
33
src/ui.rs
|
|
@ -8,7 +8,7 @@ use ratatui::{
|
||||||
|
|
||||||
use crate::app::{App, CustomFocus, Focus, Screen};
|
use crate::app::{App, CustomFocus, Focus, Screen};
|
||||||
|
|
||||||
pub fn draw(f: &mut Frame, app: &App) {
|
pub fn draw(f: &mut Frame, app: &mut App) {
|
||||||
match app.screen {
|
match app.screen {
|
||||||
Screen::Main => draw_main(f, app),
|
Screen::Main => draw_main(f, app),
|
||||||
Screen::Custom => draw_custom(f, app),
|
Screen::Custom => draw_custom(f, app),
|
||||||
|
|
@ -16,7 +16,7 @@ pub fn draw(f: &mut Frame, app: &App) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_main(f: &mut Frame, app: &App) {
|
fn draw_main(f: &mut Frame, app: &mut App) {
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.margin(1)
|
.margin(1)
|
||||||
|
|
@ -43,7 +43,7 @@ fn draw_main(f: &mut Frame, app: &App) {
|
||||||
draw_status(f, app, chunks[2]);
|
draw_status(f, app, chunks[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_custom(f: &mut Frame, app: &App) {
|
fn draw_custom(f: &mut Frame, app: &mut App) {
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.margin(1)
|
.margin(1)
|
||||||
|
|
@ -54,7 +54,7 @@ fn draw_custom(f: &mut Frame, app: &App) {
|
||||||
])
|
])
|
||||||
.split(f.area());
|
.split(f.area());
|
||||||
|
|
||||||
let Some(state) = app.custom_state.as_ref() else {
|
let Some(state) = app.custom_state.as_mut() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -94,7 +94,8 @@ fn draw_custom(f: &mut Frame, app: &App) {
|
||||||
Style::default().fg(Color::DarkGray)
|
Style::default().fg(Color::DarkGray)
|
||||||
});
|
});
|
||||||
let list = List::new(items).block(list_block);
|
let list = List::new(items).block(list_block);
|
||||||
f.render_widget(list, chunks[0]);
|
state.list_state.select(Some(state.cursor));
|
||||||
|
f.render_stateful_widget(list, chunks[0], &mut state.list_state);
|
||||||
|
|
||||||
let input = Paragraph::new(state.command.as_str())
|
let input = Paragraph::new(state.command.as_str())
|
||||||
.block(
|
.block(
|
||||||
|
|
@ -119,7 +120,7 @@ fn draw_custom(f: &mut Frame, app: &App) {
|
||||||
f.render_widget(paragraph, chunks[2]);
|
f.render_widget(paragraph, chunks[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_search(f: &mut Frame, app: &App) {
|
fn draw_search(f: &mut Frame, app: &mut App) {
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.margin(1)
|
.margin(1)
|
||||||
|
|
@ -130,7 +131,7 @@ fn draw_search(f: &mut Frame, app: &App) {
|
||||||
])
|
])
|
||||||
.split(f.area());
|
.split(f.area());
|
||||||
|
|
||||||
let Some(state) = app.search_state.as_ref() else {
|
let Some(state) = app.search_state.as_mut() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -170,7 +171,8 @@ fn draw_search(f: &mut Frame, app: &App) {
|
||||||
.title(format!(" Results ({}) ", state.results.len()))
|
.title(format!(" Results ({}) ", state.results.len()))
|
||||||
.borders(Borders::ALL),
|
.borders(Borders::ALL),
|
||||||
);
|
);
|
||||||
f.render_widget(list, chunks[1]);
|
state.list_state.select(Some(state.cursor));
|
||||||
|
f.render_stateful_widget(list, chunks[1], &mut state.list_state);
|
||||||
|
|
||||||
let preview_text = state
|
let preview_text = state
|
||||||
.results
|
.results
|
||||||
|
|
@ -183,7 +185,7 @@ fn draw_search(f: &mut Frame, app: &App) {
|
||||||
f.render_widget(preview, chunks[2]);
|
f.render_widget(preview, chunks[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_groups(f: &mut Frame, app: &App, area: ratatui::layout::Rect) {
|
fn draw_groups(f: &mut Frame, app: &mut App, area: ratatui::layout::Rect) {
|
||||||
let items: Vec<ListItem> = app
|
let items: Vec<ListItem> = app
|
||||||
.groups
|
.groups
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -207,10 +209,11 @@ fn draw_groups(f: &mut Frame, app: &App, area: ratatui::layout::Rect) {
|
||||||
.border_style(border_style(app.focus == Focus::Groups));
|
.border_style(border_style(app.focus == Focus::Groups));
|
||||||
|
|
||||||
let list = List::new(items).block(block);
|
let list = List::new(items).block(block);
|
||||||
f.render_widget(list, area);
|
app.group_list_state.select(Some(app.group_index));
|
||||||
|
f.render_stateful_widget(list, area, &mut app.group_list_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_files(f: &mut Frame, app: &App, area: ratatui::layout::Rect) {
|
fn draw_files(f: &mut Frame, app: &mut App, area: ratatui::layout::Rect) {
|
||||||
let items: Vec<ListItem> = app
|
let items: Vec<ListItem> = app
|
||||||
.current_files()
|
.current_files()
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -234,10 +237,11 @@ fn draw_files(f: &mut Frame, app: &App, area: ratatui::layout::Rect) {
|
||||||
.border_style(border_style(app.focus == Focus::Files));
|
.border_style(border_style(app.focus == Focus::Files));
|
||||||
|
|
||||||
let list = List::new(items).block(block);
|
let list = List::new(items).block(block);
|
||||||
f.render_widget(list, area);
|
app.file_list_state.select(Some(app.file_index));
|
||||||
|
f.render_stateful_widget(list, area, &mut app.file_list_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_sections(f: &mut Frame, app: &App, area: ratatui::layout::Rect) {
|
fn draw_sections(f: &mut Frame, app: &mut App, area: ratatui::layout::Rect) {
|
||||||
let items: Vec<ListItem> = app
|
let items: Vec<ListItem> = app
|
||||||
.current_sections()
|
.current_sections()
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -266,7 +270,8 @@ fn draw_sections(f: &mut Frame, app: &App, area: ratatui::layout::Rect) {
|
||||||
.border_style(border_style(app.focus == Focus::Sections));
|
.border_style(border_style(app.focus == Focus::Sections));
|
||||||
|
|
||||||
let list = List::new(items).block(block);
|
let list = List::new(items).block(block);
|
||||||
f.render_widget(list, area);
|
app.section_list_state.select(Some(app.section_index));
|
||||||
|
f.render_stateful_widget(list, area, &mut app.section_list_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_preview(f: &mut Frame, app: &App, area: ratatui::layout::Rect) {
|
fn draw_preview(f: &mut Frame, app: &App, area: ratatui::layout::Rect) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue