|
| 1 | +# nplog |
| 2 | + |
| 3 | +A live, filterable viewer for [NotePlan](https://noteplan.co)'s plugin log output. |
| 4 | + |
| 5 | +NotePlan writes every `console.log` from a plugin (and from plugin WebViews) into a rolling |
| 6 | +log file inside its app container (in addition to NotePlan's own log). |
| 7 | + |
| 8 | +`nplog` is a terminal-based program that tails that file, strips it down to just the plugin |
| 9 | +output, and puts a **regex filter box** on screen that you can retype at any moment without |
| 10 | +restarting anything. |
| 11 | + |
| 12 | +## Why not just `tail | grep`? |
| 13 | + |
| 14 | +- **It follows log rotation.** NotePlan starts a brand-new timestamped file |
| 15 | + (`co.noteplan.NotePlan3 2026-07-28--20-41-00-962.log`) every time it relaunches. A plain |
| 16 | + `tail -f` stays pinned to the old file and silently goes quiet. `nplog` notices the newer |
| 17 | + file, prints a `--- switching to … ---` marker, and keeps going. |
| 18 | +- **You can change the filter without losing history.** `grep` decides once, at the moment |
| 19 | + the line streams past. `nplog` keeps the lines in memory, so retyping the filter |
| 20 | + re-searches everything it has already read. |
| 21 | +- **Context modes.** Log lines are rarely useful alone — you usually want the few lines that |
| 22 | + came *after* the one that matched. |
| 23 | +- **Pretty-printed objects stay intact.** `console.log` of an object spans a dozen physical |
| 24 | + log lines; `grep` hands you one orphaned field. `nplog` treats the whole object as a single |
| 25 | + entry — see [Multi-line objects](#multi-line-objects). |
| 26 | + |
| 27 | +## Requirements |
| 28 | + |
| 29 | +Node.js (any recent version). No npm packages — it is a single dependency-free script. |
| 30 | + |
| 31 | +### Terminal compatibility |
| 32 | + |
| 33 | +Works in any terminal emulator, including iTerm2. It only uses the universally-supported |
| 34 | +ANSI/VT100 basics — alternate screen (`?1049h`), cursor positioning, erase-line, reverse |
| 35 | +video, dim, and 8-colour SGR — plus Node's own raw-mode key handling, which is |
| 36 | +terminal-agnostic. |
| 37 | + |
| 38 | +Verified on macOS **Terminal.app** and **iTerm2**. It should be equally happy in Ghostty, |
| 39 | +kitty, WezTerm, Alacritty, Warp, and the VS Code / Zed integrated terminals. |
| 40 | + |
| 41 | +Two deliberate choices help here: every binding is a **control key**, and the |
| 42 | +help line **collapses to a shorter form** on narrower windows rather than wrapping. |
| 43 | + |
| 44 | +## Install |
| 45 | + |
| 46 | +From the root of this repo: |
| 47 | + |
| 48 | +```bash |
| 49 | +./scripts/nplog/install.sh |
| 50 | +``` |
| 51 | + |
| 52 | +That symlinks `nplog` into `~/.local/bin` so it will run from your shell. |
| 53 | +Pass a different directory to override the location, e.g. |
| 54 | +`./scripts/nplog/install.sh /usr/local/bin`. |
| 55 | + |
| 56 | +Because it's a **symlink**, a later `git pull` updates the tool you actually run — there's no |
| 57 | +need to re-install. If `~/.local/bin` isn't on your `PATH`, the installer tells you what to add |
| 58 | +to your shell profile. |
| 59 | + |
| 60 | +You can also skip installing and just run it in place: `node scripts/nplog/nplog`. |
| 61 | + |
| 62 | +## Usage |
| 63 | + |
| 64 | +```bash |
| 65 | +nplog # follow the newest log |
| 66 | +nplog dashboard # start with a filter already applied |
| 67 | +nplog 'Section|Perspective' # regex alternation |
| 68 | +nplog --mode 5 # start in "match + next 5 lines" mode |
| 69 | +nplog --no-time # start with timestamps hidden |
| 70 | +nplog --raw # keep non-JSLog lines (NotePlan's own native logging) |
| 71 | +nplog --file "/path/to/some.log" # pin one file instead of auto-following |
| 72 | +``` |
| 73 | + |
| 74 | +### Keys |
| 75 | + |
| 76 | +Everything is a control key, because compact Mac keyboards have no PgUp / PgDn / Home / End. |
| 77 | + |
| 78 | +| Key | Action | |
| 79 | +| --- | --- | |
| 80 | +| *(type anything)* | edit the filter — applies live | |
| 81 | +| `Tab` | more context: `match only` → `+1` → `+5` → `+10` → back | |
| 82 | +| `^T` | show / hide timestamps | |
| 83 | +| `^U` | clear the filter | |
| 84 | +| `^L` | clear the buffer (drop history, keep following) | |
| 85 | +| `^B` / `^F` | page **b**ack / **f**orward | |
| 86 | +| `^P` / `^N` | one line **p**revious / **n**ext | |
| 87 | +| `^A` / `^E` | jump to top / end (`^E` also resumes following) | |
| 88 | +| `^C` | quit | |
| 89 | + |
| 90 | +The bottom line always spells out what `Tab` will do next, e.g. |
| 91 | +`TAB=more context (now match only → match +1)`. |
| 92 | + |
| 93 | +If you *do* have a full-size keyboard (or use Fn+arrow), `PgUp`, `PgDn`, `Home`, `End` and the |
| 94 | +arrow keys work too. |
| 95 | + |
| 96 | +### The status bar |
| 97 | + |
| 98 | +``` |
| 99 | + filter: getRemindersGenerated|refreshSomeSections re ctx:match only 2942/8723 FOLLOW |
| 100 | +``` |
| 101 | + |
| 102 | +- `re` — the filter compiled as a regex. If it can't (say you've typed `Section(` and haven't |
| 103 | + closed the paren yet) this reads `LITERAL(bad re)` and the text is matched literally |
| 104 | + instead. It never errors out mid-typing. |
| 105 | +- `no-time` — appears only when timestamps are hidden (`^T`). |
| 106 | +- `ctx:match only` — current context mode. |
| 107 | +- `2942/8723` — lines currently shown / lines in the buffer. |
| 108 | +- `FOLLOW` / `SCROLL` — whether new output auto-scrolls. Scrolling up switches to `SCROLL`; |
| 109 | + `^E` returns to `FOLLOW`. |
| 110 | + |
| 111 | +Matches are highlighted, and `┈┈┈` rules mark places where lines were skipped between |
| 112 | +non-adjacent matches. |
| 113 | + |
| 114 | +### Timestamps |
| 115 | + |
| 116 | +Every line carries a `2026-07-29 11:34:54`, which is mostly noise once you're reading a |
| 117 | +filtered slice. By default they're **dimmed** rather than removed, so they stay available but |
| 118 | +recede. `^T` (or `--no-time`) removes them entirely: |
| 119 | + |
| 120 | +``` |
| 121 | + 2026-07-29 11:53:01 | DEBUG | refreshSomeSections :: Starting for TB <- dimmed stamp |
| 122 | + DEBUG | refreshSomeSections :: Starting for TB <- after ^T |
| 123 | +``` |
| 124 | + |
| 125 | +Filtering always runs against the *full* line, so you can still search for a time even while |
| 126 | +timestamps are hidden. |
| 127 | + |
| 128 | +### Multi-line objects |
| 129 | + |
| 130 | +When a plugin pretty-prints an object or array, NotePlan writes it across many physical log |
| 131 | +lines — and only the *first* carries the `JSLog:` marker; the rest just repeat the timestamp: |
| 132 | + |
| 133 | +``` |
| 134 | +2026-07-28 14:46:20 JSLog: 2026-07-28 14:46:20 | DEBUG | => Reminder: :: { |
| 135 | +2026-07-28 14:46:20 "title": "You should cancel the auto renew on Medawar", |
| 136 | +2026-07-28 14:46:20 "listname": "Reminders", |
| 137 | +2026-07-28 14:46:20 } |
| 138 | +``` |
| 139 | + |
| 140 | +`nplog` detects the unbalanced `{` (or `[`), and keeps absorbing following lines until the |
| 141 | +brace balances — storing the whole thing as **one entry**: |
| 142 | + |
| 143 | +``` |
| 144 | +2026-07-28 14:46:20 | DEBUG | => Reminder: :: { |
| 145 | + "title": "You should cancel the auto renew on Medawar", |
| 146 | + "listname": "Reminders", |
| 147 | + } |
| 148 | +``` |
| 149 | + |
| 150 | +The repeated timestamps on the continuation lines are dropped, and — since NotePlan flattens |
| 151 | +the original indentation — the body is **re-indented by nesting depth**, so it reads as real |
| 152 | +JSON again. Closing braces line up with their openers: |
| 153 | + |
| 154 | +``` |
| 155 | +2026-07-29 14:40:33 | DEBUG | => Reminder: :: { |
| 156 | + "title": "Send out the newsletter", |
| 157 | + "occurences": { |
| 158 | + "weekly": true, |
| 159 | + "days": [ |
| 160 | + "mon", |
| 161 | + "tue" |
| 162 | + ] |
| 163 | + }, |
| 164 | + "time": "10:00" |
| 165 | + } |
| 166 | +``` |
| 167 | + |
| 168 | +Because it's one entry: |
| 169 | + |
| 170 | +- Filtering on **any** field shows you the **entire object**, not a lone line. `listname` |
| 171 | + matching 178 objects reports `178`, not 178 fragments. |
| 172 | +- The `+1/+5/+10` context modes count *entries*, so "+1" means the next log entry — not the |
| 173 | + next line of the object you're already looking at. |
| 174 | +- A regex can span the newlines (`.` is dotAll here), so `Reminder.*listname` matches. |
| 175 | + |
| 176 | +Robustness details, all verified against real logs: |
| 177 | + |
| 178 | +- Brace counting **ignores braces inside string values**, so `"tricky {"` or a value of `"]"` |
| 179 | + can't throw off the count. |
| 180 | +- A new `<timestamp> JSLog:` line **always** ends the block, so a truncated or malformed object |
| 181 | + can't swallow the rest of the log. |
| 182 | +- Blocks render **as they stream** — you see the object growing, rather than waiting for the |
| 183 | + closing brace. |
| 184 | +- A block left open when the log rotates is closed at the switch. |
| 185 | +- Sometimes the `{` lands on the line *after* the `JSLog:` header; that fragment starts its own |
| 186 | + block rather than being dropped. |
| 187 | + |
| 188 | +## Filtering is regex |
| 189 | + |
| 190 | +The filter is a case-insensitive JavaScript regular expression, so: |
| 191 | + |
| 192 | +| Pattern | Finds | |
| 193 | +| --- | --- | |
| 194 | +| `dashboard` | plain substring | |
| 195 | +| `Section\|Perspective` | either word | |
| 196 | +| `^\[WebView\]` | only WebView lines | |
| 197 | +| `ERROR\|WARN` | problems only | |
| 198 | +| `refresh.*Sections` | wildcards between terms | |
| 199 | +| `:: \d+ items` | digits | |
| 200 | + |
| 201 | +Anything that isn't valid regex is matched literally, so you can paste an arbitrary log |
| 202 | +fragment (`sections [TB`) without escaping it. |
| 203 | + |
| 204 | +## Where the logs come from |
| 205 | + |
| 206 | +``` |
| 207 | +~/Library/Containers/co.noteplan.NotePlan3/Data/Library/Application Support/co.noteplan.NotePlan3/Logs/ |
| 208 | +``` |
| 209 | + |
| 210 | +Files are named `co.noteplan.NotePlan3 <timestamp>.log`; beta builds may nest a second |
| 211 | +`Logs/` directory inside that one, and both are searched. If you have override the location with the |
| 212 | +`NPLOG_DIR` environment variable, e.g. if you use Setapp. |
| 213 | + |
| 214 | +```bash |
| 215 | +NPLOG_DIR=/some/other/Logs nplog |
| 216 | +e.g. |
| 217 | +NPLOG_DIR=~/Library/Containers/co.noteplan.NotePlan-setapp/Data/Library/Application nplog |
| 218 | +``` |
| 219 | + |
| 220 | +### What gets shown by default |
| 221 | + |
| 222 | +NotePlan tags plugin output with a `JSLog:` marker (basically all the plugin console contents) |
| 223 | + |
| 224 | +``` |
| 225 | +2026-07-29 11:34:54 JSLog: [WebView Log] 2026-07-29 11:34:54 | DEBUG | Webview :: sendActionToPlugin |
| 226 | +``` |
| 227 | + |
| 228 | +`nplog` keeps only those lines (plus the continuation lines of a multi-line object), drops |
| 229 | +everything up to and including the marker, and abbreviates `[WebView Log]` to `[WebView]`. |
| 230 | + |
| 231 | +**Use `--raw` to see NotePlan's own non-plugin logging too.** |
| 232 | + |
| 233 | +> **Gotcha if you write your own filter:** don't anchor on `^JSLog:`. NotePlan added the |
| 234 | +> leading `2026-07-29 11:34:54` timestamp at some point, and that anchor silently stopped |
| 235 | +> matching — the original `noteplan_log_filtered.sh` emitted *nothing* for months rather than |
| 236 | +> erroring. Strip the timestamp first, *then* anchor on `JSLog:`. |
| 237 | +
|
| 238 | +## Notes |
| 239 | + |
| 240 | +- Runs on the terminal's alternate screen, so it never pollutes your real scrollback, and |
| 241 | + quitting leaves the terminal exactly as it was. |
| 242 | +- Holds the most recent 50,000 **entries** in memory (`MAX_LINES` in the script) — a |
| 243 | + multi-line object counts as one. On startup it seeds from the last 2 MB of the current file |
| 244 | + so there's immediate history to search. |
| 245 | +- Polls once a second for new bytes and for rotation. |
| 246 | +- A block that never closes is capped at 500 continuation lines (`MAX_CONT_LINES`). |
| 247 | + |
| 248 | +## Running it alongside your watchers |
| 249 | + |
| 250 | +`nplog` is most useful in its own terminal tab, next to `npc plugin:dev` and your test watcher — |
| 251 | +you edit, the plugin rebuilds, and you watch the log react. |
| 252 | + |
| 253 | +If you drive your dev environment from a personal `.hyperlayout` file (gitignored, so yours is |
| 254 | +your own), give it a dedicated tab rather than a narrow pane — the filter bar and context modes |
| 255 | +want the width: |
| 256 | + |
| 257 | +```json |
| 258 | +"dashboard": [ |
| 259 | + [ "npc plugin:dev jgclark.Dashboard -nc" ], |
| 260 | + [ "npm run test:watch" ], |
| 261 | + [ "nplog" ] |
| 262 | +] |
| 263 | +``` |
0 commit comments