Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b27d95c
Add helix bindings
enkodr Oct 20, 2026
1d6032a
Add helix alongside vim bindings
enkodr Oct 20, 2026
9a2aaac
Cleaup configuration motions
enkodr Oct 20, 2026
31c90e5
Fix(keys): gn and gp only switch tabs with helix motions
enkodr Oct 20, 2026
a19271d
Docs(readme): consolidate the engine configuration reference
enkodr Oct 20, 2026
dbb2428
Docs(config): keep the [vim] section as the canonical engine config
enkodr Oct 20, 2026
fd4252a
Fix(helix): typing in insert mode must not grow a selection
enkodr Oct 20, 2026
bdf6e64
Fix some selection issues with helix bindings
enkodr Oct 21, 2026
7ca1429
Fix(tui): preview keeps multi-word wikilinks from scrolling the whole…
Oct 18, 2026
fea6804
Chore(homebrew): point the zn formula at v0.9.1
adibhanna Oct 20, 2026
9e0b3dc
Fix(vim): a second $ on a wrapped line reaches the end of the line
adibhanna Oct 22, 2026
18d3ba9
Fix(editor): typing ``` makes a code fence instead of four backticks
adibhanna Oct 22, 2026
b23584f
Chore(homebrew): point the zn formula at v0.9.2
adibhanna Oct 22, 2026
6f7b2a4
Feat(base): zn base pages gives every record its record page
adibhanna Oct 22, 2026
a98fe3b
Fix(base): a record's page file already in the folder gets linked, no…
adibhanna Oct 22, 2026
448dce5
Cleanup
enkodr Oct 23, 2026
055a77b
Fix(tui): import the vim engine in authoring_test
enkodr Oct 23, 2026
88fdf39
Chore(helix): mention the vim engine on the shared type comments
enkodr Oct 23, 2026
fa1f021
Merge branch 'main' into helix
enkodr Oct 27, 2026
54b8e42
Fix compile errors
enkodr Oct 27, 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
53 changes: 46 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ chrome.
### Moving around

`Space` is the leader. The which-key panel appears immediately. Timed hints
expire after `vim.which_key_hint_timeout_ms`; `vim.which_key_hint_mode = "sticky"`
expire after `helix.which_key_hint_timeout_ms`; `helix.which_key_hint_mode = "sticky"`
keeps them open until you choose an action, press Space again, or cancel.

| Keys | What |
Expand Down Expand Up @@ -529,9 +529,30 @@ and preview, `Alt+.` for zen) work where the terminal sends Option as Alt

### Editing

The editor is a full Vim: counts, registers, marks, macros, dot-repeat,
undo and redo, text objects, visual, visual line and visual block, search,
`:s` `:g` `:sort` `:norm` with ranges, `gq`, and heading motions `]]` `[[`.
The editor ships two motion engines: Vim (default) and Helix. Pick one with
`[vim] motions = "vim" | "helix"` in `config.toml` (or Settings). The Vim
engine keeps classic operator-pending editing — `dw` `cw` `yiw`, visual,
visual line and visual block, and the rest of the Vim toolkit. The Helix
engine is selection-first — motions like `w` select as they travel, so `wd`
deletes a word and `n` selects the next match; `miw` picks a word precisely.
Tab switching works in either engine: `gt` `gT` and `]b` `[b` everywhere,
plus `gn` `gp` with the Helix engine. The leader, registers and macros work
in both.

The Helix engine also grows and shrinks a selection by context: `Alt+O`
(`Alt+Up`) expands it — word, paragraph, function in a code block, code
block, heading section, note — and `Alt+I` (`Alt+Down`) shrinks it back, the
way Helix walks the syntax tree. Its word motions follow one rule: the
selection covers a whole word plus the space that follows it, and never the
first character of the word the motion lands on. `w` moves over the word the
cursor sits on, `b` lands on the previous word and selects it, so `wd` and
`bd` both delete one word. The block cursor stays inside the selection. `w`
runs to the end of the line when the last word of a line is reached, and the
next `w` moves on to the next line.

The editor also has counts, registers, marks, macros, dot-repeat, undo and
redo, text objects, search, `:s` `:g` `:sort` `:norm` with ranges, `gq`, and
heading motions `]]` `[[`.
Markdown on top: `Ctrl+L` toggles the checkbox on the line, `gd` follows a
link, `gx` opens a URL, `gy` copies a link, `zc zo zM zR` fold headings,
Enter continues lists, Tab indents list items, `[[` opens the note picker
Expand All @@ -544,10 +565,28 @@ linewise and blockwise selections, and delete history work across notes too.
Closing the source note does not discard its yank. Each note keeps its own
cursor and undo history; system-clipboard integration remains a separate preference.

Vim mode follows `[vim] enabled` in `config.toml`. With it off, the editor
Modal editing 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.

All editor engine settings live under `[vim]`:

```toml
[vim]
enabled = true # modal editing; false = plain editor
motions = "vim" # or "helix": the selection-first engine
insert_escape = "jk" # two-key escape from insert mode
yank_to_clipboard = false # yanks mirror the system clipboard
which_key_hints = true # leader hints
which_key_hint_mode = "timed" # timed, sticky or instant
which_key_hint_timeout_ms = 2500
wrapped_line_motions = "display" # $, I, A follow the wrapped row
```

`motions` picks the engine for new buffers; individual keys are overridden
under `[keymaps]` and listed by `:keymaps`. A `[helix]` section from earlier
helix-branch builds is still honored when `[vim]` is absent.

`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 @@ -714,8 +753,8 @@ layout choices do not change these terminal layouts.

| Section | Examples |
| --- | --- |
| `[vim]` | `enabled`, `which_key_hints`, `insert_escape` |
| `[editor]` | `line_number_mode`, `word_wrap`, `completed_task_style` |
| `[vim]` | `enabled`, `motions` (`vim` or `helix`), `insert_escape`, `yank_to_clipboard`, `which_key_hints`, `which_key_hint_mode`, `wrapped_line_motions` (a `[helix]` section is still honored when `[vim]` is absent) |
| `[editor]` | `motions` (`vim` or `helix`), `line_number_mode`, `word_wrap`, `completed_task_style` |
| `[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 |
Expand Down
37 changes: 22 additions & 15 deletions internal/config/prefs.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import (
// honor. Defaults match the desktop's PORTABLE_DEFAULTS; a missing file or
// key keeps the default.
type Prefs struct {
VimWrappedLineMotions string
IgnoredKeys []string
VimMode bool
VimInsertEscape string
VimYankToClipboard bool
HelixWrappedLineMotions string
IgnoredKeys []string
HelixMode bool
Motions string
HelixInsertEscape string
HelixYankToClipboard bool
WhichKeyHints bool
WhichKeyHintMode string
WhichKeyHintTimeoutMs int
Expand Down Expand Up @@ -82,8 +83,9 @@ type Prefs struct {
// DefaultPrefs are the shipped defaults.
func DefaultPrefs() Prefs {
return Prefs{
VimMode: true,
VimWrappedLineMotions: "display",
HelixMode: true,
Motions: "vim",
HelixWrappedLineMotions: "display",
TabsEnabled: true,
SavedTaskFilters: map[string]string{},
WhichKeyHints: true,
Expand Down Expand Up @@ -153,14 +155,18 @@ func applyPrefs(p *Prefs, doc map[string]any) {
m, _ := doc[name].(map[string]any)
return m
}
vim := section("vim")
setBool(vim, "enabled", &p.VimMode)
setString(vim, "wrapped_line_motions", &p.VimWrappedLineMotions)
setString(vim, "insert_escape", &p.VimInsertEscape)
setBool(vim, "yank_to_clipboard", &p.VimYankToClipboard)
setBool(vim, "which_key_hints", &p.WhichKeyHints)
setString(vim, "which_key_hint_mode", &p.WhichKeyHintMode)
setInt(vim, "which_key_hint_timeout_ms", &p.WhichKeyHintTimeoutMs)
modal := section("vim")
if modal == nil {
modal = section("helix")
}
setBool(modal, "enabled", &p.HelixMode)
setString(modal, "motions", &p.Motions)
setString(modal, "wrapped_line_motions", &p.HelixWrappedLineMotions)
setString(modal, "insert_escape", &p.HelixInsertEscape)
setBool(modal, "yank_to_clipboard", &p.HelixYankToClipboard)
setBool(modal, "which_key_hints", &p.WhichKeyHints)
setString(modal, "which_key_hint_mode", &p.WhichKeyHintMode)
setInt(modal, "which_key_hint_timeout_ms", &p.WhichKeyHintTimeoutMs)

editor := section("editor")
setBool(editor, "sync_title_heading_on_rename", &p.SyncTitleHeadingOnRename)
Expand Down Expand Up @@ -293,6 +299,7 @@ const DefaultConfigTOML = `# ZenNotes portable preferences. Shared by the deskto

[vim]
enabled = true
motions = "vim" # or "helix": the selection-first engine
insert_escape = "jk"
yank_to_clipboard = true
which_key_hints = true
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 @@ -57,7 +57,7 @@ func validateKnownValues(doc map[string]any) error {
}
}
enums := map[string]map[string][]string{
"vim": {"which_key_hint_mode": {"timed", "sticky", "instant"}, "wrapped_line_motions": {"display", "logical"}},
"vim": {"motions": {"helix", "vim"}, "which_key_hint_mode": {"timed", "sticky", "instant"}, "wrapped_line_motions": {"display", "logical"}},
"editor": {"default_view_mode": {"edit", "split", "preview"}, "line_number_mode": {"off", "absolute", "relative", "hybrid"}, "completed_task_style": {"none", "gray", "strikethrough", "gray-strikethrough"}, "time_format": {"12h", "24h"}},
"view": {"tasks_view_mode": {"list", "kanban", "board", "calendar"}, "calendar_week_start": {"monday", "sunday"}, "note_sort_order": {"none", "name-asc", "name-desc", "updated-desc", "updated-asc", "created-desc", "created-asc"}},
"appearance": {"theme_mode": {"dark", "light", "system", "auto"}},
Expand Down
2 changes: 1 addition & 1 deletion internal/config/prefs_values.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
// their file names in one place also drives the in-app settings controls.
func ValueTables(p Prefs) map[string]map[string]any {
return map[string]map[string]any{
"vim": {"enabled": p.VimMode, "insert_escape": p.VimInsertEscape, "yank_to_clipboard": p.VimYankToClipboard, "which_key_hints": p.WhichKeyHints, "which_key_hint_mode": p.WhichKeyHintMode, "which_key_hint_timeout_ms": p.WhichKeyHintTimeoutMs, "wrapped_line_motions": p.VimWrappedLineMotions},
"vim": {"motions": p.Motions, "enabled": p.HelixMode, "insert_escape": p.HelixInsertEscape, "yank_to_clipboard": p.HelixYankToClipboard, "which_key_hints": p.WhichKeyHints, "which_key_hint_mode": p.WhichKeyHintMode, "which_key_hint_timeout_ms": p.WhichKeyHintTimeoutMs, "wrapped_line_motions": p.HelixWrappedLineMotions},
"editor": {"tab_size": p.EditorTabSize, "scroll_off": p.EditorScrollOff, "word_wrap": p.WordWrap, "default_view_mode": p.DefaultPaneMode, "line_number_mode": p.LineNumberMode, "completed_task_style": p.CompletedTaskStyle, "time_format": p.TimeFormat, "auto_pairs": p.AutoPairs, "auto_pair_quotes_in_prose": p.AutoPairQuotesInProse, "tabs_enabled": p.TabsEnabled, "wrap_tabs": p.WrapTabs, "keep_view_mode_across_notes": p.RetainViewMode, "markdown_snippets": p.MarkdownSnippets, "hide_builtin_templates": p.HideBuiltinTemplates, "text_replacements_enabled": p.TextReplacementsEnabled, "sync_title_heading_on_rename": p.SyncTitleHeadingOnRename},
"appearance": {"theme_family": p.ThemeFamily, "theme_mode": p.ThemeMode, "theme_id": p.ThemeID, "unified_sidebar": p.UnifiedSidebar},
"view": {"note_sort_order": p.NoteSortOrder, "group_by_kind": p.GroupByKind, "nested_tags": p.NestedTags, "quick_note_date_title": p.QuickNoteDateTitle, "quick_note_title_prefix": p.QuickNoteTitlePrefix, "calendar_week_start": p.CalendarWeekStart, "calendar_show_week_numbers": p.CalendarShowWeekNumbers, "tasks_view_mode": p.TasksViewMode, "show_archived_tasks": p.ShowArchivedTasks, "kanban_group_by": p.KanbanGroupBy, "kanban_folder_root": p.KanbanFolderRoot},
Expand Down
Loading