Skip to content

Commit

Permalink
feat(agenda): Add custom agenda commands (#850)
Browse files Browse the repository at this point in the history
  • Loading branch information
kristijanhusak authored Jan 14, 2025
1 parent fccccd4 commit e6ae773
Show file tree
Hide file tree
Showing 31 changed files with 1,107 additions and 165 deletions.
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

0 comments on commit e6ae773

Please sign in to comment.