Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
bea0218
Feat(tui): settings page groups rows under User and Vault by prefix
spirit-of-light Oct 29, 2026
6500bb1
Feat(tui): place calendar tasks by due and scheduled date
spirit-of-light Oct 31, 2026
0381c69
Fix(tui): a new note opens in the editor whatever the default pane mode
spirit-of-light Oct 31, 2026
9f47fd8
Feat(tui): the Tasks calendar opens on a full-page month grid
spirit-of-light Oct 31, 2026
d2642ba
Feat(tui): half-page keys page the Tasks calendar by month
spirit-of-light Oct 31, 2026
84b3379
Feat(tui): Esc switches the Tasks calendar between month, week and day
spirit-of-light Oct 31, 2026
fd41049
Feat(tui): week and day calendar navigation and column focus
spirit-of-light Oct 31, 2026
f86670d
Feat(tui): half-hourly hour list and hour-list scrolling in week and day
spirit-of-light Nov 1, 2026
47370e5
Feat(tui): scroll a busy day's column in week and day view
spirit-of-light Nov 1, 2026
1622851
Feat(tui): Tab reaches the calendar's No-date strip
spirit-of-light Nov 1, 2026
e234a94
Feat(tui): a terminal output wrapper that places and shapes the cursor
spirit-of-light Nov 1, 2026
041da14
Fix(tui): `[b` and `]b` switch tabs from every view and panel
spirit-of-light Nov 1, 2026
4bbbc0b
Feat(config): a terminal view-mode field and frontmatter rules table
spirit-of-light Oct 31, 2026
cb78af2
Feat(tui): work out a note's view mode from its frontmatter
spirit-of-light Nov 1, 2026
c1de97e
Feat(tui): a note opens in the view mode its frontmatter asks for
spirit-of-light Nov 1, 2026
1cb85fb
Feat(tui): split copies, restored tabs and references respect view modes
spirit-of-light Nov 1, 2026
5271c09
Feat(tui): a newly created note opens in the editor, whatever its fro…
spirit-of-light Nov 1, 2026
71bd186
Feat(cli): zn config lists, reads and sets terminal view-mode rules
spirit-of-light Nov 1, 2026
7428c7d
Feat(tui): view-mode settings and rules on the settings page
spirit-of-light Nov 1, 2026
2e2da5f
Fix(config): a view-mode rule with an empty value is invalid
spirit-of-light Nov 1, 2026
6e05399
Docs: README and zn tui help describe frontmatter view modes
spirit-of-light Nov 1, 2026
21ea059
Test(tui): frontmatter view modes write nothing to the vault
spirit-of-light Nov 1, 2026
0b106bb
Test(tui): notes no rule or field matches open as with the feature off
spirit-of-light Nov 1, 2026
ce02956
Fix(tui): a sidebar peek stays in preview and a reopened tab follows …
spirit-of-light Nov 1, 2026
8afc2ad
Fix(config): rules differing only in case, or with a non-string mode
spirit-of-light Nov 1, 2026
abe7899
Fix(tui): reopening a closed tab whose note is gone leaves the active…
spirit-of-light Nov 1, 2026
3fe9eae
Feat(vim): the editor reports when r waits for its character
spirit-of-light Nov 1, 2026
7f3e269
Feat(tui): a bar or underline cursor for the note being edited
spirit-of-light Nov 1, 2026
481b47b
Feat(tui): the cursor finds its cell in every pane layout
spirit-of-light Nov 1, 2026
49237b9
Feat(tui): an underline while r waits for its character
spirit-of-light Nov 1, 2026
0c97bab
Feat(tui): a bar on the command line
spirit-of-light Nov 1, 2026
7d9cc36
Fix(tui): keep the terminal cursor inside the editor and on screen
spirit-of-light Nov 1, 2026
986d91b
Feat(tui): a bar in text fields and quick capture
spirit-of-light Nov 1, 2026
85815a9
Feat(tui): menus, toasts and sidebar focus hide the cursor
spirit-of-light Nov 1, 2026
b437c10
Feat(tui): zn shows the terminal cursor and resets its shape on exit
spirit-of-light Nov 1, 2026
9077380
Feat(tui): reset the cursor shape around suspend and external programs
spirit-of-light Nov 1, 2026
87a7838
Fix(tui): the field cursor sits on the field under a wrapping title
spirit-of-light Nov 1, 2026
5a224ff
Feat(tui): T opens a due-date menu from the calendar's task lists
spirit-of-light Nov 1, 2026
0c118b4
Fix(tui): right-click lands on the clicked cell in framed panes
spirit-of-light Nov 1, 2026
a39a2cc
Feat(tui): mouse on calendar entries and the No-date strip
spirit-of-light Nov 1, 2026
d3401ca
Fix(tui): every new note opens in the editor, not only a blank one
adibhanna Nov 1, 2026
730f394
Fix(tui): the calendars keep `[` and `]`, and the Kanban board keeps `g`
adibhanna Nov 1, 2026
97a823c
Fix(tui): the status bar shows a held `[`, `]` or `g`
adibhanna Nov 1, 2026
65d44f3
Test(tui): `]b` and `gt` stay off with Vim mode off
adibhanna Nov 1, 2026
fecb57b
Fix(tui): every new note opens in the editor, with frontmatter view m…
adibhanna Nov 1, 2026
28a91f1
Docs: zn help and the README say the tab keys work from any view
adibhanna Nov 1, 2026
2dbedd7
Fix(tui): the external editor writes to the terminal, not to a pipe
adibhanna Nov 1, 2026
015a97e
Fix(tui): zn tui reads the terminal when stdin is redirected
adibhanna Nov 1, 2026
e728db0
Fix(tui): Ctrl+Z on Windows leaves the cursor alone
adibhanna Nov 1, 2026
a56296c
Fix(tui): a toast hides the cursor only where it is drawn over it
adibhanna Nov 1, 2026
038ea1f
Refactor(tui): drop the drawn-cursor helpers nothing calls
adibhanna Nov 1, 2026
e6bce16
Docs: the README says how the cursor looks while you type
adibhanna Nov 1, 2026
defbdd1
Docs: README says the desktop app drops the [terminal] table when it …
adibhanna Nov 1, 2026
a5063d6
Fix(tui): grouped settings scroll back on a taller window and keep ea…
adibhanna Nov 1, 2026
b7bda03
Feat(tui): settings group headings name the file each group saves to
adibhanna Nov 1, 2026
632d060
Docs: the README says where each settings group saves
adibhanna Nov 1, 2026
f0d6fd1
Fix(tui): calendar overflow count no longer cut mid-word
spirit-of-light Nov 1, 2026
a6d5a2a
Merge #26: a new note opens in the editor always, despite the set def…
adibhanna Nov 1, 2026
200d9b3
Merge #28: frontmatter default mode definition (by @spirit-of-light)
adibhanna Nov 1, 2026
44eec1c
Merge #22: settings page groups rows under User and Vault by prefix (…
adibhanna Nov 1, 2026
83017f2
Merge #27: `[b` and `]b` switch tabs from every view and panel (by @s…
adibhanna Nov 1, 2026
b16a187
Merge #29: a bar cursor in insert mode, an underline in replace mode …
adibhanna Nov 1, 2026
e80c3fc
Docs: document the calendar's views and keys
spirit-of-light Nov 1, 2026
020afdc
Refactor(tui): a new note reaches the editor one way, not two
adibhanna Nov 1, 2026
32a7f85
Fix(tui): a focused calendar task takes no letters with Vim off
adibhanna Nov 1, 2026
557e86a
Fix(tui): T moves a calendar task to today, and @ opens the due-date …
adibhanna Nov 1, 2026
fc819fc
Fix(tui): the calendar's key hints follow its focus and view
adibhanna Nov 1, 2026
591d660
Fix(tui): the Tasks calendar keeps a compact month on narrow terminals
adibhanna Nov 1, 2026
a00fe96
Chore(tui): drop spec ids from the due-menu tests and a stray Kanban …
adibhanna Nov 1, 2026
9d1c18d
Refactor(tui): week and day columns list tasks without an hour axis
adibhanna Nov 1, 2026
072a386
Merge #30: month, week and day views for the Tasks calendar (by @spir…
adibhanna Nov 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 91 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ keeps them open until you choose an action, press Space again, or cancel.
| `Ctrl+B` | Toggle the sidebar |
| `Ctrl+W` then `v` `s` `h` `j` `k` `l` `q` `o` `=` | Split, move between and close panes |
| `Ctrl+O` `Ctrl+I` | Jump back and forward across notes |
| `gt` `gT` `]b` `[b` | Next and previous tab |
| `gt` `gT` `]b` `[b` | Next and previous tab, from any view or panel (the calendars keep `[` `]` for the month, the Kanban board keeps `g` for its grouping) |
| `:` | Ex commands; `Space ;` or `Ctrl+G` opens the command palette |
| `?` | The keys of the current view; `:help` is the full manual |
| `F2` | Context actions, including when Vim mode is off |
Expand Down Expand Up @@ -562,6 +562,13 @@ Vim mode follows `[vim] enabled` in `config.toml`. With it off, the editor
is a plain editor. Use arrows and Enter in lists, F2 for context actions,
and Ctrl+G for the command palette. Ctrl+Z undoes plain-editor changes.

While you type, zn shows the terminal's own cursor: a bar in insert mode,
with Vim mode off and in text fields, and an underline in replace mode and
while `r` waits for its character. Normal and visual mode keep the block.
zn puts the terminal's default cursor back when it quits, suspends or hands
the note to `$EDITOR`. A terminal that cannot change the cursor's shape
shows its usual cursor there instead.

`Ctrl+Space` in insert mode (or `:complete`) completes tags, frontmatter
tag values, callouts and code-fence languages. With `markdown_snippets`
enabled, Space after an opening Markdown delimiter adds its closer;
Expand Down Expand Up @@ -590,11 +597,23 @@ reloads it afterwards. In Vim mode, `Ctrl+Z` suspends to the shell.
`g` regroups by status, priority, due date, folder or `field:<key>`.
`:boardoptions` (also F2 → Board options) sets the folder root, column labels,
custom statuses and card order. Folder boards keep cross-folder moves
disabled. On the calendar Enter lists a day's tasks and `o` opens its
daily note. Filters match the desktop's substring search over task text,
disabled. Filters match the desktop's substring search over task text,
note titles, `!priority`, `#tags` and `@fields`. `:filters` / `:savefilter`
manage shared named queries. Prefix terminal operators with `where:`,
for example `where: is:open due:today -meeting`.
The calendar has month, week and day views; `Esc` cycles them, and a
start in `calendar` mode always opens the month grid. On a narrow
terminal the month shows a compact grid with a count per day and the
selected day's tasks under it. `h`/`j`/`k`/`l`
move the day, `[`/`]` the month, `t` goes to today, `<C-D>`/`<C-U>` page
by half a view, and `o` opens the day's daily note. In week and day view
each day is a column of its tasks, and `j`/`k` scroll a busy day's
column. `Enter` and `Tab` move focus
between the day grid, the selected day's items and the No-date strip. On
a focused item or strip task, `T` moves it to today and `@` opens a
due-date menu, as in the desktop calendar. Unlike the
desktop calendar, the terminal calendar also places a task on its
scheduled date (marked) as well as its due date.
- **Tags**: `Tab` multi-selects, `a` toggles any/all, `r`/`x` rename or
remove a tag everywhere.
- **Databases**: every `.base` folder and every loose `.csv` file, with
Expand Down Expand Up @@ -717,6 +736,67 @@ files by absolute path, so under `latex` a note can show in the preview the
contents of any file you can read. Leave latex off for vaults you do not
trust.

### Open notes in a pane mode from their frontmatter

By default a note opens in the mode `editor.default_view_mode` gives (or the
mode you last chose, with `keep_view_mode_across_notes`). Two `[terminal]`
settings, both off by default, let a note's own frontmatter pick the mode when
it opens in a new tab:

- **`view_mode_field`** names a frontmatter field. It is empty by default;
`view-mode` is a good name. A note with `view-mode: preview` (or `edit`, or
`split`) opens in that mode. The key and value are not case sensitive. A
list, an empty value or any other word is ignored.
- **`[terminal.view_mode_rules]`** maps a frontmatter `key=value` pair to a mode.
A list field matches when any element equals the value.

```toml
[terminal]
view_mode_field = "view-mode"

[terminal.view_mode_rules]
"page-status=done" = "preview"
"kind=journal" = "edit"
```

Rules are written `"<key>=<value>" = "<mode>"`, where the mode is `edit`,
`split` or `preview`. Keys and values are trimmed, unquoted and compared in
lower case. A rule is invalid, and ignored, when it has no `=`, an empty key,
an empty value or a mode other than those three. Invalid rules never stop zn
from starting; `:settings` lists them marked with their reason (`no "="`,
`empty key`, `empty value` or `bad mode`), and `d` or the mode picker's
`remove` deletes them.

Precedence:

- A valid `view_mode_field` value beats the rules. Among the rules, the first
one in sorted order of the lower-cased `key=value` wins, so
`kind=journal` is tried before `page-status=done`.
- Frontmatter beats the remembered mode and `keep_view_mode_across_notes`.
Applying it never changes either of them; a mode you switch to yourself
still does.
- A tab restored from the saved session keeps its saved mode.
- A note you have just created opens in the editor, whatever its frontmatter
says. Reopening it later follows the frontmatter.
- Reopening a closed tab follows the frontmatter too; the mode the tab was
closed in comes back only when the frontmatter asks for none.
- The pane opened by `:ref`, and a peek from the sidebar, stay in preview.
- Switching to a tab that is already open never changes its mode, and saving
new frontmatter does not either.

Edit them in `:settings` (the `terminal.view_mode_field` row, and the
`terminal.view_mode_rules` rows with their add row) or with `zn config`:

```bash
zn config set terminal.view_mode_field view-mode
zn config set terminal.view_mode_rules.page-status=done preview
```

Only `zn tui` reads these settings, and zn never writes them into the vault.
The desktop app does not use them, and like every `[terminal]` setting they
do not survive the desktop app saving `config.toml` (see
[Configuration](#configuration)).

### Vaults and servers inside the app

`:vault` (or `Space v`) lists every saved vault and server, with entries to
Expand Down Expand Up @@ -789,14 +869,19 @@ creates it with comments when it is missing. On Windows, it normally lives
under `%APPDATA%\zennotes`. `XDG_CONFIG_HOME` and `ZENNOTES_CONFIG_DIR` can
override the portable location.

`:settings` is searchable and editable. Supported user preferences persist
to `config.toml`; periodic-note and task-exclusion controls update the vault.
`:settings` is searchable and editable, its rows grouped by where they are
saved: User rows persist to `config.toml`, and Vault rows (periodic notes,
Task folders, the template folder) to the vault's `.zennotes/vault.json`.
`:config` opens the file in an external editor, and `:reloadconfig` reloads
it immediately. External edits are otherwise picked up within a second.
`:bind` / `:unbind` persist, and `:keymaps` can edit bindings interactively.
Updates preserve unknown keys/tables and existing symlinks; TOML comments
and formatting are normalized when the file is written. Invalid supported
values produce an error while the running preferences remain active.
The desktop app does not keep the `[terminal]` table: it rewrites
`config.toml` at launch and whenever you change a setting there, and the
rewritten file leaves `[terminal]` out. If you use both apps, keep a copy of
your `[terminal]` settings to put back.

Periodic note paths and named date tokens follow each kind's configured
locale, including legacy patterns. The TUI keeps a horizontal tab strip
Expand All @@ -810,7 +895,7 @@ layout choices do not change these terminal layouts.
| `[view]` | `tasks_view_mode`, `kanban_group_by`, `kanban_statuses`, `note_sort_order` |
| `[appearance]` | `theme_family`, `theme_mode`, `theme_id` |
| `[tweaks]` | `accent = "#ff3b30"`: the desktop's Quick tweaks colors |
| `[terminal]` | `mouse`, `preview_style`, `images`, `theme`, `math`, `render_timeout`, `render_concurrency` (terminal only) |
| `[terminal]` | `mouse`, `preview_style`, `images`, `theme`, `math`, `render_timeout`, `render_concurrency`, `view_mode_field` and `[terminal.view_mode_rules]` (terminal only) |
| `[keymaps]` | `"action.id" = "keys"`, or `""` to unbind; `:keymaps` lists the ids |
| `[kanban_column_titles]` | `review = "In Review"` |
| `[saved_filters]` | `Important = "!high"` |
Expand Down
22 changes: 21 additions & 1 deletion internal/cli/commands_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ func cmdConfig(ctx context.Context, args Args) error {
return nil
}

// viewModeRulePrefix names a frontmatter view-mode rule under terminal; the
// rest of the name is the rule's "<key>=<value>" as written in config.toml.
const viewModeRulePrefix = "view_mode_rules."

func preferenceValues(p config.Prefs) map[string]map[string]any {
values := config.ValueTables(p)
for section, entries := range map[string]map[string]string{"keymaps": p.KeymapOverrides, "saved_filters": p.SavedTaskFilters, "kanban_column_titles": p.KanbanColumnTitles, "tweaks": p.ThemeTweaks} {
Expand All @@ -69,6 +73,9 @@ func preferenceValues(p config.Prefs) map[string]map[string]any {
values[section][key] = value
}
}
for _, e := range config.ViewModeRuleEntries(p.ViewModeRules) {
values["terminal"][viewModeRulePrefix+e.Key] = e.Mode
}
return values
}

Expand Down Expand Up @@ -109,7 +116,8 @@ func cmdConfigAction(ctx context.Context, args Args, action string) error {
}
value, exists := values[section][name]
if !exists {
if action != "set" || (section != "keymaps" && section != "saved_filters" && section != "kanban_column_titles" && section != "tweaks") {
isRule := section == "terminal" && strings.HasPrefix(name, viewModeRulePrefix)
if action != "set" || (!isRule && section != "keymaps" && section != "saved_filters" && section != "kanban_column_titles" && section != "tweaks") {
return fmt.Errorf("unknown preference %q; `zn config list` shows supported keys", key)
}
value = ""
Expand All @@ -119,6 +127,18 @@ func cmdConfigAction(ctx context.Context, args Args, action string) error {
return errors.New("usage: zn config set <section.key> <value>")
}
raw := args.Positional(1)
if name, isRule := strings.CutPrefix(name, viewModeRulePrefix); isRule && section == "terminal" {
if err := config.SetViewModeRule(name, raw); err != nil {
return fmt.Errorf("invalid value for %s: %w", key, err)
}
mode := strings.ToLower(strings.TrimSpace(raw))
if args.Bool("json") {
emitJSON(map[string]any{"ok": true, "key": key, "value": mode})
} else {
emitOK(fmt.Sprintf("%s = %v", key, mode))
}
return nil
}
switch value.(type) {
case bool:
value, err = strconv.ParseBool(raw)
Expand Down
29 changes: 29 additions & 0 deletions internal/cli/config_viewmode_case_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package cli

import (
"maps"
"os"
"testing"

"github.com/ZenNotes/tui/internal/config"
)

// `zn config set` on a rule that differs from an existing one only in case
// changes the existing rule instead of adding a duplicate.
func TestConfigSetViewModeRuleCaseVariantChangesExisting(t *testing.T) {
isolatedCLI(t)
path := config.ConfigTomlPath()
if err := os.WriteFile(path, []byte("[terminal.view_mode_rules]\n\"page-status=done\" = \"preview\"\n"), 0o644); err != nil {
t.Fatal(err)
}
if o, code := runConfig(t, "set", "terminal.view_mode_rules.Page-Status=done", "split"); code != 0 {
t.Fatalf("set: exit %d: %s", code, o)
}
prefs, _, err := config.LoadPrefs()
if err != nil {
t.Fatal(err)
}
if want := map[string]string{"page-status=done": "split"}; !maps.Equal(prefs.ViewModeRules, want) {
t.Fatalf("rules %v, want %v", prefs.ViewModeRules, want)
}
}
110 changes: 110 additions & 0 deletions internal/cli/config_viewmode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package cli

import (
"os"
"strings"
"testing"

"github.com/ZenNotes/tui/internal/config"
)

func runConfig(t *testing.T, args ...string) (string, int) {
t.Helper()
var code int
out := captureOutput(t, func() { code = Main(append([]string{"config"}, args...)) })
return out, code
}

func TestConfigListShowsViewModeRules(t *testing.T) {
isolatedCLI(t)
if _, code := runConfig(t, "set", "terminal.view_mode_rules.page-status=done", "preview"); code != 0 {
t.Fatalf("set exit %d", code)
}
out, code := runConfig(t, "list")
if code != 0 || !strings.Contains(out, "terminal.view_mode_rules.page-status=done = preview\n") {
t.Fatalf("list (exit %d): %s", code, out)
}
out, _ = runConfig(t, "list", "--json")
if !strings.Contains(out, `"view_mode_rules.page-status=done": "preview"`) {
t.Fatalf("list --json: %s", out)
}
}

func TestConfigGetViewModeRule(t *testing.T) {
isolatedCLI(t)
runConfig(t, "set", "terminal.view_mode_rules.kind=journal", "split")
out, code := runConfig(t, "get", "terminal.view_mode_rules.kind=journal")
if code != 0 || strings.TrimSpace(out) != "split" {
t.Fatalf("get (exit %d): %q", code, out)
}
if _, code := runConfig(t, "get", "terminal.view_mode_rules.kind=other"); code == 0 {
t.Fatal("get of an unset rule succeeded")
}
}

func TestConfigSetViewModeRuleAddsAndChanges(t *testing.T) {
isolatedCLI(t)
path := config.ConfigTomlPath()
if err := os.WriteFile(path, []byte("[terminal]\nmath = \"latex\"\n\n[editor]\nword_wrap = false\n\n[custom]\nkeep = \"me\"\n"), 0o644); err != nil {
t.Fatal(err)
}
for _, mode := range []string{"preview", "Edit"} {
if o, code := runConfig(t, "set", "terminal.view_mode_rules.page-status=done", mode); code != 0 {
t.Fatalf("set %s: exit %d: %s", mode, code, o)
}
}
body, _ := os.ReadFile(path)
for _, want := range []string{`math = "latex"`, "[editor]", "word_wrap = false", "[custom]", `keep = "me"`, `"page-status=done" = "edit"`} {
if !strings.Contains(string(body), want) {
t.Fatalf("missing %q in:\n%s", want, body)
}
}
if strings.Contains(string(body), "preview") {
t.Fatalf("old mode left behind:\n%s", body)
}
prefs, _, err := config.LoadPrefs()
if err != nil || len(prefs.ViewModeRules) != 1 {
t.Fatalf("reload: %+v %v", prefs.ViewModeRules, err)
}
}

func TestConfigSetViewModeRuleRejectsBadMode(t *testing.T) {
isolatedCLI(t)
path := config.ConfigTomlPath()
original := "[terminal]\nmath = \"latex\"\n"
if err := os.WriteFile(path, []byte(original), 0o644); err != nil {
t.Fatal(err)
}
for _, args := range [][]string{
{"set", "terminal.view_mode_rules.page-status=done", "reading"},
{"set", "terminal.view_mode_rules.page-status", "preview"},
{"set", "terminal.view_mode_rules.=done", "preview"},
{"set", "terminal.view_mode_rules.status=", "preview"},
} {
if _, code := runConfig(t, args...); code == 0 {
t.Fatalf("%v succeeded", args)
}
}
body, _ := os.ReadFile(path)
if string(body) != original {
t.Fatalf("config changed:\n%s", body)
}
}

func TestConfigSetViewModeField(t *testing.T) {
isolatedCLI(t)
if _, code := runConfig(t, "set", "terminal.view_mode_field", "view-mode"); code != 0 {
t.Fatalf("exit %d", code)
}
prefs, _, err := config.LoadPrefs()
if err != nil || prefs.TerminalViewModeField != "view-mode" {
t.Fatalf("field: %q %v", prefs.TerminalViewModeField, err)
}
if _, code := runConfig(t, "set", "terminal.view_mode_field", ""); code != 0 {
t.Fatalf("clear exit %d", code)
}
prefs, _, err = config.LoadPrefs()
if err != nil || prefs.TerminalViewModeField != "" {
t.Fatalf("cleared field: %q %v", prefs.TerminalViewModeField, err)
}
}
21 changes: 21 additions & 0 deletions internal/config/prefs.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package config

import (
"fmt"
"os"
"strings"

Expand Down Expand Up @@ -80,6 +81,12 @@ type Prefs struct {
// TerminalMath is typst or latex: the engine for display math. Anything
// else loads as typst; zn doctor reports it.
TerminalMath string
// TerminalViewModeField names the frontmatter field that picks a note's
// pane mode when it opens in a new tab. Empty turns the feature off.
TerminalViewModeField string
// ViewModeRules maps "<key>=<value>" to a pane mode, from
// [terminal.view_mode_rules]. Entries are raw; see ViewModeRuleEntries.
ViewModeRules map[string]string
// RenderTimeout (seconds) bounds one external render; RenderConcurrency
// caps how many run at once. Non-positive values load as the defaults.
RenderTimeout int
Expand Down Expand Up @@ -115,6 +122,7 @@ func DefaultPrefs() Prefs {
PreviewStyle: "auto",
TerminalMouse: true,
TerminalMath: "typst",
ViewModeRules: map[string]string{},
RenderTimeout: 10,
RenderConcurrency: 2,
UnifiedSidebar: true,
Expand Down Expand Up @@ -236,6 +244,19 @@ func applyPrefs(p *Prefs, doc map[string]any) {
setString(terminal, "images", &p.TerminalImages)
setString(terminal, "theme", &p.TerminalTheme)
setString(terminal, "math", &p.TerminalMath)
setString(terminal, "view_mode_field", &p.TerminalViewModeField)
if rules, ok := terminal["view_mode_rules"].(map[string]any); ok {
p.ViewModeRules = map[string]string{}
for k, v := range rules {
// A mode that is not a string is kept as text, so the rule is
// listed as invalid and can be removed instead of vanishing.
s, isString := v.(string)
if !isString {
s = fmt.Sprint(v)
}
p.ViewModeRules[k] = s
}
}
setInt(terminal, "render_timeout", &p.RenderTimeout)
setInt(terminal, "render_concurrency", &p.RenderConcurrency)
if strings.TrimSpace(p.PreviewStyle) == "" {
Expand Down
2 changes: 1 addition & 1 deletion internal/config/prefs_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func ValidateConfig() error {
// lenientKeys are checked only when strict (zn doctor): LoadPrefs falls back
// to their defaults, so a bad value never stops zn from starting.
var lenientKeys = map[string]map[string]bool{
"terminal": {"math": true, "render_timeout": true, "render_concurrency": true},
"terminal": {"math": true, "view_mode_field": true, "render_timeout": true, "render_concurrency": true},
}

func validateKnownValues(doc map[string]any, strict bool) error {
Expand Down
Loading
Loading