2018-10-02 17:12:10 +02:00
|
|
|
;; Frontend for taskwarrior
|
|
|
|
;;
|
|
|
|
;; TODO: Implement modeline indicator for deadline and entries
|
|
|
|
;; TODO: Update buffer if command modifies the state
|
2018-10-02 18:09:34 +02:00
|
|
|
;; TODO: Extract "1-1000" id filter into variable
|
2018-11-13 20:31:06 +01:00
|
|
|
;; TODO: Figure out the difference between assoc and assoc-string
|
|
|
|
;; (and why alist-get does not work with strings)
|
2018-12-20 21:04:56 +01:00
|
|
|
;; TODO: Restore position after taskwarrior-update-buffer is called
|
2018-10-02 17:12:10 +02:00
|
|
|
|
|
|
|
(require 'json)
|
|
|
|
|
|
|
|
(defgroup taskwarrior nil "An emacs frontend to taskwarrior.")
|
|
|
|
|
2018-12-20 21:04:56 +01:00
|
|
|
(defvar taskwarrior-buffer-name "*taskwarrior*")
|
|
|
|
|
2018-10-23 21:32:01 +02:00
|
|
|
(defconst taskwarrior-mutating-commands '("add" "modify"))
|
|
|
|
|
2018-10-02 17:12:10 +02:00
|
|
|
(defvar taskwarrior-description 'taskwarrior-description
|
|
|
|
"Taskwarrior mode face used for tasks with a priority of C.")
|
|
|
|
|
|
|
|
(setq taskwarrior-highlight-regexps
|
2019-05-20 15:20:34 +02:00
|
|
|
`(("^\\*.*$" . font-lock-variable-name-face)
|
|
|
|
("^ [0-9]*" . font-lock-variable-name-face)
|
|
|
|
("([0-9.]*?)" . font-lock-builtin-face)
|
|
|
|
("\\+[a-zA-Z0-9\\-_]+" . font-lock-doc-face)
|
|
|
|
("\\[.*\\]" . font-lock-preprocessor-face)
|
|
|
|
("[:space:].*:" . font-lock-function-name-face)))
|
2018-10-02 17:12:10 +02:00
|
|
|
|
|
|
|
(defvar taskwarrior-mode-map nil "Keymap for `taskwarrior-mode'")
|
|
|
|
(progn
|
|
|
|
(setq taskwarrior-mode-map (make-sparse-keymap))
|
2018-10-31 20:38:57 +01:00
|
|
|
(define-key taskwarrior-mode-map (kbd "p") 'taskwarrior-previous-task)
|
|
|
|
(define-key taskwarrior-mode-map (kbd "k") 'taskwarrior-previous-task)
|
|
|
|
(define-key taskwarrior-mode-map (kbd "n") 'taskwarrior-next-task)
|
|
|
|
(define-key taskwarrior-mode-map (kbd "j") 'taskwarrior-next-task)
|
2018-10-02 17:12:10 +02:00
|
|
|
(define-key taskwarrior-mode-map (kbd "q") 'quit-window)
|
2018-10-31 20:13:24 +01:00
|
|
|
(define-key taskwarrior-mode-map (kbd "e") 'taskwarrior-change-description)
|
2019-04-14 21:38:40 +02:00
|
|
|
(define-key taskwarrior-mode-map (kbd "U") 'taskwarrior-change-priority)
|
2019-01-22 21:09:09 +01:00
|
|
|
(define-key taskwarrior-mode-map (kbd "g") 'taskwarrior-update-buffer)
|
2018-10-02 17:12:10 +02:00
|
|
|
(define-key taskwarrior-mode-map (kbd "a") 'taskwarrior-add)
|
2018-11-13 21:17:43 +01:00
|
|
|
(define-key taskwarrior-mode-map (kbd "d") 'taskwarrior-done)
|
|
|
|
(define-key taskwarrior-mode-map (kbd "D") 'taskwarrior-delete)
|
2019-01-22 21:07:46 +01:00
|
|
|
(define-key taskwarrior-mode-map (kbd "m") 'taskwarrior-mark-task)
|
|
|
|
(define-key taskwarrior-mode-map (kbd "u") 'taskwarrior-unmark-task)
|
2018-12-04 18:42:26 +01:00
|
|
|
(define-key taskwarrior-mode-map (kbd "f") 'taskwarrior-filter)
|
2019-01-14 11:00:15 +01:00
|
|
|
(define-key taskwarrior-mode-map (kbd "r") 'taskwarrior-reset-filter)
|
2019-04-09 14:42:11 +02:00
|
|
|
(define-key taskwarrior-mode-map (kbd "RET") 'taskwarrior-info)
|
2018-10-31 20:38:57 +01:00
|
|
|
(define-key taskwarrior-mode-map (kbd "P") 'taskwarrior-change-project))
|
|
|
|
|
2019-05-08 20:06:13 +02:00
|
|
|
|
|
|
|
(defun test ()
|
|
|
|
(interactive)
|
|
|
|
(let ((line (thing-at-point 'line t)))
|
|
|
|
(string-match (rx "* 1"))
|
|
|
|
(string-trim-left (match-string 0 line))))
|
|
|
|
|
2018-10-31 20:38:57 +01:00
|
|
|
(defun taskwarrior--display-task-details-in-echo-area ()
|
|
|
|
(let* ((id (taskwarrior-id-at-point))
|
|
|
|
(task (taskwarrior-export-task id))
|
2018-10-31 21:08:46 +01:00
|
|
|
(due (taskwarrior--parse-timestamp (alist-get 'due task))))
|
2018-12-04 20:44:36 +01:00
|
|
|
(when due
|
|
|
|
(message "Due: %s" due))))
|
2018-10-31 20:38:57 +01:00
|
|
|
|
|
|
|
(defun taskwarrior-previous-task ()
|
|
|
|
(interactive)
|
|
|
|
(previous-line)
|
|
|
|
(taskwarrior--display-task-details-in-echo-area))
|
|
|
|
|
|
|
|
(defun taskwarrior-next-task ()
|
|
|
|
(interactive)
|
|
|
|
(next-line)
|
|
|
|
(taskwarrior--display-task-details-in-echo-area))
|
2018-10-02 17:12:10 +02:00
|
|
|
|
2019-01-22 21:07:46 +01:00
|
|
|
(defun taskwarrior-unmark-task ()
|
|
|
|
(interactive)
|
|
|
|
(let ((id (taskwarrior-id-at-point)))
|
|
|
|
(if (local-variable-p 'taskwarrior-marks)
|
|
|
|
(setq-local taskwarrior-marks (remove id taskwarrior-marks)))))
|
|
|
|
|
|
|
|
(defun taskwarrior-mark-task ()
|
|
|
|
(interactive)
|
|
|
|
(let ((id (taskwarrior-id-at-point)))
|
2019-05-08 20:06:13 +02:00
|
|
|
(progn
|
|
|
|
(if (local-variable-p 'taskwarrior-marks)
|
|
|
|
(setq-local taskwarrior-marks (delete-dups (cons id taskwarrior-marks)))
|
|
|
|
(setq-local taskwarrior-marks (list id))))
|
|
|
|
(save-excursion
|
|
|
|
(read-only-mode -1)
|
|
|
|
(beginning-of-line)
|
|
|
|
(insert "*")
|
|
|
|
(read-only-mode 1))))
|
|
|
|
|
2019-01-22 21:07:46 +01:00
|
|
|
|
2019-04-09 14:42:11 +02:00
|
|
|
(defun taskwarrior-info ()
|
|
|
|
(interactive)
|
|
|
|
(let* ((id (taskwarrior-id-at-point))
|
|
|
|
(buf (get-buffer-create "*taskwarrior info*")))
|
|
|
|
(progn
|
|
|
|
(switch-to-buffer-other-window buf)
|
|
|
|
(insert (taskwarrior--shell-command "info" "" id)))))
|
|
|
|
|
2018-10-02 17:12:10 +02:00
|
|
|
(defun taskwarrior-id-at-point ()
|
|
|
|
(let ((line (thing-at-point 'line t)))
|
2019-05-08 20:06:13 +02:00
|
|
|
(string-match "^ [0-9]*" line)
|
|
|
|
(string-trim-left (match-string 0 line))))
|
2018-10-02 17:12:10 +02:00
|
|
|
|
2018-12-04 20:08:17 +01:00
|
|
|
(defun taskwarrior--get-filter-as-string ()
|
|
|
|
(if (local-variable-p 'taskwarrior-active-filters)
|
|
|
|
(mapconcat 'identity taskwarrior-active-filters " ")
|
2019-01-11 21:05:24 +01:00
|
|
|
""))
|
2018-12-04 18:42:26 +01:00
|
|
|
|
2018-12-04 20:08:17 +01:00
|
|
|
(defun taskwarrior--set-filter (filter)
|
|
|
|
(cond ((stringp filter) (setq-local taskwarrior-active-filters (split-string filter " ")))
|
|
|
|
((listp filter) (setq-local taskwarrior-active-filters filter))
|
|
|
|
(t (error "Filter did not match any supported type."))))
|
2018-12-04 18:42:26 +01:00
|
|
|
|
2019-01-14 11:00:15 +01:00
|
|
|
(defun taskwarrior-reset-filter ()
|
|
|
|
(interactive)
|
|
|
|
(progn
|
|
|
|
(taskwarrior--set-filter "")
|
|
|
|
(taskwarrior-update-buffer "")))
|
|
|
|
|
2018-12-04 20:08:17 +01:00
|
|
|
(defun taskwarrior-filter ()
|
|
|
|
(interactive)
|
|
|
|
(let ((new-filter (read-from-minibuffer "Filter: " (taskwarrior--get-filter-as-string))))
|
|
|
|
(progn
|
|
|
|
(taskwarrior--set-filter new-filter)
|
|
|
|
(taskwarrior-update-buffer new-filter))))
|
2018-12-04 18:42:26 +01:00
|
|
|
|
2019-01-16 19:46:37 +01:00
|
|
|
(defun taskwarrior--shell-command (command &optional filter modifications miscellaneous confirm)
|
|
|
|
(let* ((confirmation (if confirm (concat "echo " confirm " |") ""))
|
|
|
|
(cmd (format "%s task %s %s %s %s"
|
|
|
|
(or confirmation "")
|
|
|
|
(or filter "")
|
|
|
|
(or command "")
|
|
|
|
(or modifications "")
|
|
|
|
(or miscellaneous ""))))
|
2019-01-14 11:01:11 +01:00
|
|
|
(progn
|
|
|
|
(message cmd)
|
|
|
|
(shell-command-to-string cmd))))
|
2018-10-02 17:12:10 +02:00
|
|
|
|
|
|
|
(defun taskwarrior-export (filter)
|
|
|
|
"Export taskwarrior entries as JSON"
|
2018-10-23 22:13:21 +02:00
|
|
|
(vector-to-list
|
|
|
|
(json-read-from-string
|
|
|
|
(taskwarrior--shell-command "export" filter))))
|
2018-10-02 17:12:10 +02:00
|
|
|
|
2018-10-24 21:11:46 +02:00
|
|
|
(defun taskwarrior-load-tasks (filter)
|
|
|
|
"Load tasks into buffer-local variable"
|
|
|
|
(setq-local taskwarrior-tasks (taskwarrior-export filter)))
|
|
|
|
|
2018-10-31 20:13:24 +01:00
|
|
|
(defun taskwarrior-export-task (id)
|
|
|
|
(let ((task (vector-to-list
|
|
|
|
(json-read-from-string
|
|
|
|
(taskwarrior--shell-command "export" (concat "id:" id))))))
|
|
|
|
(if (< (length task) 1)
|
|
|
|
(error "Seems like two task have the same id.")
|
|
|
|
(car task))))
|
|
|
|
|
2018-11-04 21:13:32 +01:00
|
|
|
(defun taskwarrior--change-attribute (attribute)
|
|
|
|
"Change an attribute of a task"
|
2019-01-14 11:01:37 +01:00
|
|
|
(let* ((prefix (concat attribute ":"))
|
2018-11-04 21:13:32 +01:00
|
|
|
(id (taskwarrior-id-at-point))
|
|
|
|
(task (taskwarrior-export-task id))
|
2018-11-13 20:31:06 +01:00
|
|
|
(old-value (cdr (assoc-string attribute task)))
|
2019-04-14 21:33:57 +02:00
|
|
|
(new-value (read-from-minibuffer (concat prefix " ") old-value))
|
|
|
|
(quoted-value (concat "\"" new-value "\"")))
|
|
|
|
(taskwarrior--mutable-shell-command "modify" id (concat prefix quoted-value))))
|
2018-11-04 21:13:32 +01:00
|
|
|
|
2018-10-31 20:13:24 +01:00
|
|
|
(defun taskwarrior-change-description ()
|
2018-11-04 21:13:32 +01:00
|
|
|
"Change the description of a task"
|
2018-10-31 20:13:24 +01:00
|
|
|
(interactive)
|
2018-11-04 21:13:32 +01:00
|
|
|
(taskwarrior--change-attribute "description"))
|
|
|
|
|
2019-04-14 21:38:40 +02:00
|
|
|
(defun taskwarrior-change-priority ()
|
|
|
|
"Change the priority of a task"
|
|
|
|
(interactive)
|
|
|
|
(taskwarrior--change-attribute "priority"))
|
|
|
|
|
2018-11-04 21:13:32 +01:00
|
|
|
(defun taskwarrior-change-project ()
|
|
|
|
"Change the project of a task"
|
|
|
|
(interactive)
|
|
|
|
(taskwarrior--change-attribute "project"))
|
2018-10-02 17:12:10 +02:00
|
|
|
|
|
|
|
(defun taskwarrior-add (description)
|
|
|
|
(interactive "sDescription: ")
|
2019-04-09 14:40:01 +02:00
|
|
|
(taskwarrior--mutable-shell-command "add" "" description))
|
2018-11-13 21:17:43 +01:00
|
|
|
|
|
|
|
(defun taskwarrior-done ()
|
|
|
|
"Mark current task as done."
|
|
|
|
(interactive)
|
|
|
|
(let ((id (taskwarrior-id-at-point))
|
2019-04-09 13:42:53 +02:00
|
|
|
(confirmation (yes-or-no-p "Done?")))
|
|
|
|
(when confirmation
|
2019-04-09 14:40:01 +02:00
|
|
|
(taskwarrior--mutable-shell-command "done" id))))
|
2018-11-13 21:17:43 +01:00
|
|
|
|
|
|
|
(defun taskwarrior-delete ()
|
|
|
|
"Delete current task."
|
|
|
|
(interactive)
|
|
|
|
(let ((id (taskwarrior-id-at-point))
|
2019-04-09 13:42:53 +02:00
|
|
|
(confirmation (yes-or-no-p "Delete?")))
|
|
|
|
(when confirmation
|
2019-04-09 14:40:01 +02:00
|
|
|
(taskwarrior--mutable-shell-command "delete" id "" "" "yes"))))
|
|
|
|
|
|
|
|
(defun taskwarrior--mutable-shell-command (command &optional filter modifications misc confirm)
|
|
|
|
"Run shell command and restore taskwarrior buffer."
|
|
|
|
(let ((line-number (line-number-at-pos)))
|
|
|
|
(taskwarrior--shell-command command filter modifications misc confirm)
|
|
|
|
(taskwarrior-update-buffer)
|
|
|
|
(goto-line line-number)))
|
|
|
|
|
2018-10-02 17:12:10 +02:00
|
|
|
|
|
|
|
;; Setup a major mode for taskwarrior
|
|
|
|
;;;###autoload
|
|
|
|
(define-derived-mode taskwarrior-mode text-mode "taskwarrior"
|
2018-10-04 18:37:14 +02:00
|
|
|
"Major mode for interacting with taskwarrior. \\{taskwarrior-mode-map}"
|
2018-10-02 17:12:10 +02:00
|
|
|
(setq font-lock-defaults '(taskwarrior-highlight-regexps))
|
|
|
|
(setq goal-column 0)
|
|
|
|
(auto-revert-mode)
|
|
|
|
(setq buffer-read-only t))
|
|
|
|
|
2018-10-04 18:37:14 +02:00
|
|
|
;;; Externally visible functions
|
2018-10-02 17:12:10 +02:00
|
|
|
;;;###autoload
|
|
|
|
(defun taskwarrior ()
|
|
|
|
"Open the taskwarrior buffer. If one already exists, bring it to
|
|
|
|
the front and focus it. Otherwise, create one and load the data."
|
|
|
|
(interactive)
|
2019-01-11 21:06:46 +01:00
|
|
|
(let* ((buf (get-buffer-create taskwarrior-buffer-name)))
|
|
|
|
(progn
|
|
|
|
(switch-to-buffer buf)
|
|
|
|
(taskwarrior-update-buffer)
|
|
|
|
(setq font-lock-defaults '(taskwarrior-highlight-regexps))
|
|
|
|
(taskwarrior-mode)
|
|
|
|
(hl-line-mode))))
|
2018-10-02 18:10:03 +02:00
|
|
|
|
2018-12-04 18:42:26 +01:00
|
|
|
(defun taskwarrior-update-buffer (&optional filter)
|
2018-10-24 21:43:09 +02:00
|
|
|
(interactive)
|
2019-01-11 21:06:46 +01:00
|
|
|
(let ((filter (taskwarrior--get-filter-as-string)))
|
2018-10-02 17:12:10 +02:00
|
|
|
(progn
|
2019-01-11 21:06:46 +01:00
|
|
|
(read-only-mode -1)
|
2018-10-02 17:12:10 +02:00
|
|
|
(erase-buffer)
|
2018-12-04 18:42:26 +01:00
|
|
|
(taskwarrior-load-tasks (concat "1-1000 " filter))
|
2018-10-02 17:12:10 +02:00
|
|
|
(taskwarrior-write-entries)
|
2019-01-11 21:06:46 +01:00
|
|
|
(read-only-mode t)
|
2018-10-04 18:37:30 +02:00
|
|
|
(goto-char (point-min))
|
2018-10-02 17:12:10 +02:00
|
|
|
(while (not (equal (overlays-at (point)) nil))
|
2019-01-11 21:06:46 +01:00
|
|
|
(forward-char))
|
|
|
|
(taskwarrior--set-filter filter))))
|
2018-10-02 17:12:10 +02:00
|
|
|
|
2018-10-23 22:13:43 +02:00
|
|
|
(defun taskwarrior--sort-by-urgency (entries &optional asc)
|
2018-10-24 21:11:46 +02:00
|
|
|
;; TODO: Figure out how to store a function in the cmp variable.
|
2018-10-23 22:13:43 +02:00
|
|
|
(let ((cmp (if asc '< '>)))
|
|
|
|
(sort entries #'(lambda (x y)
|
|
|
|
(> (alist-get 'urgency x)
|
|
|
|
(alist-get 'urgency y))))))
|
|
|
|
|
2018-10-23 22:13:21 +02:00
|
|
|
(defun vector-to-list (vector)
|
|
|
|
"Convert a vector to a list"
|
|
|
|
(append vector nil))
|
|
|
|
|
2019-01-15 20:45:30 +01:00
|
|
|
|
|
|
|
(defun taskwarrior--get-max-length (key lst)
|
|
|
|
"Get the length of the longst element in a list"
|
|
|
|
(apply 'max
|
|
|
|
(-map 'length
|
|
|
|
(-map (-partial 'alist-get key) lst))))
|
|
|
|
|
2018-10-02 17:12:10 +02:00
|
|
|
(defun taskwarrior-write-entries ()
|
2018-10-24 21:11:46 +02:00
|
|
|
(let ((entries (taskwarrior--sort-by-urgency taskwarrior-tasks)))
|
2018-10-02 17:12:10 +02:00
|
|
|
(dolist (entry entries)
|
2019-05-20 15:20:34 +02:00
|
|
|
(let* ((id (format "%-2d" (alist-get 'id entry)))
|
|
|
|
(urgency (format "(%05.2f)" (alist-get 'urgency entry)))
|
|
|
|
(tags (format "%s" (taskwarrior--concat-tag-list (alist-get 'tags entry))))
|
|
|
|
(project (format "[%s]" (alist-get 'project entry)))
|
|
|
|
;; (project-max-length (taskwarrior--get-max-length 'project entries))
|
|
|
|
;; (project-spacing (- project-max-length (length project)))
|
2019-01-15 20:45:30 +01:00
|
|
|
(description (alist-get 'description entry)))
|
2019-05-20 15:20:34 +02:00
|
|
|
(insert (concat " " id " " urgency " " project " " tags " " description "\n"))))))
|
2018-10-31 21:08:46 +01:00
|
|
|
|
|
|
|
|
2019-05-20 15:20:34 +02:00
|
|
|
(defun taskwarrior--concat-tag-list (tags)
|
|
|
|
(mapconcat (function (lambda (x) (format "+%s" x))) (vector-to-list tags) " "))
|
|
|
|
|
2018-10-31 21:08:46 +01:00
|
|
|
(defun taskwarrior--parse-timestamp (ts)
|
|
|
|
"Turn the taskwarrior timestamp into a readable format"
|
2018-12-04 20:44:36 +01:00
|
|
|
(if (not ts)
|
|
|
|
ts
|
2019-01-15 20:45:08 +01:00
|
|
|
(let ((year (substring ts 0 4))
|
|
|
|
(month (substring ts 4 6))
|
|
|
|
(day (substring ts 6 8))
|
|
|
|
(hour (substring ts 9 11))
|
2018-12-04 20:44:36 +01:00
|
|
|
(minute (substring ts 11 13))
|
|
|
|
(second (substring ts 13 15)))
|
|
|
|
(format "%s-%s-%s %s:%s:%s" year month day hour minute second))))
|
2018-12-04 18:42:44 +01:00
|
|
|
|
|
|
|
|
|
|
|
(global-set-key (kbd "C-x t") 'taskwarrior)
|