things-to-remember checklist
git clone https://git.lucas.co/cce-list.git
perf: with a vault, the watcher wakes the list instead of a 1 s poll
The lists directory was re-read every second (read_dir + a stat per file
+ the current pointer) to notice outside edits, and the vault watcher's
batches sat queued until that same tick drained them: nothing woke the
loop when they arrived.
VaultTasks::open takes a wake callback, which sends ListMessage::
VaultChanged from the watcher's thread; tick then applies the batch and,
when not mid-name, compares the lists folder's signature and reloads.
With a vault (the lists are its Tasks/ notes) nothing polls any more and
idle_poll_interval is None; without one, the app's own lists folder is
still looked at every second as before.
Shadow, vault copy: an outside append to a Tasks note shows after
~158 ms instead of ~471 ms; idle wakes unchanged at the runner's 1 s cap
(20 per 20 s); the shown list pixel-identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
src/main.rs | 45 ++++++++++++++++++++++++++++++++++++---------
src/vault_tasks.rs | 11 ++++++++++-
2 files changed, 46 insertions(+), 10 deletions(-)
diff --git a/src/main.rs b/src/main.rs
index e450e79..7811546 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -73,6 +73,8 @@ struct RowView {
#[derive(Debug, Clone)]
enum ListMessage {
Exit,
+ /// The vault watcher queued changes (`VaultTasks::open`'s `wake`).
+ VaultChanged,
}
/// What the input box is for right now.
@@ -188,6 +190,10 @@ struct ListApp {
/// frame after an idle sleep, and a list nobody is typing into is idle —
/// so the once-a-second look actually happened about once a minute.
watch_at: std::time::Instant,
+ /// Something outside the app may have changed the lists: the vault
+ /// watcher said so, or (unwatched) the cadence came round. Checked in
+ /// `tick` once the input box is not taking a name.
+ outside_change: bool,
}
impl ListApp {
@@ -551,12 +557,20 @@ impl ListApp {
}
}
+impl ListApp {
+ /// Whether outside changes to the lists arrive as messages: lists live
+ /// in the vault and its watcher is running.
+ fn watched(&self) -> bool {
+ cce_list::in_vault() && self.vault.as_ref().is_some_and(|v| v.watching())
+ }
+}
+
impl Application for ListApp {
type Message = ListMessage;
fn new(
_qh: &QueueHandle<EngineState<Self>>,
- _sender: calloop::channel::Sender<Self::Message>,
+ sender: calloop::channel::Sender<Self::Message>,
) -> Self {
cce_ui::scale::set_scale_factor(1.0);
let mut app = Self {
@@ -576,9 +590,12 @@ impl Application for ListApp {
pointer: None,
hovered_row: None,
disk_sig: Vec::new(),
- vault: VaultTasks::open(),
+ vault: VaultTasks::open(move || {
+ let _ = sender.send(ListMessage::VaultChanged);
+ }),
in_vault: false,
watch_at: std::time::Instant::now(),
+ outside_change: false,
};
app.load_from_disk();
app
@@ -598,14 +615,19 @@ impl Application for ListApp {
fn update(&mut self, msg: Self::Message, _needs_rebuild: &mut bool, exit: &mut bool) {
match msg {
ListMessage::Exit => *exit = true,
+ // Applied by `tick`, which the message's turn runs next.
+ ListMessage::VaultChanged => self.outside_change = true,
}
}
- /// The directory watch in `tick` is work the runner cannot see — nothing
- /// redraws until the files change underneath us — so name the cadence the
- /// loop has to come back at.
+ /// With a vault, the lists are its `Tasks/` notes and the vault watcher
+ /// wakes the loop when they change (`ListMessage::VaultChanged`), so
+ /// nothing needs polling. Without one, the app's own lists folder is
+ /// re-read every [`WATCH_EVERY`] as before — work the runner cannot see,
+ /// so name the cadence. Until 2026-10-06 the folder was re-read every
+ /// second either way, and the watcher's batches waited for that tick.
fn idle_poll_interval(&self) -> Option<std::time::Duration> {
- Some(WATCH_EVERY)
+ (!self.watched()).then_some(WATCH_EVERY)
}
fn tick(&mut self, dt: f32, needs_rebuild: &mut bool) {
@@ -628,11 +650,16 @@ impl Application for ListApp {
}
// Outside changes (a vault sync, a hand edit) show up without a
// relaunch — but never while typing a name, which a reload would
- // interrupt; that waits a second.
+ // interrupt; that waits until the name is done. Watched, a change
+ // is announced; unwatched, the folder is looked at on the cadence.
let now = std::time::Instant::now();
- if now >= self.watch_at {
+ if !self.watched() && now >= self.watch_at {
self.watch_at = now + WATCH_EVERY;
- if self.mode == Mode::Items && disk_signature() != self.disk_sig {
+ self.outside_change = true;
+ }
+ if self.outside_change && self.mode == Mode::Items {
+ self.outside_change = false;
+ if disk_signature() != self.disk_sig {
self.load_from_disk();
*needs_rebuild = true;
}
diff --git a/src/vault_tasks.rs b/src/vault_tasks.rs
index 883d328..d522a76 100644
--- a/src/vault_tasks.rs
+++ b/src/vault_tasks.rs
@@ -54,13 +54,17 @@ pub fn build_rows<'a>(tasks: impl Iterator<Item = (&'a str, &'a Task)>, ticked:
impl VaultTasks {
/// The configured vault's tasks, or `None` when no vault is set up.
- pub fn open() -> Option<VaultTasks> {
+ /// `wake` runs on the watcher's thread after each batch is queued, so
+ /// the app's loop comes round to [`poll`](Self::poll) instead of
+ /// finding the batch on its next timed tick.
+ pub fn open(wake: impl Fn() + Send + 'static) -> Option<VaultTasks> {
let root = cce_vault::config::vault_root(None).ok()?;
let index = Index::open(&root, true).map_err(|e| log::warn!("vault tasks: {e}")).ok()?;
let pending = Arc::new(Mutex::new(Vec::new()));
let queue = pending.clone();
let watcher = VaultWatcher::spawn(&root, move |paths| {
queue.lock().unwrap_or_else(|e| e.into_inner()).extend(paths);
+ wake();
})
.map_err(|e| log::warn!("vault watcher: {e}"))
.ok();
@@ -73,6 +77,11 @@ impl VaultTasks {
self.rows = build_rows(self.index.tasks(), &self.ticked);
}
+ /// Whether the vault watcher is running (it can fail to start).
+ pub fn watching(&self) -> bool {
+ self._watcher.is_some()
+ }
+
/// Apply queued vault changes; true when the rows may have changed.
pub fn poll(&mut self) -> bool {
let paths: Vec<PathBuf> = std::mem::take(&mut *self.pending.lock().unwrap_or_else(|e| e.into_inner()));