notedeck

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

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:
Mcrates/notedeck_dave/src/lib.rs | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/notedeck_dave/src/ui/keybindings.rs | 11+++++++++--
Mtodos.txt | 20--------------------
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