├── .gitignore ├── img └── timeline1.png ├── .github └── FUNDING.yml ├── Cask ├── .travis.yml ├── tests ├── org-timeline-test-helper.el └── org-timeline-test.el ├── README.md └── org-timeline.el /.gitignore: -------------------------------------------------------------------------------- 1 | .cask 2 | -------------------------------------------------------------------------------- /img/timeline1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fuco1/org-timeline/HEAD/img/timeline1.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [Fuco1] 4 | patreon: matusgoljer 5 | custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A5PMGVKCQBT88 6 | -------------------------------------------------------------------------------- /Cask: -------------------------------------------------------------------------------- 1 | (source melpa) 2 | (source org) 3 | 4 | (package "org-timeline" "0.3.0" "Add graphical view of agenda to agenda buffer") 5 | 6 | (depends-on "dash" "2.13.0") 7 | (depends-on "org-plus-contrib") 8 | 9 | (development 10 | (depends-on "undercover") 11 | (depends-on "buttercup")) 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: emacs-lisp 2 | sudo: false 3 | env: 4 | - EVM_EMACS=emacs-24.4-travis 5 | - EVM_EMACS=emacs-24.5-travis 6 | - EVM_EMACS=emacs-25.3-travis 7 | - EVM_EMACS=emacs-26.1-travis 8 | - EVM_EMACS=emacs-git-snapshot-travis 9 | before_install: 10 | - curl -fsSkL https://gist.github.com/rejeep/ebcd57c3af83b049833b/raw > x.sh && source ./x.sh 11 | - evm install $EVM_EMACS --use --skip 12 | - cask install 13 | script: 14 | - cask exec buttercup -L . -L tests 15 | 16 | matrix: 17 | fast_finish: true 18 | allow_failures: 19 | - env: EVM_EMACS=emacs-git-snapshot-travis 20 | -------------------------------------------------------------------------------- /tests/org-timeline-test-helper.el: -------------------------------------------------------------------------------- 1 | ;; -*- lexical-binding: t -*- 2 | 3 | (require 'org-timeline) 4 | 5 | (defmacro org-timeline-test-helper-with-agenda (agenda start-date &rest forms) 6 | (declare (indent 1) 7 | (debug (form form body))) 8 | (let ((org-file (make-symbol "org-file"))) 9 | `(progn 10 | (let* ((,org-file (make-temp-file "org-timeline")) 11 | (org-agenda-files (list ,org-file)) 12 | (org-agenda-start-day ,start-date) 13 | (org-agenda-span 'day)) 14 | (unwind-protect 15 | (progn 16 | (with-temp-file ,org-file 17 | (insert ,agenda)) 18 | (with-current-buffer (find-file-noselect ,org-file) 19 | (org-mode) 20 | (org-agenda nil "a")) 21 | (with-current-buffer org-agenda-buffer 22 | ,@forms)) 23 | (delete-file ,org-file)))))) 24 | 25 | (provide 'org-timeline-test-helper) 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # org-timeline [![Build Status](https://travis-ci.org/Fuco1/org-timeline.svg?branch=master)](https://travis-ci.org/Fuco1/org-timeline) 2 | 3 | Add graphical view of agenda to agenda buffer. 4 | 5 | ![Preview](./img/timeline1.png) 6 | 7 | # Installation 8 | 9 | After you install this package from MELPA Stable, add the following line to your org configuration: 10 | 11 | ``` emacs-lisp 12 | (add-hook 'org-agenda-finalize-hook 'org-timeline-insert-timeline :append) 13 | ``` 14 | 15 | # How it works 16 | 17 | This package adds a graphical view of the agenda after the last agenda line. By default the display starts at 5 AM today and goes up to 4 AM next day (this covers 24 hours). 18 | 19 | Scheduled tasks or tasks with time ranges are rendered in the display with `org-timeline-block` face. Clocked entires are displayed in `org-timeline-clocked` face. The background of timeslots which are in the past is highlighted with `org-timeline-elapsed` face. 20 | 21 | You can use custom color for a task by adding the property `TIMELINE_FACE` with either a string which is a color name or a list which specifies the face properties or a symbol which is taken to be a face name. 22 | 23 | # TODO 24 | 25 | - [x] Add faces instead of colors 26 | - [X] Make "midnight"/change-of-day configurable (currently 5 AM) 27 | - [X] Add a tooltip showing the task description/name 28 | - [X] Make the blocks navigable to the task 29 | -------------------------------------------------------------------------------- /tests/org-timeline-test.el: -------------------------------------------------------------------------------- 1 | ;; -*- lexical-binding: t -*- 2 | 3 | (require 'org-timeline-test-helper) 4 | 5 | ;; (prin1 (buffer-substring-no-properties (point-min) (point-max))) 6 | 7 | (describe "org-timeline" 8 | 9 | 10 | (describe "when working with non-overlapping events" 11 | 12 | 13 | (it "should add scheduled item to the timeline" 14 | (org-timeline-test-helper-with-agenda 15 | "* TODO 16 | SCHEDULED: <2017-04-19 Wed 10:00-11:00>" 17 | "2017-04-19" 18 | (org-timeline-insert-timeline) 19 | (let* ((start (text-property-any (point-min) (point-max) 'occupied t)) 20 | (end (text-property-not-all start (point-max) 'occupied t))) 21 | (goto-char start) 22 | (expect (plist-get (text-properties-at (point)) 'font-lock-face) :to-be 'org-timeline-block) 23 | (save-excursion 24 | (previous-line) 25 | (expect (looking-at-p "|10:00") :to-be-truthy)) 26 | (expect (- end start) :to-be 6)))) 27 | 28 | (it "should add time-range item to the timeline" 29 | (org-timeline-test-helper-with-agenda 30 | "* TODO 31 | <2017-04-19 Wed 10:00-11:50>" 32 | "2017-04-19" 33 | (org-timeline-insert-timeline) 34 | (let* ((start (text-property-any (point-min) (point-max) 'occupied t)) 35 | (end (text-property-not-all start (point-max) 'occupied t))) 36 | (goto-char start) 37 | (expect (plist-get (text-properties-at (point)) 'font-lock-face) :to-be 'org-timeline-block) 38 | (save-excursion 39 | (previous-line) 40 | (expect (looking-at-p "|10:00") :to-be-truthy)) 41 | (expect (- end start) :to-be 11)))) 42 | 43 | (it "should add clocked item to the timeline in log mode" 44 | (org-timeline-test-helper-with-agenda 45 | "* TODO 46 | :CLOCK: 47 | CLOCK: [2017-04-18 Tue 20:59]--[2017-04-18 Tue 21:12] => 0:13 48 | :END:" 49 | "2017-04-18" 50 | (org-agenda-log-mode) 51 | (org-timeline-insert-timeline) 52 | (let* ((start (text-property-any (point-min) (point-max) 'occupied t)) 53 | (end (text-property-not-all start (point-max) 'occupied t))) 54 | (goto-char start) 55 | (expect (plist-get (text-properties-at (point)) 'font-lock-face) :to-be 'org-timeline-clocked) 56 | (save-excursion 57 | (previous-line) 58 | (expect (looking-at-p "0|21:00") :to-be-truthy)) 59 | (expect (- end start) :to-be 2))))) 60 | 61 | 62 | (describe "when working with overlapping events" 63 | 64 | (it "should add overlapping items to separate lines" 65 | (org-timeline-test-helper-with-agenda 66 | "* TODO 67 | SCHEDULED: <2017-04-19 Wed 10:00-11:00> 68 | * TODO 69 | SCHEDULED: <2017-04-19 Wed 10:30-11:30>" 70 | "2017-04-19" 71 | (org-timeline-insert-timeline) 72 | ;; (prin1 (buffer-substring-no-properties (point-min) (point-max))) 73 | (let* ((start (text-property-any (point-min) (point-max) 'occupied t)) 74 | (end (text-property-not-all start (point-max) 'occupied t))) 75 | (goto-char start) 76 | (expect (plist-get (text-properties-at (point)) 'font-lock-face) :to-be 'org-timeline-block) 77 | (save-excursion 78 | (previous-line) 79 | (expect (looking-at-p "|10:00") :to-be-truthy)) 80 | (expect (- end start) :to-be 6) 81 | (goto-char end)) 82 | (let* ((start (text-property-any (point) (point-max) 'occupied t)) 83 | (end (text-property-not-all start (point-max) 'occupied t))) 84 | (goto-char start) 85 | (expect (plist-get (text-properties-at (point)) 'font-lock-face) :to-be 'org-timeline-block) 86 | (save-excursion 87 | (previous-line) 88 | (previous-line) 89 | (expect (looking-at-p ":00|11:00") :to-be-truthy)) 90 | (expect (- end start) :to-be 6)))))) 91 | -------------------------------------------------------------------------------- /org-timeline.el: -------------------------------------------------------------------------------- 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 6 | ;; Maintainer: Matúš Goljer 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 . 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. The background of 37 | ;; timeslots which are in the past is highlighted with 38 | ;; `org-timeline-elapsed' face. 39 | 40 | ;; You can use custom color for a task by adding the property 41 | ;; `TIMELINE_FACE' with either a string which is a color name or a 42 | ;; list which specifies the face properties or a symbol which is 43 | ;; taken to be a face name. 44 | 45 | ;;; Code: 46 | 47 | (require 'dash) 48 | 49 | (require 'org-agenda) 50 | 51 | (defgroup org-timeline () 52 | "Graphical view of agenda in agenda buffer." 53 | :group 'org 54 | :prefix "org-timeline-") 55 | 56 | (defgroup org-timeline-faces () 57 | "Faces for org-timeline." 58 | :group 'org-timeline) 59 | 60 | (defcustom org-timeline-prepend nil 61 | "Option to prepend the timeline to the agenda." 62 | :type 'boolean 63 | :group 'org-timeline) 64 | 65 | (defcustom org-timeline-start-hour 5 66 | "Starting hour of the timeline." 67 | :type 'integer 68 | :group 'org-timeline) 69 | 70 | (defvar org-timeline-first-line 0 71 | "Computer first line of the timeline in the buffer.") 72 | 73 | (defvar org-timeline-height 0 74 | "Computed height (number of lines) of the timeline.") 75 | 76 | (defconst org-timeline-current-info nil 77 | "Current displayed info. Used to fix flickering of info.") 78 | 79 | (defface org-timeline-block 80 | '((t (:inherit secondary-selection))) 81 | "Face used for printing blocks with time range information. 82 | 83 | These are blocks that are scheduled for specific time range or 84 | have an active timestamp with a range." 85 | :group 'org-timeline-faces) 86 | 87 | (defface org-timeline-elapsed 88 | '((t (:inherit region))) 89 | "Face used for highlighting elapsed portion of the day." 90 | :group 'org-timeline-faces) 91 | 92 | (defface org-timeline-clocked 93 | '((t (:inherit highlight))) 94 | "Face used for printing clocked blocks. 95 | 96 | Clocked blocks appear in the agenda when `org-agenda-log-mode' is 97 | activated." 98 | :group 'org-timeline-faces) 99 | 100 | 101 | (defmacro org-timeline-with-each-line (&rest body) 102 | "Execute BODY on each line in buffer." 103 | (declare (indent 0) 104 | (debug (body))) 105 | `(save-excursion 106 | (goto-char (point-min)) 107 | ,@body 108 | (while (= (forward-line) 0) 109 | ,@body))) 110 | 111 | (defun org-timeline--get-face () 112 | "Get the face with which to draw the current block." 113 | (--if-let (org-entry-get (org-get-at-bol 'org-marker) "TIMELINE_FACE" t) 114 | (let ((read-face (car (read-from-string it)))) 115 | (if (stringp read-face) 116 | (list :background read-face) 117 | read-face)) 118 | (cond 119 | ((save-excursion 120 | (search-forward "Clocked:" (line-end-position) t)) 121 | 'org-timeline-clocked) 122 | (t 'org-timeline-block)))) 123 | 124 | (defun org-timeline--add-elapsed-face (string current-offset) 125 | "Add `org-timeline-elapsed' to STRING's elapsed portion. 126 | 127 | Return new copy of STRING." 128 | (let ((string-copy (copy-sequence string))) 129 | (when (< 0 current-offset) 130 | (put-text-property 0 current-offset 'font-lock-face 'org-timeline-elapsed string-copy)) 131 | string-copy)) 132 | 133 | (defun org-timeline--clear-info () 134 | "Clear the info line" 135 | (save-excursion 136 | (goto-line org-timeline-first-line) 137 | (forward-line (- org-timeline-height 1)) 138 | (let ((inhibit-read-only t)) 139 | (while (not (get-text-property (point) 'org-timeline-end)) 140 | (kill-whole-line))))) 141 | 142 | (defun org-timeline--hover-info (win txt) 143 | "Displays info about a hovered block" 144 | (unless (eq txt org-timeline-current-info) 145 | (setq org-timeline-current-info txt) 146 | (save-window-excursion 147 | (save-excursion 148 | (select-window win) 149 | (org-timeline--clear-info) 150 | (goto-line org-timeline-first-line) 151 | (forward-line (- org-timeline-height 1)) 152 | (let ((inhibit-read-only t)) 153 | (insert txt) 154 | (insert "\n")))))) 155 | 156 | (defun org-timeline--move-to-task () 157 | "Move to a blocks correponding task." 158 | (interactive 159 | (let ((line (get-text-property (point) 'org-timeline-task-line))) 160 | (org-timeline--clear-info) 161 | (when org-timeline-prepend 162 | (setq line (+ line org-timeline-height))) 163 | (goto-line line) 164 | (search-forward (get-text-property (point) 'time))))) 165 | 166 | (defun org-timeline--list-tasks () 167 | "Build the list of tasks to display." 168 | (let* ((tasks nil) 169 | (start-offset (* org-timeline-start-hour 60)) 170 | (current-time (+ (* 60 (string-to-number (format-time-string "%H"))) 171 | (string-to-number (format-time-string "%M"))))) 172 | (org-timeline-with-each-line 173 | (-when-let* ((time-of-day (org-get-at-bol 'time-of-day)) 174 | (marker (org-get-at-bol 'org-marker)) 175 | (type (org-get-at-bol 'type))) 176 | (when (member type (list "scheduled" "clock" "timestamp")) 177 | (let ((duration (org-get-at-bol 'duration)) 178 | (txt (buffer-substring (line-beginning-position) (line-end-position))) 179 | (line (line-number-at-pos))) 180 | (when (and (numberp duration) 181 | (< duration 0)) 182 | (cl-incf duration 1440)) 183 | (let* ((hour (/ time-of-day 100)) 184 | (minute (mod time-of-day 100)) 185 | (beg (+ (* hour 60) minute)) 186 | (end (if duration 187 | (round (+ beg duration)) 188 | current-time)) 189 | (face (org-timeline--get-face))) 190 | (when (>= beg start-offset) 191 | (push (list beg end face txt line) tasks))))))) 192 | (nreverse tasks))) 193 | 194 | (defun org-timeline--generate-timeline () 195 | "Generate the timeline string that will represent current agenda view." 196 | (let* ((start-offset (* org-timeline-start-hour 60)) 197 | (current-time (+ (* 60 (string-to-number (format-time-string "%H"))) 198 | (string-to-number (format-time-string "%M")))) 199 | (current-offset (/ (- current-time start-offset) 10)) 200 | (slotline (org-timeline--add-elapsed-face 201 | "| | | | | | | | | | | | | | | | | | | | | | | | |" 202 | current-offset)) 203 | (hourline (org-timeline--add-elapsed-face 204 | (concat "|" 205 | (mapconcat (lambda (x) (format "%02d:00" (mod x 24))) 206 | (number-sequence org-timeline-start-hour (+ org-timeline-start-hour 23)) 207 | "|") 208 | "|") 209 | current-offset)) 210 | (timeline (concat hourline "\n" slotline)) 211 | (tasks (org-timeline--list-tasks))) 212 | (cl-labels ((get-start-pos (current-line beg) (+ 1 (* current-line (1+ (length slotline))) (/ (- beg start-offset) 10))) 213 | (get-end-pos (current-line end) (+ 1 (* current-line (1+ (length slotline))) (/ (- end start-offset) 10)))) 214 | (let ((current-line 1) 215 | (move-to-task-map (make-sparse-keymap))) 216 | (define-key move-to-task-map [mouse-1] 'org-timeline--move-to-task) 217 | (with-temp-buffer 218 | (insert timeline) 219 | (-each tasks 220 | (-lambda ((beg end face txt line)) 221 | (while (get-text-property (get-start-pos current-line beg) 'org-timeline-occupied) 222 | (cl-incf current-line) 223 | (when (> (get-start-pos current-line beg) (point-max)) 224 | (save-excursion 225 | (goto-char (point-max)) 226 | (insert "\n" slotline)))) 227 | (let ((start-pos (get-start-pos current-line beg)) 228 | (end-pos (get-end-pos current-line end)) 229 | (props (list 'font-lock-face face 230 | 'org-timeline-occupied t 231 | 'mouse-face 'highlight 232 | 'keymap move-to-task-map 233 | 'txt txt 234 | 'help-echo (lambda (w obj pos) 235 | (org-timeline--hover-info w txt) 236 | txt) ;; the lambda will be called on block hover 237 | 'org-timeline-task-line line 238 | 'cursor-sensor-functions '(org-timeline--display-info)))) 239 | (add-text-properties start-pos end-pos props)) 240 | (setq current-line 1))) 241 | (buffer-string)))))) 242 | 243 | ;;;###autoload 244 | (defun org-timeline-insert-timeline () 245 | "Insert graphical timeline into agenda buffer." 246 | (unless (buffer-narrowed-p) 247 | (goto-char (point-min)) 248 | (unless org-timeline-prepend 249 | (while (and (eq (get-text-property (line-beginning-position) 'org-agenda-type) 'agenda) 250 | (not (eobp))) 251 | (forward-line))) 252 | (forward-line) 253 | (let ((inhibit-read-only t)) 254 | (cursor-sensor-mode 1) 255 | (setq org-timeline-first-line (line-number-at-pos)) 256 | (insert (org-timeline--generate-timeline)) 257 | (insert (propertize (concat "\n" (make-string (/ (window-width) 2) ?─)) 'face 'org-time-grid 'org-timeline-end t) "\n") 258 | (setq org-timeline-height (- (line-number-at-pos) org-timeline-first-line))) 259 | ;; enable `font-lock-mode' in agenda view to display the "chart" 260 | (font-lock-mode))) 261 | 262 | (provide 'org-timeline) 263 | ;;; org-timeline.el ends here 264 | --------------------------------------------------------------------------------