Config restructure: flatten and modernize the option surface #33

Closed
opened 2026-09-21 18:54:21 +00:00 by barrettruth · 0 comments
Owner

Original issue: barrettruth/canola.nvim#183
Original author: barrettruth
Original date: 2026-03-18T20:31:12Z

Problem

The config inherited from oil.nvim is deeply nested, has mixed concerns, function-valued fields that prevent declarative `vim.g.canola` configuration, and redundant/confusing boolean pairs. v1.0.0 is a clean break — every setting gets reconsidered.

Finalized Spec

Type Definitions

```lua
---@alias canola.SortPreset
---|'"default"' -- { { 'type', 'asc' }, { 'name', 'asc' } }
---|'"name"' -- { { 'name', 'asc' } }
---|'"modified"' -- { { 'mtime', 'desc' }, { 'name', 'asc' } }
---|'"size"' -- { { 'size', 'desc' }, { 'name', 'asc' } }
---|'"extension"' -- { { 'name', 'asc' } } grouped by ext

---@alias canola.ConfirmMode
---| true -- confirm all mutations
---| '"delete"' -- only confirm deletes
---| false -- confirm nothing

---@alias canola.SaveMode
---| '"prompt"' -- prompt before writing mutations on select
---| '"auto"' -- write silently
---| false -- don't write

---@alias canola.WindowDimension number|{ [1]: number, [2]: number }

---@class (exact) canola.SortSpec
---@field [1] string
---@field [2] "asc"|"desc"

---@class (exact) canola.SortConfig
---@field by canola.SortPreset|canola.SortSpec[]
---@field natural? boolean --- default: true
---@field ignore_case? boolean --- default: false

---@class (exact) canola.HiddenConfig
---@field patterns string[] --- Lua patterns matched against filename
---@field always string[] --- never shown, even with show_hidden

---@class (exact) canola.DeleteConfig
---@field wipe_buffers boolean

---@class (exact) canola.CreateConfig
---@field file_mode integer --- decimal (420 = 0644)
---@field dir_mode integer --- decimal (493 = 0755)

---@class (exact) canola.LspConfig
---@field enabled boolean
---@field timeout_ms integer
---@field autosave boolean|"unmodified"

---@class (exact) canola.FloatConfig
---@field default boolean
---@field padding integer
---@field max_width integer --- 0 = auto
---@field max_height integer --- 0 = auto
---@field border? string|string[] --- overrides global border
---@field preview_split "auto"|"left"|"right"|"above"|"below"
---@field win_options table<string, any>

---@class (exact) canola.PreviewConfig
---@field follow boolean --- update on cursor move
---@field live boolean --- false=scratch, true=full buffer load
---@field max_file_size_mb number
---@field disable string[] --- Lua patterns to skip preview
---@field win_options table<string, any>

---@class (exact) canola.ConfirmationConfig
---@field max_width canola.WindowDimension
---@field min_width canola.WindowDimension
---@field width? number
---@field max_height canola.WindowDimension
---@field min_height canola.WindowDimension
---@field height? number
---@field border? string|string[] --- overrides global border
---@field win_options table<string, any>

---@class (exact) canola.Config
---@field columns canola.ColumnSpec[]
---@field cursor boolean
---@field watch boolean
---@field border? string|string[]
---@field show_hidden boolean
---@field hidden canola.HiddenConfig
---@field sort canola.SortPreset|canola.SortConfig
---@field highlights { [1]: string, [2]: string }[]
---@field confirm canola.ConfirmMode
---@field save canola.SaveMode
---@field delete canola.DeleteConfig
---@field create canola.CreateConfig
---@field keymaps table<string, string|table|false>
---@field lsp canola.LspConfig
---@field float canola.FloatConfig
---@field preview canola.PreviewConfig
---@field confirmation canola.ConfirmationConfig
---@field buf_options table<string, any>
---@field win_options table<string, any>
```

Defaults

```lua
local default_config = {
columns = { 'icon' },
cursor = true,
watch = false,
border = nil,

show_hidden = false,
hidden = {
patterns = { '^%.' },
always = {},
},

sort = 'default',

highlights = {},

confirm = true,
save = 'prompt',

delete = {
wipe_buffers = false,
},

create = {
file_mode = 420,
dir_mode = 493,
},

keymaps = {
['g?'] = { 'actions.show_help', mode = 'n' },
[''] = 'actions.select',
[''] = { 'actions.select', opts = { vertical = true } },
[''] = { 'actions.select', opts = { horizontal = true } },
[''] = { 'actions.select', opts = { tab = true } },
[''] = 'actions.preview',
[''] = { 'actions.close', mode = 'n' },
[''] = 'actions.refresh',
['-'] = { 'actions.parent', mode = 'n' },
['_'] = { 'actions.open_cwd', mode = 'n' },
['`'] = { 'actions.cd', mode = 'n' },
['g~'] = { 'actions.cd', opts = { scope = 'tab' }, mode = 'n' },
['gs'] = { 'actions.change_sort', mode = 'n' },
['gx'] = 'actions.open_external',
['g.'] = { 'actions.toggle_hidden', mode = 'n' },
['g\'] = { 'actions.toggle_trash', mode = 'n' },
},

lsp = {
enabled = true,
timeout_ms = 1000,
autosave = false,
},

float = {
default = false,
padding = 2,
max_width = 0,
max_height = 0,
border = nil,
preview_split = 'auto',
win_options = { winblend = 0 },
},

preview = {
follow = true,
live = false,
max_file_size_mb = 10,
disable = {},
win_options = {},
},

confirmation = {
max_width = 0.9,
min_width = { 40, 0.4 },
width = nil,
max_height = 0.9,
min_height = { 5, 0.1 },
height = nil,
border = nil,
win_options = { winblend = 0 },
},

buf_options = {
buflisted = false,
bufhidden = 'hide',
},

win_options = {
wrap = false,
signcolumn = 'no',
cursorcolumn = false,
foldcolumn = '0',
spell = false,
list = false,
conceallevel = 3,
concealcursor = 'nvic',
},
}
```

Sort Presets

Preset Expands to natural ignore_case
`'default'` `{ { 'type', 'asc' }, { 'name', 'asc' } }` `true` `false`
`'name'` `{ { 'name', 'asc' } }` `true` `false`
`'modified'` `{ { 'mtime', 'desc' }, { 'name', 'asc' } }` `true` `false`
`'size'` `{ { 'size', 'desc' }, { 'name', 'asc' } }` `true` `false`
`'extension'` `{ { 'name', 'asc' } }` grouped by ext `true` `false`

Sort accepts a preset string or full config table:
```lua
sort = 'modified'
sort = { by = 'modified', natural = true, ignore_case = false }
sort = { by = { { 'type', 'asc' }, { 'name', 'asc' } }, natural = true }
```

Highlights

Pattern-based filename highlighting. Array of `{ lua_pattern, highlight_group }` pairs, first match wins. Built-in highlights (`CanolaHidden`, `CanolaExecutable`, `CanolaOrphanLink`) apply as base layer; user patterns override.

```lua
highlights = {
{ '%.lua$', 'CanolaLua' },
{ '%.md$', 'CanolaMarkdown' },
{ '^Makefile$', 'CanolaMakefile' },
}
```

Migration Map

Old (oil.nvim / canola v0) New (canola v1) Change
`default_file_explorer` — removed, always true
`default_to_float` `float.default` moved
`columns` `columns` unchanged
`delete_to_trash` — → `vim.g.canola_trash`
`cleanup_buffers_on_delete` `delete.wipe_buffers` renamed + grouped
`skip_confirm_for_simple_edits` `confirm` consolidated into enum
`skip_confirm_for_delete` `confirm` consolidated into enum
`prompt_save_on_select_new_entry` `save` consolidated into enum
`auto_save_on_select_new_entry` `save` consolidated into enum
`cleanup_delay_ms` — removed, smart internal logic
`constrain_cursor` `cursor` renamed, collapsed to bool
`watch_for_changes` `watch` renamed
`use_default_keymaps` — removed, set `keymaps = {}`
`keymaps` `keymaps` unchanged (functions disallowed)
`new_file_mode` `create.file_mode` renamed + grouped
`new_dir_mode` `create.dir_mode` renamed + grouped
`extra_scp_args` — → `vim.g.canola_ssh`
`ssh_hosts` — → `vim.g.canola_ssh`
`extra_s3_args` — → `vim.g.canola_s3`
`s3_buckets` — → `vim.g.canola_s3`
`extra_curl_args` — → `vim.g.canola_ftp`
`ftp_hosts` — → `vim.g.canola_ftp`
`silence_scp_warning` — → `vim.g.canola_ssh`
`buf_options` `buf_options` unchanged
`win_options` `win_options` unchanged
`view_options.show_hidden` `show_hidden` flattened
`view_options.show_hidden_when_empty` — removed
`view_options.is_hidden_file` `hidden.patterns` fn → pattern list
`view_options.is_always_hidden` `hidden.always` fn → pattern list
`view_options.natural_order` `sort.natural` absorbed into sort
`view_options.case_insensitive` `sort.ignore_case` absorbed into sort
`view_options.sort` `sort` / `sort.by` presets added
`view_options.highlight_filename` `highlights` fn → pattern pairs
`lsp_file_methods.enabled` `lsp.enabled` renamed parent
`lsp_file_methods.timeout_ms` `lsp.timeout_ms` renamed parent
`lsp_file_methods.autosave_changes` `lsp.autosave` renamed
`git.add` — removed → autocmd (#182)
`git.mv` — removed → autocmd (#182)
`git.rm` — removed → autocmd (#182)
`float.padding` `float.padding` unchanged
`float.max_width` `float.max_width` unchanged
`float.max_height` `float.max_height` unchanged
`float.border` `float.border` unchanged
`float.preview_split` `float.preview_split` unchanged
`float.get_win_title` — removed
`float.override` — removed
`float.win_options` `float.win_options` unchanged
`preview_win.update_on_cursor_moved` `preview.follow` renamed
`preview_win.preview_method` `preview.live` 3-enum → bool
`preview_win.disable_preview` `preview.disable` fn → pattern list
`preview_win.max_file_size` `preview.max_file_size_mb` explicit unit
`preview_win.win_options` `preview.win_options` renamed parent
`confirmation.*` `confirmation.*` unchanged
`progress.*` — tabled for separate discussion
`ssh.border` — → `vim.g.canola_ssh`
`keymaps_help.border` — removed, inherits `border`
`adapters` — removed, internal
`adapter_aliases` — removed, internal

Adapter Config

Each canola-collection adapter owns its config in a separate `vim.g` variable:

  • `vim.g.canola_trash` — trash adapter (replaces `delete_to_trash`)
  • `vim.g.canola_ssh` — SSH adapter (replaces `extra_scp_args`, `ssh_hosts`, `silence_scp_warning`, `ssh.border`)
  • `vim.g.canola_s3` — S3 adapter (replaces `extra_s3_args`, `s3_buckets`)
  • `vim.g.canola_ftp` — FTP adapter (replaces `extra_curl_args`, `ftp_hosts`)

Design Principles

  1. Flat over deep — settings are top-level unless they have natural siblings
  2. Separate theming from behavior — `highlights` is its own concern
  3. No functions in config — everything serializes into `vim.g.canola`
  4. No setter APIs — declarative patterns replace callbacks
  5. Global `border` — inherited by all windows, overridable per-window
  6. Enums over boolean pairs — `confirm`, `save`, `sort` replace confusing combinations
  7. Adapter config is separate — each adapter owns `vim.g.canola_*`

Relationship to Other Issues

  • #82 — introduced `vim.g.canola` access mechanism (done)
  • #181 — canola-collection adapter extraction (removes adapter config from core)
  • #182 — enhanced User autocmds (replaces git hooks)
> Original issue: barrettruth/canola.nvim#183 > Original author: `barrettruth` > Original date: 2026-03-18T20:31:12Z ## Problem The config inherited from oil.nvim is deeply nested, has mixed concerns, function-valued fields that prevent declarative \`vim.g.canola\` configuration, and redundant/confusing boolean pairs. v1.0.0 is a clean break — every setting gets reconsidered. ## Finalized Spec ### Type Definitions \`\`\`lua ---@alias canola.SortPreset ---|'"default"' -- { { 'type', 'asc' }, { 'name', 'asc' } } ---|'"name"' -- { { 'name', 'asc' } } ---|'"modified"' -- { { 'mtime', 'desc' }, { 'name', 'asc' } } ---|'"size"' -- { { 'size', 'desc' }, { 'name', 'asc' } } ---|'"extension"' -- { { 'name', 'asc' } } grouped by ext ---@alias canola.ConfirmMode ---| true -- confirm all mutations ---| '"delete"' -- only confirm deletes ---| false -- confirm nothing ---@alias canola.SaveMode ---| '"prompt"' -- prompt before writing mutations on select ---| '"auto"' -- write silently ---| false -- don't write ---@alias canola.WindowDimension number|{ [1]: number, [2]: number } ---@class (exact) canola.SortSpec ---@field [1] string ---@field [2] "asc"|"desc" ---@class (exact) canola.SortConfig ---@field by canola.SortPreset|canola.SortSpec[] ---@field natural? boolean --- default: true ---@field ignore_case? boolean --- default: false ---@class (exact) canola.HiddenConfig ---@field patterns string[] --- Lua patterns matched against filename ---@field always string[] --- never shown, even with show_hidden ---@class (exact) canola.DeleteConfig ---@field wipe_buffers boolean ---@class (exact) canola.CreateConfig ---@field file_mode integer --- decimal (420 = 0644) ---@field dir_mode integer --- decimal (493 = 0755) ---@class (exact) canola.LspConfig ---@field enabled boolean ---@field timeout_ms integer ---@field autosave boolean|"unmodified" ---@class (exact) canola.FloatConfig ---@field default boolean ---@field padding integer ---@field max_width integer --- 0 = auto ---@field max_height integer --- 0 = auto ---@field border? string|string[] --- overrides global border ---@field preview_split "auto"|"left"|"right"|"above"|"below" ---@field win_options table<string, any> ---@class (exact) canola.PreviewConfig ---@field follow boolean --- update on cursor move ---@field live boolean --- false=scratch, true=full buffer load ---@field max_file_size_mb number ---@field disable string[] --- Lua patterns to skip preview ---@field win_options table<string, any> ---@class (exact) canola.ConfirmationConfig ---@field max_width canola.WindowDimension ---@field min_width canola.WindowDimension ---@field width? number ---@field max_height canola.WindowDimension ---@field min_height canola.WindowDimension ---@field height? number ---@field border? string|string[] --- overrides global border ---@field win_options table<string, any> ---@class (exact) canola.Config ---@field columns canola.ColumnSpec[] ---@field cursor boolean ---@field watch boolean ---@field border? string|string[] ---@field show_hidden boolean ---@field hidden canola.HiddenConfig ---@field sort canola.SortPreset|canola.SortConfig ---@field highlights { [1]: string, [2]: string }[] ---@field confirm canola.ConfirmMode ---@field save canola.SaveMode ---@field delete canola.DeleteConfig ---@field create canola.CreateConfig ---@field keymaps table<string, string|table|false> ---@field lsp canola.LspConfig ---@field float canola.FloatConfig ---@field preview canola.PreviewConfig ---@field confirmation canola.ConfirmationConfig ---@field buf_options table<string, any> ---@field win_options table<string, any> \`\`\` ### Defaults \`\`\`lua local default_config = { columns = { 'icon' }, cursor = true, watch = false, border = nil, show_hidden = false, hidden = { patterns = { '^%.' }, always = {}, }, sort = 'default', highlights = {}, confirm = true, save = 'prompt', delete = { wipe_buffers = false, }, create = { file_mode = 420, dir_mode = 493, }, keymaps = { ['g?'] = { 'actions.show_help', mode = 'n' }, ['<CR>'] = 'actions.select', ['<C-s>'] = { 'actions.select', opts = { vertical = true } }, ['<C-h>'] = { 'actions.select', opts = { horizontal = true } }, ['<C-t>'] = { 'actions.select', opts = { tab = true } }, ['<C-p>'] = 'actions.preview', ['<C-c>'] = { 'actions.close', mode = 'n' }, ['<C-l>'] = 'actions.refresh', ['-'] = { 'actions.parent', mode = 'n' }, ['_'] = { 'actions.open_cwd', mode = 'n' }, ['`'] = { 'actions.cd', mode = 'n' }, ['g~'] = { 'actions.cd', opts = { scope = 'tab' }, mode = 'n' }, ['gs'] = { 'actions.change_sort', mode = 'n' }, ['gx'] = 'actions.open_external', ['g.'] = { 'actions.toggle_hidden', mode = 'n' }, ['g\\'] = { 'actions.toggle_trash', mode = 'n' }, }, lsp = { enabled = true, timeout_ms = 1000, autosave = false, }, float = { default = false, padding = 2, max_width = 0, max_height = 0, border = nil, preview_split = 'auto', win_options = { winblend = 0 }, }, preview = { follow = true, live = false, max_file_size_mb = 10, disable = {}, win_options = {}, }, confirmation = { max_width = 0.9, min_width = { 40, 0.4 }, width = nil, max_height = 0.9, min_height = { 5, 0.1 }, height = nil, border = nil, win_options = { winblend = 0 }, }, buf_options = { buflisted = false, bufhidden = 'hide', }, win_options = { wrap = false, signcolumn = 'no', cursorcolumn = false, foldcolumn = '0', spell = false, list = false, conceallevel = 3, concealcursor = 'nvic', }, } \`\`\` ### Sort Presets | Preset | Expands to | natural | ignore_case | |---|---|---|---| | \`'default'\` | \`{ { 'type', 'asc' }, { 'name', 'asc' } }\` | \`true\` | \`false\` | | \`'name'\` | \`{ { 'name', 'asc' } }\` | \`true\` | \`false\` | | \`'modified'\` | \`{ { 'mtime', 'desc' }, { 'name', 'asc' } }\` | \`true\` | \`false\` | | \`'size'\` | \`{ { 'size', 'desc' }, { 'name', 'asc' } }\` | \`true\` | \`false\` | | \`'extension'\` | \`{ { 'name', 'asc' } }\` grouped by ext | \`true\` | \`false\` | Sort accepts a preset string or full config table: \`\`\`lua sort = 'modified' sort = { by = 'modified', natural = true, ignore_case = false } sort = { by = { { 'type', 'asc' }, { 'name', 'asc' } }, natural = true } \`\`\` ### Highlights Pattern-based filename highlighting. Array of \`{ lua_pattern, highlight_group }\` pairs, first match wins. Built-in highlights (\`CanolaHidden\`, \`CanolaExecutable\`, \`CanolaOrphanLink\`) apply as base layer; user patterns override. \`\`\`lua highlights = { { '%.lua$', 'CanolaLua' }, { '%.md$', 'CanolaMarkdown' }, { '^Makefile$', 'CanolaMakefile' }, } \`\`\` ### Migration Map | Old (oil.nvim / canola v0) | New (canola v1) | Change | |---|---|---| | \`default_file_explorer\` | — | removed, always true | | \`default_to_float\` | \`float.default\` | moved | | \`columns\` | \`columns\` | unchanged | | \`delete_to_trash\` | — | → \`vim.g.canola_trash\` | | \`cleanup_buffers_on_delete\` | \`delete.wipe_buffers\` | renamed + grouped | | \`skip_confirm_for_simple_edits\` | \`confirm\` | consolidated into enum | | \`skip_confirm_for_delete\` | \`confirm\` | consolidated into enum | | \`prompt_save_on_select_new_entry\` | \`save\` | consolidated into enum | | \`auto_save_on_select_new_entry\` | \`save\` | consolidated into enum | | \`cleanup_delay_ms\` | — | removed, smart internal logic | | \`constrain_cursor\` | \`cursor\` | renamed, collapsed to bool | | \`watch_for_changes\` | \`watch\` | renamed | | \`use_default_keymaps\` | — | removed, set \`keymaps = {}\` | | \`keymaps\` | \`keymaps\` | unchanged (functions disallowed) | | \`new_file_mode\` | \`create.file_mode\` | renamed + grouped | | \`new_dir_mode\` | \`create.dir_mode\` | renamed + grouped | | \`extra_scp_args\` | — | → \`vim.g.canola_ssh\` | | \`ssh_hosts\` | — | → \`vim.g.canola_ssh\` | | \`extra_s3_args\` | — | → \`vim.g.canola_s3\` | | \`s3_buckets\` | — | → \`vim.g.canola_s3\` | | \`extra_curl_args\` | — | → \`vim.g.canola_ftp\` | | \`ftp_hosts\` | — | → \`vim.g.canola_ftp\` | | \`silence_scp_warning\` | — | → \`vim.g.canola_ssh\` | | \`buf_options\` | \`buf_options\` | unchanged | | \`win_options\` | \`win_options\` | unchanged | | \`view_options.show_hidden\` | \`show_hidden\` | flattened | | \`view_options.show_hidden_when_empty\` | — | removed | | \`view_options.is_hidden_file\` | \`hidden.patterns\` | fn → pattern list | | \`view_options.is_always_hidden\` | \`hidden.always\` | fn → pattern list | | \`view_options.natural_order\` | \`sort.natural\` | absorbed into sort | | \`view_options.case_insensitive\` | \`sort.ignore_case\` | absorbed into sort | | \`view_options.sort\` | \`sort\` / \`sort.by\` | presets added | | \`view_options.highlight_filename\` | \`highlights\` | fn → pattern pairs | | \`lsp_file_methods.enabled\` | \`lsp.enabled\` | renamed parent | | \`lsp_file_methods.timeout_ms\` | \`lsp.timeout_ms\` | renamed parent | | \`lsp_file_methods.autosave_changes\` | \`lsp.autosave\` | renamed | | \`git.add\` | — | removed → autocmd (#182) | | \`git.mv\` | — | removed → autocmd (#182) | | \`git.rm\` | — | removed → autocmd (#182) | | \`float.padding\` | \`float.padding\` | unchanged | | \`float.max_width\` | \`float.max_width\` | unchanged | | \`float.max_height\` | \`float.max_height\` | unchanged | | \`float.border\` | \`float.border\` | unchanged | | \`float.preview_split\` | \`float.preview_split\` | unchanged | | \`float.get_win_title\` | — | removed | | \`float.override\` | — | removed | | \`float.win_options\` | \`float.win_options\` | unchanged | | \`preview_win.update_on_cursor_moved\` | \`preview.follow\` | renamed | | \`preview_win.preview_method\` | \`preview.live\` | 3-enum → bool | | \`preview_win.disable_preview\` | \`preview.disable\` | fn → pattern list | | \`preview_win.max_file_size\` | \`preview.max_file_size_mb\` | explicit unit | | \`preview_win.win_options\` | \`preview.win_options\` | renamed parent | | \`confirmation.*\` | \`confirmation.*\` | unchanged | | \`progress.*\` | — | tabled for separate discussion | | \`ssh.border\` | — | → \`vim.g.canola_ssh\` | | \`keymaps_help.border\` | — | removed, inherits \`border\` | | \`adapters\` | — | removed, internal | | \`adapter_aliases\` | — | removed, internal | ### Adapter Config Each canola-collection adapter owns its config in a separate \`vim.g\` variable: - \`vim.g.canola_trash\` — trash adapter (replaces \`delete_to_trash\`) - \`vim.g.canola_ssh\` — SSH adapter (replaces \`extra_scp_args\`, \`ssh_hosts\`, \`silence_scp_warning\`, \`ssh.border\`) - \`vim.g.canola_s3\` — S3 adapter (replaces \`extra_s3_args\`, \`s3_buckets\`) - \`vim.g.canola_ftp\` — FTP adapter (replaces \`extra_curl_args\`, \`ftp_hosts\`) ### Design Principles 1. **Flat over deep** — settings are top-level unless they have natural siblings 2. **Separate theming from behavior** — \`highlights\` is its own concern 3. **No functions in config** — everything serializes into \`vim.g.canola\` 4. **No setter APIs** — declarative patterns replace callbacks 5. **Global \`border\`** — inherited by all windows, overridable per-window 6. **Enums over boolean pairs** — \`confirm\`, \`save\`, \`sort\` replace confusing combinations 7. **Adapter config is separate** — each adapter owns \`vim.g.canola_*\` ### Relationship to Other Issues - #82 — introduced \`vim.g.canola\` access mechanism (done) - #181 — canola-collection adapter extraction (removes adapter config from core) - #182 — enhanced User autocmds (replaces git hooks)
barrettruth added this to the v1.1 milestone 2026-09-21 18:54:21 +00:00
Sign in to join this conversation.
No description provided.