notedeck

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

commit dacce6bc90bb88ed411d6cb0fd97a7b8e1c36afe
parent 328613fdfccc49d8366ebddb1a65a403a99c2ae4
Author: William Casarin <jb55@jb55.com>
Date:   Fri, 27 Feb 2026 11:00:08 -0800

chrome: toggle side menu with Escape key from any app

Apps like Nostrverse don't provide a ToggleChrome button, so
once you switch to them there's no way to reopen the side menu.

Add an Escape key handler at the Chrome level that toggles the
drawer. It uses egui's consume_key so it only fires when no app
has already consumed the event. Migrate all existing Escape
handlers across Dave, Columns, and Messages from key_pressed
(non-consuming read) to consume_key (check-and-remove), so
apps that handle Escape for their own purposes take priority.

In Columns, Escape is only consumed when there's actually a
route to go back to (routes > 1); at the top level it falls
through to Chrome, opening the side menu.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Diffstat:
Mcrates/notedeck_chrome/src/chrome.rs | 10++++++++++
Mcrates/notedeck_columns/src/app.rs | 15+++++++++++++--
Mcrates/notedeck_columns/src/ui/note/post.rs | 5++++-
Mcrates/notedeck_dave/src/ui/directory_picker.rs | 6+++++-
Mcrates/notedeck_dave/src/ui/host_picker.rs | 6+++++-
Mcrates/notedeck_dave/src/ui/keybindings.rs | 7+++++--
Mcrates/notedeck_dave/src/ui/session_list.rs | 2+-
Mcrates/notedeck_dave/src/ui/session_picker.rs | 2+-
Mcrates/notedeck_dave/src/ui/settings.rs | 5++++-
Mcrates/notedeck_messages/src/ui/nav.rs | 2+-
10 files changed, 49 insertions(+), 11 deletions(-)

diff --git a/crates/notedeck_chrome/src/chrome.rs b/crates/notedeck_chrome/src/chrome.rs @@ -463,6 +463,16 @@ impl notedeck::App for Chrome { action.process(ctx, self, ui); self.nav.close(); } + + // Toggle the side menu on Escape if no app consumed the key + if ui + .ctx() + .input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) + { + self.toggle(); + } + + // TODO: unify this constant with the columns side panel width. ui crate? AppResponse::none() } } diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs @@ -124,7 +124,7 @@ fn handle_egui_events( columns.select_left(); } */ - egui::Key::BrowserBack | egui::Key::Escape => { + egui::Key::BrowserBack => { columns.get_selected_router().go_back(); } _ => {} @@ -182,6 +182,13 @@ fn try_process_event( ) }); + // Handle Escape separately: only consume the key if there's a route to go back to, + // otherwise let Chrome handle it (e.g. to open the side menu) + let can_go_back = current_columns.get_selected_router().routes().len() > 1; + if can_go_back && ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) { + current_columns.get_selected_router().go_back(); + } + let selected_account_pk = *app_ctx.accounts.selected_account_pubkey(); for (kind, timeline) in &mut damus.timeline_cache { if timeline.subscription.dependers(&selected_account_pk) == 0 { @@ -427,7 +434,11 @@ fn fullscreen_media_viewer_ui( .fullscreen(true) .ui(img_cache, jobs, ui); - if resp.clicked() || ui.input(|i| i.key_pressed(egui::Key::Escape)) { + if resp.clicked() + || ui + .ctx() + .input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) + { fullscreen_media_close(state); } } diff --git a/crates/notedeck_columns/src/ui/note/post.rs b/crates/notedeck_columns/src/ui/note/post.rs @@ -271,7 +271,10 @@ impl<'a, 'd> PostView<'a, 'd> { return None; } - if ui.ctx().input(|r| r.key_pressed(egui::Key::Escape)) { + if ui + .ctx() + .input_mut(|r| r.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) + { self.draft.buffer.delete_mention(mention.index); return None; } diff --git a/crates/notedeck_dave/src/ui/directory_picker.rs b/crates/notedeck_dave/src/ui/directory_picker.rs @@ -388,7 +388,11 @@ impl DirectoryPicker { }); // Handle Escape key (only if cancellation is allowed) - if has_sessions && ui.ctx().input(|i| i.key_pressed(egui::Key::Escape)) { + if has_sessions + && ui + .ctx() + .input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) + { action = Some(DirectoryPickerAction::Cancelled); } diff --git a/crates/notedeck_dave/src/ui/host_picker.rs b/crates/notedeck_dave/src/ui/host_picker.rs @@ -131,7 +131,11 @@ pub fn host_picker_overlay_ui( }); // Escape to cancel - if has_sessions && ui.ctx().input(|i| i.key_pressed(egui::Key::Escape)) { + if has_sessions + && ui + .ctx() + .input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) + { action = Some(HostPickerAction::Cancelled); } diff --git a/crates/notedeck_dave/src/ui/keybindings.rs b/crates/notedeck_dave/src/ui/keybindings.rs @@ -62,12 +62,15 @@ pub fn check_keybindings( let is_agentic = ai_mode == AiMode::Agentic; // Escape in tentative state cancels the tentative mode (agentic only) - if is_agentic && in_tentative_state && ctx.input(|i| i.key_pressed(Key::Escape)) { + if is_agentic + && in_tentative_state + && ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, Key::Escape)) + { return Some(KeyAction::CancelTentative); } // Escape otherwise works to interrupt AI (even when text input has focus) - if ctx.input(|i| i.key_pressed(Key::Escape)) { + if ctx.input_mut(|i| i.consume_key(egui::Modifiers::NONE, Key::Escape)) { return Some(KeyAction::Interrupt); } diff --git a/crates/notedeck_dave/src/ui/session_list.rs b/crates/notedeck_dave/src/ui/session_list.rs @@ -431,7 +431,7 @@ fn inline_rename_ui( if edit.has_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) { Some(RenameOutcome::Confirmed(buf.clone())) } else if edit.lost_focus() { - if ui.input(|i| i.key_pressed(egui::Key::Escape)) { + if ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) { Some(RenameOutcome::Cancelled) } else { Some(RenameOutcome::Confirmed(buf.clone())) diff --git a/crates/notedeck_dave/src/ui/session_picker.rs b/crates/notedeck_dave/src/ui/session_picker.rs @@ -118,7 +118,7 @@ impl SessionPicker { // Handle Escape key or Ctrl+B to go back // B key requires Ctrl to avoid intercepting TextEdit input - if ui.input(|i| i.key_pressed(egui::Key::Escape)) + if ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) || (ctrl_held && ui.input(|i| i.key_pressed(egui::Key::B))) { return Some(SessionPickerAction::BackToDirectoryPicker); diff --git a/crates/notedeck_dave/src/ui/settings.rs b/crates/notedeck_dave/src/ui/settings.rs @@ -143,7 +143,10 @@ impl DaveSettingsPanel { }); // Handle Escape key - if ui.ctx().input(|i| i.key_pressed(egui::Key::Escape)) { + if ui + .ctx() + .input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) + { action = Some(SettingsPanelAction::Cancel); } diff --git a/crates/notedeck_messages/src/ui/nav.rs b/crates/notedeck_messages/src/ui/nav.rs @@ -125,7 +125,7 @@ fn render_nav_body( } Route::CreateConvo => 's: { // Escape key goes back - if ui.input(|i| i.key_pressed(egui::Key::Escape)) { + if ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) { break 's Some(MessagesAction::Back); }