aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-04-02 19:05:24 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-04-02 23:24:55 +0300
commit61204f7d0c68f723e82fb3c8c98ad066e527863b (patch)
tree1fefe3f419cabb502d7a5625407dd9e195eefbea /src
parentb695adee60e50764f4713c8e586ca02fb844f99a (diff)
feat: refactor keybind discoverability surfaces
Diffstat (limited to 'src')
-rw-r--r--src/app/input.rs424
-rw-r--r--src/app/mod.rs1
-rw-r--r--src/app/state.rs11
-rw-r--r--src/config.rs61
-rw-r--r--src/ui.rs548
5 files changed, 814 insertions, 231 deletions
diff --git a/src/app/input.rs b/src/app/input.rs
index 9a4c9aaf..6169b42f 100644
--- a/src/app/input.rs
+++ b/src/app/input.rs
@@ -78,6 +78,8 @@ impl App {
Mode::ConfirmClose => handle_confirm_close_key(&mut self.state, key),
Mode::ContextMenu => handle_context_menu_key(&mut self.state, key),
Mode::Settings => self.handle_settings_key(key),
+ Mode::GlobalMenu => handle_global_menu_key(&mut self.state, key),
+ Mode::KeybindHelp => handle_keybind_help_key(&mut self.state, key),
Mode::Terminal => unreachable!(),
}
}
@@ -306,6 +308,16 @@ enum SettingsAction {
SaveToast(bool),
}
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum GlobalMenuAction {
+ Keybinds,
+ Settings,
+}
+
+impl GlobalMenuAction {
+ const ALL: [Self; 2] = [Self::Keybinds, Self::Settings];
+}
+
fn normalize_theme_name(name: &str) -> String {
name.to_lowercase().replace([' ', '_'], "-")
}
@@ -426,6 +438,49 @@ fn update_settings_state(state: &mut AppState, key: KeyEvent) -> Option<Settings
None
}
+fn open_global_menu(state: &mut AppState) {
+ state.global_menu_selected = 0;
+ state.mode = Mode::GlobalMenu;
+}
+
+fn open_keybind_help(state: &mut AppState) {
+ state.mode = Mode::KeybindHelp;
+}
+
+fn apply_global_menu_action(state: &mut AppState, action: GlobalMenuAction) {
+ match action {
+ GlobalMenuAction::Keybinds => open_keybind_help(state),
+ GlobalMenuAction::Settings => open_settings(state),
+ }
+}
+
+fn handle_global_menu_key(state: &mut AppState, key: KeyEvent) {
+ match key.code {
+ KeyCode::Esc | KeyCode::Char('q') => leave_modal(state),
+ KeyCode::Up | KeyCode::Char('k') => {
+ state.global_menu_selected = state.global_menu_selected.saturating_sub(1);
+ }
+ KeyCode::Down | KeyCode::Char('j') => {
+ state.global_menu_selected =
+ (state.global_menu_selected + 1).min(GlobalMenuAction::ALL.len() - 1);
+ }
+ KeyCode::Enter => {
+ let action = GlobalMenuAction::ALL[state.global_menu_selected];
+ apply_global_menu_action(state, action);
+ }
+ _ => {}
+ }
+}
+
+fn handle_keybind_help_key(state: &mut AppState, key: KeyEvent) {
+ match key.code {
+ KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') | KeyCode::Char('?') => {
+ leave_modal(state)
+ }
+ _ => {}
+ }
+}
+
fn handle_navigate_key(state: &mut AppState, key: KeyEvent) {
state.update_dismissed = true;
@@ -457,6 +512,7 @@ fn handle_navigate_key(state: &mut AppState, key: KeyEvent) {
KeyCode::Char('s') => {
open_settings(state);
}
+ KeyCode::Char('?') => open_keybind_help(state),
KeyCode::Up => {
if state.selected > 0 {
state.selected -= 1;
@@ -911,7 +967,7 @@ impl AppState {
return false;
}
let button =
- ratatui::layout::Rect::new(inner.x + inner.width.saturating_sub(11), inner.y, 9, 1);
+ crate::ui::release_notes_close_button_rect(Rect::new(inner.x, inner.y, inner.width, 1));
col >= button.x
&& col < button.x + button.width
&& row >= button.y
@@ -1038,12 +1094,15 @@ impl AppState {
let Some(inner) = self.onboarding_modal_inner(64, 15) else {
return;
};
- let footer_y = inner.y + 9;
- let button_x = inner.x;
- let button_w = 14;
- if mouse.row == footer_y
- && mouse.column >= button_x
- && mouse.column < button_x + button_w
+ let button = crate::ui::onboarding_welcome_continue_rect(Rect::new(
+ inner.x,
+ inner.y + 9,
+ inner.width,
+ 1,
+ ));
+ if mouse.row == button.y
+ && mouse.column >= button.x
+ && mouse.column < button.x + button.width
{
self.onboarding_step = 1;
}
@@ -1061,15 +1120,16 @@ impl AppState {
return;
}
- let footer_y = inner.y + 6;
- let back_x = inner.x;
- let back_w = 10;
- let save_x = inner.x + 12;
- let save_w = 10;
- if mouse.row == footer_y {
- if mouse.column >= back_x && mouse.column < back_x + back_w {
+ let (back, save) = crate::ui::onboarding_notification_button_rects(Rect::new(
+ inner.x,
+ inner.y + 6,
+ inner.width,
+ 1,
+ ));
+ if mouse.row == back.y {
+ if mouse.column >= back.x && mouse.column < back.x + back.width {
self.onboarding_step = 0;
- } else if mouse.column >= save_x && mouse.column < save_x + save_w {
+ } else if mouse.column >= save.x && mouse.column < save.x + save.width {
self.request_complete_onboarding = true;
}
}
@@ -1077,6 +1137,72 @@ impl AppState {
}
}
+ pub(crate) fn sidebar_footer_rect(&self) -> Rect {
+ let sidebar = self.view.sidebar_rect;
+ if self.sidebar_collapsed || sidebar.width <= 1 || sidebar.height == 0 {
+ return Rect::default();
+ }
+ let content = Rect::new(
+ sidebar.x,
+ sidebar.y,
+ sidebar.width.saturating_sub(1),
+ sidebar.height,
+ );
+ let total_h = content.height as usize;
+ let ws_h = (total_h + 1) / 2;
+ let ws_area = Rect::new(content.x, content.y, content.width, ws_h as u16);
+ let y = ws_area.y + ws_area.height.saturating_sub(1);
+ Rect::new(ws_area.x, y, ws_area.width, 1)
+ }
+
+ pub(crate) fn sidebar_new_button_rect(&self) -> Rect {
+ let footer = self.sidebar_footer_rect();
+ let width = 5u16.min(footer.width.max(1));
+ Rect::new(footer.x, footer.y, width, footer.height)
+ }
+
+ pub(crate) fn global_launcher_rect(&self) -> Rect {
+ let footer = self.sidebar_footer_rect();
+ let width = 6u16.min(footer.width.max(1));
+ let x = footer.x + footer.width.saturating_sub(width);
+ Rect::new(x, footer.y, width, footer.height)
+ }
+
+ pub(crate) fn global_menu_rect(&self) -> Rect {
+ let screen = self.screen_rect();
+ let launcher = self.global_launcher_rect();
+ let menu_w = 14u16.min(screen.width.max(1));
+ let menu_h = (GlobalMenuAction::ALL.len() as u16 + 2).min(screen.height.max(1));
+ let max_x = screen.x + screen.width.saturating_sub(menu_w);
+ let desired_x = launcher.x + launcher.width.saturating_sub(menu_w);
+ let x = desired_x.min(max_x);
+ let y = launcher.y.saturating_sub(menu_h);
+ Rect::new(x, y, menu_w, menu_h)
+ }
+
+ fn global_menu_item_at(&self, col: u16, row: u16) -> Option<GlobalMenuAction> {
+ let rect = self.global_menu_rect();
+ if col <= rect.x
+ || col >= rect.x + rect.width.saturating_sub(1)
+ || row <= rect.y
+ || row >= rect.y + rect.height.saturating_sub(1)
+ {
+ return None;
+ }
+ let idx = (row - rect.y - 1) as usize;
+ GlobalMenuAction::ALL.get(idx).copied()
+ }
+
+ pub(crate) fn keybind_help_rect(&self) -> Rect {
+ let area = self.screen_rect();
+ let launcher = self.global_launcher_rect();
+ let popup_w = 54u16.min(area.width.saturating_sub(2).max(1));
+ let popup_h = 24u16.min(area.height.saturating_sub(2).max(1));
+ let x = area.x + area.width.saturating_sub(popup_w);
+ let y = launcher.y.saturating_sub(popup_h);
+ Rect::new(x, y, popup_w, popup_h)
+ }
+
fn settings_popup_rect(&self) -> Rect {
let area = self.screen_rect();
let popup_w = 56u16.min(area.width.saturating_sub(4));
@@ -1121,9 +1247,10 @@ impl AppState {
Rect::new(inner.x, y, inner.width, inner.y + inner.height - y)
}
- fn settings_list_index_at(&self, row: u16) -> Option<usize> {
+ fn settings_list_index_at(&self, col: u16, row: u16) -> Option<usize> {
let area = self.settings_content_rect();
- if row < area.y || row >= area.y + area.height {
+ if row < area.y || row >= area.y + area.height || col < area.x || col >= area.x + area.width
+ {
return None;
}
@@ -1152,16 +1279,13 @@ impl AppState {
fn settings_button_at(&self, col: u16, row: u16) -> Option<&'static str> {
let inner = self.settings_inner_rect();
- let footer_y = inner.y + inner.height.saturating_sub(1);
- if row != footer_y {
+ let (apply, close) = crate::ui::settings_button_rects(inner);
+ if row != apply.y {
return None;
}
- let total_w = 7u16 + 2 + 7u16;
- let apply_x = inner.x + inner.width.saturating_sub(total_w) / 2;
- let close_x = apply_x + 9;
- if col >= apply_x && col < apply_x + 7 {
+ if col >= apply.x && col < apply.x + apply.width {
Some("apply")
- } else if col >= close_x && col < close_x + 7 {
+ } else if col >= close.x && col < close.x + close.width {
Some("close")
} else {
None
@@ -1172,24 +1296,6 @@ impl AppState {
use crate::app::state::SettingsSection;
match mouse.kind {
- MouseEventKind::Moved => {
- if let Some(section) = self.settings_tab_at(mouse.column, mouse.row) {
- self.settings.section = section;
- self.settings.selected = match section {
- SettingsSection::Theme => current_theme_index(&self.theme_name),
- SettingsSection::Sound => usize::from(!self.sound_enabled()),
- SettingsSection::Toast => usize::from(!self.toast_config.enabled),
- };
- return None;
- }
- if let Some(idx) = self.settings_list_index_at(mouse.row) {
- self.settings.selected = idx;
- if self.settings.section == SettingsSection::Theme {
- preview_selected_theme(self);
- }
- }
- None
- }
MouseEventKind::Down(MouseButton::Left) => {
if let Some(section) = self.settings_tab_at(mouse.column, mouse.row) {
self.settings.section = section;
@@ -1200,7 +1306,7 @@ impl AppState {
};
return None;
}
- if let Some(idx) = self.settings_list_index_at(mouse.row) {
+ if let Some(idx) = self.settings_list_index_at(mouse.column, mouse.row) {
self.settings.selected = idx;
return match self.settings.section {
SettingsSection::Theme => {
@@ -1245,6 +1351,66 @@ impl AppState {
return self.handle_settings_mouse(mouse);
}
+ let launcher_enabled = !self.sidebar_collapsed
+ && matches!(
+ self.mode,
+ Mode::Terminal
+ | Mode::Navigate
+ | Mode::Resize
+ | Mode::GlobalMenu
+ | Mode::KeybindHelp
+ );
+ let launcher = self.global_launcher_rect();
+ let launcher_hit = launcher_enabled
+ && mouse.column >= launcher.x
+ && mouse.column < launcher.x + launcher.width
+ && mouse.row >= launcher.y
+ && mouse.row < launcher.y + launcher.height;
+
+ if matches!(mouse.kind, MouseEventKind::Moved) && self.mode == Mode::GlobalMenu {
+ if let Some(action) = self.global_menu_item_at(mouse.column, mouse.row) {
+ self.global_menu_selected = GlobalMenuAction::ALL
+ .iter()
+ .position(|item| *item == action)
+ .unwrap_or(0);
+ }
+ return None;
+ }
+
+ if matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) && launcher_hit {
+ if self.mode == Mode::GlobalMenu {
+ leave_modal(self);
+ } else {
+ open_global_menu(self);
+ }
+ return None;
+ }
+
+ if self.mode == Mode::GlobalMenu {
+ if matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) {
+ if let Some(action) = self.global_menu_item_at(mouse.column, mouse.row) {
+ apply_global_menu_action(self, action);
+ } else {
+ leave_modal(self);
+ }
+ }
+ return None;
+ }
+
+ if self.mode == Mode::KeybindHelp {
+ if matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) {
+ let rect = self.keybind_help_rect();
+ let inside = mouse.column >= rect.x
+ && mouse.column < rect.x + rect.width
+ && mouse.row >= rect.y
+ && mouse.row < rect.y + rect.height;
+ if !inside {
+ leave_modal(self);
+ }
+ }
+ return None;
+ }
+
let sidebar = self.view.sidebar_rect;
let in_sidebar = mouse.column >= sidebar.x
&& mouse.column < sidebar.x + sidebar.width
@@ -1355,11 +1521,12 @@ impl AppState {
return None;
}
- let total_h = sidebar.height as usize;
- let ws_h = (total_h + 1) / 2;
- let ws_bottom = sidebar.y + ws_h as u16;
- let new_row = ws_bottom.saturating_sub(1);
- if mouse.row == new_row {
+ let new_button = self.sidebar_new_button_rect();
+ let on_new_button = mouse.row >= new_button.y
+ && mouse.row < new_button.y + new_button.height
+ && mouse.column >= new_button.x
+ && mouse.column < new_button.x + new_button.width;
+ if on_new_button {
self.request_new_workspace = true;
return None;
}
@@ -1584,17 +1751,16 @@ impl AppState {
}
fn workspace_at_row(&self, row: u16) -> Option<usize> {
- let sidebar = self.view.sidebar_rect;
- let total_h = sidebar.height as usize;
- let ws_h = (total_h + 1) / 2;
- let ws_bottom = sidebar.y + ws_h as u16;
- let new_row = ws_bottom.saturating_sub(1);
+ let footer = self.sidebar_footer_rect();
+ if footer == Rect::default() {
+ return None;
+ }
- if row < sidebar.y || row >= new_row {
+ if row < self.view.sidebar_rect.y || row >= footer.y {
return None;
}
- let mut row_y = sidebar.y;
+ let mut row_y = self.view.sidebar_rect.y;
for (i, ws) in self.workspaces.iter().enumerate() {
let has_branch = ws.branch().is_some();
let card_h: u16 = if has_branch { 2 } else { 1 };
@@ -1602,7 +1768,7 @@ impl AppState {
return Some(i);
}
row_y += card_h + 1; // +1 for gap
- if row_y >= new_row {
+ if row_y >= footer.y {
break;
}
}
@@ -1652,18 +1818,13 @@ impl AppState {
popup.width.saturating_sub(2),
popup.height.saturating_sub(2),
);
- let confirm_w = 9u16;
- let cancel_w = 8u16;
- let gap = 2u16;
- let total_w = confirm_w + gap + cancel_w;
- let x = inner.x + inner.width.saturating_sub(total_w) / 2;
- let y = inner.y + 2.min(inner.height.saturating_sub(1));
- if row != y {
+ let (confirm, cancel) = crate::ui::confirm_close_button_rects(inner);
+ if row != confirm.y {
return None;
}
- if col >= x && col < x + confirm_w {
+ if col >= confirm.x && col < confirm.x + confirm.width {
Some(true)
- } else if col >= x + confirm_w + gap && col < x + total_w {
+ } else if col >= cancel.x && col < cancel.x + cancel.width {
Some(false)
} else {
None
@@ -2135,6 +2296,139 @@ mod tests {
}
#[test]
+ fn question_mark_opens_keybind_help_from_navigate() {
+ let mut state = state_with_workspaces(&["test"]);
+
+ handle_navigate_key(
+ &mut state,
+ KeyEvent::new(KeyCode::Char('?'), KeyModifiers::SHIFT),
+ );
+
+ assert_eq!(state.mode, Mode::KeybindHelp);
+ }
+
+ #[test]
+ fn clicking_launcher_opens_global_menu() {
+ let mut app = app_for_mouse_test();
+ let rect = app.state.global_launcher_rect();
+
+ app.handle_mouse(mouse(
+ MouseEventKind::Down(MouseButton::Left),
+ rect.x + rect.width.saturating_sub(1),
+ rect.y,
+ ));
+
+ assert_eq!(app.state.mode, Mode::GlobalMenu);
+ }
+
+ #[test]
+ fn clicking_keybinds_menu_item_opens_help() {
+ let mut app = app_for_mouse_test();
+ let launcher = app.state.global_launcher_rect();
+ app.handle_mouse(mouse(
+ MouseEventKind::Down(MouseButton::Left),
+ launcher.x,
+ launcher.y,
+ ));
+
+ let menu = app.state.global_menu_rect();
+ app.handle_mouse(mouse(
+ MouseEventKind::Down(MouseButton::Left),
+ menu.x + 2,
+ menu.y + 1,
+ ));
+
+ assert_eq!(app.state.mode, Mode::KeybindHelp);
+ }
+
+ #[test]
+ fn clicking_settings_menu_item_opens_settings() {
+ let mut app = app_for_mouse_test();
+ let launcher = app.state.global_launcher_rect();
+ app.handle_mouse(mouse(
+ MouseEventKind::Down(MouseButton::Left),
+ launcher.x,
+ launcher.y,
+ ));
+
+ let menu = app.state.global_menu_rect();
+ app.handle_mouse(mouse(
+ MouseEventKind::Down(MouseButton::Left),
+ menu.x + 2,
+ menu.y + 2,
+ ));
+
+ assert_eq!(app.state.mode, Mode::Settings);
+ }
+
+ #[test]
+ fn clicking_confirm_close_accepts_workspace_close() {
+ let mut app = app_for_mouse_test();
+ app.state.workspaces = vec![Workspace::test_new("a"), Workspace::test_new("b")];
+ app.state.active = Some(0);
+ app.state.selected = 1;
+ app.state.mode = Mode::ConfirmClose;
+
+ let popup = app.state.confirm_close_rect();
+ let inner = Rect::new(
+ popup.x + 1,
+ popup.y + 1,
+ popup.width.saturating_sub(2),
+ popup.height.saturating_sub(2),
+ );
+ let (confirm, _) = crate::ui::confirm_close_button_rects(inner);
+
+ app.handle_mouse(mouse(
+ MouseEventKind::Down(MouseButton::Left),
+ confirm.x,
+ confirm.y,
+ ));
+
+ assert_eq!(app.state.workspaces.len(), 1);
+ assert_eq!(app.state.mode, Mode::Terminal);
+ }
+
+ #[test]
+ fn clicking_confirm_close_accepts_after_workspace_context_menu_close() {
+ let mut app = app_for_mouse_test();
+ app.state.workspaces = vec![Workspace::test_new("a"), Workspace::test_new("b")];
+ app.state.active = Some(0);
+ app.state.selected = 0;
+ app.state.mode = Mode::Terminal;
+
+ app.state.context_menu = Some(ContextMenuState {
+ kind: ContextMenuKind::Workspace { ws_idx: 1 },
+ x: 2,
+ y: 2,
+ selected: 1,
+ });
+ app.state.mode = Mode::ContextMenu;
+ handle_context_menu_key(
+ &mut app.state,
+ KeyEvent::new(KeyCode::Enter, KeyModifiers::empty()),
+ );
+ assert_eq!(app.state.mode, Mode::ConfirmClose);
+ assert_eq!(app.state.selected, 1);
+
+ let popup = app.state.confirm_close_rect();
+ let inner = Rect::new(
+ popup.x + 1,
+ popup.y + 1,
+ popup.width.saturating_sub(2),
+ popup.height.saturating_sub(2),
+ );
+ let (confirm, _) = crate::ui::confirm_close_button_rects(inner);
+ app.handle_mouse(mouse(
+ MouseEventKind::Down(MouseButton::Left),
+ confirm.x + 1,
+ confirm.y,
+ ));
+
+ assert_eq!(app.state.workspaces.len(), 1);
+ assert_eq!(app.state.workspaces[0].display_name(), "a");
+ }
+
+ #[test]
fn dragging_sidebar_divider_sets_manual_width() {
let mut app = app_for_mouse_test();
diff --git a/src/app/mod.rs b/src/app/mod.rs
index b69c324e..126d7c92 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -237,6 +237,7 @@ impl App {
original_palette: None,
original_theme: None,
},
+ global_menu_selected: 0,
};
for ws in &mut state.workspaces {
diff --git a/src/app/state.rs b/src/app/state.rs
index 5770154d..32da8dfe 100644
--- a/src/app/state.rs
+++ b/src/app/state.rs
@@ -344,6 +344,8 @@ pub enum Mode {
ConfirmClose,
ContextMenu,
Settings,
+ GlobalMenu,
+ KeybindHelp,
}
// ---------------------------------------------------------------------------
@@ -512,6 +514,8 @@ pub struct AppState {
pub theme_name: String,
/// Settings panel state.
pub settings: SettingsState,
+ /// Currently selected item in the bottom-right global launcher menu.
+ pub global_menu_selected: usize,
}
impl AppState {
@@ -609,13 +613,19 @@ impl AppState {
close_workspace: (KeyCode::Char('d'), KeyModifiers::empty()),
close_workspace_label: "d".into(),
previous_workspace: None,
+ previous_workspace_label: None,
next_workspace: None,
+ next_workspace_label: None,
new_tab: (KeyCode::Char('c'), KeyModifiers::empty()),
new_tab_label: "c".into(),
rename_tab: None,
+ rename_tab_label: None,
previous_tab: None,
+ previous_tab_label: None,
next_tab: None,
+ next_tab_label: None,
close_tab: None,
+ close_tab_label: None,
split_vertical: (KeyCode::Char('v'), KeyModifiers::empty()),
split_vertical_label: "v".into(),
split_horizontal: (KeyCode::Char('-'), KeyModifiers::empty()),
@@ -638,6 +648,7 @@ impl AppState {
original_palette: None,
original_theme: None,
},
+ global_menu_selected: 0,
}
}
}
diff --git a/src/config.rs b/src/config.rs
index 6f5d17ae..bbbab461 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -370,9 +370,9 @@ impl Config {
field: &'static str,
configured_label: &str,
diagnostics: &mut Vec<String>,
- ) -> (Option<(KeyCode, KeyModifiers)>, String) {
+ ) -> (Option<(KeyCode, KeyModifiers)>, Option<String>) {
if configured_label.trim().is_empty() {
- return (None, String::new());
+ return (None, None);
}
let (value, diag) = parse_key_combo_with_diagnostic(
configured_label,
@@ -381,9 +381,9 @@ impl Config {
);
if let Some(diag) = diag {
diagnostics.push(diag);
- (None, String::new())
+ (None, None)
} else {
- (Some(value), configured_label.to_string())
+ (Some(value), Some(configured_label.to_string()))
}
}
@@ -506,13 +506,19 @@ impl Config {
close_workspace: bindings[2].value,
close_workspace_label: bindings[2].label.clone(),
previous_workspace: optional_bindings[0].0,
+ previous_workspace_label: optional_bindings[0].1.clone(),
next_workspace: optional_bindings[1].0,
+ next_workspace_label: optional_bindings[1].1.clone(),
new_tab: bindings[3].value,
new_tab_label: bindings[3].label.clone(),
rename_tab: optional_bindings[2].0,
+ rename_tab_label: optional_bindings[2].1.clone(),
previous_tab: optional_bindings[3].0,
+ previous_tab_label: optional_bindings[3].1.clone(),
next_tab: optional_bindings[4].0,
+ next_tab_label: optional_bindings[4].1.clone(),
close_tab: optional_bindings[5].0,
+ close_tab_label: optional_bindings[5].1.clone(),
split_vertical: bindings[4].value,
split_vertical_label: bindings[4].label.clone(),
split_horizontal: bindings[5].value,
@@ -541,13 +547,19 @@ pub struct Keybinds {
pub close_workspace: (KeyCode, KeyModifiers),
pub close_workspace_label: String,
pub previous_workspace: Option<(KeyCode, KeyModifiers)>,
+ pub previous_workspace_label: Option<String>,
pub next_workspace: Option<(KeyCode, KeyModifiers)>,
+ pub next_workspace_label: Option<String>,
pub new_tab: (KeyCode, KeyModifiers),
pub new_tab_label: String,
pub rename_tab: Option<(KeyCode, KeyModifiers)>,
+ pub rename_tab_label: Option<String>,
pub previous_tab: Option<(KeyCode, KeyModifiers)>,
+ pub previous_tab_label: Option<String>,
pub next_tab: Option<(KeyCode, KeyModifiers)>,
+ pub next_tab_label: Option<String>,
pub close_tab: Option<(KeyCode, KeyModifiers)>,
+ pub close_tab_label: Option<String>,
pub split_vertical: (KeyCode, KeyModifiers),
pub split_vertical_label: String,
pub split_horizontal: (KeyCode, KeyModifiers),
@@ -796,6 +808,47 @@ fn parse_key_combo(s: &str) -> Option<(KeyCode, KeyModifiers)> {
Some((code, modifiers))
}
+pub fn format_key_combo(binding: (KeyCode, KeyModifiers)) -> String {
+ let (code, modifiers) = binding;
+ let mut parts = Vec::new();
+ if modifiers.contains(KeyModifiers::CONTROL) {
+ parts.push("ctrl".to_string());
+ }
+ if modifiers.contains(KeyModifiers::ALT) {
+ parts.push("alt".to_string());
+ }
+ if modifiers.contains(KeyModifiers::SHIFT) {
+ parts.push("shift".to_string());
+ }
+
+ let key = match code {
+ KeyCode::Char(' ') => "space".to_string(),
+ KeyCode::Char(c) => c.to_string(),
+ KeyCode::Enter => "enter".to_string(),
+ KeyCode::Esc => "esc".to_string(),
+ KeyCode::Tab => "tab".to_string(),
+ KeyCode::BackTab => "shift+tab".to_string(),
+ KeyCode::Backspace => "backspace".to_string(),
+ KeyCode::Left => "left".to_string(),
+ KeyCode::Right => "right".to_string(),
+ KeyCode::Up => "up".to_string(),
+ KeyCode::Down => "down".to_string(),
+ KeyCode::F(n) => format!("f{n}"),
+ _ => format!("{:?}", code).to_lowercase(),
+ };
+
+ if matches!(code, KeyCode::BackTab) {
+ return if parts.is_empty() {
+ key
+ } else {
+ format!("{}+tab", parts.join("+"))
+ };
+ }
+
+ parts.push(key);
+ parts.join("+")
+}
+
fn parse_key_combo_with_diagnostic(
s: &str,
field: &str,
diff --git a/src/ui.rs b/src/ui.rs
index cbf8fdd2..c21f9a7b 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -103,6 +103,8 @@ pub fn render(app: &AppState, frame: &mut Frame) {
}
Mode::Settings => render_settings_overlay(app, frame, frame.area()),
Mode::RenameWorkspace | Mode::RenameTab => render_rename_overlay(app, frame, frame.area()),
+ Mode::GlobalMenu => render_global_launcher_menu(app, frame),
+ Mode::KeybindHelp => render_keybind_help_overlay(app, frame),
Mode::Terminal => {}
}
@@ -320,6 +322,8 @@ fn render_sidebar_collapsed(app: &AppState, frame: &mut Frame, area: Rect) {
| Mode::ConfirmClose
| Mode::ContextMenu
| Mode::Settings
+ | Mode::GlobalMenu
+ | Mode::KeybindHelp
);
let p = &app.palette;
@@ -387,6 +391,8 @@ fn render_sidebar(app: &AppState, frame: &mut Frame, area: Rect) {
| Mode::ConfirmClose
| Mode::ContextMenu
| Mode::Settings
+ | Mode::GlobalMenu
+ | Mode::KeybindHelp
);
let sep_style = if is_navigating {
Style::default().fg(p.accent)
@@ -572,11 +578,19 @@ fn render_workspace_list(app: &AppState, frame: &mut Frame, area: Rect, is_navig
row_y += 1;
}
- // "new" at the bottom of workspace section
+ // Footer actions for workspace section
if list_bottom > area.y {
+ let new_rect = app.sidebar_new_button_rect();
frame.render_widget(
Paragraph::new(Span::styled("new", Style::default().fg(p.overlay0))),
- Rect::new(area.x, list_bottom, area.width, 1),
+ new_rect,
+ );
+
+ let menu_rect = app.global_launcher_rect();
+ frame.render_widget(
+ Paragraph::new(Span::styled("menu", Style::default().fg(p.overlay0)))
+ .alignment(Alignment::Right),
+ menu_rect,
);
}
}
@@ -1123,16 +1137,15 @@ fn render_release_notes_overlay(app: &AppState, frame: &mut Frame, area: Rect) {
.style(Style::default().fg(app.palette.overlay1)),
rows[1],
);
- frame.render_widget(
- Paragraph::new(Span::styled(
- " [ close ] ",
- Style::default()
- .fg(app.palette.panel_bg)
- .bg(app.palette.accent)
- .add_modifier(Modifier::BOLD),
- ))
- .alignment(Alignment::Right),
- rows[0],
+ render_action_button(
+ frame,
+ release_notes_close_button_rect(rows[0]),
+ Some("esc"),
+ "close",
+ Style::default()
+ .fg(app.palette.panel_bg)
+ .bg(app.palette.accent)
+ .add_modifier(Modifier::BOLD),
);
let body_area = rows[3];
@@ -1221,6 +1234,33 @@ fn render_release_notes_lines<'a>(body: &'a str, p: &Palette) -> Vec<Line<'a>> {
lines
}
+pub(crate) fn release_notes_close_button_rect(area: Rect) -> Rect {
+ let width = action_button_width(Some("esc"), "close");
+ Rect::new(area.x + area.width.saturating_sub(width), area.y, width, 1)
+}
+
+pub(crate) fn onboarding_welcome_continue_rect(area: Rect) -> Rect {
+ Rect::new(
+ area.x,
+ area.y,
+ action_button_width(Some("↵"), "continue"),
+ 1,
+ )
+}
+
+pub(crate) fn onboarding_notification_button_rects(area: Rect) -> (Rect, Rect) {
+ let rects = centered_button_row(
+ area,
+ &[
+ action_button_width(Some("esc"), "back"),
+ action_button_width(Some("↵"), "save"),
+ ],
+ 2,
+ 0,
+ );
+ (rects[0], rects[1])
+}
+
fn render_onboarding_welcome(app: &AppState, frame: &mut Frame, area: Rect) {
let Some(inner) = render_modal_shell(frame, area, 64, 15, &app.palette) else {
return;
@@ -1308,21 +1348,26 @@ fn render_onboarding_welcome(app: &AppState, frame: &mut Frame, area: Rect) {
])),
rows[7],
);
+ let continue_rect = onboarding_welcome_continue_rect(rows[9]);
+ render_action_button(
+ frame,
+ continue_rect,
+ Some("↵"),
+ "continue",
+ Style::default()
+ .fg(app.palette.panel_bg)
+ .bg(app.palette.accent)
+ .add_modifier(Modifier::BOLD),
+ );
frame.render_widget(
- Paragraph::new(Line::from(vec![
- Span::styled(
- " continue ",
- Style::default()
- .fg(app.palette.panel_bg)
- .bg(app.palette.accent)
- .add_modifier(Modifier::BOLD),
- ),
- Span::styled(
- " readme has config and more",
- Style::default().fg(app.palette.overlay0),
- ),
- ])),
- rows[9],
+ Paragraph::new(" readme has config and more")
+ .style(Style::default().fg(app.palette.overlay0)),
+ Rect::new(
+ continue_rect.x + continue_rect.width + 2,
+ rows[9].y,
+ rows[9].width.saturating_sub(continue_rect.width + 2),
+ 1,
+ ),
);
}
@@ -1376,19 +1421,26 @@ fn render_onboarding_notifications(app: &AppState, frame: &mut Frame, area: Rect
);
}
- frame.render_widget(
- Paragraph::new(Line::from(vec![
- Span::styled(" [ back ] ", Style::default().fg(app.palette.overlay0)),
- Span::raw(" "),
- Span::styled(
- " [ save ] ",
- Style::default()
- .fg(app.palette.panel_bg)
- .bg(app.palette.accent)
- .add_modifier(Modifier::BOLD),
- ),
- ])),
- rows[6],
+ let (back_rect, save_rect) = onboarding_notification_button_rects(rows[6]);
+ render_action_button(
+ frame,
+ back_rect,
+ Some("esc"),
+ "back",
+ Style::default()
+ .fg(app.palette.text)
+ .bg(app.palette.surface0)
+ .add_modifier(Modifier::BOLD),
+ );
+ render_action_button(
+ frame,
+ save_rect,
+ Some("↵"),
+ "save",
+ Style::default()
+ .fg(app.palette.panel_bg)
+ .bg(app.palette.accent)
+ .add_modifier(Modifier::BOLD),
);
}
@@ -1398,96 +1450,216 @@ fn render_navigate_overlay(app: &AppState, frame: &mut Frame, area: Rect) {
.fg(app.palette.accent)
.add_modifier(Modifier::BOLD);
let dim = Style::default().fg(app.palette.overlay0);
- let label = Style::default().fg(app.palette.text);
-
- let kb = &app.keybinds;
- let line1 = Line::from(vec![
- Span::styled(format!(" {}", kb.new_workspace_label), key),
- Span::styled(" new ws ", dim),
- Span::styled(kb.rename_workspace_label.as_str(), key),
- Span::styled(" rename ws ", dim),
- Span::styled(kb.close_workspace_label.as_str(), key),
- Span::styled(" close ws ", dim),
- Span::styled(kb.new_tab_label.as_str(), key),
- Span::styled(" new tab ", dim),
- Span::styled(kb.split_vertical_label.as_str(), key),
- Span::styled(" split│ ", dim),
- Span::styled(kb.split_horizontal_label.as_str(), key),
- Span::styled(" split─ ", dim),
- Span::styled(kb.close_pane_label.as_str(), key),
- Span::styled(" close pane ", dim),
- Span::styled(kb.fullscreen_label.as_str(), key),
- Span::styled(" fullscreen", dim),
- ]);
-
- let ws_name = app
- .active
- .and_then(|i| app.workspaces.get(i))
- .map(|ws| ws.display_name())
- .unwrap_or_else(|| "—".to_string());
-
- let pane_info = app
- .active
- .and_then(|i| app.workspaces.get(i))
- .filter(|ws| ws.layout.pane_count() > 1)
- .map(|ws| {
- let ids = ws.layout.pane_ids();
- let pos = ids
- .iter()
- .position(|id| *id == ws.layout.focused())
- .unwrap_or(0);
- format!(" [{}/{}]", pos + 1, ids.len())
- })
- .unwrap_or_default();
let mode_style = Style::default()
.fg(app.palette.panel_bg)
.bg(app.palette.accent)
.add_modifier(Modifier::BOLD);
- let line2 = Line::from(vec![
+ let kb = &app.keybinds;
+ let line = Line::from(vec![
Span::styled(" NAVIGATE ", mode_style),
Span::raw(" "),
- Span::styled(ws_name, label),
- Span::styled(&pane_info, dim),
- Span::raw(" "),
Span::styled("esc", key),
Span::styled(" back ", dim),
Span::styled("↑↓", key),
Span::styled(" ws ", dim),
Span::styled("⇥", key),
Span::styled(" pane ", dim),
+ Span::styled(kb.new_tab_label.as_str(), key),
+ Span::styled(" new tab ", dim),
+ Span::styled(kb.split_vertical_label.as_str(), key),
+ Span::styled(" split│ ", dim),
+ Span::styled(kb.split_horizontal_label.as_str(), key),
+ Span::styled(" split─ ", dim),
+ Span::styled(kb.close_pane_label.as_str(), key),
+ Span::styled(" close ", dim),
+ Span::styled(kb.fullscreen_label.as_str(), key),
+ Span::styled(" full ", dim),
Span::styled(kb.resize_mode_label.as_str(), key),
Span::styled(" resize ", dim),
- Span::styled(kb.toggle_sidebar_label.as_str(), key),
- Span::styled(" sidebar ", dim),
+ Span::styled("?", key),
+ Span::styled(" keybinds ", dim),
Span::styled("s", key),
- Span::styled(" settings ", dim),
- Span::styled("⏎", key),
- Span::styled(" open ", dim),
- Span::styled("q", key),
- Span::styled(" quit", dim),
+ Span::styled(" settings", dim),
]);
- let overlay_height = 2;
- let overlay_y = area.y + area.height.saturating_sub(overlay_height);
- let overlay_area = Rect::new(area.x, overlay_y, area.width, overlay_height);
+ let overlay_y = area.y + area.height.saturating_sub(1);
+ let overlay_area = Rect::new(area.x, overlay_y, area.width, 1);
- // Clear the area behind the overlay
frame.render_widget(Clear, overlay_area);
-
let bg = Style::default().bg(app.palette.panel_bg);
let buf = frame.buffer_mut();
- for y in overlay_area.y..overlay_area.y + overlay_area.height {
- for x in overlay_area.x..overlay_area.x + overlay_area.width {
- buf[(x, y)].set_style(bg);
+ for x in overlay_area.x..overlay_area.x + overlay_area.width {
+ buf[(x, overlay_y)].set_style(bg);
+ }
+ frame.render_widget(Paragraph::new(line), overlay_area);
+}
+
+fn render_global_launcher_menu(app: &AppState, frame: &mut Frame) {
+ let rect = app.global_menu_rect();
+ let Some(inner) = render_panel_shell(frame, rect, app.palette.accent, app.palette.panel_bg)
+ else {
+ return;
+ };
+
+ let items = ["keybinds", "settings"];
+ for (idx, item) in items.iter().enumerate() {
+ let y = inner.y + idx as u16;
+ if y >= inner.y + inner.height {
+ break;
}
+ let selected = idx == app.global_menu_selected;
+ let style = if selected {
+ Style::default()
+ .fg(app.palette.panel_bg)
+ .bg(app.palette.accent)
+ .add_modifier(Modifier::BOLD)
+ } else {
+ Style::default().fg(app.palette.text)
+ };
+ frame.render_widget(
+ Paragraph::new(format!(" {item}"))
+ .style(style)
+ .alignment(Alignment::Left),
+ Rect::new(inner.x, y, inner.width, 1),
+ );
+ }
+}
+
+fn keybind_help_groups(app: &AppState) -> Vec<(&'static str, Vec<(String, &'static str)>)> {
+ let kb = &app.keybinds;
+ let mut groups = Vec::new();
+
+ groups.push((
+ "global",
+ vec![
+ (
+ crate::config::format_key_combo((app.prefix_code, app.prefix_mods)),
+ "navigate mode",
+ ),
+ ("prefix + ?".to_string(), "keybinds"),
+ ],
+ ));
+
+ groups.push((
+ "navigation",
+ vec![
+ ("esc".to_string(), "back"),
+ ("↑ / ↓".to_string(), "workspace list"),
+ ("h j k l / arrows".to_string(), "move focus"),
+ ("tab / shift+tab".to_string(), "cycle pane"),
+ ("enter".to_string(), "open workspace"),
+ ("s".to_string(), "settings"),
+ ("q".to_string(), "quit"),
+ ],
+ ));
+
+ let mut workspace_tab = vec![
+ (kb.new_workspace_label.clone(), "new workspace"),
+ (kb.rename_workspace_label.clone(), "rename workspace"),
+ (kb.close_workspace_label.clone(), "close workspace"),
+ (kb.new_tab_label.clone(), "new 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 [row1, row2] =
- Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(overlay_area);
- frame.render_widget(Paragraph::new(line1), row1);
- frame.render_widget(Paragraph::new(line2), row2);
+ groups
+}
+
+fn render_keybind_help_overlay(app: &AppState, frame: &mut Frame) {
+ let rect = app.keybind_help_rect();
+ let Some(inner) = render_panel_shell(frame, rect, app.palette.accent, app.palette.panel_bg)
+ else {
+ return;
+ };
+ if inner.width < 12 || inner.height < 6 {
+ return;
+ }
+
+ let title_style = Style::default()
+ .fg(app.palette.text)
+ .add_modifier(Modifier::BOLD);
+ 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 hint_style = Style::default().fg(app.palette.overlay0);
+
+ frame.render_widget(
+ Paragraph::new(Line::from(vec![
+ Span::styled(" keybinds", title_style),
+ Span::styled(" esc to close", hint_style),
+ ])),
+ Rect::new(inner.x, inner.y, inner.width, 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 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),
+ );
+ y += 1;
+ for (key, label) in entries {
+ if y >= inner.y + inner.height {
+ break;
+ }
+ let padded_key = format!(" {:<width$} ", key, width = key_width as usize);
+ frame.render_widget(
+ Paragraph::new(Line::from(vec![
+ Span::styled(padded_key, key_style),
+ Span::styled(label, label_style),
+ ])),
+ Rect::new(inner.x, y, inner.width, 1),
+ );
+ y += 1;
+ }
+ y += 1;
+ }
}
/// Floating overlay for resize mode.
@@ -1527,18 +1699,17 @@ fn render_resize_overlay(app: &AppState, frame: &mut Frame, area: Rect) {
/// Centered popup confirmation dialog with dimmed background.
pub(crate) fn rename_button_rects(inner: Rect) -> (Rect, Rect, Rect) {
- let save_w = 10u16;
- let clear_w = 11u16;
- let cancel_w = 12u16;
- let gap = 2u16;
- let total_w = save_w + gap + clear_w + gap + cancel_w;
- let x = inner.x + inner.width.saturating_sub(total_w) / 2;
- let y = inner.y + 3;
- (
- Rect::new(x, y, save_w, 1),
- Rect::new(x + save_w + gap, y, clear_w, 1),
- Rect::new(x + save_w + gap + clear_w + gap, y, cancel_w, 1),
- )
+ let rects = centered_button_row(
+ inner,
+ &[
+ action_button_width(Some("↵"), "save"),
+ action_button_width(Some("^c"), "clear"),
+ action_button_width(Some("esc"), "cancel"),
+ ],
+ 2,
+ 3,
+ );
+ (rects[0], rects[1], rects[2])
}
fn render_rename_overlay(app: &AppState, frame: &mut Frame, area: Rect) {
@@ -1581,32 +1752,35 @@ fn render_rename_overlay(app: &AppState, frame: &mut Frame, area: Rect) {
let (save_rect, clear_rect, cancel_rect) = rename_button_rects(inner);
- frame.render_widget(
- Paragraph::new(" ↵ save ").style(
- Style::default()
- .fg(app.palette.panel_bg)
- .bg(app.palette.accent)
- .add_modifier(Modifier::BOLD),
- ),
+ render_action_button(
+ frame,
save_rect,
+ Some("↵"),
+ "save",
+ Style::default()
+ .fg(app.palette.panel_bg)
+ .bg(app.palette.accent)
+ .add_modifier(Modifier::BOLD),
);
- frame.render_widget(
- Paragraph::new(" ^c clear ").style(
- Style::default()
- .fg(app.palette.text)
- .bg(app.palette.surface0)
- .add_modifier(Modifier::BOLD),
- ),
+ render_action_button(
+ frame,
clear_rect,
+ Some("^c"),
+ "clear",
+ Style::default()
+ .fg(app.palette.text)
+ .bg(app.palette.surface0)
+ .add_modifier(Modifier::BOLD),
);
- frame.render_widget(
- Paragraph::new(" esc cancel ").style(
- Style::default()
- .fg(app.palette.text)
- .bg(app.palette.surface0)
- .add_modifier(Modifier::BOLD),
- ),
+ render_action_button(
+ frame,
cancel_rect,
+ Some("esc"),
+ "cancel",
+ Style::default()
+ .fg(app.palette.text)
+ .bg(app.palette.surface0)
+ .add_modifier(Modifier::BOLD),
);
}
@@ -1679,8 +1853,12 @@ fn render_confirm_close_overlay(app: &AppState, frame: &mut Frame, area: Rect) {
let (confirm_rect, cancel_rect) = confirm_close_button_rects(inner);
let confirm_selected = app.confirm_close_selected_confirm;
- frame.render_widget(
- Paragraph::new(" confirm ").style(if confirm_selected {
+ render_action_button(
+ frame,
+ confirm_rect,
+ Some("↵"),
+ "confirm",
+ if confirm_selected {
Style::default()
.fg(app.palette.panel_bg)
.bg(app.palette.red)
@@ -1690,11 +1868,14 @@ fn render_confirm_close_overlay(app: &AppState, frame: &mut Frame, area: Rect) {
.fg(app.palette.text)
.bg(app.palette.surface0)
.add_modifier(Modifier::BOLD)
- }),
- confirm_rect,
+ },
);
- frame.render_widget(
- Paragraph::new(" cancel ").style(if confirm_selected {
+ render_action_button(
+ frame,
+ cancel_rect,
+ Some("esc"),
+ "cancel",
+ if confirm_selected {
Style::default()
.fg(app.palette.text)
.bg(app.palette.surface0)
@@ -1704,12 +1885,37 @@ fn render_confirm_close_overlay(app: &AppState, frame: &mut Frame, area: Rect) {
.fg(app.palette.panel_bg)
.bg(app.palette.accent)
.add_modifier(Modifier::BOLD)
- }),
- cancel_rect,
+ },
);
}
}
+pub(crate) fn action_button_text(hint: Option<&str>, label: &str) -> String {
+ match hint {
+ Some(hint) => format!(" {hint} {label} "),
+ None => format!(" {label} "),
+ }
+}
+
+pub(crate) fn action_button_width(hint: Option<&str>, label: &str) -> u16 {
+ action_button_text(hint, label).chars().count() as u16
+}
+
+fn render_action_button(
+ frame: &mut Frame,
+ rect: Rect,
+ hint: Option<&str>,
+ label: &str,
+ style: Style,
+) {
+ frame.render_widget(
+ Paragraph::new(action_button_text(hint, label))
+ .style(style)
+ .alignment(Alignment::Center),
+ rect,
+ );
+}
+
fn centered_button_row(inner: Rect, widths: &[u16], gap: u16, row_offset: u16) -> Vec<Rect> {
let total_w = widths
.iter()
@@ -1733,13 +1939,29 @@ fn centered_button_row(inner: Rect, widths: &[u16], gap: u16, row_offset: u16) -
.collect()
}
-fn confirm_close_button_rects(inner: Rect) -> (Rect, Rect) {
- let rects = centered_button_row(inner, &[9, 8], 2, 2);
+pub(crate) fn confirm_close_button_rects(inner: Rect) -> (Rect, Rect) {
+ let rects = centered_button_row(
+ inner,
+ &[
+ action_button_width(Some("↵"), "confirm"),
+ action_button_width(Some("esc"), "cancel"),
+ ],
+ 2,
+ 2,
+ );
(rects[0], rects[1])
}
-fn settings_button_rects(inner: Rect) -> (Rect, Rect) {
- let rects = centered_button_row(inner, &[7, 7], 2, inner.height.saturating_sub(1));
+pub(crate) fn settings_button_rects(inner: Rect) -> (Rect, Rect) {
+ let rects = centered_button_row(
+ inner,
+ &[
+ action_button_width(Some("↵"), "apply"),
+ action_button_width(Some("esc"), "close"),
+ ],
+ 2,
+ inner.height.saturating_sub(1),
+ );
(rects[0], rects[1])
}
@@ -1850,23 +2072,25 @@ fn render_settings_overlay(app: &AppState, frame: &mut Frame, area: Rect) {
let footer_y = inner.y + inner.height - 1;
if footer_y > y {
let (apply_rect, close_rect) = settings_button_rects(inner);
- frame.render_widget(
- Paragraph::new(" apply ").style(
- Style::default()
- .fg(p.panel_bg)
- .bg(p.accent)
- .add_modifier(Modifier::BOLD),
- ),
+ render_action_button(
+ frame,
apply_rect,
+ Some("↵"),
+ "apply",
+ Style::default()
+ .fg(p.panel_bg)
+ .bg(p.accent)
+ .add_modifier(Modifier::BOLD),
);
- frame.render_widget(
- Paragraph::new(" close ").style(
- Style::default()
- .fg(p.text)
- .bg(p.surface0)
- .add_modifier(Modifier::BOLD),
- ),
+ render_action_button(
+ frame,
close_rect,
+ Some("esc"),
+ "close",
+ Style::default()
+ .fg(p.text)
+ .bg(p.surface0)
+ .add_modifier(Modifier::BOLD),
);
let hint_area = Rect::new(inner.x, footer_y.saturating_sub(1), inner.width, 1);