From 290a57e3331bb25dcd16284099bf1dc6afa68ee5 Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Thu, 2 Apr 2026 23:13:47 +0300 Subject: feat: add direct pane navigation keybinds --- src/ui.rs | 277 +++++++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 184 insertions(+), 93 deletions(-) (limited to 'src/ui.rs') diff --git a/src/ui.rs b/src/ui.rs index c48d1e7c..d729b918 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1555,6 +1555,10 @@ fn render_global_launcher_menu(app: &AppState, frame: &mut Frame) { } } +fn optional_keybind_label(label: &Option) -> String { + label.clone().unwrap_or_else(|| "unset".to_string()) +} + fn keybind_help_groups(app: &AppState) -> Vec<(&'static str, Vec<(String, &'static str)>)> { let kb = &app.keybinds; let mut groups = Vec::new(); @@ -1583,129 +1587,186 @@ fn keybind_help_groups(app: &AppState) -> Vec<(&'static str, Vec<(String, &'stat ], )); - let mut workspace_tab = vec![ + let workspace_tab = vec![ (kb.new_workspace_label.clone(), "new workspace"), (kb.rename_workspace_label.clone(), "rename workspace"), (kb.close_workspace_label.clone(), "close workspace"), + ( + optional_keybind_label(&kb.previous_workspace_label), + "previous workspace", + ), + ( + optional_keybind_label(&kb.next_workspace_label), + "next workspace", + ), (kb.new_tab_label.clone(), "new tab"), + (optional_keybind_label(&kb.rename_tab_label), "rename tab"), + ( + optional_keybind_label(&kb.previous_tab_label), + "previous tab", + ), + (optional_keybind_label(&kb.next_tab_label), "next tab"), + (optional_keybind_label(&kb.close_tab_label), "close tab"), ]; - if let Some(label) = &kb.previous_workspace_label { - workspace_tab.push((label.clone(), "previous workspace")); - } - if let Some(label) = &kb.next_workspace_label { - workspace_tab.push((label.clone(), "next workspace")); - } - if let Some(label) = &kb.rename_tab_label { - workspace_tab.push((label.clone(), "rename tab")); - } - if let Some(label) = &kb.previous_tab_label { - workspace_tab.push((label.clone(), "previous tab")); - } - if let Some(label) = &kb.next_tab_label { - workspace_tab.push((label.clone(), "next tab")); - } - if let Some(label) = &kb.close_tab_label { - workspace_tab.push((label.clone(), "close tab")); - } groups.push(("workspaces / tabs", workspace_tab)); - groups.push(( - "panes", - vec![ - (kb.split_vertical_label.clone(), "split vertical"), - (kb.split_horizontal_label.clone(), "split horizontal"), - (kb.close_pane_label.clone(), "close pane"), - (kb.fullscreen_label.clone(), "fullscreen"), - (kb.resize_mode_label.clone(), "resize mode"), - (kb.toggle_sidebar_label.clone(), "toggle sidebar"), - ], - )); + let panes = vec![ + (kb.split_vertical_label.clone(), "split vertical"), + (kb.split_horizontal_label.clone(), "split horizontal"), + (kb.close_pane_label.clone(), "close pane"), + (kb.fullscreen_label.clone(), "fullscreen"), + (kb.resize_mode_label.clone(), "resize mode"), + (kb.toggle_sidebar_label.clone(), "toggle sidebar"), + ( + optional_keybind_label(&kb.focus_pane_left_label), + "focus pane left", + ), + ( + optional_keybind_label(&kb.focus_pane_down_label), + "focus pane down", + ), + ( + optional_keybind_label(&kb.focus_pane_up_label), + "focus pane up", + ), + ( + optional_keybind_label(&kb.focus_pane_right_label), + "focus pane right", + ), + ]; + groups.push(("panes", panes)); groups } -pub(crate) fn keybind_help_close_button_rect(area: Rect) -> Rect { - action_button_row_rects( - area, - &[ActionButtonSpec { - hint: Some("esc"), - label: "close", - }], - 0, - 0, - )[0] +pub(crate) fn keybind_help_lines(app: &AppState) -> Vec<(usize, Line<'static>)> { + let heading_style = Style::default() + .fg(app.palette.accent) + .add_modifier(Modifier::BOLD); + let key_style = Style::default() + .fg(app.palette.mauve) + .add_modifier(Modifier::BOLD); + let label_style = Style::default().fg(app.palette.text); + + let groups = keybind_help_groups(app); + let key_width = groups + .iter() + .flat_map(|(_, entries)| entries.iter().map(|(key, _)| key.chars().count())) + .max() + .unwrap_or(8); + + let mut lines = Vec::new(); + + for (group, entries) in groups { + lines.push(( + group.len() + 1, + Line::from(vec![Span::styled(format!(" {group}"), heading_style)]), + )); + for (key, label) in entries { + let padded_key = format!(" {:(inner); + + render_modal_header(frame, rows[0], "keybinds", &app.palette); render_action_button( frame, - keybind_help_close_button_rect(header), + release_notes_close_button_rect(rows[0]), Some("esc"), "close", Style::default() - .fg(app.palette.text) - .bg(app.palette.surface0) + .fg(app.palette.panel_bg) + .bg(app.palette.accent) .add_modifier(Modifier::BOLD), ); + frame.render_widget( + Paragraph::new(" available commands and configured shortcuts") + .style(Style::default().fg(app.palette.overlay1)), + rows[1], + ); - let groups = keybind_help_groups(app); - let key_width = groups - .iter() - .flat_map(|(_, entries)| entries.iter().map(|(key, _)| key.chars().count() as u16)) - .max() - .unwrap_or(8) - .min(inner.width.saturating_sub(8)); + let body_area = rows[2]; + let metrics = crate::pane::ScrollMetrics { + offset_from_bottom: app + .keybind_help_max_scroll() + .saturating_sub(app.keybind_help.scroll) as usize, + max_offset_from_bottom: app.keybind_help_max_scroll() as usize, + viewport_rows: body_area.height.max(1) as usize, + }; + let track = release_notes_scrollbar_rect(body_area, metrics); + let text_area = track + .map(|_| { + Rect::new( + body_area.x, + body_area.y, + body_area.width.saturating_sub(1), + body_area.height, + ) + }) + .unwrap_or(body_area); - let mut y = inner.y + 2; - for (group, entries) in groups { - if y >= inner.y + inner.height { - break; - } - frame.render_widget( - Paragraph::new(Span::styled(format!(" {group}"), heading_style)), - Rect::new(inner.x, y, inner.width, 1), + let body = Paragraph::new( + keybind_help_lines(app) + .into_iter() + .map(|(_, line)| line) + .collect::>(), + ) + .wrap(Wrap { trim: false }) + .scroll((app.keybind_help.scroll, 0)); + frame.render_widget(body, text_area); + if let Some(track) = track { + render_scrollbar( + frame, + metrics, + track, + app.palette.overlay0, + app.palette.overlay1, + "▐", ); - y += 1; - for (key, label) in entries { - if y >= inner.y + inner.height { - break; - } - let padded_key = format!(" {: