Skip to content

Commit e6ae773

Browse files
feat(agenda): Add custom agenda commands (#850)
1 parent fccccd4 commit e6ae773

31 files changed

+1107
-165
lines changed

DOCS.md

+112-1
Original file line numberDiff line numberDiff line change
@@ -506,8 +506,18 @@ Determine on which day the week will start in calendar modal (ex: [changing the
506506
#### **emacs_config**
507507

508508
_type_: `table`<br />
509-
_default value_: `{ executable_path = 'emacs', config_path='$HOME/.emacs.d/init.el' }`<br />
509+
_default value_: `{ executable_path = 'emacs', config_path=nil }`<br />
510510
Set configuration for your emacs. This is useful for having the emacs export properly pickup your emacs config and plugins.
511+
If `config_path` is not provided, exporter tries to find a configuration file from these locations:
512+
513+
1. `~/.config/emacs/init.el`
514+
2. `~/.emacs.d/init.el`
515+
3. `~/.emacs.el`
516+
517+
If there is no configuration found, it will still process the export.
518+
519+
If it finds a configuration and export attempt fails because of the configuration issue, there will be a prompt to
520+
attempt the same export without the configuration file.
511521

512522
### Agenda settings
513523

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

561+
#### **org_agenda_custom_commands**
562+
563+
_type_: `table<string, OrgAgendaCustomCommand>`<br />
564+
_default value_: `{}`<br />
565+
566+
Define custom agenda views that are available through the (org_agenda)[#org_agenda] mapping.
567+
It is possible to combine multiple agenda types into single view.
568+
An example:
569+
570+
```lua
571+
require('orgmode').setup({
572+
org_agenda_files = {'~/org/**/*'},
573+
org_agenda_custom_commands = {
574+
-- "c" is the shortcut that will be used in the prompt
575+
c = {
576+
description = 'Combined view', -- Description shown in the prompt for the shortcut
577+
types = {
578+
{
579+
type = 'tags_todo', -- Type can be agenda | tags | tags_todo
580+
match = '+PRIORITY="A"', --Same as providing a "Match:" for tags view <leader>oa + m, See: https://orgmode.org/manual/Matching-tags-and-properties.html
581+
org_agenda_overriding_header = 'High priority todos',
582+
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
583+
},
584+
{
585+
type = 'agenda',
586+
org_agenda_overriding_header = 'My daily agenda',
587+
org_agenda_span = 'day' -- can be any value as org_agenda_span
588+
},
589+
{
590+
type = 'tags',
591+
match = 'WORK', --Same as providing a "Match:" for tags view <leader>oa + m, See: https://orgmode.org/manual/Matching-tags-and-properties.html
592+
org_agenda_overriding_header = 'My work todos',
593+
org_agenda_todo_ignore_scheduled = 'all', -- Ignore all headlines that are scheduled. Possible values: past | future | all
594+
},
595+
{
596+
type = 'agenda',
597+
org_agenda_overriding_header = 'Whole week overview',
598+
org_agenda_span = 'week', -- 'week' is default, so it's not necessary here, just an example
599+
org_agenda_start_on_weekday = 1 -- Start on Monday
600+
org_agenda_remove_tags = true -- Do not show tags only for this view
601+
},
602+
}
603+
},
604+
p = {
605+
description = 'Personal agenda',
606+
types = {
607+
{
608+
type = 'tags_todo',
609+
org_agenda_overriding_header = 'My personal todos',
610+
org_agenda_category_filter_preset = 'todos', -- Show only headlines from `todos` category. Same value providad as when pressing `/` in the Agenda view
611+
org_agenda_sorting_strategy = {'todo-state-up', 'priority-down'} -- See all options available on org_agenda_sorting_strategy
612+
},
613+
{
614+
type = 'agenda',
615+
org_agenda_overriding_header = 'Personal projects agenda',
616+
org_agenda_files = {'~/my-projects/**/*'}, -- Can define files outside of the default org_agenda_files
617+
},
618+
{
619+
type = 'tags',
620+
org_agenda_overriding_header = 'Personal projects notes',
621+
org_agenda_files = {'~/my-projects/**/*'},
622+
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
623+
},
624+
}
625+
}
626+
}
627+
})
628+
```
629+
630+
#### **org_agenda_sorting_strategy**
631+
_type_: `table<'agenda' | 'todo' | 'tags', OrgAgendaSortingStrategy[]><`<br />
632+
default value: `{ agenda = {'time-up', 'priority-down', 'category-keep'}, todo = {'priority-down', 'category-keep'}, tags = {'priority-down', 'category-keep'}}`<br />
633+
List of sorting strategies to apply to a given view.
634+
Available strategies:
635+
636+
- `time-up` - Sort entries by time of day. Applicable only in `agenda` view
637+
- `time-down` - Opposite of `time-up`
638+
- `priority-down` - Sort by priority, from highest to lowest
639+
- `priority-up` - Sort by priority, from lowest to highest
640+
- `tag-up` - Sort by sorted tags string, ascending
641+
- `tag-down` - Sort by sorted tags string, descending
642+
- `todo-state-up` - Sort by todo keyword by position (example: 'TODO, PROGRESS, DONE' has a sort value of 1, 2 and 3), ascending
643+
- `todo-state-down` - Sort by todo keyword, descending
644+
- `clocked-up` - Show clocked in headlines first
645+
- `clocked-down` - Show clocked in headines last
646+
- `category-up` - Sort by category name, ascending
647+
- `category-down` - Sort by category name, descending
648+
- `category-keep` - Keep default category sorting, as it appears in org-agenda-files
649+
650+
651+
#### **org_agenda_block_separator**
652+
_type_: `string`<br />
653+
default value: `-`<br />
654+
Separator used to separate multiple agenda views generated by org_agenda_custom_commands.<br />
655+
To change the highlight, override `@org.agenda.separator` hl group.
656+
657+
#### **org_agenda_remove_tags**
658+
_type_: `boolean`<br />
659+
default value: `false`<br />
660+
Should tags be hidden from all agenda views.
661+
551662
#### **org_capture_templates**
552663

553664
_type_: `table<string, table>`<br />

lua/orgmode/agenda/agenda_item.lua

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ end
2020
---@field is_in_date_range boolean
2121
---@field date_range_days number
2222
---@field label string
23+
---@field index number
2324
local AgendaItem = {}
2425

2526
---@param headline_date OrgDate single date in a headline

lua/orgmode/agenda/filter.lua

+33-7
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
---@class OrgAgendaFilter
22
---@field value string
33
---@field available_values table<string, boolean>
4+
---@field types? ('tags' | 'categories')[]
45
---@field values table[]
56
---@field term string
67
---@field parsed boolean
78
local AgendaFilter = {}
89

10+
---@param opts? { types?: ('tags' | 'categories')[] }
911
---@return OrgAgendaFilter
10-
function AgendaFilter:new()
12+
function AgendaFilter:new(opts)
13+
opts = opts or {}
1114
local data = {
1215
value = '',
1316
available_values = {},
1417
values = {},
1518
term = '',
19+
types = opts.types or { 'tags', 'categories' },
1620
parsed = false,
1721
}
1822
setmetatable(data, self)
@@ -52,13 +56,31 @@ end
5256
---@param headline OrgHeadline
5357
---@return boolean
5458
function AgendaFilter:_match(headline)
59+
local filters = {}
60+
if vim.tbl_contains(self.types, 'tags') then
61+
table.insert(filters, function(tag)
62+
return headline:has_tag(tag)
63+
end)
64+
end
65+
if vim.tbl_contains(self.types, 'categories') then
66+
table.insert(filters, function(category)
67+
return headline:matches_category(category)
68+
end)
69+
end
5570
for _, value in ipairs(self.values) do
5671
if value.operator == '-' then
57-
if headline:has_tag(value.value) or headline:matches_category(value.value) then
72+
for _, filter in ipairs(filters) do
73+
if filter(value.value) then
74+
return false
75+
end
76+
end
77+
else
78+
local result = vim.tbl_filter(function(filter)
79+
return filter(value.value)
80+
end, filters)
81+
if #result == 0 then
5882
return false
5983
end
60-
elseif not headline:has_tag(value.value) and not headline:matches_category(value.value) then
61-
return false
6284
end
6385
end
6486

@@ -104,9 +126,13 @@ function AgendaFilter:parse_available_filters(agenda_views)
104126
for _, agenda_view in ipairs(agenda_views) do
105127
for _, line in ipairs(agenda_view:get_lines()) do
106128
if line.headline then
107-
values[line.headline:get_category()] = true
108-
for _, tag in ipairs(line.headline:get_tags()) do
109-
values[tag] = true
129+
if vim.tbl_contains(self.types, 'categories') then
130+
values[line.headline:get_category()] = true
131+
end
132+
if vim.tbl_contains(self.types, 'tags') then
133+
for _, tag in ipairs(line.headline:get_tags()) do
134+
values[tag] = true
135+
end
110136
end
111137
end
112138
end

lua/orgmode/agenda/init.lua

+99-3
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ local AgendaTypes = require('orgmode.agenda.types')
1313
---@field views OrgAgendaViewType[]
1414
---@field filters OrgAgendaFilter
1515
---@field files OrgFiles
16+
---@field highlighter OrgHighlighter
1617
local Agenda = {}
1718

18-
---@param opts? table
19+
---@param opts? { highlighter: OrgHighlighter, files: OrgFiles }
1920
function Agenda:new(opts)
2021
opts = opts or {}
2122
local data = {
@@ -24,6 +25,7 @@ function Agenda:new(opts)
2425
content = {},
2526
highlights = {},
2627
files = opts.files,
28+
highlighter = opts.highlighter,
2729
}
2830
setmetatable(data, self)
2931
self.__index = self
@@ -37,6 +39,7 @@ function Agenda:open_view(type, opts)
3739
local view_opts = vim.tbl_extend('force', opts or {}, {
3840
files = self.files,
3941
agenda_filter = self.filters,
42+
highlighter = self.highlighter,
4043
})
4144

4245
local view = AgendaTypes[type]:new(view_opts)
@@ -53,7 +56,7 @@ function Agenda:render()
5356
for i, view in ipairs(self.views) do
5457
view:render(bufnr, line)
5558
if #self.views > 1 and i < #self.views then
56-
colors.add_hr(bufnr, vim.fn.line('$'))
59+
colors.add_hr(bufnr, vim.fn.line('$'), config.org_agenda_block_separator)
5760
end
5861
end
5962
vim.bo[bufnr].modifiable = false
@@ -90,6 +93,74 @@ function Agenda:tags_todo(opts)
9093
return self:open_view('tags_todo', opts)
9194
end
9295

96+
function Agenda:_build_custom_commands()
97+
if not config.org_agenda_custom_commands then
98+
return {}
99+
end
100+
local custom_commands = {}
101+
---@param opts OrgAgendaCustomCommandType
102+
local get_type_opts = function(opts, id)
103+
local opts_by_type = {
104+
agenda = {
105+
span = opts.org_agenda_span,
106+
start_day = opts.org_agenda_start_day,
107+
start_on_weekday = opts.org_agenda_start_on_weekday,
108+
},
109+
tags = {
110+
match_query = opts.match,
111+
todo_ignore_scheduled = opts.org_agenda_todo_ignore_scheduled,
112+
todo_ignore_deadlines = opts.org_agenda_todo_ignore_deadlines,
113+
},
114+
tags_todo = {
115+
match_query = opts.match,
116+
todo_ignore_scheduled = opts.org_agenda_todo_ignore_scheduled,
117+
todo_ignore_deadlines = opts.org_agenda_todo_ignore_deadlines,
118+
},
119+
}
120+
121+
if not opts_by_type[opts.type] then
122+
return
123+
end
124+
125+
opts_by_type[opts.type].sorting_strategy = opts.org_agenda_sorting_strategy
126+
opts_by_type[opts.type].filters = self.filters
127+
opts_by_type[opts.type].files = self.files
128+
opts_by_type[opts.type].header = opts.org_agenda_overriding_header
129+
opts_by_type[opts.type].agenda_files = opts.org_agenda_files
130+
opts_by_type[opts.type].tag_filter = opts.org_agenda_tag_filter_preset
131+
opts_by_type[opts.type].category_filter = opts.org_agenda_category_filter_preset
132+
opts_by_type[opts.type].highlighter = self.highlighter
133+
opts_by_type[opts.type].remove_tags = opts.org_agenda_remove_tags
134+
opts_by_type[opts.type].id = id
135+
136+
return opts_by_type[opts.type]
137+
end
138+
for shortcut, command in pairs(config.org_agenda_custom_commands) do
139+
table.insert(custom_commands, {
140+
label = command.description or '',
141+
key = shortcut,
142+
action = function()
143+
local views = {}
144+
for i, agenda_type in ipairs(command.types) do
145+
local opts = get_type_opts(agenda_type, ('%s_%s_%d'):format(shortcut, agenda_type.type, i))
146+
if not opts then
147+
utils.echo_error('Invalid custom agenda command type ' .. agenda_type.type)
148+
break
149+
end
150+
table.insert(views, AgendaTypes[agenda_type.type]:new(opts))
151+
end
152+
self.views = views
153+
local result = self:render()
154+
if #self.views > 1 then
155+
vim.fn.cursor({ 1, 0 })
156+
end
157+
return result
158+
end,
159+
})
160+
end
161+
return custom_commands
162+
end
163+
93164
---@private
94165
---@return number buffer number
95166
function Agenda:_open_window()
@@ -157,6 +228,18 @@ function Agenda:prompt()
157228
return self:search()
158229
end,
159230
})
231+
232+
local custom_commands = self:_build_custom_commands()
233+
if #custom_commands > 0 then
234+
for _, command in ipairs(custom_commands) do
235+
menu:add_option({
236+
label = command.label,
237+
key = command.key,
238+
action = command.action,
239+
})
240+
end
241+
end
242+
160243
menu:add_option({ label = 'Quit', key = 'q' })
161244
menu:add_separator({ icon = ' ', length = 1 })
162245

@@ -169,10 +252,11 @@ end
169252

170253
---@param source? string
171254
function Agenda:redo(source, preserve_cursor_pos)
255+
self:_call_all_views('redo')
172256
return self.files:load(true):next(vim.schedule_wrap(function()
173257
local save_view = preserve_cursor_pos and vim.fn.winsaveview()
174258
if source == 'mapping' then
175-
self:_call_view_and_render('redo')
259+
self:_call_view_and_render('redraw')
176260
end
177261
self:render()
178262
if save_view then
@@ -478,6 +562,18 @@ function Agenda:_call_view(method, ...)
478562
return executed
479563
end
480564

565+
function Agenda:_call_all_views(method, ...)
566+
local executed = false
567+
for _, view in ipairs(self.views) do
568+
if view[method] then
569+
view[method](view, ...)
570+
executed = true
571+
end
572+
end
573+
574+
return executed
575+
end
576+
481577
function Agenda:_call_view_and_render(method, ...)
482578
local executed = self:_call_view(method, ...)
483579
if executed then

0 commit comments

Comments
 (0)