notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

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:
Mcrates/notedeck_dave/src/ui/directory_picker.rs | 152++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Mcrates/notedeck_dave/src/ui/settings.rs | 19++++++++++++++++++-
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"); + } }); }, );