Open a note in $EDITOR - #5
Conversation
The card editor is a textarea — fine for a few lines, cramped for a long note, and it has none of the bindings people have spent years learning. `e` on the board (or ctrl+e from inside the card) hands the note to the user's real editor instead. The note goes to a temp .md file as title, blank line, body — the same first-line-is-title convention the built-in editor and clipboard paste already use, so all three agree about what a note's text means. parseNoteFile delegates to the existing splitter rather than reimplementing it, and a test asserts the two stay in step. The .md extension is deliberate: it is what turns on markdown highlighting in the editor. tea.ExecProcess suspends the TUI while the editor owns the terminal and posts a message when it exits; the model reapplies the text, flashes the note, and re-enters the alt screen. VISUAL wins over EDITOR, per the usual convention. The value is treated as a command line rather than a program name, so "code -w" and "emacsclient -nw" work. With neither set, `e` says so — falling back to vi would drop someone into an editor they may not know how to leave. Every failure path leaves the note exactly as it was: a non-zero editor exit (:cq), an unreadable or missing file, and a file that came back empty. That last one matters most — a stray :q! on the wrong buffer should not blank a note. An unchanged file is a no-op, so quitting without saving does not bump the timestamp. Tested with a real subprocess, not a mock: a script that rewrites the file, and one that exits non-zero. Also driven end to end through tmux against the built binary, confirming the suspend/resume cycle and that the result reaches disk.
There was a problem hiding this comment.
🟡 Changes recommended
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds an “external editor” workflow so users can open a note in their configured $VISUAL/$EDITOR as a temporary .md file, then read the result back into the app when the editor exits.
Changes:
- Adds external-editor handoff (
eon board,ctrl+ein card) via a temp.mdfile andtea.ExecProcess, with guarded read-back behavior. - Wires the new message flow into the Bubble Tea update loop and updates in-app help/footer hints.
- Adds focused unit/integration tests for the round-trip, including real subprocess scripts.
File summaries
| File | Description |
|---|---|
| README.md | Documents the external editor feature and keybindings. |
| internal/app/model.go | Adds keybindings and handles externalEditDoneMsg to apply results and restore UI state. |
| internal/app/extedit.go | Implements editor resolution, temp file write/read, and note-apply logic. |
| internal/app/extedit_test.go | Adds tests for parsing, temp file behavior, and real subprocess editor execution. |
| internal/app/edit.go | Updates the card footer hint to include ctrl+e. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| m.setToast(toast) | ||
| // The editor owned the terminal; take the screen back. | ||
| return m, tea.Batch(tickCmd(), tea.EnterAltScreen) |
| title, body := m.editor.Split() | ||
| n.Title = title | ||
| n.Body = body | ||
| cmd, why := openInEditor(n) |
| The file uses the same convention as the built-in editor: the first | ||
| non-empty line is the title, the rest is the body. |
|
One thing worth knowing before this merges — a gap between this PR and #4 that neither can show on its own. Both were written against The fix is three lines: snapshot before the TUI suspends, at both entry points ( I cannot push it here yet — I found this by combining all four branches locally to check they compose. Everything else did, and the only other cross-feature issue was cosmetic (the edit-mode footer hint, which #6 replaces with a variable — worth a glance when you merge that one). No rush on any of it. |
What
eon a selected note (orctrl+efrom inside the card) hands it to theuser's real editor as a markdown file, and reads it back when they quit.
The card editor is great for a few lines. Past that people want their own
editor with their own bindings, and for a note that's really a document
there's no reason redthread should be in the way.
The file
Title, blank line, body — the same first-line-is-title convention the
built-in editor and clipboard paste already use:
parseNoteFiledelegates to the existingsplitClipboardTextrather thanreimplementing the split, and a test asserts the two agree across the
awkward inputs (no title, no body, leading blank lines). Writing a third
copy of that logic seemed like the obvious way to end up with three
subtly different notions of what a note's text means.
The
.mdextension is deliberate — it's what makes the editor turn onmarkdown highlighting and wrapping.
Resolution
VISUALbeatsEDITOR, per the usual convention (EDITOR may be a lineeditor; VISUAL is the full-screen one). The value is treated as a command
line, not a program name, so flags survive —
code -wandsubl -wneedtheir wait flags or the editor returns instantly and the note appears
unchanged.
With neither set,
eshows$EDITOR is not setrather than falling backto
vi. Dropping someone into an editor they may not know how to exit,from a TUI they were happily using, seemed worse than saying nothing
happened.
Failure paths
The note is left exactly as it was when:
:cq):q!on the wrong buffershouldn't blank a note
UpdatedEach of those has a test; removing the empty-file guard or the exit-code
check fails one.
Testing
go test ./...,-race,vet,gofmtclean.The interesting tests use a real subprocess rather than a mock — a
shell script that rewrites the file, and one that exits 1 — so the
exec.Cmdwe hand totea.ExecProcessis the thing under test.I also drove the built binary end to end through tmux with a scripted
$EDITOR: pressede, confirmed the title and body updated on the board,that the TUI redrew correctly after the editor released the terminal, and
that the result reached
notes.json. Then again withEDITOR/VISUALunset to see the message.
Notes
Independent of #3 and #4 — different files, any merge order works.
editorCommandsplits on whitespace, so an editor path containing spacesneeds a wrapper script. That's the same limitation git has, and handling
it properly means a shell-quoting parser I don't think this earns. Easy to
add if you disagree.