commit 8095f92267a63497e1abf08eaf4283882aac41f1
parent 0b4456b2cc26e9aecc0a56a5bdd52be1c1fd0351
Author: William Casarin <jb55@jb55.com>
Date: Tue, 27 Jan 2026 07:22:21 -0800
dave: add KeybindHint widget for visual keybinding indicators
Introduces a reusable widget that displays keybindings as small framed
boxes with monospace text. Used in the scene view for agent numbers
when Ctrl is held, and next to the New Agent button in the toolbar.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat:
4 files changed, 97 insertions(+), 20 deletions(-)
diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs
@@ -333,19 +333,17 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
strip.cell(|ui| {
// Scene toolbar at top
ui.horizontal(|ui| {
- // Show keybinding hint only when Ctrl is held
- let new_agent_label = if ctrl_held {
- "+ New Agent [N]"
- } else {
- "+ New Agent"
- };
if ui
- .button(new_agent_label)
+ .button("+ New Agent")
.on_hover_text("Hold Ctrl to see keybindings")
.clicked()
{
dave_response = DaveResponse::new(DaveAction::NewChat);
}
+ // Show keybinding hint only when Ctrl is held
+ if ctrl_held {
+ ui::keybind_hint(ui, "N");
+ }
ui.separator();
if ui
.button("Classic View")
@@ -602,7 +600,8 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
if let Some(pending_since) = self.interrupt_pending_since {
// Check if we're within the confirmation timeout
- if now.duration_since(pending_since).as_secs_f32() < Self::INTERRUPT_CONFIRM_TIMEOUT_SECS
+ if now.duration_since(pending_since).as_secs_f32()
+ < Self::INTERRUPT_CONFIRM_TIMEOUT_SECS
{
// Second Escape within timeout - confirm interrupt
self.handle_interrupt(ui);
diff --git a/crates/notedeck_dave/src/ui/keybind_hint.rs b/crates/notedeck_dave/src/ui/keybind_hint.rs
@@ -0,0 +1,75 @@
+use egui::{Pos2, Rect, Response, Sense, Ui, Vec2};
+
+/// A visual keybinding hint - a small framed box with a letter or number inside.
+/// Used to indicate keyboard shortcuts in the UI.
+pub struct KeybindHint<'a> {
+ text: &'a str,
+ size: f32,
+}
+
+impl<'a> KeybindHint<'a> {
+ /// Create a new keybinding hint with the given text
+ pub fn new(text: &'a str) -> Self {
+ Self { text, size: 18.0 }
+ }
+
+ /// Set the size of the hint box (default: 18.0)
+ pub fn size(mut self, size: f32) -> Self {
+ self.size = size;
+ self
+ }
+
+ /// Show the keybinding hint and return the response
+ pub fn show(self, ui: &mut Ui) -> Response {
+ let (rect, response) = ui.allocate_exact_size(Vec2::splat(self.size), Sense::hover());
+ self.paint(ui, rect);
+ response
+ }
+
+ /// Paint the keybinding hint at a specific position (for use with painters)
+ pub fn paint_at(self, ui: &Ui, center: Pos2) {
+ let rect = Rect::from_center_size(center, Vec2::splat(self.size));
+ self.paint(ui, rect);
+ }
+
+ fn paint(self, ui: &Ui, rect: Rect) {
+ let painter = ui.painter();
+ let visuals = ui.visuals();
+
+ // Frame/border
+ let stroke_color = visuals.widgets.noninteractive.fg_stroke.color;
+ let bg_color = visuals.widgets.noninteractive.bg_fill;
+ let corner_radius = 3.0;
+
+ // Background fill
+ painter.rect_filled(rect, corner_radius, bg_color);
+
+ // Border stroke
+ painter.rect_stroke(
+ rect,
+ corner_radius,
+ egui::Stroke::new(1.0, stroke_color.gamma_multiply(0.6)),
+ egui::StrokeKind::Inside,
+ );
+
+ // Text in center
+ let font_size = self.size * 0.65;
+ painter.text(
+ rect.center(),
+ egui::Align2::CENTER_CENTER,
+ self.text,
+ egui::FontId::monospace(font_size),
+ visuals.text_color(),
+ );
+ }
+}
+
+/// Draw a keybinding hint inline (for use in horizontal layouts)
+pub fn keybind_hint(ui: &mut Ui, text: &str) -> Response {
+ KeybindHint::new(text).show(ui)
+}
+
+/// Draw a keybinding hint at a specific position using the painter
+pub fn paint_keybind_hint(ui: &Ui, center: Pos2, text: &str, size: f32) {
+ KeybindHint::new(text).size(size).paint_at(ui, center);
+}
diff --git a/crates/notedeck_dave/src/ui/mod.rs b/crates/notedeck_dave/src/ui/mod.rs
@@ -1,11 +1,13 @@
mod dave;
pub mod diff;
+pub mod keybind_hint;
pub mod keybindings;
pub mod scene;
pub mod session_list;
mod settings;
pub use dave::{DaveAction, DaveResponse, DaveUi};
+pub use keybind_hint::{keybind_hint, paint_keybind_hint, KeybindHint};
pub use keybindings::{check_keybindings, KeyAction};
pub use scene::{AgentScene, SceneAction, SceneResponse};
pub use session_list::{SessionListAction, SessionListUi};
diff --git a/crates/notedeck_dave/src/ui/scene.rs b/crates/notedeck_dave/src/ui/scene.rs
@@ -1,5 +1,6 @@
use crate::agent_status::AgentStatus;
use crate::session::{SessionId, SessionManager};
+use crate::ui::paint_keybind_hint;
use egui::{Color32, Pos2, Rect, Response, Sense, Vec2};
/// The RTS-style scene view for managing agents
@@ -356,19 +357,19 @@ impl AgentScene {
};
painter.circle_filled(center, agent_radius - 2.0, fill_color);
- // Agent icon in center: show number when Ctrl held (keybinding hint), otherwise first letter
- let icon_text = if show_keybinding {
- id.to_string()
+ // Agent icon in center: show keybind frame when Ctrl held, otherwise first letter
+ if show_keybinding {
+ paint_keybind_hint(ui, center, &id.to_string(), 24.0);
} else {
- title.chars().next().unwrap_or('?').to_uppercase().collect()
- };
- painter.text(
- center,
- egui::Align2::CENTER_CENTER,
- &icon_text,
- egui::FontId::proportional(20.0),
- ui.visuals().text_color(),
- );
+ let icon_text: String = title.chars().next().unwrap_or('?').to_uppercase().collect();
+ painter.text(
+ center,
+ egui::Align2::CENTER_CENTER,
+ &icon_text,
+ egui::FontId::proportional(20.0),
+ ui.visuals().text_color(),
+ );
+ }
// Title below
let title_pos = center + Vec2::new(0.0, agent_radius + 10.0);