Skip to content

feat(agenda): Add custom agenda commands #850

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
113 changes: 112 additions & 1 deletion DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -506,8 +506,18 @@ Determine on which day the week will start in calendar modal (ex: [changing the
#### **emacs_config**

_type_: `table`<br />
_default value_: `{ executable_path = 'emacs', config_path='$HOME/.emacs.d/init.el' }`<br />
_default value_: `{ executable_path = 'emacs', config_path=nil }`<br />
Set configuration for your emacs. This is useful for having the emacs export properly pickup your emacs config and plugins.
If `config_path` is not provided, exporter tries to find a configuration file from these locations:

1. `~/.config/emacs/init.el`
2. `~/.emacs.d/init.el`
3. `~/.emacs.el`

If there is no configuration found, it will still process the export.

If it finds a configuration and export attempt fails because of the configuration issue, there will be a prompt to
attempt the same export without the configuration file.

### Agenda settings

Expand Down Expand Up @@ -548,6 +558,107 @@ Example:<br />
If `org_agenda_start_on_weekday` is `false`, and `org_agenda_start_day` is `-2d`,<br />
agenda will always show current week from today - 2 days

#### **org_agenda_custom_commands**

_type_: `table<string, OrgAgendaCustomCommand>`<br />
_default value_: `{}`<br />

Define custom agenda views that are available through the (org_agenda)[#org_agenda] mapping.
It is possible to combine multiple agenda types into single view.
An example:

```lua
require('orgmode').setup({
org_agenda_files = {'~/org/**/*'},
org_agenda_custom_commands = {
-- "c" is the shortcut that will be used in the prompt
c = {
description = 'Combined view', -- Description shown in the prompt for the shortcut
types = {
{
type = 'tags_todo', -- Type can be agenda | tags | tags_todo
match = '+PRIORITY="A"', --Same as providing a "Match:" for tags view <leader>oa + m, See: https://orgmode.org/manual/Matching-tags-and-properties.html
org_agenda_overriding_header = 'High priority todos',
org_agenda_todo_ignore_deadlines = 'far', -- Ignore all deadlines that are too far in future (over org_deadline_warning_days). Possible values: all | near | far | past | future
},
{
type = 'agenda',
org_agenda_overriding_header = 'My daily agenda',
org_agenda_span = 'day' -- can be any value as org_agenda_span
},
{
type = 'tags',
match = 'WORK', --Same as providing a "Match:" for tags view <leader>oa + m, See: https://orgmode.org/manual/Matching-tags-and-properties.html
org_agenda_overriding_header = 'My work todos',
org_agenda_todo_ignore_scheduled = 'all', -- Ignore all headlines that are scheduled. Possible values: past | future | all
},
{
type = 'agenda',
org_agenda_overriding_header = 'Whole week overview',
org_agenda_span = 'week', -- 'week' is default, so it's not necessary here, just an example
org_agenda_start_on_weekday = 1 -- Start on Monday
org_agenda_remove_tags = true -- Do not show tags only for this view
},
}
},
p = {
description = 'Personal agenda',
types = {
{
type = 'tags_todo',
org_agenda_overriding_header = 'My personal todos',
org_agenda_category_filter_preset = 'todos', -- Show only headlines from `todos` category. Same value providad as when pressing `/` in the Agenda view
org_agenda_sorting_strategy = {'todo-state-up', 'priority-down'} -- See all options available on org_agenda_sorting_strategy
},
{
type = 'agenda',
org_agenda_overriding_header = 'Personal projects agenda',
org_agenda_files = {'~/my-projects/**/*'}, -- Can define files outside of the default org_agenda_files
},
{
type = 'tags',
org_agenda_overriding_header = 'Personal projects notes',
org_agenda_files = {'~/my-projects/**/*'},
org_agenda_tag_filter_preset = 'NOTES-REFACTOR' -- Show only headlines with NOTES tag that does not have a REFACTOR tag. Same value providad as when pressing `/` in the Agenda view
},
}
}
}
})
```

#### **org_agenda_sorting_strategy**
_type_: `table<'agenda' | 'todo' | 'tags', OrgAgendaSortingStrategy[]><`<br />
default value: `{ agenda = {'time-up', 'priority-down', 'category-keep'}, todo = {'priority-down', 'category-keep'}, tags = {'priority-down', 'category-keep'}}`<br />
List of sorting strategies to apply to a given view.
Available strategies:

- `time-up` - Sort entries by time of day. Applicable only in `agenda` view
- `time-down` - Opposite of `time-up`
- `priority-down` - Sort by priority, from highest to lowest
- `priority-up` - Sort by priority, from lowest to highest
- `tag-up` - Sort by sorted tags string, ascending
- `tag-down` - Sort by sorted tags string, descending
- `todo-state-up` - Sort by todo keyword by position (example: 'TODO, PROGRESS, DONE' has a sort value of 1, 2 and 3), ascending
- `todo-state-down` - Sort by todo keyword, descending
- `clocked-up` - Show clocked in headlines first
- `clocked-down` - Show clocked in headines last
- `category-up` - Sort by category name, ascending
- `category-down` - Sort by category name, descending
- `category-keep` - Keep default category sorting, as it appears in org-agenda-files


#### **org_agenda_block_separator**
_type_: `string`<br />
default value: `-`<br />
Separator used to separate multiple agenda views generated by org_agenda_custom_commands.<br />
To change the highlight, override `@org.agenda.separator` hl group.

#### **org_agenda_remove_tags**
_type_: `boolean`<br />
default value: `false`<br />
Should tags be hidden from all agenda views.

#### **org_capture_templates**

_type_: `table<string, table>`<br />
Expand Down
1 change: 1 addition & 0 deletions lua/orgmode/agenda/agenda_item.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ end
---@field is_in_date_range boolean
---@field date_range_days number
---@field label string
---@field index number
local AgendaItem = {}

---@param headline_date OrgDate single date in a headline
Expand Down
40 changes: 33 additions & 7 deletions lua/orgmode/agenda/filter.lua
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
---@class OrgAgendaFilter
---@field value string
---@field available_values table<string, boolean>
---@field types? ('tags' | 'categories')[]
---@field values table[]
---@field term string
---@field parsed boolean
local AgendaFilter = {}

---@param opts? { types?: ('tags' | 'categories')[] }
---@return OrgAgendaFilter
function AgendaFilter:new()
function AgendaFilter:new(opts)
opts = opts or {}
local data = {
value = '',
available_values = {},
values = {},
term = '',
types = opts.types or { 'tags', 'categories' },
parsed = false,
}
setmetatable(data, self)
Expand Down Expand Up @@ -52,13 +56,31 @@ end
---@param headline OrgHeadline
---@return boolean
function AgendaFilter:_match(headline)
local filters = {}
if vim.tbl_contains(self.types, 'tags') then
table.insert(filters, function(tag)
return headline:has_tag(tag)
end)
end
if vim.tbl_contains(self.types, 'categories') then
table.insert(filters, function(category)
return headline:matches_category(category)
end)
end
for _, value in ipairs(self.values) do
if value.operator == '-' then
if headline:has_tag(value.value) or headline:matches_category(value.value) then
for _, filter in ipairs(filters) do
if filter(value.value) then
return false
end
end
else
local result = vim.tbl_filter(function(filter)
return filter(value.value)
end, filters)
if #result == 0 then
return false
end
elseif not headline:has_tag(value.value) and not headline:matches_category(value.value) then
return false
end
end

Expand Down Expand Up @@ -104,9 +126,13 @@ function AgendaFilter:parse_available_filters(agenda_views)
for _, agenda_view in ipairs(agenda_views) do
for _, line in ipairs(agenda_view:get_lines()) do
if line.headline then
values[line.headline:get_category()] = true
for _, tag in ipairs(line.headline:get_tags()) do
values[tag] = true
if vim.tbl_contains(self.types, 'categories') then
values[line.headline:get_category()] = true
end
if vim.tbl_contains(self.types, 'tags') then
for _, tag in ipairs(line.headline:get_tags()) do
values[tag] = true
end
end
end
end
Expand Down
102 changes: 99 additions & 3 deletions lua/orgmode/agenda/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ local AgendaTypes = require('orgmode.agenda.types')
---@field views OrgAgendaViewType[]
---@field filters OrgAgendaFilter
---@field files OrgFiles
---@field highlighter OrgHighlighter
local Agenda = {}

---@param opts? table
---@param opts? { highlighter: OrgHighlighter, files: OrgFiles }
function Agenda:new(opts)
opts = opts or {}
local data = {
Expand All @@ -24,6 +25,7 @@ function Agenda:new(opts)
content = {},
highlights = {},
files = opts.files,
highlighter = opts.highlighter,
}
setmetatable(data, self)
self.__index = self
Expand All @@ -37,6 +39,7 @@ function Agenda:open_view(type, opts)
local view_opts = vim.tbl_extend('force', opts or {}, {
files = self.files,
agenda_filter = self.filters,
highlighter = self.highlighter,
})

local view = AgendaTypes[type]:new(view_opts)
Expand All @@ -53,7 +56,7 @@ function Agenda:render()
for i, view in ipairs(self.views) do
view:render(bufnr, line)
if #self.views > 1 and i < #self.views then
colors.add_hr(bufnr, vim.fn.line('$'))
colors.add_hr(bufnr, vim.fn.line('$'), config.org_agenda_block_separator)
end
end
vim.bo[bufnr].modifiable = false
Expand Down Expand Up @@ -90,6 +93,74 @@ function Agenda:tags_todo(opts)
return self:open_view('tags_todo', opts)
end

function Agenda:_build_custom_commands()
if not config.org_agenda_custom_commands then
return {}
end
local custom_commands = {}
---@param opts OrgAgendaCustomCommandType
local get_type_opts = function(opts, id)
local opts_by_type = {
agenda = {
span = opts.org_agenda_span,
start_day = opts.org_agenda_start_day,
start_on_weekday = opts.org_agenda_start_on_weekday,
},
tags = {
match_query = opts.match,
todo_ignore_scheduled = opts.org_agenda_todo_ignore_scheduled,
todo_ignore_deadlines = opts.org_agenda_todo_ignore_deadlines,
},
tags_todo = {
match_query = opts.match,
todo_ignore_scheduled = opts.org_agenda_todo_ignore_scheduled,
todo_ignore_deadlines = opts.org_agenda_todo_ignore_deadlines,
},
}

if not opts_by_type[opts.type] then
return
end

opts_by_type[opts.type].sorting_strategy = opts.org_agenda_sorting_strategy
opts_by_type[opts.type].filters = self.filters
opts_by_type[opts.type].files = self.files
opts_by_type[opts.type].header = opts.org_agenda_overriding_header
opts_by_type[opts.type].agenda_files = opts.org_agenda_files
opts_by_type[opts.type].tag_filter = opts.org_agenda_tag_filter_preset
opts_by_type[opts.type].category_filter = opts.org_agenda_category_filter_preset
opts_by_type[opts.type].highlighter = self.highlighter
opts_by_type[opts.type].remove_tags = opts.org_agenda_remove_tags
opts_by_type[opts.type].id = id

return opts_by_type[opts.type]
end
for shortcut, command in pairs(config.org_agenda_custom_commands) do
table.insert(custom_commands, {
label = command.description or '',
key = shortcut,
action = function()
local views = {}
for i, agenda_type in ipairs(command.types) do
local opts = get_type_opts(agenda_type, ('%s_%s_%d'):format(shortcut, agenda_type.type, i))
if not opts then
utils.echo_error('Invalid custom agenda command type ' .. agenda_type.type)
break
end
table.insert(views, AgendaTypes[agenda_type.type]:new(opts))
end
self.views = views
local result = self:render()
if #self.views > 1 then
vim.fn.cursor({ 1, 0 })
end
return result
end,
})
end
return custom_commands
end

---@private
---@return number buffer number
function Agenda:_open_window()
Expand Down Expand Up @@ -157,6 +228,18 @@ function Agenda:prompt()
return self:search()
end,
})

local custom_commands = self:_build_custom_commands()
if #custom_commands > 0 then
for _, command in ipairs(custom_commands) do
menu:add_option({
label = command.label,
key = command.key,
action = command.action,
})
end
end

menu:add_option({ label = 'Quit', key = 'q' })
menu:add_separator({ icon = ' ', length = 1 })

Expand All @@ -169,10 +252,11 @@ end

---@param source? string
function Agenda:redo(source, preserve_cursor_pos)
self:_call_all_views('redo')
return self.files:load(true):next(vim.schedule_wrap(function()
local save_view = preserve_cursor_pos and vim.fn.winsaveview()
if source == 'mapping' then
self:_call_view_and_render('redo')
self:_call_view_and_render('redraw')
end
self:render()
if save_view then
Expand Down Expand Up @@ -478,6 +562,18 @@ function Agenda:_call_view(method, ...)
return executed
end

function Agenda:_call_all_views(method, ...)
local executed = false
for _, view in ipairs(self.views) do
if view[method] then
view[method](view, ...)
executed = true
end
end

return executed
end

function Agenda:_call_view_and_render(method, ...)
local executed = self:_call_view(method, ...)
if executed then
Expand Down
Loading
Loading