changelog shortlog graph tags branches changeset files revisions annotate raw help

Mercurial > infra > home / .emacs.d/lib/org-timeline.el

changeset 94: 978ce75e54af
author: Richard Westhaver <ellis@rwest.io>
date: Fri, 30 Aug 2024 17:07:30 -0400
permissions: -rw-r--r--
description: add stumpwm modules and org-timeline
1 ;;; org-timeline.el --- Add graphical view of agenda to agenda buffer. -*- lexical-binding: t -*-
2 
3 ;; Copyright (C) 2017 Matúš Goljer
4 
5 ;; Author: Matúš Goljer <matus.goljer@gmail.com>
6 ;; Maintainer: Matúš Goljer <matus.goljer@gmail.com>
7 ;; Version: 0.3.0
8 ;; Created: 16th April 2017
9 ;; Package-requires: ((dash "2.13.0") (emacs "24.3"))
10 ;; Keywords: calendar
11 ;; URL: https://github.com/Fuco1/org-timeline/
12 
13 ;; This program is free software; you can redistribute it and/or
14 ;; modify it under the terms of the GNU General Public License
15 ;; as published by the Free Software Foundation; either version 3
16 ;; of the License, or (at your option) any later version.
17 
18 ;; This program is distributed in the hope that it will be useful,
19 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
20 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 ;; GNU General Public License for more details.
22 
23 ;; You should have received a copy of the GNU General Public License
24 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
25 
26 ;;; Commentary:
27 
28 ;; Add graphical view of agenda to agenda buffer.
29 
30 ;; This package adds a graphical view of the agenda after the last
31 ;; agenda line. By default the display starts at 5 AM today and
32 ;; goes up to 4 AM next day (this covers 24 hours).
33 
34 ;; Scheduled tasks or tasks with time ranges are rendered in the
35 ;; display with `org-timeline-block' face. Clocked entires are
36 ;; displayed in `org-timeline-clocked' face.
37 
38 ;; You can use custom color for a task by adding the property
39 ;; `TIMELINE_FACE' with either a string which is a color name or a
40 ;; list which specifies the face properties or a symbol which is
41 ;; taken to be a face name.
42 
43 ;;; Code:
44 
45 (require 'dash)
46 
47 (require 'org-agenda)
48 
49 (defgroup org-timeline ()
50  "Graphical view of agenda in agenda buffer."
51  :group 'org
52  :prefix "org-timeline-")
53 
54 (defgroup org-timeline-faces ()
55  "Faces for org-timeline."
56  :group 'org-timeline)
57 
58 (defcustom org-timeline-default-duration 60
59  "Default event duration in minutes"
60  :type 'integer
61  :group 'org-timeline)
62 
63 (defcustom org-timeline-prepend nil
64  "Option to prepend the timeline to the agenda."
65  :type 'boolean
66  :group 'org-timeline)
67 
68 (defcustom org-timeline-show-clocked t
69  "Option to show or hide clocked items."
70  :type 'boolean
71  :group 'org-timeline)
72 
73 (defcustom org-timeline-dedicated-clocked-line t
74  "Option to show clocked items in a dedicated line with 'group-name' '$'."
75  :type 'boolean
76  :group 'org-timeline)
77 
78 (defcustom org-timeline-overlap-in-new-line nil
79  "Option to create new lines for blocks that would otherwise overlap."
80  :type 'boolean
81  :group 'org-timeline)
82 
83 (defcustom org-timeline-emphasize-next-block nil
84  "Option to apply the face `org-timeline-next-block' to the next block happening today."
85  :type 'boolean
86  :group 'org-timeline)
87 
88 (defcustom org-timeline-show-text-in-blocks nil
89  "Option to show the text of the event in the block.
90 
91 If the item has a property `TIMELINE_TEXT', use this as a title.
92 Otherwise, the title will be the item's headline, stripped of its todo state."
93  :type 'boolean
94  :group 'org-timeline)
95 
96 (defcustom org-timeline-beginning-of-day-hour 5
97  "When the timeline begins.
98 
99 Due to the way 'org-agenda' works, if you set this to any other value than 0
100 \(e.g. 5), then events that happen after midnight will not appear (even though
101 the timeline shows the slots).
102 If you view the agenda in week mode, those events will not appear in any of
103 the week's day.
104 
105 The workaround for this in day view is to use `org-timeline-keep-elapsed' that
106 will make the timeline show you a 24h cycle. See this variable's documentation
107 for more information."
108  :type 'integer
109  :group 'org-timeline)
110 
111 (defcustom org-timeline-keep-elapsed -1
112  "In day view, for today, keep only this number of fully elapsed hours.
113 
114 For negative values, do not hide elapsed hours.
115 
116 This can be used to see a rolling 24h cycle in the timeline.
117 In order to do that, set `org-timeline-beginning-of-day-hour' to 0, and set
118 `org-timeline-keep-elapsed' to any positive number.
119 Set `org-agenda-span' to 2, and open the day agenda view for today.
120 You will see a rolling 24h cycle, starting `org-timeline-keep-elapsed' hours ago."
121  :type 'integer
122  :group 'org-timeline)
123 
124 (defcustom org-timeline-insert-before-text "\u275A"
125  "String inserted before the block's text.
126 
127 It makes consecutive blocks distinct.
128 
129 The default value '\u275A' is a heavy vertical bar ❚."
130  :type 'string
131  :group 'org-timeline)
132 
133 (defvar org-timeline-first-line-in-agenda-buffer 0
134  "Line number of the first line of the timeline in the agenda buffer.")
135 
136 (defvar org-timeline-height 0
137  "Final height of the timeline.")
138 
139 (defvar org-timeline-current-info nil
140  "Current displayed info. Used to fix flickering of info.")
141 
142 (defvar org-timeline-slotline (concat (mapconcat 'not (number-sequence 0 24) "| ") "|")
143  "The undecorated slotline string.")
144 
145 (defvar org-timeline-next-task-today nil
146  "The next task happening today.")
147 
148 (cl-defstruct org-timeline-task
149  id
150  beg ; in minutes
151  end ; in minutes
152  offset-beg ; in points
153  offset-end ; in points
154  info ; copy of the agenda buffer's line
155  line-in-agenda-buffer
156  face
157  day ; absolute, see `calendar-absolute-from-gregorian'
158  type ; "scheduled", "clocked" ...
159  text
160  group-name
161  do-not-overlap-p ; make sure this block doesn't overlap with any other
162  )
163 
164 
165 (defface org-timeline-block
166  '((t (:inherit secondary-selection)))
167  "Face used for printing blocks with time range information.
168 
169 These are blocks that are scheduled for specific time range or
170 have an active timestamp with a range."
171  :group 'org-timeline-faces)
172 
173 (defface org-timeline-elapsed
174  '((t (:inherit region)))
175  "Face used for highlighting clocked items."
176  :group 'org-timeline-faces)
177 
178 (defface org-timeline-clocked
179  '((t (:inherit highlight)))
180  "Face used for printing clocked blocks.
181 
182 Clocked blocks appear in the agenda when `org-agenda-log-mode' is
183 activated."
184  :group 'org-timeline-faces)
185 
186 (defface org-timeline-overlap
187  '((t (:background "dark red")))
188  "Face used for printing overlapping blocks."
189  :group 'org-timeline-faces)
190 
191 (defface org-timeline-next-block
192  '((t (:background "dark olive green")))
193  "Face used for printing the next block happening today.
194 
195 Used when `org-timeline-emphasize-next-block' is non-nil."
196  :group 'org-timeline-faces)
197 
198 
199 (defmacro org-timeline-with-each-line (&rest body)
200  "Execute BODY on each line in buffer."
201  (declare (indent 0)
202  (debug (body)))
203  `(save-excursion
204  (goto-char (point-min))
205  ,@body
206  (while (= (forward-line) 0)
207  ,@body)))
208 
209 (defun org-timeline--get-face (type)
210  "Get the face with which to draw the current block, according to TYPE."
211  (--if-let (org-entry-get (org-get-at-bol 'org-marker) "TIMELINE_FACE" t)
212  (let ((read-face (car (read-from-string it))))
213  (if (stringp read-face)
214  (list :background read-face)
215  read-face))
216  (if (string= type "clock")
217  (list 'org-timeline-clocked)
218  (list 'org-timeline-block))))
219 
220 (defun org-timeline--get-block-text ()
221  "Get the text to print inside the current block."
222  (let ((item-marker (org-get-at-bol 'org-marker)))
223  (--if-let (org-entry-get item-marker "TIMELINE_TEXT" t)
224  it
225  (with-current-buffer (marker-buffer item-marker)
226  (save-excursion
227  (goto-char item-marker)
228  (outline-previous-heading)
229  (org-element-property :raw-value (org-element-context)))))))
230 
231 (defun org-timeline--get-group-name (type)
232  "Get the current block's 'group-name' according to TYPE.
233 
234 The first three chars will be printed at the beginning of the block's line."
235  (--if-let (org-entry-get (org-get-at-bol 'org-marker) "TIMELINE_GROUP" t)
236  (if (< (length it) 3)
237  (concat (substring " " 0 (- 3 (length it))) it)
238  (substring it 0 3))
239  (if (and (string= type "clock") org-timeline-dedicated-clocked-line)
240  " $"
241  " ")))
242 
243 (defun org-timeline--get-do-not-overlap (type)
244  "Whether the current block is allowed to overlap in the timeline according to TYPE."
245  (--if-let (org-entry-get (org-get-at-bol 'org-marker) "TIMELINE_DO_NOT_OVERLAP" t)
246  it
247  (if (and (not (string= type "clock")) org-timeline-overlap-in-new-line)
248  t
249  nil)))
250 
251 (defun org-timeline--overlapping-at-point (task)
252  "List of points where an already drawn blocks would overlap with TASK."
253  (save-excursion
254  (let (overlap-points)
255  (goto-char (+ (line-beginning-position) (org-timeline-task-offset-beg task)))
256  (while (and (<= (point) (+ (line-beginning-position) (org-timeline-task-offset-end task)))
257  (< (point) (point-max)))
258  (when (get-text-property (point) 'org-timeline-occupied)
259  (push (point) overlap-points))
260  (forward-char))
261  overlap-points)))
262 
263 (defun org-timeline--new-overlap-line-required-at-point-p (task)
264  "Whether a new overlap line needs to be created to insert TASK."
265  (let* ((overlapping (org-timeline--overlapping-at-point task))
266  (overlapping-blocks-that-do-not-overlap
267  (delq nil (mapcar (lambda (point) (get-text-property point 'org-timeline-do-not-overlap)) overlapping))))
268  (and (not (eq overlapping nil))
269  (or (org-timeline-task-do-not-overlap-p task)
270  (not (eq overlapping-blocks-that-do-not-overlap nil))))))
271 
272 (defun org-timeline--add-elapsed-face (string)
273  "Add `org-timeline-elapsed' to STRING's elapsed portion.
274 
275 Return new copy of STRING."
276  (let* ((string-copy (copy-sequence string))
277  (start-offset (* org-timeline-beginning-of-day-hour 60))
278  (current-time (+ (* 60 (string-to-number (format-time-string "%H")))
279  (string-to-number (format-time-string "%M"))))
280  (current-offset (/ (- current-time start-offset) 10)))
281  (when (< 0 current-offset)
282  (put-text-property 0 (+ 1 current-offset) 'font-lock-face (list 'org-timeline-elapsed) string-copy))
283  string-copy))
284 
285 (defun org-timeline--kill-info ()
286  "Kill the info line."
287  (save-excursion
288  (goto-line org-timeline-first-line-in-agenda-buffer)
289  (while (and (not (get-text-property (point) 'org-timeline-info-line))
290  (eq (forward-line) 0)))
291  (unless (eq (point) (point-max)) ; info line not found
292  (let ((inhibit-read-only t))
293  (kill-whole-line)))))
294 
295 (defun org-timeline--decorate-info (info)
296  "Make INFO string clickable."
297  (let ((info-keymap (make-sparse-keymap)))
298  (define-key info-keymap [mouse-1] 'org-agenda-goto)
299  (define-key info-keymap [mouse-2] 'org-find-file-at-mouse)
300  (propertize info 'keymap info-keymap
301  'help-echo "mouse-1 jump to org file"
302  'org-timeline-info-line t)))
303 
304 (defun org-timeline--draw-new-info (win info)
305  "Displays INFO about a hovered block.
306 
307 WIN is the agenda buffer's window."
308  (unless (eq info org-timeline-current-info) ; prevents flickering
309  (setq org-timeline-current-info info)
310  (save-window-excursion
311  (save-excursion
312  (select-window win) ; because one can hover blocks without being in the agenda window.
313  (org-timeline--kill-info)
314  (goto-line org-timeline-first-line-in-agenda-buffer)
315  (forward-line (- org-timeline-height 2))
316  (let ((inhibit-read-only t))
317  (insert (org-timeline--decorate-info info) "\n"))))))
318 
319 (defun org-timeline--move-to-task-in-agenda-buffer ()
320  "Move to a block's correponding task in the agenda buffer."
321  (interactive)
322  (let ((line (get-text-property (point) 'org-timeline-task-line)))
323  (when org-timeline-prepend
324  (setq line (+ line org-timeline-height -1)))
325  (goto-line line)
326  (search-forward (get-text-property (point) 'time)))) ; makes point more visible to user.
327 
328 (defun org-timeline--list-tasks ()
329  "Build the list of tasks to display."
330  (let* ((tasks nil)
331  (id 0)
332  (start-offset (* org-timeline-beginning-of-day-hour 60))
333  (current-time (+ (* 60 (string-to-number (format-time-string "%H")))
334  (string-to-number (format-time-string "%M")))))
335  (org-timeline-with-each-line
336  (-when-let* ((time-of-day (org-get-at-bol 'time-of-day))
337  (marker (org-get-at-bol 'org-marker))
338  (type (org-get-at-bol 'type))
339  (duration (or (org-get-at-bol 'duration)
340  org-timeline-default-duration)))
341  (when (member type (list "past-scheduled" "scheduled" "clock" "timestamp"))
342  (when (and (numberp duration)
343  (< duration 0))
344  (cl-incf duration 1440))
345  (let* ((hour (/ time-of-day 100))
346  (minute (mod time-of-day 100))
347  (beg (+ (* hour 60) minute))
348  (end (round (+ beg duration))))
349  (setq beg (max beg start-offset))
350  (setq end (min end (+ start-offset (* 24 60))))
351  (setq duration (- end beg))
352  (when (eq end (* 24 60)) (cl-incf end -1)) ; FIXME fixes a bug that shouldn't happen (crash when events end at midnight).
353  (when (and (>= end start-offset)
354  (<= beg (+ start-offset (* 24 60)))
355  (or org-timeline-show-clocked
356  (not (string= type "clock"))))
357  (push (make-org-timeline-task
358  :id id
359  :beg beg
360  :end end
361  :offset-beg (+ 5 (- (/ beg 10) (* 6 org-timeline-beginning-of-day-hour)))
362  :offset-end (+ 5 (- (/ end 10) (* 6 org-timeline-beginning-of-day-hour)))
363  :info (buffer-substring (line-beginning-position) (line-end-position))
364  :line-in-agenda-buffer (line-number-at-pos)
365  :face (org-timeline--get-face type)
366  :day (org-get-at-bol 'day)
367  :type type
368  :text (org-timeline--get-block-text)
369  :group-name (org-timeline--get-group-name type)
370  :do-not-overlap-p (org-timeline--get-do-not-overlap type)
371  )
372  tasks)
373  (cl-incf id))))))
374  ;; find the next task
375  (setq org-timeline-next-task nil)
376  (dolist (task tasks)
377  (let* ((beg (org-timeline-task-beg task))
378  (end (org-timeline-task-end task))
379  (today (calendar-absolute-from-gregorian (calendar-current-date)))
380  (is-today (eq today (org-timeline-task-day task)))
381  (is-now (and (<= beg current-time)
382  (>= end current-time)))
383  (is-after (> beg current-time))
384  (is-closer-to-now (and is-after
385  (or (eq org-timeline-next-task nil)
386  (< beg (org-timeline-task-beg org-timeline-next-task))))))
387  (when (and is-today (or is-now is-closer-to-now))
388  (setq org-timeline-next-task task))))
389  ;; change the next task's face
390  (when (and org-timeline-emphasize-next-block
391  org-timeline-next-task)
392  (dolist (task tasks)
393  (when (eq (org-timeline-task-id task) (org-timeline-task-id org-timeline-next-task))
394  (setf (org-timeline-task-face task) (list 'org-timeline-next-block)))))
395  (nreverse tasks)))
396 
397 (defun org-timeline--goto-block-position (task)
398  "Go to TASK's block's line and position cursor in line...
399 
400 Return t if this task will overlap another one when inserted."
401  (let* ((slotline (org-timeline--add-elapsed-face org-timeline-slotline))
402  (offset-beg (org-timeline-task-offset-beg task))
403  (offset-end (org-timeline-task-offset-end task))
404  (day (org-timeline-task-day task))
405  (group-name (org-timeline-task-group-name task))
406  (do-not-overlap (org-timeline-task-do-not-overlap-p task)))
407  (goto-char 1)
408  (while (and (not (eq (get-text-property (point) 'org-timeline-day) day))
409  (not (eq (forward-line) 1))))
410  (unless (eq (get-text-property (point) 'org-timeline-day) day)
411  (insert (concat "\n" ; creating the necessary lines, up to the current task's day
412  (mapconcat (lambda (line-day)
413  (propertize (concat (calendar-day-name (mod line-day 7) t t) ; found in https://github.com/deopurkar/org-timeline
414  " "
415  slotline)
416  'org-timeline-day line-day 'org-timeline-group-name " "))
417  (if-let ((last-day (get-text-property (line-beginning-position) 'org-timeline-day)))
418  (number-sequence (+ 1 last-day) day)
419  (list day))
420  "\n"))))
421  ;; cursor is now at beginning of the task's day's first line
422  (while (and (not (string= (get-text-property (point) 'org-timeline-group-name) group-name))
423  (eq (get-text-property (point) 'org-timeline-day) day))
424  (forward-line))
425  (unless (string= (-if-let (group-here (get-text-property (point) 'org-timeline-group-name)) group-here " ") group-name)
426  (when (not (eq (line-end-position) (point-max))) (forward-line -1))
427  (goto-char (line-end-position))
428  (insert "\n"
429  (propertize (concat group-name " " slotline) 'org-timeline-day day 'org-timeline-group-name group-name)))
430  ;; cursor is now at beginning of the task's group's first line
431  (let ((new-overlap-line-required-flag (org-timeline--new-overlap-line-required-at-point-p task)))
432  (while (and (org-timeline--new-overlap-line-required-at-point-p task)
433  (eq (get-text-property (point) 'org-timeline-day) day)
434  (eq (get-text-property (point) 'org-timeline-group-name) group-name)
435  (not (eq (line-end-position) (point-max))))
436  (setq new-overlap-line-required-flag t)
437  (forward-line))
438  (let ((decorated-slotline (propertize (concat " " " " slotline)
439  'org-timeline-day day
440  'org-timeline-group-name group-name)))
441  (when new-overlap-line-required-flag
442  (end-of-line)
443  (insert "\n" decorated-slotline))))
444  ;; cursor is now placed on the right line, at the right position.
445  (goto-char (+ (line-beginning-position) offset-beg))))
446 
447 (defun org-timeline--make-basic-block (task)
448  "Make TASK's block and return it as a propertized string.
449 
450 This does not take the block's context (e.g. overlap) into account."
451  (let* ((blank-block (mapconcat 'not (number-sequence 1 24) " "))
452  (id (org-timeline-task-id task))
453  (offset-beg (org-timeline-task-offset-beg task))
454  (offset-end (org-timeline-task-offset-end task))
455  (info (org-timeline-task-info task))
456  (face (org-timeline-task-face task))
457  (line (org-timeline-task-line-in-agenda-buffer task))
458  (group-name (org-timeline-task-group-name task))
459  (do-not-overlap (org-timeline-task-do-not-overlap-p task))
460  (move-to-task-map '(keymap mouse-1 . org-timeline--move-to-task-in-agenda-buffer))
461  (block-length (- offset-end offset-beg))
462  (props (list 'font-lock-face face
463  'org-timeline-occupied t
464  'org-timeline-do-not-overlap do-not-overlap
465  'org-timeline-task-id id
466  'org-timeline-group-name group-name
467  'mouse-face '(:highlight t :box t)
468  'keymap move-to-task-map
469  'task-info info
470  'help-echo (lambda (w obj pos) ; called on block hover
471  (org-timeline--draw-new-info w info)
472  info)
473  'org-timeline-task-line line))
474  (title (concat org-timeline-insert-before-text
475  (org-timeline-task-text task)
476  blank-block))
477  (block (if org-timeline-show-text-in-blocks
478  title
479  blank-block)))
480  (add-text-properties 0 block-length props block)
481  (substring block 0 block-length)))
482 
483 (defun org-timeline--make-and-insert-block (task)
484  "Insert the TASK's block at the right position in the timeline.
485 
486 Changes the block's face according to context."
487  (org-timeline--goto-block-position task)
488  (let ((overlapp (not (eq (org-timeline--overlapping-at-point task) nil)))
489  (is-next (if (not (eq org-timeline-next-task nil))
490  (eq (org-timeline-task-id task) (org-timeline-task-id org-timeline-next-task))
491  nil))
492  (block (org-timeline--make-basic-block task)))
493  (when overlapp (setq block (propertize block 'font-lock-face (list 'org-timeline-overlap))))
494  (when is-next (setq block (propertize block 'font-lock-face (list 'org-timeline-next-block))))
495  (unless (get-text-property (- (point) 1) 'org-timeline-overline)
496  (add-text-properties 0 (length block)
497  (list 'org-timeline-overline t
498  'font-lock-face (append (get-text-property 0 'font-lock-face block) '((:overline t)))
499  'mouse-face (append (get-text-property 0 'mouse-face block) '((:overline t))))
500  block))
501  (setq block (substring block 0 (min (length block) (- (line-end-position) (point)))))
502  (delete-char (length block))
503  (insert block)))
504 
505 (defun org-timeline--merge-for-24h-cycle ()
506  "Kill elapsed columns in day's line according to `org-timeline-keep-elapsed'.
507 
508 Move tomorrow's line to the right of today's line, to show a complete 24h cycle.
509 See the documentation of `org-timeline-keep-elapsed' for more information."
510  ;; FIXME: quite hacky. This should probably be done directly when making the tasks list,
511  ;; maybe by making all those events happen the same fake '0' day and change the offsets accordingly.
512  (let* ((today (calendar-absolute-from-gregorian (calendar-current-date)))
513  (current-hour (string-to-number (format-time-string "%H")))
514  (current-time (+ (* 60 current-hour)
515  (string-to-number (format-time-string "%M"))))
516  (elapsed-hours (- (floor (/ current-time 60)) org-timeline-beginning-of-day-hour))
517  (number-of-columns-tomorrow (max 0 (- elapsed-hours org-timeline-keep-elapsed)))
518  (number-of-columns-today (- 24 number-of-columns-tomorrow))
519  (hourline-piece (delete-and-extract-region 6 (+ 6 (* 6 number-of-columns-tomorrow))))
520  (today-line-pieces nil)
521  (tomorrow-line-pieces nil)
522  (cycle-offset (* 6 (- (max org-timeline-beginning-of-day-hour (- current-hour org-timeline-keep-elapsed)) org-timeline-beginning-of-day-hour)))
523  (blank-today-line-piece (concat " " (substring (org-timeline--add-elapsed-face org-timeline-slotline)
524  cycle-offset
525  (+ cycle-offset (* 6 number-of-columns-today)))
526  "|"))
527  (blank-tomorrow-line-piece (concat " " (substring org-timeline-slotline 0 (* 6 number-of-columns-tomorrow)))))
528  (goto-char 1)
529  (goto-char (line-end-position))
530  (insert hourline-piece)
531  ;; build (today|tomorrow)-line-pieces lists.
532  (while (not (eq (line-end-position) (point-max)))
533  (forward-line)
534  (let* ((lbeg (line-beginning-position))
535  (lend (line-end-position))
536  (today-portion (concat (buffer-substring lbeg (+ lbeg 4))
537  (buffer-substring (- lend (* 6 number-of-columns-today) 1) lend)))
538  (tomorrow-portion (buffer-substring (+ 5 lbeg) (+ 5 lbeg (* 6 number-of-columns-tomorrow)))))
539  (when (eq (get-text-property lbeg 'org-timeline-day) today)
540  (setq today-line-pieces (append today-line-pieces (list today-portion))))
541  (when (eq (get-text-property lbeg 'org-timeline-day) (+ today 1))
542  (setq tomorrow-line-pieces (append tomorrow-line-pieces (list tomorrow-portion))))))
543  ;; handle groups and balance lines
544  ;; FIXME: not efficient, doesn't jump once group done
545  ;; (print "today")
546  ;; (dolist (line today-line-pieces) (print line))
547  ;; (print "tomorrow")
548  ;; (dolist (line tomorrow-line-pieces) (print line))
549  (let (groups-handled)
550  (dotimes (i (length today-line-pieces))
551  (let* ((group-handled (get-text-property 0 'org-timeline-group-name (seq-elt today-line-pieces i)))
552  (group-handled-p (lambda (piece) (string= (get-text-property 1 'org-timeline-group-name piece) group-handled)))
553  (prev-pieces-today (seq-take today-line-pieces i))
554  (next-pieces-today (seq-drop today-line-pieces i))
555  (same-group-pieces-today (seq-filter group-handled-p next-pieces-today))
556  (rest-of-pieces-today (seq-remove group-handled-p next-pieces-today))
557  (prev-pieces-tomorrow (seq-take tomorrow-line-pieces i))
558  (next-pieces-tomorrow (seq-drop tomorrow-line-pieces i))
559  (same-group-pieces-tomorrow (seq-filter group-handled-p next-pieces-tomorrow))
560  (rest-of-pieces-tomorrow (seq-remove group-handled-p next-pieces-tomorrow)))
561  ;; balance groups
562  (let* ((line-diff (- (length same-group-pieces-tomorrow) (length same-group-pieces-today)))
563  (number-of-blank-lines-to-add-today (max 0 line-diff))
564  (number-of-blank-lines-to-add-tomorrow (max 0 (- 0 line-diff))))
565  (dotimes (n number-of-blank-lines-to-add-today)
566  (setq same-group-pieces-today (append same-group-pieces-today (list blank-today-line-piece))))
567  (dotimes (n number-of-blank-lines-to-add-tomorrow)
568  (setq same-group-pieces-tomorrow (append same-group-pieces-tomorrow (list blank-tomorrow-line-piece)))))
569  ;; rebuild the pieces lists
570  (setq today-line-pieces (append prev-pieces-today same-group-pieces-today rest-of-pieces-today))
571  (setq tomorrow-line-pieces (append prev-pieces-tomorrow same-group-pieces-tomorrow rest-of-pieces-tomorrow))))
572  (let* ((unhandled-groups-tomorrow (seq-drop tomorrow-line-pieces (length today-line-pieces))))
573  (dolist (piece unhandled-groups-tomorrow)
574  (if (member (get-text-property 0 'org-timeline-group-name piece) groups-handled)
575  (setq today-line-pieces (append today-line-pieces (list blank-today-line-piece)))
576  (setq today-line-pieces (append today-line-pieces (list (concat (get-text-property 0 'org-timeline-group-name piece)
577  (substring blank-today-line-piece 3 nil))))))
578  (push (get-text-property 0 'org-timeline-group-name piece) groups-handled))))
579  ;; (print "today")
580  ;; (dolist (line today-line-pieces) (print line))
581  ;; (print "tomorrow")
582  ;; (dolist (line tomorrow-line-pieces) (print line))
583  ;; insert them
584  (goto-char 1)
585  (let ((hourline (buffer-substring 1 (line-end-position))))
586  (delete-region (point-min) (point-max))
587  (insert hourline))
588  (dolist (piece today-line-pieces)
589  (insert "\n" piece))
590  (goto-line 2)
591  (dolist (piece tomorrow-line-pieces)
592  (goto-char (line-end-position))
593  (insert piece)
594  (forward-line))
595  ;; remove elapsed face from tomorrow lines
596  (goto-char 1)
597  (put-text-property (+ 5 (* 6 number-of-columns-today)) (line-end-position) 'face nil)
598  (while (and (eq (forward-line) 0)
599  (not (eq (point) (point-max))))
600  (forward-char (+ 5 (* 6 number-of-columns-today)))
601  (dotimes (i (- (line-end-position) (point)))
602  (when (not (get-text-property (point) 'org-timeline-occupied))
603  (put-text-property (point) (+ (point) 1) 'face nil))
604  (forward-char)))))
605 
606 ;; Some ideas for the the generation of the timeline were inspired by the
607 ;; forked repo: https://github.com/deopurkar/org-timeline.
608 (defun org-timeline--generate-timeline ()
609  "Generate the timeline string that will represent current agenda view."
610  (let* ((hourline (concat " "
611  (org-timeline--add-elapsed-face
612  (concat "|"
613  (mapconcat (lambda (x) (format "%02d:00" (mod x 24)))
614  (number-sequence org-timeline-beginning-of-day-hour (+ org-timeline-beginning-of-day-hour 23))
615  "|")
616  "|"))))
617  (tasks (org-timeline--list-tasks))
618  (today (calendar-absolute-from-gregorian (calendar-current-date)))
619  (today-onlyp (eq 0 (length (delq nil (mapcar (lambda (task) (if (eq (org-timeline-task-day task) today) nil task)) tasks)))))
620  (today-or-tomorrow-only-p (eq 0 (length (delq nil (mapcar (lambda (task) (if (member (org-timeline-task-day task) `(,today ,(+ today 1))) nil task)) tasks))))))
621  (with-temp-buffer
622  (insert hourline)
623  (dolist (task tasks)
624  ;; (print (buffer-substring (point-min) (point-max)))
625  (org-timeline--make-and-insert-block task))
626  ;; (print (buffer-substring (point-min) (point-max)))
627  (when (and (>= org-timeline-keep-elapsed 0)
628  today-or-tomorrow-only-p
629  (> (length tasks) 0))
630  (org-timeline--merge-for-24h-cycle))
631  ;; display the next block's info
632  (goto-char (point-max))
633  (unless (eq (length tasks) 0)
634  (insert "\n"
635  (if (eq org-timeline-next-task nil)
636  (propertize " no incoming event" 'org-timeline-info-line t)
637  (org-timeline--decorate-info (org-timeline-task-info org-timeline-next-task)))))
638  (buffer-string))))
639 
640 ;;;###autoload
641 (defun org-timeline-insert-timeline ()
642  "Insert graphical timeline into agenda buffer."
643  (unless (buffer-narrowed-p)
644  (goto-char (point-min))
645  (unless org-timeline-prepend
646  (while (and (eq (get-text-property (line-beginning-position) 'org-agenda-type) 'agenda)
647  (not (eobp)))
648  (forward-line)))
649  (forward-line)
650  (let ((inhibit-read-only t))
651  (setq org-timeline-first-line-in-agenda-buffer (line-number-at-pos))
652  (insert (propertize (concat (make-string (window-width) ?─)) 'face 'org-time-grid) "\n")
653  (insert (org-timeline--generate-timeline))
654  (insert (propertize (concat "\n" (make-string (window-width) ?─)) 'face 'org-time-grid 'org-timeline-end t) "\n")
655  (setq org-timeline-height (- (line-number-at-pos) org-timeline-first-line-in-agenda-buffer)))
656  ;; enable `font-lock-mode' in agenda view to display the "chart"
657  (font-lock-mode)))
658 
659 (provide 'org-timeline)
660 ;;; org-timeline.el ends here