commit 935fecb9b84554dfee92be0259a0807e0ad1e92e
parent 4d1ccd6165dacd7aa7d1a4e6e254e0096986cdc9
Author: William Casarin <jb55@jb55.com>
Date: Wed, 28 Jan 2026 22:09:39 -0800
feat(dave): add keyboard shortcuts to settings and directory picker overlays
Directory picker now supports 1-9 keys to select recent directories,
B to browse, and Escape to cancel. Settings panel supports Ctrl+S to
save and Escape to cancel. Keyboard hints are shown when Ctrl is held.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat:
2 files changed, 122 insertions(+), 49 deletions(-)
diff --git a/crates/notedeck_dave/src/ui/directory_picker.rs b/crates/notedeck_dave/src/ui/directory_picker.rs
@@ -1,3 +1,4 @@
+use crate::ui::keybind_hint::paint_keybind_hint;
use egui::{RichText, Vec2};
use std::path::PathBuf;
@@ -102,6 +103,30 @@ impl DirectoryPicker {
let mut action = None;
let is_narrow = notedeck::ui::is_narrow(ui.ctx());
+ let ctrl_held = ui.input(|i| i.modifiers.ctrl);
+
+ // Handle keyboard shortcuts for recent directories (1-9)
+ for (idx, path) in self.recent_directories.iter().take(9).enumerate() {
+ let key = match idx {
+ 0 => egui::Key::Num1,
+ 1 => egui::Key::Num2,
+ 2 => egui::Key::Num3,
+ 3 => egui::Key::Num4,
+ 4 => egui::Key::Num5,
+ 5 => egui::Key::Num6,
+ 6 => egui::Key::Num7,
+ 7 => egui::Key::Num8,
+ 8 => egui::Key::Num9,
+ _ => continue,
+ };
+ if ui.input(|i| i.key_pressed(key)) {
+ return Some(DirectoryPickerAction::DirectorySelected(path.clone()));
+ }
+ }
+
+ // Handle B key for browse (track whether we need to trigger it)
+ let trigger_browse =
+ ui.input(|i| i.key_pressed(egui::Key::B)) && self.pending_folder_pick.is_none();
// Full panel frame
egui::Frame::new()
@@ -149,29 +174,49 @@ impl DirectoryPicker {
egui::ScrollArea::vertical()
.max_height(scroll_height)
.show(ui, |ui| {
- for path in &self.recent_directories.clone() {
+ for (idx, path) in
+ self.recent_directories.clone().iter().enumerate()
+ {
let display = abbreviate_path(path);
// Full-width button style with larger touch targets on mobile
let button_height = if is_narrow { 44.0 } else { 32.0 };
- let button =
- egui::Button::new(RichText::new(&display).monospace())
- .min_size(Vec2::new(
- ui.available_width(),
- button_height,
- ))
- .fill(ui.visuals().widgets.inactive.weak_bg_fill);
-
- if ui
- .add(button)
- .on_hover_text(path.display().to_string())
- .clicked()
- {
- action =
- Some(DirectoryPickerAction::DirectorySelected(
- path.clone(),
- ));
- }
+ let hint_width =
+ if ctrl_held && idx < 9 { 24.0 } else { 0.0 };
+ let button_width = ui.available_width() - hint_width - 4.0;
+
+ ui.horizontal(|ui| {
+ let button = egui::Button::new(
+ RichText::new(&display).monospace(),
+ )
+ .min_size(Vec2::new(button_width, button_height))
+ .fill(ui.visuals().widgets.inactive.weak_bg_fill);
+
+ let response = ui.add(button);
+
+ // Show keybind hint when Ctrl is held (for first 9 items)
+ if ctrl_held && idx < 9 {
+ let hint_text = format!("{}", idx + 1);
+ let hint_center = response.rect.right_center()
+ + egui::vec2(hint_width / 2.0 + 2.0, 0.0);
+ paint_keybind_hint(
+ ui,
+ hint_center,
+ &hint_text,
+ 18.0,
+ );
+ }
+
+ if response
+ .on_hover_text(path.display().to_string())
+ .clicked()
+ {
+ action =
+ Some(DirectoryPickerAction::DirectorySelected(
+ path.clone(),
+ ));
+ }
+ });
ui.add_space(4.0);
}
@@ -183,36 +228,47 @@ impl DirectoryPicker {
}
// Browse button (larger touch target on mobile)
- let browse_button =
- egui::Button::new(RichText::new("Browse...").size(if is_narrow {
- 16.0
- } else {
- 14.0
- }))
- .min_size(Vec2::new(
- if is_narrow {
- ui.available_width()
+ ui.horizontal(|ui| {
+ let browse_button =
+ egui::Button::new(RichText::new("Browse...").size(if is_narrow {
+ 16.0
} else {
- 120.0
- },
- if is_narrow { 48.0 } else { 32.0 },
- ));
-
- if ui
- .add(browse_button)
- .on_hover_text("Open folder picker dialog")
- .clicked()
- {
- // Spawn async folder picker
- let (tx, rx) = std::sync::mpsc::channel();
- let ctx_clone = ui.ctx().clone();
- std::thread::spawn(move || {
- let result = rfd::FileDialog::new().pick_folder();
- let _ = tx.send(result);
- ctx_clone.request_repaint();
- });
- self.pending_folder_pick = Some(rx);
- }
+ 14.0
+ }))
+ .min_size(Vec2::new(
+ if is_narrow {
+ ui.available_width() - 28.0
+ } else {
+ 120.0
+ },
+ if is_narrow { 48.0 } else { 32.0 },
+ ));
+
+ let response = ui.add(browse_button);
+
+ // Show keybind hint when Ctrl is held
+ if ctrl_held {
+ let hint_center =
+ response.rect.right_center() + egui::vec2(14.0, 0.0);
+ paint_keybind_hint(ui, hint_center, "B", 18.0);
+ }
+
+ if response
+ .on_hover_text("Open folder picker dialog (B)")
+ .clicked()
+ || trigger_browse
+ {
+ // Spawn async folder picker
+ let (tx, rx) = std::sync::mpsc::channel();
+ let ctx_clone = ui.ctx().clone();
+ std::thread::spawn(move || {
+ let result = rfd::FileDialog::new().pick_folder();
+ let _ = tx.send(result);
+ ctx_clone.request_repaint();
+ });
+ self.pending_folder_pick = Some(rx);
+ }
+ });
if self.pending_folder_pick.is_some() {
ui.horizontal(|ui| {
diff --git a/crates/notedeck_dave/src/ui/settings.rs b/crates/notedeck_dave/src/ui/settings.rs
@@ -1,4 +1,5 @@
use crate::config::{AiProvider, DaveSettings};
+use crate::ui::keybind_hint::keybind_hint;
/// Tracks the state of the settings panel
pub struct DaveSettingsPanel {
@@ -81,6 +82,12 @@ impl DaveSettingsPanel {
let mut action: Option<SettingsPanelAction> = None;
let is_narrow = notedeck::ui::is_narrow(ui.ctx());
+ let ctrl_held = ui.input(|i| i.modifiers.ctrl);
+
+ // Handle Ctrl+S to save
+ if ui.input(|i| i.modifiers.ctrl && i.key_pressed(egui::Key::S)) {
+ action = Some(SettingsPanelAction::Save(self.editing.clone()));
+ }
// Full panel frame with padding
egui::Frame::new()
@@ -92,6 +99,9 @@ impl DaveSettingsPanel {
if ui.button("< Back").clicked() {
action = Some(SettingsPanelAction::Cancel);
}
+ if ctrl_held {
+ keybind_hint(ui, "Esc");
+ }
ui.add_space(16.0);
ui.heading("Settings");
});
@@ -112,14 +122,21 @@ impl DaveSettingsPanel {
ui.add_space(24.0);
- // Action buttons
+ // Action buttons with keyboard hints
ui.horizontal(|ui| {
if ui.button("Save").clicked() {
action = Some(SettingsPanelAction::Save(self.editing.clone()));
}
+ if ctrl_held {
+ keybind_hint(ui, "S");
+ }
+ ui.add_space(8.0);
if ui.button("Cancel").clicked() {
action = Some(SettingsPanelAction::Cancel);
}
+ if ctrl_held {
+ keybind_hint(ui, "Esc");
+ }
});
},
);