Skip to content

zn v0.14.0: a Tasks calendar with month, week and day views, and a bar cursor while you type - #31

Merged
adibhanna merged 71 commits into
mainfrom
zn-0.14.0
Nov 1, 2026
Merged

adibhanna merged 71 commits into
mainfrom
zn-0.14.0

Conversation

@adibhanna

@adibhanna adibhanna commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

zn 0.14.0: six PRs from spirit-of-light (#22, #26, #27, #28, #29, #30), each merged with its review follow-ups on top, plus one cleanup where two of them met.

Not in this release: #13 (Helix keybindings), which needs changes first.

Verified on 020afdc, and again on 072a386 with #30:

  • gofmt -l, go vet ./... and go test ./... are clean in every package.
  • A tmux smoke of the built binary with default_view_mode = "preview":
    • :new opens in INSERT and saves;
    • :settings shows the User group;
    • ]b from Tasks moves to the next tab;
    • :tasks calendar shows the month with tasks on their days and the No date strip, ] moves to the next month at once, and Esc switches to the week view.
  • Each PR was also reviewed, tested and driven in the TUI on its own, and Feat(tui): a bar cursor in insert mode, an underline in replace mode #29's cursor shapes were checked in a real terminal.

spirit-of-light and others added 30 commits October 29, 2026 14:30
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Tasks calendar is getting month, week and day views, and each needs
to know which open tasks fall on which day. Until now only the due date
counted, through the grouping the side calendar and Home share.

bucketCalendarEntries places a task on its due day and, when it has a
different scheduled date, on that day too, flagged as scheduled; a task
whose two dates match shows once. Due entries come before scheduled
ones and each group keeps the scan order. Done, cancelled and forwarded
tasks are left out. Tasks with no date are returned for a separate
"No date" list, ordered by priority and then alphabetically.

The calendar uses its own grouping so the side calendar panel and
BucketTasksByDueDate stay exactly as they are; new tests pin the
panel's rendering and click mapping and the existing grouping, so a
later change to the Tasks calendar cannot alter them by accident.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
createNote opened the note with the default pane mode (or the mode
remembered for that path) and then entered insert mode. With a preview or
split default, you were typing into a buffer the screen didn't show.
The new note's tab is now set to edit mode directly, which leaves the
default pane mode and the remembered per-note modes unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The calendar mode drew a 35-column mini-grid with the selected day's
tasks listed underneath, so a month's work could only be read one day
at a time. It now fills the page: weekday names across the top, a box
per day with its date in the top-left corner and its tasks inside, cut
to the box's width (wide characters included) and closed with "+N more"
when they do not fit. The selected day's tasks sit in a panel beside
the grid and tasks without a date in a strip below it; an entry placed
by its scheduled date carries a "»" marker. Week numbers and the week
start follow the existing calendar preferences, and the task filter
applies as before.

Below 72x18 cells the body says "Terminal too small" instead of
drawing a grid too cramped to read.

The calendar moves into its own component that computes the layout
once per frame; drawing, mouse hit-testing and the selection position
all read that layout, so they cannot disagree about where a day is.
The side calendar panel keeps its own renderer and is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the month grid, the half-page keys (Ctrl+D and Ctrl+U by default)
now show the next and previous month, the same move as ] and [, and
the selected day follows into the month shown. They go through the
keymap, so a user who rebinds nav.halfPageDown or nav.halfPageUp gets
the move on their own keys, with Vim mode on or off.

The new tests also pin the month keys that already worked: hjkl and
the arrows, Enter into the day's tasks and Esc or h back to the grid,
the due-date keys, t for today and o for the daily note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The calendar showed only a month. Esc on the grid now steps through
month, week and day and back, keeping the selected day, so the week
shown is the one that contains it. Esc still works below a layout's
minimum size, which is how to reach a smaller layout when the terminal
is too small for the current one; nothing switches by itself.

Week view shows seven columns in the configured week-start order and
day view one full-width column, each with its tasks listed from the
top, "+N more" when they overflow, and the "No date" strip pinned
below. An hour list runs down the left, 00 to 23 or 12 AM to 11 PM
following time_format. Tasks have no time of day yet, so the list only
gives the day its shape and no task is placed in an hour row. Week
numbers stay in the month view only.

The layout chosen lasts for the session, across switching Tasks views
and :tasks calendar, and every new session starts on the month. It is
never written to the configuration, so tasks_view_mode, which the
desktop app shares, still only says "calendar".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In the week and day views the half-page keys moved a whole month, as
on the month grid. They now move by the layout's own unit: a month in
month view, a week in week view, a day in day view, and the selected
day follows.

Tab, and Enter on a day with tasks, move focus from the grid into the
selected day's tasks, where j and k pick an entry and the usual task
keys act on it: Enter or o opens it, x toggles it, < and > move its
due date and the menu key opens its task menu. Esc, Left or h return
to the grid. Tab skips a day with no tasks, and Enter on such a day
still opens its daily note. Tab also works without Vim mode.

h and l, the arrows across week edges, t, o and n already behaved the
same in every layout; new tests pin that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Vim mode on, z switches the week and day hour list between hourly and
half-hourly rows. The variant goes back to hourly each time the calendar is
opened, and stays put while moving between days, weeks and the two column
views.

When the list does not fit the body, j/k, the arrow keys and the mouse wheel
scroll it by one row, clamped at both ends. The position is kept as a time of
day rather than a row index, so it survives a change of variant and moving to
other days. On the grid in week and day view, j/k no longer move the selected
day; h/l and the half-page keys do that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the whole hour list fits, j/k and the arrow keys on the grid scroll the
selected day's column one entry at a time instead, so a day with more tasks
than lines can be read without leaving the grid. A scrolled column counts the
entries hidden above it with a "+N more" line at its top, as well as those
still below. Picking another day returns the column to its top.

With focus inside the column, the column follows the selection, so the
selected task never sits off screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tab now cycles the calendar between the grid, the selected day's tasks and
the strip of undated tasks below, skipping whichever of the two is empty.
In the strip, j/k and the arrow keys move the selection and the usual task
keys act on it, so undated tasks can be opened, toggled or given a date
without leaving the calendar.

When a task leaves the strip, for example because it got a due date, the
selection stays at the same position, and focus goes back to the grid once
the strip is empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zn draws its own cursor as a reverse-video cell because Bubble Tea v1
hides the terminal cursor and parks it at the bottom after every frame.
A bar or underline cursor in insert and replace mode needs the real
terminal cursor, so cursorOut sits between the program and the terminal:
it appends the wanted position and shape (steady bar, CSI 6 SP q, or
steady underline, CSI 4 SP q) to every frame, writes it on its own when
only the cursor moved, since Bubble Tea skips unchanged frames, and resets
the terminal to its default shape on request. A write that is a lone
control sequence gets nothing appended, and while released (for suspend
or an external program) the wrapper writes no cursor bytes at all.

It keeps the method set of Bubble Tea's term.File so window-size and raw
mode detection still work, and uses only charmbracelet/x/ansi, which is
already a dependency. Nothing uses it yet; later steps wire it in.

Tests, written red first: steady shapes, the reset sequence, frame and
lone-sequence writes, set-on-change, release, concurrent writes under
-race, and the forwarded file descriptor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Vim mode on, `[b` / `]b` (and `gt` / `gT`) switched tabs only while
a note editor had focus, because the two-key sequences were matched
only in the editor's key path. On Home, Tasks, Calendar, the database
views and the side panels the first key went straight to the surface,
so the sequence never completed.

handleKey now matches the buffer and tab sequences before handing a
key to any surface other than a note editor, honouring custom keymaps.
When the second key does not complete a sequence, the held first key
is replayed to the surface and the second follows, so `]` month
navigation and `gg` keep working, one key later. Counts such as `3]b`
stay editor-only.

The held key lasts only until the next key: an overlay, chord, leader
key, Ctrl+W prefix, mouse click, focus moving to a note editor or Vim
mode turning off drops it without replaying it, and Esc cancels it.

Tests, the cycling one red first: `]b` / `[b` from a view, a key that
does not complete the sequence reaching the view, and a held key
dropped by each of those paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zn tui is gaining a way to open a note in edit, split or preview from its
own frontmatter. This adds the configuration behind it, without changing
what any note does yet.

`[terminal] view_mode_field` names the frontmatter field that sets a
note's view mode; it is empty by default, which keeps the feature off.
`[terminal.view_mode_rules]` maps `"field=value"` to a mode, so a field
such as `page-status = done` can choose preview. Both live under
`[terminal]`, which the desktop app ignores, so a save from the desktop
cannot drop them.

Rule keys and values are trimmed, unquoted and lower-cased, and rules
are ordered by their lower-cased `field=value`, which is the order the
first match will be taken in. A rule with a bad
mode, no `=` or an empty field name is kept in the file and reported with
its reason but never applied, and an invalid rule never stops the config
from loading. SetViewModeRule and RemoveViewModeRule rewrite only that
table; removing works on the key exactly as written, so a malformed rule
can still be deleted, and removing the last rule drops the table.

Tests, written red first, cover the defaults, loading the sub-table,
invalid rules and field values, normalising and ordering, and writing and
removing rules while keeping the rest of the file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frontmatterPaneMode reads a note's frontmatter and says which pane mode
it asks for, for the coming change that opens notes in that mode. Nothing
calls it yet.

A valid value in the configured field (`view-mode: preview`) wins. The
field name and its value compare without regard to case; a list, an empty
value or any word other than edit, split or preview gives no mode, and the
rules are tried instead. A rule matches when its field holds its value, or
any element of a list field does, compared case-insensitively after
unquoting; rules are tried in the order they arrive in, so the first match
wins. A note without frontmatter, or a vault with no field and no rules,
gets no mode.

Tests cover the field, every way it can fall through, rule matching with
case, quotes and lists, rule order, and a field that beats a matching rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With `[terminal] view_mode_field` set or a valid rule in
`[terminal.view_mode_rules]`, a note opened in a new tab now takes the
pane mode its frontmatter asks for, both from openNote and from quiet
opens such as the jump list and pickers.

A note's body is often read in the background after its tab appears, so
the tab opens in the mode it would have had anyway and is marked as
waiting; once the text is loaded the frontmatter sets the mode, and the
tab visibly switches on a slow read rather than holding up the open. A
manual mode switch before the load clears the mark, so the user's choice
is never overridden. The frontmatter mode beats the remembered per-note
mode and the keep-across mode, but only writes the tab: neither of those
is changed by it, so one finished note does not make every later note
open in preview. A tab that is already open keeps its mode when the note
is saved with new frontmatter; the new value applies the next time the
note is opened.

With the field empty and no valid rules, which is the default, no tab is
marked and opening a note behaves exactly as before.

Tests cover the default config, the field and a rule on open, both
beating the remembered and keep-across modes, a deferred read, a manual
switch before and after the load, saving and reopening, quiet opens, and
that applying a mode leaves the remembered modes alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A tab opened in the mode its frontmatter asks for has three neighbours
that must not lose or override that choice.

Splitting a tab before its note has loaded now gives the copy the same
waiting state, so both tabs take the frontmatter mode once the text is
in. A copy of a tab whose mode was switched by hand keeps that mode, as
before. A tab restored from the last session keeps the mode it was saved
with; the frontmatter does not change it. A note pinned as a reference
with `:ref` always stays in preview, whatever its frontmatter asks for.

Tests cover a restored tab, splits before and after the load, a split
of a manually switched tab, and a reference whose frontmatter asks for
edit or split.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntmatter

With a view-mode field or rules configured, a note created from a
template that says `view-mode: preview`, or whose first fields match a
preview rule, would otherwise open straight into preview. A note that has
just been made is about to be written, so it should never start there.

Every action that creates a note and opens it now marks the note first:
a new note, a note from a template, a periodic note created on first
open, a new task file, and today's daily note when a rollover creates
it. The open that follows uses the mark up, starts in the editor and does
not wait for the frontmatter. Notes created without being opened, such
as a daily note made by adding a task to it or a saved capture, carry no
mark; the first time they are opened they follow their frontmatter like
any other note. With the feature off, a new note opens in the mode it
does today.

Tests cover each of the five creation paths, an existing periodic note,
notes created without opening, the feature off, and reopening a new note
after closing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The view-mode rules live in `[terminal.view_mode_rules]`, a table inside
`[terminal]` that `zn config` could not reach: it splits a key at the
first dot and only knew the four top-level map tables.

`zn config list` now shows every rule as
`terminal.view_mode_rules.<field>=<value> = <mode>`, invalid ones too, so
a hand-edited mistake is visible; `list --json` carries the same entries.
`zn config get` reads a rule by that name, and `zn config set` adds a new
rule or changes an existing one. Set checks the rule before writing: a
bad mode, a key without `=`, an empty field or an empty value exits with
an error and leaves config.toml as it was. The mode is stored lower-cased.
`terminal.view_mode_field` reads and sets like any other terminal key.
Removing a rule stays with the settings view and `zn config edit`, since
`zn config` has no unset.

Tests cover listing, reading, adding and changing a rule, every kind of
bad input, and setting the field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`:settings` now shows and edits the frontmatter view-mode configuration,
so it never needs config.toml opened by hand.

`terminal.view_mode_field` appears with the other terminal settings and
is edited like them; while it is empty its prompt suggests `view-mode`
as an example rather than filling it in. Right after the terminal rows
come the rules, as `terminal.view_mode_rules.<field>=<value> = <mode>`:
the valid ones in the order they are matched, then the invalid ones,
each ending in its reason, such as `(invalid: no "=")`. A rule shows its
key as written in the file. Last comes an "(add rule)" row, which stays
visible whatever the filter.

Enter on the add row asks for the field, then the value, then offers
edit, split and preview; nothing is written until a mode is picked, Esc
at any step writes nothing, and an empty field, a field holding "=", an
empty value or a rule that already exists in another case is refused.
Enter on a rule offers the three modes and remove; `d` on a rule row
also removes it, and the footer names `d` while one is selected. A rule
with a bad mode can be fixed by picking a mode; one whose key is
malformed offers only remove. Rules are changed and removed by their key
as written, so hand-typed keys are found. After every change the
preferences are reloaded and the rows refreshed.

settingRow keeps its four fields: a rule row's key is the rule's key,
and whether a row is a setting, a rule, an invalid rule or the add row
is worked out from the row itself.

Tests cover editing the field, rule order, invalid rules marked and
ignored when a note opens, filtering, adding with every kind of bad
input and cancel, changing, removing with the picker and with `d`, the
footer hint, fixing a bad mode, and malformed rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A hand-written rule such as `"status=" = "edit"` was read as valid and
listed in `:settings` as an ordinary rule, yet choosing a new mode for it
there failed, because writing a rule already refused an empty value. The
rule looked usable but could only be removed.

An empty value, including an empty quoted one, now makes a rule invalid
when it is read, the same as a missing `=` or an empty field. Such a rule
never matches a note; `:settings` and `zn config list` show it with the
reason "empty value", and its picker offers only remove. The check that
runs before a rule is written is now the same parse, so reading and
writing agree on what a valid rule is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README gains a section on opening notes in a pane mode from their
frontmatter: `[terminal] view_mode_field`, off by default with
`view-mode` suggested, the `[terminal.view_mode_rules]` format with a
`page-status=done` example, what makes a rule invalid and how to remove
one, the order in which the field, the rules, the remembered and
keep-across modes, restored tabs, new notes and `:ref` win, and how to
edit both in `:settings` or with `zn config`. It says only zn tui reads
them. The settings table lists both under `[terminal]`.

zn tui's help adds a row for the field and the rules next to the pane
mode keys, and says a manual mode switch always wins over frontmatter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two end-to-end tests guard the feature as a whole. One opens notes in
frontmatter modes, lets a slow read land, switches, splits and closes
tabs, and adds and removes a rule from `:settings`, then checks that
every file in the vault, `.zennotes` included, is byte-for-byte as it
was: the configuration lives only in zn's own config.toml. The other
adds a rule in `:settings`, opens a matching note in that mode, changes
the rule's mode, and reopens the note in the new one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three cases were covered only below the open path or not at all. A note
whose frontmatter matches nothing (a bad field value, a list in the
field, or no matching rule) now has a test that it opens exactly as it
would with the feature off, against a preview default, a remembered
mode and the keep-across mode, before and after a slow read. Invalid
rules are shown to be ignored when a note opens while a valid rule
beside them still applies, and an empty field is shown to leave the
rules working after a slow read. A last test keeps the README's
description of the settings from going missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…frontmatter

Two ways of opening a note set the tab's mode themselves right after
opening it, and with frontmatter view modes on, a slow read of the note
could then land and replace that mode.

Peeking a note from the sidebar asks for preview, as `:ref` does; the
frontmatter could then switch the peek to edit or split. Both now set
their mode through one helper that also stops the frontmatter from
taking it back.

Reopening a closed tab put back the mode the tab was closed in, and the
frontmatter replaced it only when the read happened to be slow. A
reopened note is now treated like any note opened in a new tab: its
frontmatter mode wins whether the read is quick or slow, and the closed
tab's mode stands only when the frontmatter asks for none.

Tests cover a peek of a note whose frontmatter asks for edit or split,
and reopening a closed tab with a quick and a slow read, with and
without a frontmatter mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two hand-edited or command-line cases slipped past the rule handling.

`zn config set terminal.view_mode_rules.Page-Status=done split` added a
second entry next to an existing `page-status=done`, although both are
the same rule: the new one then won the tie, and `:settings` listed two
rows for one rule. Writing a rule now looks for an entry with the same
field and value, compared the way rules are matched, in any case or
quoting and whether or not its mode is valid, and changes that entry
under the key it already has.

A rule such as `"a=b" = 1` or `= true` was dropped while loading, so it
was ignored but also missing from `:settings` and `zn config list`,
where invalid rules are shown with a reason and can be removed. Such a
mode is now kept as text, so the rule shows up as invalid with "bad
mode" and can be removed like any other.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… tab alone

When the note behind a closed tab had been deleted, reopening it opened
nothing, but the closed tab's mode was still written to whichever tab
was active, and with frontmatter view modes on that tab was then given
its own frontmatter mode, replacing one the user had picked. The
reopened mode is now applied only to the reopened note's tab.

The README's list of what wins now says that a reopened tab follows its
frontmatter, and that a sidebar peek stays in preview like `:ref`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After `r`, with or without a count or register, the editor stays in
normal mode until the replacement character arrives, so a host cannot
tell from the mode that a character is pending. ReplacePending answers
that: it is true only in normal mode while the pending keys are an
optional register and count followed by `r`, and false once the
character or Esc arrives, in visual mode and in insert mode. The TUI
uses it to show an underline cursor while it waits.

Tests: `r`, `3r` and `"ar` are pending; `rx` replaces and clears it;
`r<Esc>` clears it with the text unchanged; `vr`, insert mode, a typed
`r` in insert mode and another pending operator are not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a note is edited, the reverse-video cell that stands in for the
cursor hides the character under it and gives no sign of the mode. The
TUI now works out, for every frame, the cursor the terminal should show:
a steady bar in insert mode or with vim mode off, a steady underline in
replace mode, and nothing in normal, visual and command-line mode or
while the note is loading, where the drawn block stays exactly as it
was. The cell is found the way the tag menu finds its anchor: the pane
origin, the gutter, then the width of the text before the cursor on its
wrapped row, so tabs and wide runes land on the right cell. While the
terminal cursor owns the insertion point the editor stops drawing the
block there and past the end of the line.

Nothing reaches the terminal yet; a later commit connects the frame's
cursor to the output wrapper.

Tests: the bar on a plain, wrapped, CJK, tab and end-of-line position
with no reverse cell drawn; the underline in replace mode; the bar with
vim mode off; the block in normal and visual mode and after Esc; and the
block while the note loads, with vim mode on and off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cursor follows typing, arrow keys, backspace and mouse clicks, and
lands on the character under it in a single pane, either split, with
the sidebar or a side panel open, with tabs hidden, in zen mode and in
split note mode, and again after a resize rewraps the line.

A buffer's wrapped rows are shared by every pane that draws it, and the
last pane drawn wins. With one note open in two panes of different
widths, a focused pane drawn first would have placed the cursor using
the other pane's wrapping, on a blank cell. The buffer now keeps a copy
of the rows taken when the focused pane draws it, and the cursor reads
that copy.

Tests: the cursor through editing and mouse moves, across the layouts
above, in the second pane of each split, after a resize, and with the
same wrapping note in two unequal panes with either pane focused. The
normal and visual mode block is compared byte for byte with the cursor
style, so a change to how it looks fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nvim shows an underline while `r` waits for the replacement character,
which tells the user the next key replaces rather than moves. zn now
does the same: while the editor reports a pending `r`, the terminal
cursor is an underline on the character to be replaced, and once the
character or Esc arrives the drawn block comes back. In visual mode `r`
keeps the block.

Tests: `r` and `3r` show the underline on the cursor character with no
reverse cell; `rx`, `r<Esc>` and `2rx` end with one block and no
terminal cursor; `v` then `r` keeps the block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
adibhanna and others added 26 commits November 1, 2026 13:49
The README's key table and the manual now say `gt`, `gT`, `]b` and `[b`
switch tabs from any view or panel, and that the calendars keep `[` `]`
for the month and the Kanban board keeps `g` for its grouping.
Bubble Tea gives a program it runs the program's own output as stdout.
With the cursor wrapper in place, that output is no longer os.Stdout but
a writer that is not a file, so os/exec connected the child's stdout to a
pipe. `Space l e`, `:editor` and opening config.toml then broke: Vim
printed "Warning: Output is not to a terminal" and Neovim drew a blank
screen, while the same keys on main opened the note normally.

execOnTerminal now sets the command's stdout to the terminal file the
wrapper writes to (leaving a stdout the command already has alone),
resets the cursor shape, and queues the hand-off. The external editor and
the config action both go through it.

Test: the command's stdout is the wrapper's terminal, the cursor is reset
and released, and a stdout set beforehand is kept. Checked by hand in
tmux with Vim and Neovim as $EDITOR, and with a probe script that reports
`[ -t 1 ]` (now a tty, as on main).
runProgram passed os.Stdin to Bubble Tea as a custom input. Bubble Tea
only falls back to opening the terminal when it picks the input itself,
so `zn tui < /dev/null` (or a launcher that gives zn a pipe) put no
terminal in raw mode: keys were echoed by the tty and never reached zn,
and it could not be quit from the keyboard. main opened /dev/tty there.

Run now passes no input and runProgram adds WithInput only when a caller
gives one, as the tests do with their pipe. Checked by hand in tmux:
`zn tui < /dev/null` opens a note from `:e` again.
Ctrl+Z and `:suspend` reset the cursor shape and released the wrapper
before queueing tea.Suspend, and a ResumeMsg took it back. Bubble Tea
cannot suspend on Windows: Suspend does nothing there and no ResumeMsg
follows, so the wrapper stayed released for the rest of the session. The
terminal cursor was left at the bottom in its default shape, and in insert
mode, where the editor no longer draws its block, nothing marked the
insertion point.

suspend now hands the cursor over only where Bubble Tea can suspend. Both
the chord and the ex command go through it.

Tests: the existing suspend tests pin suspend support on, so they run on
the Windows CI too; with it off, Ctrl+Z and `:suspend` keep the wrapper
held, write no reset, and the bar stays.
Any toast hid the terminal cursor for its four seconds. In insert mode,
and always with Vim mode off, the editor no longer draws its block there,
so the insertion point vanished while the user kept typing: copying a
link from the palette, "Reloaded <note>" after the external editor, or
any error. A toast takes no keys, so typing goes on behind it.

renderToasts now records the cells each toast is drawn over, from its
left edge to the end of its rows, since the rest of those rows is cut off
too. The cursor is hidden only when it falls inside one of them, and comes
back on the same cell when the toast expires.

Tests, with Vim mode on and off: a toast in its corner keeps the bar at
the insertion point; a toast wide enough to cover the insertion point
hides it, and its expiry restores it. Checked by hand in tmux with Vim
mode off: after "Copy note link" the bar stays on the typing row.
The command line and text fields drew their cursor with firstCellOr and
restAfterFirst. They place the terminal's cursor now, so nothing calls
either helper.
The editor section says when zn shows a bar, an underline or the block,
that the terminal's default cursor comes back on quit, suspend and the
external editor, and what a terminal without cursor shapes shows.
…saves

The view-mode section said the desktop app ignores view_mode_field and
[terminal.view_mode_rules]. It does more than ignore them: the desktop
rebuilds config.toml from the settings it knows, at launch and on every
settings change, and the rebuilt file has no [terminal] table at all
(serializeConfig in apps/desktop/src/main/app-config.ts). So rules set in
zn tui, like mouse, preview_style or math, are gone once the desktop app
opens.

The Configuration section now says so once for every [terminal] setting,
and the view-mode section points there instead of promising the settings
are left alone. The real fix belongs in the desktop app, which should keep
the [terminal] table as it found it.
…ch row's whole text

#22 lays the settings page out under User and Vault headings and gave the
view a second scroll offset, counted in display lines. Two things went
wrong with it:

- The offset was only capped at the page's last line, so after G in a short
  window, growing the window left the page scrolled down: the User and vim
  headings stayed off the top with blank lines below. It now stops where
  the last line sits at the bottom, so a taller window or a shorter
  filtered list shows the top again.
- A row rebuilt its text from key and value, so anything refresh adds to a
  row's line was lost. A row now shows its filter line less the scope and
  prefix its headings already name; the "(invalid: ...)" note #28 adds to a
  bad view-mode rule shows that way.

The view keeps one scroll field: v.scroll is the display offset now, and
the setting-index scroll that no longer drove anything is gone from render
and from the mouse wheel.

Tests: the layout test keeps a row's whole line; every row a real refresh
lists sits under the group it saves to; moving onto the first Vault row
from above and from below keeps its headings on screen; and a tall window
shows the page from the top. The cap and the heading walk-back each fail
their test when reverted.
The User and Vault headings now draw the rule the Tasks list and the manual
use for their sections ("─ User · config.toml ───" and
"─ Vault · .zennotes/vault.json ───"), so the page says where an edit
lands. That file is what separates the two groups: the desktop's Settings
files rows by topic (Task folders sit under Editing), while this page lists
config keys and groups them by where they are saved.

Prefix headings (vim, editor, dailyNotes, ...) become bold body text under
the accent rule, and the blank line before Vault goes, since the rule
already separates the groups.
The settings paragraph named only periodic-note and task-exclusion controls
as vault settings. Task folders and the template folder save to the vault
too, and the page now groups its rows by that, so the paragraph says so.
A day with more tasks than its box or column can show ends with a
"+N more" line, but in narrow boxes it was cut to the width, so it read
"+4 mor" or "+2 mo". When "+N more" does not fit, the line now reads
just "+N".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ault mode (by @spirit-of-light)

With the review follow-ups on top of the contributor's commits.
With the review follow-ups on top of the contributor's commits.
@spirit-of-light)

With the review follow-ups on top of the contributor's commits.
…pirit-of-light)

With the review follow-ups on top of the contributor's commits.
…(by @spirit-of-light)

With the review follow-ups on top of the contributor's commits.
The README's Views section now describes the calendar's month, week and
day views, the hourly and half-hourly hour list, moving focus between the
grid, the day's tasks and the No-date strip, and the due-date menu. It
also notes that, unlike the desktop calendar, the terminal calendar
places tasks on their scheduled date too. The key hints at the bottom of
the calendar list the new keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#26 and #28 fixed the same thing from two sides. #26's showNewNoteInEditor
set a new note's tab to edit right after it opened, and its follow-up called
it from every creation path. #28 marks a note as new before the open
(markNewNote), and since its follow-up a marked note always opens in the
editor. Merged together, every creation path called both, one after the
other, to the same end.

The mark stays, since it acts inside the open itself, before a default,
remembered or frontmatter mode is applied, and showNewNoteInEditor goes.
Every creation path #26's follow-up covers already marks its note, and its
tests (a new periodic, template, task-file and rollover note under a
preview and a split default) pass on the mark alone, as do #28's.
The Tasks list takes no printable key with Vim off, and the calendar's
grid follows the same rule, but a calendar task with focus (in the
selected day's items or the No-date strip) still took every letter:
`T` opened the due-date menu, `c` cancelled the task, `d`, `e`, `w`,
`s`, `/` and `<`/`>` edited it, and `h` and `o` moved focus or opened
the note. One of the PR's tests asserted that `T` opens the menu with
Vim off.

A focused task now passes letters on with Vim off, so the arrow keys,
Enter, Tab and Esc do the work there, as the PR describes. In Vim mode
`:` on a focused task opens the ex line, as it does on the grid; before,
it did nothing there.

Tests, red on the PR head: with Vim off, `T`, `c`, `d`, `w`, `/` and
`e` on a focused task open nothing and leave the note unchanged, `h`
leaves focus on the task, and `:` on a strip task opens the ex line.
…menu

The desktop calendar binds `T` on a focused task to "move it to today"
and `@` to "pick any due date", and its demo vault teaches `T` that way.
The PR bound `T` to its new due-date menu (today, tomorrow, next week,
the selected day, clear, other), so the same key did two different
things in the two apps' calendars.

`T` on a focused item or strip task now sets its due date to today, and
the PR's menu moves to `@`, the desktop's key for choosing a date. Both
stay Vim keys. On the grid `t` and `T` still go to today, as before.

Tests: the due-menu tests press `@`; a new test checks that `T` moves a
focused item and a strip task to today without opening a menu.
The calendar showed one hint line everywhere. It named `z hour step` on
the month grid, where `z` does nothing, and `T due menu` while the grid
had focus, where `T` goes to today. With a task focused it still named
the grid's keys and none of the task's.

The grid's hints now differ by view: the month grid names `h/j/k/l` for
the day, and the week and day grids name `h/l` for the day, `j/k` to
scroll and `z` for the hour step. With a task focused, the hints name
the task keys: `j/k`, toggle, open, `T` due today, `@` due date, `<`/`>`,
the menu, Tab and `Esc` back. `zn help` lists the calendar's views,
focus and task keys, which the PR left at the old one-line entry.

Test: the month grid hint names no hour step or due keys, the week grid
hint names the hour step, and a focused task's hint names `T`, `@` and
`Esc` back but no grid keys.
The month view needs a 72x18 body for its day boxes and showed
"Terminal too small" below that. In an 80x24 terminal with the sidebar
open the body is about 43x18, so the default view of `:tasks calendar`
showed nothing there, where the calendar before the PR showed a month
grid and the selected day's tasks.

Below the minimum, the month view now draws a compact month: one line
per week with the date and a count of the day's entries (as the side
calendar panel does), the selected day's items under it, and the No-date
strip at the bottom. It uses the same layout regions as the full month,
so keys, Tab focus, clicks and menu anchors work unchanged. "Terminal too
small" is left for bodies under 38 columns (with week numbers) or with
no room for one item and one strip task under the grid.

Tests: the compact month renders at one column and one row below the
full minimum and at 43x18 and 38x12, with the day's count, its items and
the strip; it is too small at 37x18 and 72x11; Enter, j, the menu anchor
and a click on a day work in it. The too-small test that pressed Esc now
uses a body too small for the compact month too.
…gate

The due-menu tests carried `// AC88` to `// AC112` comments, ids from a
spec that is not in the repository, so they tell a reader nothing.

The PR's commit for week and day navigation also added `tab` to the
Kanban board's key gate, not the calendar's. Tab is not a printable key,
so the gate let it through already and the change did nothing. It goes.
Tasks carry a date and no time of day (the desktop's new `@remind:` time
is relative to the date-only due date), and the PR lists a day's entries
from the top of its column on purpose. But it drew an hour list beside
the columns, so the first task sat next to `00`, the second next to `01`
(or `00:30` after `z`), and the week read as a schedule of tasks at
midnight that nobody wrote.

The week and day views are now columns of the day's tasks under the
header, with the strip at the bottom. The hour list, its scrolling, the
half-hourly variant and `z` are gone; `j`/`k` scroll a busy day's column,
which the PR already did when the hour list fit. Columns use the whole
height, so the week view needs 56x8 and the day view 30x7 instead of
64x12 and 40x10. The wheel moves the selected day a week in every view,
as it did in the PR when the hour list fit.

Tests: the hour-list tests go; the week and day show no hour labels in
either time format, `z` changes nothing in any view, column scrolling
and the overflow counts are checked on a body shorter than the busy day,
the strip sits under the columns, and the minimum sizes follow calMin.
The editor's fold keys test stays.
…it-of-light)

With six review follow-ups on top of the contributor's commits. The merge also updates ownsKey from #27's follow-up, which read v.dayFocus, a field #30 removes: it now checks that the calendar grid has focus, so [ and ] still change the month at once.
@adibhanna adibhanna changed the title zn v0.14.0: a bar cursor while you type, and notes that choose how they open zn v0.14.0: a Tasks calendar with month, week and day views, and a bar cursor while you type Nov 1, 2026
@adibhanna
adibhanna merged commit 072a386 into main Nov 1, 2026
5 checks passed
adibhanna added a commit to ZenNotes/zennotes that referenced this pull request Nov 1, 2026
The bundled CLI is the floor a desktop build guarantees, and 0.14.0 is
the newest release (ZenNotes/tui#31). It brings month, week and day views
to the TUI's Tasks calendar (#30), a bar cursor while you type (#29), a
note's own view mode from its frontmatter (#28), new notes that always
open in the editor (#26), [b and ]b from every view (#27), and settings
grouped by the file they save to (#22).

The pin is the release's terminal-release.json copied byte for byte
after it verified against the zn-release-1 key this app trusts
(cli-update-core.ts), naming source commit 072a3866 (the v0.14.0 tag),
integration protocol 1, and four archives that match both the manifest
and checksums.txt. Staged through `npm run terminal:stage`; the native
darwin-arm64 binary reports zn v0.14.0. The in-app manual's CLI card
says 0.14.0.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants