git.lucas.co / cce-notes
notes editor over the vault (Obsidian-compatible)
git clone https://git.lucas.co/cce-notes.git

commite1471ed2312fa41e80f2659c4e24c015d2b816a4
parentb0e0c6e9da
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-01 13:24
feat: `current` on the instance socket; Ctrl+G opens the local graph

The socket answers `current` with the open note's vault path, straight
from the listener thread, so cce-graph's local graph can follow the note
on screen. Ctrl+G (input.kdl `graph`) launches `cce-graph --vault <vault>
--local`, which is single-instance.

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

 CLAUDE.md       |  7 ++++++-
 src/instance.rs | 19 +++++++++++++++++++
 src/main.rs     | 21 +++++++++++++++++++++
 3 files changed, 46 insertions(+), 1 deletion(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index b1d7163..8f959d5 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -108,9 +108,14 @@ A second launch forwards its command to the running instance and exits.
 Keys come from input.kdl's `cce-notes` domain: `quick_switcher` (ctrl+o),
 `toggle_mode` (ctrl+e), `save` (ctrl+s), `reload` (ctrl+r), `back`
 (alt+arrowleft), `forward` (alt+arrowright), `toggle_tree` (ctrl+\\),
-`toggle_side` (ctrl+]), `search` (ctrl+shift+f), `rename` (f2), `daily`
+`toggle_side` (ctrl+]), `search` (ctrl+shift+f), `rename` (f2), `graph`
+(ctrl+g: `cce-graph --vault <this vault> --local`, single-instance), `daily`
 (alt+d), `quit` (ctrl+q). Mouse back/forward walk the history too.
 
+The instance socket also answers `current` (`ok <vault path>`) straight
+from its listener thread, from a value `open_path`/`rename` keep up to
+date (`instance::set_current`) — cce-graph's local graph polls it.
+
 ## MCP
 
 `claude mcp add --transport http cce-notes http://127.0.0.1:3002/mcp`.
diff --git a/src/instance.rs b/src/instance.rs
index 89c942c..c776861 100644
--- a/src/instance.rs
+++ b/src/instance.rs
@@ -11,6 +11,9 @@
 //! - `daily [YYYY-MM-DD]` — open (creating it from the template) a day's note.
 //! - `search <query>` — show the search pane holding `query` (`#tag` too).
 //! - `show` — nothing but bringing the instance up.
+//! - `current` — answered `ok <vault path>` (or a bare `ok` with no note
+//!   open) straight from the listener thread; cce-graph's local graph
+//!   polls it to follow the note on screen.
 //!
 //! Connect before binding, as cce-browser does: a refused connect means a
 //! crashed instance left its socket file, which is removed; losing the bind
@@ -26,6 +29,13 @@ const PREFIX: &str = "cce-notes";
 
 static CLAIMED: Mutex<Option<UnixListener>> = Mutex::new(None);
 static OWNED_PATH: Mutex<Option<String>> = Mutex::new(None);
+/// The open note's vault path, for `current`. Set by the app whenever it
+/// changes; read by the listener without a trip through the event loop.
+static CURRENT: Mutex<Option<String>> = Mutex::new(None);
+
+pub fn set_current(path: Option<&str>) {
+    *CURRENT.lock().unwrap_or_else(|e| e.into_inner()) = path.map(String::from);
+}
 
 #[derive(Debug, Clone, PartialEq)]
 pub enum Command {
@@ -158,6 +168,15 @@ pub fn spawn_listener(sender: calloop::channel::Sender<Message>) -> bool {
             if reader.read_line(&mut line).is_err() {
                 continue;
             }
+            if line.trim() == "current" {
+                let cur = CURRENT.lock().unwrap_or_else(|e| e.into_inner()).clone();
+                let reply = match cur {
+                    Some(p) => format!("ok {p}\n"),
+                    None => "ok\n".to_string(),
+                };
+                let _ = reader.get_mut().write_all(reply.as_bytes());
+                continue;
+            }
             let reply: &[u8] = match Command::parse(line.trim()) {
                 Some(cmd) => {
                     if sender.send(Message::Command(cmd)).is_err() {
diff --git a/src/main.rs b/src/main.rs
index 8d18209..ee8c3aa 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -101,6 +101,7 @@ struct Keys {
     toggle_side: String,
     search: String,
     rename: String,
+    graph: String,
     daily: String,
     quit: String,
 }
@@ -119,6 +120,7 @@ impl Keys {
             toggle_side: get("toggle_side", "ctrl+]"),
             search: get("search", "ctrl+shift+f"),
             rename: get("rename", "f2"),
+            graph: get("graph", "ctrl+g"),
             daily: get("daily", "alt+d"),
             quit: get("quit", "ctrl+q"),
         }
@@ -438,6 +440,7 @@ impl NotesApp {
         };
         let same = self.current.as_deref() == Some(path);
         self.current = Some(path.to_string());
+        instance::set_current(Some(path));
         self.load_text(text);
         if !same {
             self.read_scroll = 0.0;
@@ -829,6 +832,7 @@ impl NotesApp {
                     // Its own self-links may have been rewritten too.
                     let to = plan.to.clone();
                     self.current = Some(to.clone());
+                    instance::set_current(Some(&to));
                     if let Ok(text) = self.index.as_ref().map(|ix| ix.read_text(&to)).transpose().map(Option::unwrap_or_default) {
                         let scroll = self.read_scroll;
                         self.load_text(text);
@@ -2012,6 +2016,23 @@ impl Application for NotesApp {
                 self.open_rename();
                 return None;
             }
+            if k(&self.keys.graph) && self.index.is_some() {
+                // The local graph follows this window's note (it polls
+                // `current` on the instance socket). cce-graph's vault mode
+                // is single-instance, so a second press only raises nothing
+                // new.
+                let vault = self.index.as_ref().map(|ix| ix.root().to_path_buf());
+                let mut cmd = std::process::Command::new("cce-graph");
+                cmd.arg("--vault");
+                if let Some(v) = vault {
+                    cmd.arg(v);
+                }
+                cmd.arg("--local");
+                if let Err(e) = cmd.spawn() {
+                    self.set_status(format!("Could not start cce-graph: {e}"), true);
+                }
+                return None;
+            }
         }
 
         // The search box, while it holds the keyboard.