From dc2fea4c8d0d70440e30cd7689d705dfbdb8db22 Mon Sep 17 00:00:00 2001 From: summer-boythink <2071792205@qq.com> Date: Sun, 10 May 2026 23:11:21 +0800 Subject: [PATCH 1/3] feat(ui): implement mouse selection for table rows and update navigation instructions --- src/ui.rs | 82 +++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 68 insertions(+), 14 deletions(-) diff --git a/src/ui.rs b/src/ui.rs index e9c5374..8531e8c 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -3,7 +3,7 @@ use crossterm::{ cursor::{Hide, Show}, event::{ self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind, - KeyModifiers, + KeyModifiers, MouseButton, MouseEvent, MouseEventKind, }, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, @@ -24,7 +24,7 @@ use unicode_width::UnicodeWidthStr; use crate::{searchable::Searchable, ssh}; -const INFO_TEXT: &str = "(Esc) quit | (↑) move up | (↓) move down | (enter) select"; +const INFO_TEXT: &str = "(Esc) quit | (↑/↓/click) navigate | (enter) connect"; #[derive(Clone)] #[allow(clippy::struct_excessive_bools)] @@ -52,6 +52,7 @@ pub struct App { table_columns_constraints: Vec, palette: tailwind::Palette, + table_area: Rect, } #[derive(PartialEq)] @@ -103,6 +104,7 @@ impl App { table_state: TableState::default().with_selected(0), table_columns_constraints: Vec::new(), palette: tailwind::BLUE, + table_area: Rect::default(), hosts: Searchable::new( config.sort_by_levenshtein, @@ -155,30 +157,79 @@ impl App { let ev = event::read()?; - if let Event::Key(key) = ev { - if key.kind == KeyEventKind::Press { - let action = self.on_key_press(terminal, key)?; + match ev { + Event::Key(key) => { + if key.kind == KeyEventKind::Press { + let action = self.on_key_press(terminal, key)?; + match action { + AppKeyAction::Ok => continue, + AppKeyAction::Stop => break, + AppKeyAction::Continue => {} + } + } + + self.search.handle_event(&ev); + self.hosts.search(self.search.value()); + + let selected = self.table_state.selected().unwrap_or(0); + if selected >= self.hosts.len() { + self.table_state.select(Some(match self.hosts.len() { + 0 => 0, + _ => self.hosts.len() - 1, + })); + } + } + Event::Mouse(mouse) => { + let action = self.on_mouse_event(terminal, mouse)?; match action { AppKeyAction::Ok => continue, AppKeyAction::Stop => break, AppKeyAction::Continue => {} } } + _ => {} + } + } - self.search.handle_event(&ev); - self.hosts.search(self.search.value()); + Ok(()) + } - let selected = self.table_state.selected().unwrap_or(0); - if selected >= self.hosts.len() { - self.table_state.select(Some(match self.hosts.len() { - 0 => 0, - _ => self.hosts.len() - 1, - })); + fn on_mouse_event( + &mut self, + _terminal: &Rc>>, + mouse: MouseEvent, + ) -> Result + where + B: Backend + std::io::Write, + ::Error: Send + Sync + 'static, + { + match mouse.kind { + MouseEventKind::Down(MouseButton::Left) => { + // Check if click is within table area + let table_area = self.table_area; + if mouse.column >= table_area.x + && mouse.column < table_area.x + table_area.width + && mouse.row >= table_area.y + && mouse.row < table_area.y + table_area.height + { + // Calculate which row was clicked + // Account for header (1 line) and top border (1 line) + let header_height = 1u16; + let top_border = 1u16; + let row_offset = table_area.y + top_border + header_height; + + if mouse.row >= row_offset { + let clicked_row = usize::from(mouse.row - row_offset); + if clicked_row < self.hosts.len() { + self.table_state.select(Some(clicked_row)); + } + } } } + _ => {} } - Ok(()) + Ok(AppKeyAction::Ok) } fn on_key_press( @@ -453,6 +504,9 @@ fn render_searchbar(f: &mut Frame, app: &mut App, area: Rect) { } fn render_table(f: &mut Frame, app: &mut App, area: Rect) { + // Store the table area for mouse click detection + app.table_area = area; + let header_style = Style::default().fg(tailwind::CYAN.c500); let selected_style = Style::default().add_modifier(Modifier::REVERSED); From 9302deec6d5577bd6f8b47553c4da8ea561fab69 Mon Sep 17 00:00:00 2001 From: summer-boythink <2071792205@qq.com> Date: Sun, 10 May 2026 23:11:26 +0800 Subject: [PATCH 2/3] fix(ui): adjust mouse click handling to account for scroll offset in table rows --- src/ui.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ui.rs b/src/ui.rs index 8531e8c..9087999 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -219,7 +219,8 @@ impl App { let row_offset = table_area.y + top_border + header_height; if mouse.row >= row_offset { - let clicked_row = usize::from(mouse.row - row_offset); + let scroll_offset = self.table_state.offset(); + let clicked_row = usize::from(mouse.row - row_offset) + scroll_offset; if clicked_row < self.hosts.len() { self.table_state.select(Some(clicked_row)); } From 4c063e6576ac65e39027a6a83f36674759e8535a Mon Sep 17 00:00:00 2001 From: summer-boythink <2071792205@qq.com> Date: Thu, 21 May 2026 22:25:30 +0800 Subject: [PATCH 3/3] feat(ui): add configurable table header height and top border for improved layout --- src/ui.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/ui.rs b/src/ui.rs index 9087999..2f2d44f 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -53,6 +53,8 @@ pub struct App { palette: tailwind::Palette, table_area: Rect, + table_header_height: u16, + table_top_border: u16, } #[derive(PartialEq)] @@ -105,6 +107,8 @@ impl App { table_columns_constraints: Vec::new(), palette: tailwind::BLUE, table_area: Rect::default(), + table_header_height: 0, + table_top_border: 0, hosts: Searchable::new( config.sort_by_levenshtein, @@ -213,9 +217,8 @@ impl App { && mouse.row < table_area.y + table_area.height { // Calculate which row was clicked - // Account for header (1 line) and top border (1 line) - let header_height = 1u16; - let top_border = 1u16; + let header_height = self.table_header_height; + let top_border = self.table_top_border; let row_offset = table_area.y + top_border + header_height; if mouse.row >= row_offset { @@ -516,13 +519,14 @@ fn render_table(f: &mut Frame, app: &mut App, area: Rect) { header_names.push("Proxy"); } + let header_height = 1u16; let header = header_names .iter() .copied() .map(Cell::from) .collect::() .style(header_style) - .height(1); + .height(header_height); let rows = app.hosts.iter().map(|host| { let mut content = vec![ @@ -560,6 +564,10 @@ fn render_table(f: &mut Frame, app: &mut App, area: Rect) { .border_type(BorderType::Rounded), ); + let top_border = u16::from(Borders::ALL.intersects(Borders::TOP)); + app.table_header_height = header_height; + app.table_top_border = top_border; + f.render_stateful_widget(t, area, &mut app.table_state); }