Skip to content

Commit a8f3df1

Browse files
feat(agenda)!: Add time grid
This is considered a breaking change commit because it adds a new time grid option to the agenda view, which was not there before. It is set to `true` by default to align with how Emacs Orgmode works out of the box. To disable the time grid, set `org_agenda_use_time_grid = false` in your orgmode config.
1 parent ad5d652 commit a8f3df1

File tree

8 files changed

+258
-26
lines changed

8 files changed

+258
-26
lines changed

docs/configuration.org

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,53 @@ To change the highlight, override [email protected]= hl group.
787787
- Default: =false=
788788
Should tags be hidden from all agenda views.
789789

790+
*** org_agenda_time_grid
791+
:PROPERTIES:
792+
:CUSTOM_ID: org_agenda_time_grid
793+
:END:
794+
- Type: ={ type: ('daily', 'weekly', 'require-timed', 'remove-match')[], times: number[], time_separator: string, time_label: string }=
795+
- Default:
796+
#+begin_src lua
797+
{
798+
type = { 'daily', 'today', 'require-timed' },
799+
times = { 800, 1000, 1200, 1400, 1600, 1800, 2000 },
800+
time_separator = '┄┄┄┄┄',
801+
time_label = '┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄'
802+
}
803+
#+end_src
804+
805+
Settings for the time grid visible in agenda. To disable showing the time grid altogether, set [[#org_agenda_use_time_grid][org_agenda_use_time_grid]] to =false=.
806+
807+
- =type=: List of options where all have to apply to show the grid
808+
- =daily= - Show grid in daily agenda (1 day view)
809+
- =weekly= - Show grid in any agenda type
810+
- =today= - Show grid only for today
811+
- =require-timed= - Show grid only if day has any entries with time specification
812+
- =remove-match= - Hide grid entries that overlap with the existing time slot taken by an agenda item
813+
- =times=: List of times (in 24h format) to show on the grid. It should be integer value, example =1030= represents =10:30=
814+
- =time_separator=: Value that is showed after the grid time as a separator
815+
- =time_label=: Value that is showed after the =time_separator= to fill in the place that is usually for the agenda item title
816+
817+
To customize the label for the current time, check [[#org_agenda_current_time_string][org_agenda_current_time_string]]
818+
819+
*** org_agenda_use_time_grid
820+
:PROPERTIES:
821+
:CUSTOM_ID: org_agenda_use_time_grid
822+
:END:
823+
- Type: =boolean=
824+
- Default: =true=
825+
Show time grid in agenda. See [[#org_agenda_time_grid][org_agenda_time_grid]] for configuration options.
826+
827+
*** org_agenda_current_time_string
828+
:PROPERTIES:
829+
:CUSTOM_ID: org_agenda_current_time_string
830+
:END:
831+
- Type: =string=
832+
- Default: =<- now -----------------------------------------------=
833+
Label value for the current time in the agenda time grid.
834+
See [[#org_agenda_time_grid][org_agenda_time_grid]] for time grid configuration or [[#org_agenda_use_time_grid][org_agenda_use_time_grid]] to disable the grid.
835+
836+
790837
*** org_capture_templates
791838
:PROPERTIES:
792839
:CUSTOM_ID: org_capture_templates
@@ -2872,6 +2919,7 @@ The following highlight groups are used:
28722919
- [email protected]=: A item deadline in the agenda view - Parsed from =Error= (see note below)
28732920
- [email protected]=: A scheduled item in the agenda view - Parsed from =DiffAdd= (see note below)
28742921
- [email protected]_past=: A item past its scheduled date in the agenda view - Parsed from =WarningMsg= (see note below)
2922+
- [email protected]_grid=: Time grid line - Parsed from =WarningMsg= (see note below)
28752923
- [email protected]=: Highlight for all days in Agenda view - linked to =Statement=
28762924
- [email protected]=: Highlight for today in Agenda view - linked to [email protected]=
28772925
- [email protected]=: Highlight for weekend days in Agenda view - linked to [email protected]=

lua/orgmode/agenda/agenda_item.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ local hl_map = Highlights.get_agenda_hl_map()
33
local config = require('orgmode.config')
44
local FUTURE_DEADLINE_AS_WARNING_DAYS = math.floor(config.org_deadline_warning_days / 2)
55
local function add_padding(datetime)
6-
if datetime:len() >= 11 then
6+
if datetime:len() >= 10 then
77
return datetime .. ' '
88
end
9-
return datetime .. string.rep('.', 11 - datetime:len()) .. ' '
9+
return datetime .. ' ' .. config.org_agenda_time_grid.time_separator .. ' '
1010
end
1111

1212
---@class OrgAgendaItem

lua/orgmode/agenda/sorting_strategy.lua

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ local SortingStrategy = {}
1717

1818
---@class SortableEntry
1919
---@field date OrgDate Available only in agenda view
20-
---@field headline OrgHeadline
20+
---@field headline? OrgHeadline
2121
---@field index number Index of the entry in the fetched list
2222
---@field is_day_match? boolean Is this entry a match for the given day. Available only in agenda view
2323

@@ -53,6 +53,9 @@ end
5353
---@param a SortableEntry
5454
---@param b SortableEntry
5555
function SortingStrategy.priority_down(a, b)
56+
if not a.headline or not b.headline then
57+
return
58+
end
5659
if a.headline:get_priority_sort_value() ~= b.headline:get_priority_sort_value() then
5760
return a.headline:get_priority_sort_value() > b.headline:get_priority_sort_value()
5861
end
@@ -77,6 +80,9 @@ end
7780
---@param a SortableEntry
7881
---@param b SortableEntry
7982
function SortingStrategy.tag_up(a, b)
83+
if not a.headline or not b.headline then
84+
return
85+
end
8086
local a_tags = a.headline:tags_to_string(true)
8187
local b_tags = b.headline:tags_to_string(true)
8288
if a_tags == '' and b_tags == '' then
@@ -106,6 +112,9 @@ end
106112
---@param a SortableEntry
107113
---@param b SortableEntry
108114
function SortingStrategy.todo_state_up(a, b)
115+
if not a.headline or not b.headline then
116+
return
117+
end
109118
local _, _, _, a_index = a.headline:get_todo()
110119
local _, _, _, b_index = b.headline:get_todo()
111120
if a_index and b_index then
@@ -134,6 +143,9 @@ end
134143
---@param a SortableEntry
135144
---@param b SortableEntry
136145
function SortingStrategy.category_up(a, b)
146+
if not a.headline or not b.headline then
147+
return
148+
end
137149
if a.headline.file:get_category() ~= b.headline.file:get_category() then
138150
return a.headline.file:get_category() < b.headline.file:get_category()
139151
end
@@ -151,6 +163,9 @@ end
151163
---@param a SortableEntry
152164
---@param b SortableEntry
153165
function SortingStrategy.category_keep(a, b)
166+
if not a.headline or not b.headline then
167+
return
168+
end
154169
if a.headline.file.index ~= b.headline.file.index then
155170
return a.headline.file.index < b.headline.file.index
156171
end
@@ -179,6 +194,9 @@ end
179194
---@param a SortableEntry
180195
---@param b SortableEntry
181196
local fallback_sort = function(a, b)
197+
if not a.headline or not b.headline then
198+
return
199+
end
182200
if a.headline.file.index ~= b.headline.file.index then
183201
return a.headline.file.index < b.headline.file.index
184202
end

lua/orgmode/agenda/types/agenda.lua

Lines changed: 145 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ local utils = require('orgmode.utils')
1111
local SortingStrategy = require('orgmode.agenda.sorting_strategy')
1212
local Promise = require('orgmode.utils.promise')
1313

14+
---@alias OrgAgendaDay { day: OrgDate, agenda_items: OrgAgendaItem[], category_length: number, label_length: 0 }
15+
1416
---@class OrgAgendaTypeOpts
1517
---@field files OrgFiles
1618
---@field highlighter OrgHighlighter
@@ -51,6 +53,7 @@ local Promise = require('orgmode.utils.promise')
5153
---@field remove_tags? boolean
5254
---@field valid_filters? OrgAgendaFilter[]
5355
---@field id? string
56+
---@field private _grid_times { hour: number, min: number }[]
5457
local OrgAgendaType = {}
5558
OrgAgendaType.__index = OrgAgendaType
5659

@@ -264,7 +267,12 @@ function OrgAgendaType:render(bufnr, current_line)
264267
}))
265268

266269
for _, agenda_item in ipairs(agenda_day.agenda_items) do
267-
agendaView:add_line(self:_build_line(agenda_item, agenda_day))
270+
-- If there is an index value, this is an AgendaItem instance
271+
if agenda_item.index then
272+
agendaView:add_line(self:_build_line(agenda_item, agenda_day))
273+
else
274+
agendaView:add_line(self:_build_time_grid_line(agenda_item, agenda_day))
275+
end
268276
end
269277
end
270278

@@ -318,6 +326,140 @@ function OrgAgendaType:render(bufnr, current_line)
318326
return self.view
319327
end
320328

329+
---@param grid_line { real_date: OrgDate, is_same_day: boolean, is_now: boolean }
330+
---@param agenda_day OrgAgendaDay
331+
---@return OrgAgendaLine
332+
function OrgAgendaType:_build_time_grid_line(grid_line, agenda_day)
333+
local line = AgendaLine:new({
334+
hl_group = '@org.agenda.time_grid',
335+
metadata = {
336+
date = grid_line.real_date,
337+
},
338+
})
339+
340+
line:add_token(AgendaLineToken:new({
341+
content = ' ' .. utils.pad_right(' ', agenda_day.category_length),
342+
}))
343+
line:add_token(AgendaLineToken:new({
344+
content = grid_line.real_date:format_time() .. ' ' .. config.org_agenda_time_grid.time_separator,
345+
}))
346+
line:add_token(AgendaLineToken:new({
347+
content = grid_line.is_now and config.org_agenda_current_time_string or config.org_agenda_time_grid.time_label,
348+
}))
349+
350+
return line
351+
end
352+
353+
---@param date_range OrgDate[]
354+
---@param agenda_day OrgAgendaDay
355+
---@return { real_date: OrgDate, is_same_day: boolean, is_now: boolean }[]
356+
function OrgAgendaType:_prepare_grid_lines(date_range, agenda_day)
357+
if not config.org_agenda_use_time_grid then
358+
return {}
359+
end
360+
361+
local time_grid_opts = config.org_agenda_time_grid
362+
if not time_grid_opts or not time_grid_opts.type or #time_grid_opts.type == 0 then
363+
return {}
364+
end
365+
366+
local today = false
367+
local weekly = false
368+
local daily = false
369+
local require_timed = false
370+
local remove_match = false
371+
372+
for _, t in ipairs(time_grid_opts.type) do
373+
if t == 'daily' then
374+
daily = true
375+
end
376+
if t == 'weekly' then
377+
weekly = true
378+
end
379+
if t == 'today' then
380+
today = true
381+
end
382+
if t == 'require-timed' then
383+
require_timed = true
384+
end
385+
if t == 'remove-match' then
386+
remove_match = true
387+
end
388+
end
389+
390+
local show_grid = (daily and #date_range == 1) or weekly
391+
if not show_grid and today then
392+
show_grid = agenda_day.day:is_today()
393+
end
394+
395+
local same_day_agenda_items_with_time = {}
396+
397+
if require_timed or remove_match then
398+
for _, agenda_item in ipairs(agenda_day.agenda_items) do
399+
if agenda_item.is_same_day and agenda_item.real_date:has_time() then
400+
table.insert(same_day_agenda_items_with_time, agenda_item)
401+
end
402+
end
403+
end
404+
405+
if show_grid and require_timed then
406+
show_grid = #same_day_agenda_items_with_time > 0
407+
end
408+
409+
if not show_grid then
410+
return {}
411+
end
412+
413+
local grid_lines = {}
414+
local now = Date.now()
415+
for _, time in ipairs(self:_parse_grid_times()) do
416+
local date = agenda_day.day:set({
417+
hour = time.hour,
418+
min = time.min,
419+
date_only = false,
420+
})
421+
if remove_match then
422+
for _, item in ipairs(same_day_agenda_items_with_time) do
423+
if item.real_date:is_same(date) then
424+
goto continue
425+
end
426+
end
427+
end
428+
if date:is_today() and date > now and (#grid_lines == 0 or grid_lines[#grid_lines].real_date < now) then
429+
local now_line = {
430+
real_date = now,
431+
is_same_day = true,
432+
is_now = true,
433+
}
434+
table.insert(grid_lines, now_line)
435+
end
436+
table.insert(grid_lines, {
437+
real_date = date,
438+
is_same_day = true,
439+
is_now = false,
440+
})
441+
442+
::continue::
443+
end
444+
return grid_lines
445+
end
446+
447+
function OrgAgendaType:_parse_grid_times()
448+
if self._grid_times then
449+
return self._grid_times
450+
end
451+
local grid_times = {}
452+
for _, time in ipairs(config.org_agenda_time_grid.times) do
453+
local str = tostring(time)
454+
table.insert(grid_times, {
455+
min = tonumber(str:sub(#str - 1, #str)),
456+
hour = tonumber(str:sub(1, #str - 2)),
457+
})
458+
end
459+
self._grid_times = grid_times
460+
return grid_times
461+
end
462+
321463
---@private
322464
---@param agenda_item OrgAgendaItem
323465
---@param metadata table<string, any>
@@ -381,7 +523,7 @@ function OrgAgendaType:rerender_agenda_line(agenda_line, headline)
381523
self.view:replace_line(agenda_line, line)
382524
end
383525

384-
---@return { day: OrgDate, agenda_items: OrgAgendaItem[], category_length: number, label_length: 0 }[]
526+
---@return OrgAgendaDay[]
385527
function OrgAgendaType:_get_agenda_days()
386528
local dates = self.from:get_range_until(self.to)
387529
local agenda_days = {}
@@ -413,6 +555,7 @@ function OrgAgendaType:_get_agenda_days()
413555
end
414556
end
415557

558+
vim.list_extend(date.agenda_items, self:_prepare_grid_lines(dates, date))
416559
date.agenda_items = self:_sort(date.agenda_items)
417560
date.category_length = math.max(11, date.category_length + 1)
418561
date.label_length = math.min(11, date.label_length)

lua/orgmode/colors/highlights.lua

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ function M.define_agenda_colors()
9595
string.format('hi default %s guifg=%s ctermfg=%s', hlname, keyword_colors[type].gui, keyword_colors[type].cterm)
9696
)
9797
end
98+
vim.cmd(
99+
('hi default @org.agenda.time_grid guifg=%s ctermfg=%s'):format(
100+
keyword_colors.warning.gui,
101+
keyword_colors.warning.cterm
102+
)
103+
)
98104

99105
M.define_org_todo_keyword_colors()
100106
end

lua/orgmode/config/_meta.lua

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@
2121
---@field org_agenda_todo_ignore_scheduled? OrgAgendaTodoIgnoreScheduledTypes
2222
---@field org_agenda_todo_ignore_deadlines? OrgAgendaTodoIgnoreDeadlinesTypes
2323

24+
---@class OrgAgendaTimeGridOpts
25+
---@field type ('daily' | 'weekly' | 'today' | 'require-timed' | 'remove-match')[]
26+
---@field times number[]
27+
---@field time_separator string
28+
---@field time_label string
29+
2430
---@alias OrgAgendaCustomCommandType (OrgAgendaCustomCommandAgenda | OrgAgendaCustomCommandTags)
2531

2632
---@class OrgAgendaCustomCommand
@@ -211,6 +217,9 @@
211217
---@field org_agenda_block_separator? string Separator for blocks in the agenda view. Default: '-'
212218
---@field org_agenda_sorting_strategy? table<'agenda' | 'todo' | 'tags', OrgAgendaSortingStrategy[]> Sorting strategy for the agenda view. See docs for default value
213219
---@field org_agenda_remove_tags? boolean If true, tags will be removed from the all agenda views. Default: false
220+
---@field org_agenda_use_time_grid? boolean If true, Render time grid in agenda as set by org_agenda_time_grid. Default: true
221+
---@field org_agenda_time_grid? OrgAgendaTimeGridOpts Agenda time grid configuration. Default: { type = { 'daily', 'today', 'require-timed' }, times = { 800, 1000, 1200, 1400, 1600, 1800, 2000 }, time_separator = '┄┄┄┄┄', time_label = '┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄' }
222+
---@field org_agenda_current_time_string? string String to indicate current time on the time grid. Default: '<- now -----------------------------------------------'
214223
---@field org_priority_highest? string | number Highest priority level. Default: 'A'
215224
---@field org_priority_default? string | number Default priority level. Default: 'B'
216225
---@field org_priority_lowest? string | number Lowest priority level. Default: 'C'

lua/orgmode/config/defaults.lua

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ local DefaultConfig = {
3333
tags = { 'priority-down', 'category-keep' },
3434
},
3535
org_agenda_remove_tags = false,
36+
org_agenda_time_grid = {
37+
type = { 'daily', 'today', 'require-timed' },
38+
times = { 800, 1000, 1200, 1400, 1600, 1800, 2000 },
39+
time_separator = '┄┄┄┄┄',
40+
time_label = '┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄',
41+
},
42+
org_agenda_current_time_string = '<- now -----------------------------------------------',
43+
org_agenda_use_time_grid = true,
3644
org_priority_highest = 'A',
3745
org_priority_default = 'B',
3846
org_priority_lowest = 'C',

0 commit comments

Comments
 (0)