From 6a7cf9113ddf8f84db7bfa9abff420bcea233e75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Fl=C3=BCgge?= <952313+seflue@users.noreply.github.com> Date: Thu, 11 Jun 2026 08:43:19 +0200 Subject: [PATCH] feat(todos): allow custom UI for fast-access The fast-access TODO prompt echoed its keys straight to the message area, so UI plugins could not theme or relocate it. With noice or snacks notifier it times out before you can read it. It now goes through ui.menu.handler when one is set, the same path the agenda menu uses; without a handler the echo output is unchanged. The menu data keeps the keyword sequences grouped and carries each keyword's highlight group, and exposes a stable kind, so a handler can render the cycles in their own colors. items stays as the flat view for handlers that ignore grouping. --- lua/orgmode/objects/todo_state.lua | 52 ++++++++++++++++++++++++++++++ lua/orgmode/ui/menu.lua | 30 +++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/lua/orgmode/objects/todo_state.lua b/lua/orgmode/objects/todo_state.lua index b52c2c7db..8faaa0192 100644 --- a/lua/orgmode/objects/todo_state.lua +++ b/lua/orgmode/objects/todo_state.lua @@ -25,6 +25,58 @@ end ---@return OrgTodoKeyword | nil function TodoState:open_fast_access() + if config.ui.menu.handler then + return self:_open_fast_access_menu() + end + return self:_open_fast_access_echo() +end + +--- Delegates to the configurable menu handler so UI plugins can render the +--- prompt in a window instead of the message area. +---@private +---@return OrgTodoKeyword | nil +function TodoState:_open_fast_access_menu() + local Menu = require('orgmode.ui.menu') + local menu = Menu:new({ + title = 'Select a TODO keyword', + prompt = 'Press key', + kind = 'todo_fast_access', + }) + + -- One group per keyword sequence, so a handler can keep the cycle grouping. + for _, sequence in ipairs(self.todos.sequences) do + local group = {} + for _, todo in ipairs(sequence) do + table.insert(group, { + label = todo.value, + key = todo.shortcut, + hl = todo.hl, + action = function() + return todo + end, + }) + end + if #group > 0 then + menu:add_group(group) + end + end + + menu:add_group({ + { + label = 'Clear keyword', + key = ' ', + action = function() + return TodoKeyword:empty() + end, + }, + }) + + return menu:open() +end + +---@private +---@return OrgTodoKeyword | nil +function TodoState:_open_fast_access_echo() local output = {} -- Group keywords by sequence diff --git a/lua/orgmode/ui/menu.lua b/lua/orgmode/ui/menu.lua index c0e0ebdad..372d1478c 100644 --- a/lua/orgmode/ui/menu.lua +++ b/lua/orgmode/ui/menu.lua @@ -4,6 +4,7 @@ local config = require('orgmode.config') ---@field label string Description of the action ---@field key string Key that will be processed when the keys are pressed in the menu ---@field action? function Handler that will be called when the `key` is pressed in the menu. +---@field hl? string Highlight group for the label, for handlers that render it ---@class OrgMenuSeparator ---@field icon string? Character used as separator. The default character is `-` @@ -11,14 +12,18 @@ local config = require('orgmode.config') ---@alias OrgMenuItem OrgMenuOption | OrgMenuSeparator +---@alias OrgMenuGroup OrgMenuOption[] + ---@class OrgMenuOpts ---@field title string Menu title ---@field items OrgMenuItem[]? Menu items, may include options and separators ---@field prompt string Prompt text used to prompt a keystroke ---@field separator OrgMenuSeparator? Default separator +---@field kind string? Stable identifier of the menu type, for handlers that branch on it --- Menu for selecting an action by pressing a key by the user ---@class OrgMenu:OrgMenuOpts +---@field groups OrgMenuGroup[] local Menu = {} ---@param data OrgMenuOpts @@ -30,6 +35,8 @@ function Menu:new(data) opts.title = data.title opts.prompt = data.prompt opts.items = data.items or {} + opts.groups = {} + opts.kind = data.kind opts.separator = vim.tbl_deep_extend('force', { icon = '-', length = 80 }, data.separator or {}) setmetatable(opts, self) @@ -42,6 +49,7 @@ function Menu:_validate_option(option) vim.validate('label', option.label, 'string') vim.validate('key', option.key, 'string') vim.validate('action', option.action, 'function', true) + vim.validate('hl', option.hl, 'string', true) end ---@param items OrgMenuItem[]? @@ -75,6 +83,7 @@ end function Menu:_validate_data(data) vim.validate('title', data.title, 'string') vim.validate('prompt', data.prompt, 'string') + vim.validate('kind', data.kind, 'string', true) self:_validate_items(data.items) self:_validate_separator(data.separator) end @@ -91,10 +100,27 @@ function Menu:add_separator(separator) table.insert(self.items, vim.tbl_deep_extend('force', self.separator, separator or {})) end +--- Adds a group of options. Groups carry structure (e.g. one todo sequence per +--- group) for handlers that lay them out; the options are also appended to the +--- flat `items` list so handlers that ignore groups keep working. +---@param options OrgMenuGroup +function Menu:add_group(options) + vim.validate('options', options, 'table') + for _, option in ipairs(options) do + self:_validate_option(option) + end + table.insert(self.groups, options) + for _, option in ipairs(options) do + table.insert(self.items, option) + end +end + ---@class OrgMenuData ---@field title string Menu title ---@field items OrgMenuItem[] Menu items, may include options and separators ---@field prompt string Prompt text used to prompt a keystroke +---@field groups? OrgMenuGroup[] Options grouped by structure; `items` is the flat view +---@field kind? string Stable identifier of the menu type ---@param data OrgMenuData function Menu._default_menu(data) @@ -145,7 +171,11 @@ function Menu:open() title = self.title, items = self.items, prompt = self.prompt, + kind = self.kind, } + if #self.groups > 0 then + menu_data.groups = self.groups + end local custom_handler = config.ui.menu.handler if custom_handler then return custom_handler(menu_data)