commit 72105d384dedcc7a08cb239a1205b9abba1c3195
parent d4ef2594bcb5a273033fa5c9d8a254646634a838
Author: William Casarin <jb55@jb55.com>
Date: Wed, 28 Jan 2026 21:56:40 -0800
dave: add Ctrl+G keybinding to open external editor for input
Opens $VISUAL directly if set (for GUI editors like gvim), otherwise
spawns $TERMINAL with $EDITOR. Auto-detects terminal from common
options (alacritty, kitty, gnome-terminal, konsole, urxvtc, urxvt, xterm).
Also moves toggle view keybinding from Ctrl+G to Ctrl+L.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat:
3 files changed, 109 insertions(+), 22 deletions(-)
diff --git a/crates/notedeck_dave/src/lib.rs b/crates/notedeck_dave/src/lib.rs
@@ -1114,6 +1114,103 @@ You are an AI agent for the nostr protocol called Dave, created by Damus. nostr
}
}
+ /// Open an external editor for composing the input text
+ fn open_external_editor(&mut self) {
+ use std::process::Command;
+
+ let Some(session) = self.session_manager.get_active_mut() else {
+ return;
+ };
+
+ // Create temp file with current input content
+ let temp_path = std::env::temp_dir().join("notedeck_input.txt");
+ if let Err(e) = std::fs::write(&temp_path, &session.input) {
+ tracing::error!("Failed to write temp file for external editor: {}", e);
+ return;
+ }
+
+ // Try $VISUAL first (GUI editors), then fall back to terminal + $EDITOR
+ let visual = std::env::var("VISUAL").ok();
+ let editor = std::env::var("EDITOR").ok();
+
+ let result = if let Some(visual_editor) = visual {
+ // $VISUAL is set - use it directly (assumes GUI editor)
+ tracing::debug!("Opening external editor via $VISUAL: {}", visual_editor);
+ Command::new(&visual_editor).arg(&temp_path).status()
+ } else {
+ // Fall back to terminal + $EDITOR
+ let editor_cmd = editor.unwrap_or_else(|| "vim".to_string());
+ let terminal = std::env::var("TERMINAL")
+ .ok()
+ .or_else(Self::find_terminal)
+ .unwrap_or_else(|| "xterm".to_string());
+
+ tracing::debug!(
+ "Opening external editor via terminal: {} -e {} {}",
+ terminal,
+ editor_cmd,
+ temp_path.display()
+ );
+ Command::new(&terminal)
+ .arg("-e")
+ .arg(&editor_cmd)
+ .arg(&temp_path)
+ .status()
+ };
+
+ match result {
+ Ok(status) if status.success() => {
+ // Read the edited content back
+ match std::fs::read_to_string(&temp_path) {
+ Ok(content) => {
+ // Re-get mutable session reference after potential borrow issues
+ if let Some(session) = self.session_manager.get_active_mut() {
+ session.input = content;
+ session.focus_requested = true;
+ }
+ }
+ Err(e) => {
+ tracing::error!("Failed to read temp file after editing: {}", e);
+ }
+ }
+ }
+ Ok(status) => {
+ tracing::warn!("External editor exited with status: {}", status);
+ }
+ Err(e) => {
+ tracing::error!("Failed to spawn external editor: {}", e);
+ }
+ }
+
+ // Clean up temp file
+ let _ = std::fs::remove_file(&temp_path);
+ }
+
+ /// Try to find a common terminal emulator
+ fn find_terminal() -> Option<String> {
+ use std::process::Command;
+ let terminals = [
+ "alacritty",
+ "kitty",
+ "gnome-terminal",
+ "konsole",
+ "urxvtc",
+ "urxvt",
+ "xterm",
+ ];
+ for term in terminals {
+ if Command::new("which")
+ .arg(term)
+ .output()
+ .map(|o| o.status.success())
+ .unwrap_or(false)
+ {
+ return Some(term.to_string());
+ }
+ }
+ None
+ }
+
/// Process auto-steal focus logic: switch to focus queue items as needed
fn process_auto_steal_focus(&mut self) {
if !self.auto_steal_focus {
@@ -1350,6 +1447,9 @@ impl notedeck::App for Dave {
KeyAction::ToggleAutoSteal => {
self.toggle_auto_steal();
}
+ KeyAction::OpenExternalEditor => {
+ self.open_external_editor();
+ }
}
}
diff --git a/crates/notedeck_dave/src/ui/keybindings.rs b/crates/notedeck_dave/src/ui/keybindings.rs
@@ -37,6 +37,8 @@ pub enum KeyAction {
FocusQueueDismiss,
/// Toggle auto-steal focus mode (Ctrl+Space)
ToggleAutoSteal,
+ /// Open external editor for composing input (Ctrl+G)
+ OpenExternalEditor,
}
/// Check for keybinding actions.
@@ -92,11 +94,16 @@ pub fn check_keybindings(
return Some(KeyAction::NewAgent);
}
- // Ctrl+G to toggle between scene view and classic view
- if ctx.input(|i| i.modifiers.matches_exact(ctrl) && i.key_pressed(Key::G)) {
+ // Ctrl+L to toggle between scene view and list view
+ if ctx.input(|i| i.modifiers.matches_exact(ctrl) && i.key_pressed(Key::L)) {
return Some(KeyAction::ToggleView);
}
+ // Ctrl+G to open external editor for composing input
+ if ctx.input(|i| i.modifiers.matches_exact(ctrl) && i.key_pressed(Key::G)) {
+ return Some(KeyAction::OpenExternalEditor);
+ }
+
// Ctrl+M to toggle plan mode
if ctx.input(|i| i.modifiers.matches_exact(ctrl) && i.key_pressed(Key::M)) {
return Some(KeyAction::TogglePlanMode);
diff --git a/todos.txt b/todos.txt
@@ -1,23 +1,7 @@
- [ ] in crates/notedeck_dave, when an agent steals focus from another agent to ask a question, i want it to focus back to where it was
-- [x] in crates/notedeck_dave, i want to be able to switch to plan mode like you can in the claude-code cli. I believe there
-
-- [x] plan: plan a feature for creating a queue of tool requests/questions
-
-- [x] plan: need a way to respond to a approve/deny in crates/notedeck_dave but giving a response as well
-
-- [x] plan: plan a way to run claude commands such as /compact, with the ability to specify arguments as well (/compact <input>) etc
-
-- [x] have the default view be classic instead of scene view. also don't call it classic, call it list
-
- [ ] make crates/notedeck_dave/src/lib.rs smaller
-- [x] update [1] Yes and [2] No options to Allow/Deny, use key indicator widget instead of [1] text
-
-- [x] have the approve/deny message appear near the bottom right of the diff instead of top right
-
-- [x] ctrl-shift-tab should go in reverse node selection order
-
- [ ] chat sidebar text should show the user's or AI's last message, not our last message
- [ ] add a modeswitch (ctrl-something) for "auto-steal focus". when enabled, AI requests instantly steal focus. when disabled, requests are queued instead
@@ -30,10 +14,6 @@
- [ ] handle ExitPlanMode which simply exits plan mode. claude-code sends this when its done its planning phase
-- [x] handle claude-code questions/answers (AskUserQuestion tool - could be a list of questions)
-
-- [x] AskUserQuestion: show a small summary view of the question and selected option(s) after answering
-
- [ ] auto-accept single line changes: automatically approve Edit tool calls that only modify a single line, reducing confirmation friction for trivial edits
- [ ] preserve edit view after approval/denial: when approving or denying an edit, keep the diff visible instead of making it disappear. allows reviewing what was changed even after responding