5.6 KiB
Plan: Emacs Integration for napi — Remote File-Notify via TRAMP
Context
Fénix wants real-time awareness in Emacs of student deliveries on zzz (qu3v3d0.tech). The GNU Emacs manual confirms that file-notify-add-watch works on remote machines via TRAMP — it runs inotifywait on the remote host through SSH and streams events back in real-time. No polling.
This means: no modifications to watcher scripts on zzz, no reverse SSH tunnels, no Porthole/JSON-RPC. Pure Emacs, using built-in capabilities + a dedicated daemon.
Architecture
emacs --daemon=napi (local)
│
├── TRAMP SSH → zzz:/home/*/python/ (ASIR1 — 21 dirs)
│ └── inotifywait running on zzz (auto-started by TRAMP)
│
├── TRAMP SSH → zzz:/home/*/html/ (DDAW2 — 19 dirs)
│ └── inotifywait running on zzz (auto-started by TRAMP)
│
└── On file event → forward notification to `water` daemon
├── Desktop notification (D-Bus)
├── Sound alert (paplay)
├── *napi-log* buffer (persistent log)
└── Minibuffer message
emacsclient --socket-name=napi --eval '(napi-dashboard)' ← interact
emacsclient --socket-name=water --eval '(napi-log)' ← view in main UI
Prerequisites
inotify-toolsinstalled on zzz (providesinotifywait) — already present (watchers use it)- SSH key access from local → zzz — already working (
ssh fenix@qu3v3d0.tech) - Emacs 30.1 on local — confirmed
Implementation Steps
Step 1: Create ~/napi/emacs/napi.el
The core elisp package. ~200 lines. Key components:
| Component | Purpose |
|---|---|
napi-watch-start |
Sets up TRAMP file-notify watches on all student dirs |
napi-watch-stop |
Removes all watches cleanly |
napi-notify |
Handles a file event: log + desktop notification + sound |
napi-log |
Interactive: open *napi-log* buffer |
napi-open-student |
Interactive: completing-read → open student dir/notas.md |
napi-dashboard |
Interactive: overview of all students + last delivery |
C-c n prefix |
Keybindings for all interactive commands |
Student name maps (username → full name) embedded in elisp, mirroring watcher scripts.
TRAMP watch setup — the core innovation:
;; For each ASIR1 student:
(file-notify-add-watch
"/ssh:fenix@qu3v3d0.tech:/home/jara/python/"
'(change)
#'napi--handle-event)
;; 40 watches total (21 ASIR1 + 19 DDAW2)
;; Each spawns a persistent inotifywait on zzz via SSH
Event handler filters noise (Thumbs.db, pycache, .tmp, etc.) and forwards clean notifications to water daemon via:
(shell-command
"emacsclient --socket-name=water --eval '(napi--show-notification ...)' &")
Reconnection: TRAMP sends a stopped event when SSH drops. The handler auto-re-establishes the watch after a delay.
Step 2: Create systemd user unit for napi daemon
File: ~/.config/systemd/user/emacs-napi.service
[Unit]
Description=Emacs daemon for napi student monitoring
After=network-online.target
Wants=network-online.target
[Service]
Type=forking
ExecStart=/usr/bin/emacs --daemon=napi -l ~/napi/emacs/napi-init.el
ExecStop=/usr/bin/emacsclient --socket-name=napi --eval "(kill-emacs)"
Restart=on-failure
RestartSec=30
[Install]
WantedBy=default.target
Step 3: Create ~/napi/emacs/napi-init.el
Minimal init file for the napi daemon (not the full ~/.emacs.d/init.el):
;; Loaded ONLY by emacs --daemon=napi
(load "~/napi/emacs/napi.el")
(napi-watch-start)
(message "napi: watching %d student directories on zzz" napi--watch-count)
Step 4: Also load napi.el in water daemon
Add to ~/.emacs.d/init.el (near line 1478, following funciones-art.el pattern):
(load "~/napi/emacs/napi.el")
This gives the water daemon the napi-log, napi-dashboard, napi-open-student commands and the C-c n keybindings — but no watches (those run in the napi daemon only).
Step 5: Verify inotifywait on zzz
ssh fenix@qu3v3d0.tech "which inotifywait && inotifywait --help | head -1"
Files to Create/Modify
| File | Action | Location |
|---|---|---|
emacs/napi.el |
CREATE | ~/napi/emacs/napi.el |
emacs/napi-init.el |
CREATE | ~/napi/emacs/napi-init.el |
emacs-napi.service |
CREATE | ~/.config/systemd/user/emacs-napi.service |
init.el |
MODIFY (1 line) | ~/.emacs.d/init.el |
No modifications to zzz — no watcher changes, no new SSH keys, no reverse tunnels.
Testing
- Unit test — load
napi.elinwater, call(napi-notify ...)manually - TRAMP watch test — single watch on one student dir, touch a file on zzz
- Full integration — start
napidaemon, simulate SFTP upload as student - Stress test — confirm 40 concurrent TRAMP watches are stable
- Reconnection — kill SSH, verify watch auto-restores
- UI test —
C-c n l(log),C-c n s(student),C-c n d(dashboard)
Risk: 40 Concurrent SSH/inotifywait Processes
Each file-notify-add-watch via TRAMP spawns a separate inotifywait process on zzz. 40 watches = 40 persistent SSH channels + 40 inotifywait processes.
Mitigation: TRAMP reuses SSH connections (ControlMaster). So it's 1 SSH connection + 40 remote processes. The zzz server's inotify limit (fs.inotify.max_user_watches) is typically 65536 — 40 is nothing.
Fallback: If 40 watches proves unstable, consolidate to 2 watches on parent dirs (/home/ with recursive) + filter events by path. Or switch to the emacsclient-via-SSH approach (see FINDINGS.md).