notes editor over the vault (Obsidian-compatible)
git clone https://git.lucas.co/cce-notes.git
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.