taskwarrior.el/taskwarrior.el
2019-10-23 11:38:57 +02:00

413 lines
14 KiB
EmacsLisp

;; Frontend for taskwarrior
;;
;; TODO: Implement modeline indicator for deadline and entries
;; TODO: Update buffer if command modifies the state
;; TODO: Extract "1-1000" id filter into variable
;; TODO: Figure out the difference between assoc and assoc-string
;; (and why alist-get does not work with strings)
;; TODO: Restore position after taskwarrior-update-buffer is called
(require 'json)
(require 'dash)
(require 'transient)
(defgroup taskwarrior nil "An emacs frontend to taskwarrior.")
(defvar taskwarrior-buffer-name "*taskwarrior*")
(defvar taskwarrior-active-filter nil "Currently active filter.")
(defvar taskwarrior-active-profile nil "Name of the currently active profile.")
(defvar taskwarrior-description 'taskwarrior-description
"Taskwarrior mode face used for tasks with a priority of C.")
(defvar taskwarrior-profile-alist nil
"A list of named filters in the form an associative list.")
(defface taskwarrior-priority-high-face
'((((min-colors 88) (class color))
(:foreground "red1"))
(((class color))
(:foreground "red"))
(t (:weight bold :underline t)))
"Face used for H priority label."
:group 'taskwarrior-faces)
(defvar taskwarrior-priority-high-face 'taskwarrior-priority-high-face
"Face name to use for high priority label.")
(defface taskwarrior-priority-medium-face
'((((min-colors 88) (class color))
(:foreground "orange1"))
(((class color))
(:foreground "orange"))
(t (:weight bold :underline t)))
"Face used for M priority label."
:group 'taskwarrior-faces)
(defvar taskwarrior-priority-medium-face 'taskwarrior-priority-medium-face
"Face name to use for medium priority label.")
(defface taskwarrior-priority-low-face
'((((min-colors 88) (class color))
(:foreground "grey1"))
(((class color))
(:foreground "grey"))
(t (:weight bold :underline t)))
"Face used for L priority label."
:group 'taskwarrior-faces)
(defvar taskwarrior-priority-low-face 'taskwarrior-priority-low-face
"Face name to use for low priority label.")
(setq taskwarrior-highlight-regexps
`(("^\\*.*$" . 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)
(" L " . taskwarrior-priority-low-face)
(" M " . taskwarrior-priority-medium-face)
(" H " . taskwarrior-priority-high-face)))
(defvar taskwarrior-mode-map nil "Keymap for `taskwarrior-mode'")
(progn
(setq taskwarrior-mode-map (make-sparse-keymap))
(define-key taskwarrior-mode-map (kbd "a") 'taskwarrior-add)
(define-key taskwarrior-mode-map (kbd "A") 'taskwarrior-annotate)
(define-key taskwarrior-mode-map (kbd "d") 'taskwarrior-date)
(define-key taskwarrior-mode-map (kbd "x") 'taskwarrior-done)
(define-key taskwarrior-mode-map (kbd "D") 'taskwarrior-delete)
(define-key taskwarrior-mode-map (kbd "e") 'taskwarrior-change-description)
(define-key taskwarrior-mode-map (kbd "q") 'quit-window)
(define-key taskwarrior-mode-map (kbd "g") 'taskwarrior-update-buffer)
(define-key taskwarrior-mode-map (kbd "q") 'quit-window)
(define-key taskwarrior-mode-map (kbd "U") 'taskwarrior-edit-priority)
(define-key taskwarrior-mode-map (kbd "l") 'taskwarrior-load-profile)
(define-key taskwarrior-mode-map (kbd "o") 'taskwarrior-open-annotation)
(define-key taskwarrior-mode-map (kbd "m") 'taskwarrior-mark-task)
(define-key taskwarrior-mode-map (kbd "u") 'taskwarrior-unmark-task)
(define-key taskwarrior-mode-map (kbd "r") 'taskwarrior-reset-filter)
(define-key taskwarrior-mode-map (kbd "f") 'taskwarrior-set-filter)
(define-key taskwarrior-mode-map (kbd "t") 'taskwarrior-edit-tags)
(define-key taskwarrior-mode-map (kbd "RET") 'taskwarrior-info)
(define-key taskwarrior-mode-map (kbd "P") 'taskwarrior-edit-project))
(defun taskwarrior-load-profile (profile)
(interactive
(list (completing-read "Profile: " (-map 'car taskwarrior-profile-alist))))
(let ((filter (cdr (assoc-string profile taskwarrior-profile-alist))))
(progn
(setq taskwarrior-active-profile profile)
(setq taskwarrior-active-filter filter)
(taskwarrior-update-buffer))))
(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))))
(progn
(save-excursion
(read-only-mode -1)
(beginning-of-line)
(delete-forward-char 1)
(insert " ")
(read-only-mode 1))
(next-line)))
(defun taskwarrior-mark-task ()
(interactive)
(let ((id (taskwarrior-id-at-point)))
(if (local-variable-p 'taskwarrior-marks)
(setq-local taskwarrior-marks (delete-dups (cons id taskwarrior-marks)))
(setq-local taskwarrior-marks (list id)))
(progn
(save-excursion
(read-only-mode -1)
(beginning-of-line)
(delete-forward-char 1)
(insert "*")
(read-only-mode 1))
(next-line))))
(defun taskwarrior-open-annotation ()
(interactive)
(let* ((id (taskwarrior-id-at-point))
(task (taskwarrior-export-task id))
(annotations (-map (lambda (x) (alist-get 'description x)) (vector-to-list (alist-get 'annotations task))))
(choice (completing-read "Tags: " annotations)))
(org-open-link-from-string choice)))
(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)))))
(defun taskwarrior--parse-created-task-id (output)
(when (string-match "^.*Created task \\([0-9]+\\)\\.*$" output)
(message (match-string 1 output))))
(defun taskwarrior--parse-org-link (link)
(string-match org-bracket-link-regexp link)
(list
(match-string 1 link)
(match-string 3 link)))
(defun taskwarrior-capture (arg)
(interactive "P")
(let* ((link (car (cdr (taskwarrior--parse-org-link (org-store-link arg)))))
(description (read-from-minibuffer "Description: "))
(id (taskwarrior--parse-created-task-id
(shell-command-to-string (format "task add %s" description)))))
(shell-command-to-string (format "task %s annotate %s" id link))))
(defun taskwarrior-id-at-point ()
(let ((line (thing-at-point 'line t)))
(string-match "^ [0-9]*" line)
(string-trim-left (match-string 0 line))))
(defun taskwarrior-reset-filter ()
(interactive)
(progn
(setq-local taskwarrior-active-filter nil)
(taskwarrior-update-buffer)))
(defun taskwarrior-set-filter ()
(interactive)
(let ((new-filter (read-from-minibuffer "Filter: " taskwarrior-active-filter)))
(progn
(setq-local taskwarrior-active-filter new-filter)
(taskwarrior-update-buffer))))
(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 ""))))
(progn
(message cmd)
(shell-command-to-string cmd))))
(defun vector-to-list (vector)
"Convert a vector to a list"
(append vector nil))
(defun taskwarrior--concat-tag-list (tags)
(mapconcat
(function (lambda (x) (format "+%s" x)))
(vector-to-list tags)
" "))
(defun taskwarrior-export (&optional filter)
"Turn task export into the tabulated list entry form"
(let ((filter (concat filter " id.not:0")))
(mapcar
(lambda (entry)
(let* ((id (format "%s" (alist-get 'id entry)))
(urgency (format "%0.2f" (alist-get 'urgency entry)))
(priority (format " %s " (or (alist-get 'priority entry) "")))
(annotations (format "%d" (length (or (alist-get 'annotations entry) '()))))
(project (or (format "%s" (alist-get 'project entry)) ""))
(tags (or (taskwarrior--concat-tag-list (alist-get 'tags entry)) ""))
(description (format "%s" (alist-get 'description entry))))
`(,id [,id ,urgency ,priority ,annotations ,project ,tags ,description])))
(vector-to-list
(json-read-from-string
(taskwarrior--shell-command "export" filter))))))
(defun taskwarrior-update-buffer ()
(interactive)
(progn
(setq tabulated-list-entries
(if taskwarrior-active-filter
(taskwarrior-export taskwarrior-active-filter)
(taskwarrior-export)))
(tabulated-list-print t)))
(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))))
(defun taskwarrior--change-attribute (attribute)
"Change an attribute of a task"
(let* ((prefix (concat attribute ":"))
(id (taskwarrior-id-at-point))
(task (taskwarrior-export-task id))
(old-value (cdr (assoc-string attribute task)))
(new-value (read-from-minibuffer (concat prefix " ") old-value))
(quoted-value (concat "\"" new-value "\"")))
(taskwarrior--mutable-shell-command "modify" id (concat prefix quoted-value))))
(defun taskwarrior-edit-tags ()
(interactive)
(let* ((id (taskwarrior-id-at-point))
(task (taskwarrior-export-task id))
(options (split-string (shell-command-to-string "task _tags") "\n"))
(old (vector-to-list (alist-get 'tags task)))
(current-tags (mapconcat 'identity old " "))
(new (split-string (completing-read "Tags: " options nil nil current-tags) " "))
(added-tags (mapconcat
(function (lambda (x) (concat "+" x)))
(set-difference new old :test #'string-equal) " "))
(removed-tags (mapconcat
(function (lambda (x) (concat "-" x)))
(set-difference old new :test #'string-equal) " ")))
(taskwarrior--mutable-shell-command "modify" id (concat added-tags " " removed-tags))))
(defun taskwarrior-edit-project ()
"Change the project of a task"
(interactive)
(let* ((id (taskwarrior-id-at-point))
(task (taskwarrior-export-task id))
(options (split-string (shell-command-to-string "task _projects") "\n"))
(old (alist-get 'project task))
(new (completing-read "Project: " options nil nil old)))
(taskwarrior--mutable-shell-command "modify" id (concat "project:" new))))
(defun taskwarrior-change-description ()
"Change the description of a task"
(interactive)
(taskwarrior--change-attribute "description"))
(defun taskwarrior-edit-priority ()
"Change the priority of a task"
(interactive)
(let* ((id (taskwarrior-id-at-point))
(options '("" "H" "M" "L"))
(new (completing-read "Priority: " options)))
(taskwarrior--mutable-shell-command "modify" id (concat "priority:" new))))
(defun taskwarrior-add (description)
(interactive "sDescription: ")
(progn
(taskwarrior--add description)
(taskwarrior--revert-buffer)))
(defun taskwarrior--add (description)
(let ((output (taskwarrior--shell-command "add" "" description)))
(when (string-match "Created task \\([[:digit:]]+\\)." output)
(match-string 1 output))))
(defun taskwarrior-mark-p ()
"Whether there are any marked tasks"
(and
(boundp 'taskwarrior-marks)
(> (length taskwarrior-marks) 0)))
(defun taskwarrior-delete ()
(interactive)
(taskwarrior-multi-action 'taskwarrior--delete "Delete?"))
(defun taskwarrior--delete (id)
"Delete task with id."
(taskwarrior--mutable-shell-command "delete" id "" "" "yes"))
(defun taskwarrior-done ()
(interactive)
(taskwarrior-multi-action 'taskwarrior--done "Done?"))
(defun taskwarrior-multi-action (action confirmation-text)
(when (yes-or-no-p confirmation-text)
(if (taskwarrior-mark-p)
(dolist (id taskwarrior-marks)
(funcall action id))
(let ((id (taskwarrior-id-at-point)))
(funcall action id)))))
(defun taskwarrior--done (id)
"Mark task as done."
(taskwarrior--mutable-shell-command "done" id))
(defun taskwarrior-annotate (annotation)
"Delete current task."
(interactive "sAnnotation: ")
(let ((id (taskwarrior-id-at-point)))
(taskwarrior--mutable-shell-command "annotate" id annotation)))
(defun taskwarrior--revert-buffer ()
"Revert taskwarrior buffer."
(let ((line-number (line-number-at-pos)))
(taskwarrior-update-buffer)
(goto-line line-number)))
(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)))
(defun taskwarrior--urgency-predicate (A B)
(let ((a (aref (cadr A) 1))
(b (aref (cadr B) 1)))
(>
(string-to-number a)
(string-to-number b))))
;; Setup a major mode for taskwarrior
;;;###autoload
(define-derived-mode taskwarrior-mode tabulated-list-mode "taskwarrior"
"Major mode for interacting with taskwarrior. \\{taskwarrior-mode-map}"
(setq font-lock-defaults '(taskwarrior-highlight-regexps))
(setq tabulated-list-format
`[("Id" 3 nil)
("Urg" 6 taskwarrior--urgency-predicate)
("Pri" 3 nil)
("Ann" 3 nil)
("Project" 15 nil)
("Tags" 15 nil)
("Description" 100 nil)])
(setq tabulated-list-padding 2)
(setq tabulated-list-sort-key (cons "Urg" nil)) (tabulated-list-init-header)
(taskwarrior-update-buffer))
;;; Externally visible functions
;;;###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)
(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))))
(defun taskwarrior-set-due ()
(interactive)
(taskwarrior--change-attribute "due"))
(defun taskwarrior-set-scheduled ()
(interactive)
(taskwarrior--change-attribute "scheduled"))
(defun taskwarrior-set-wait ()
(interactive)
(taskwarrior--change-attribute "wait"))
(defun taskwarrior-set-untl ()
(interactive)
(taskwarrior--change-attribute "until"))
(define-transient-command taskwarrior-date ()
"Edit date on task"
[["Date"
("d" "due" taskwarrior-set-due)
("s" "scheduled" taskwarrior-set-scheduled)
("w" "wait" taskwarrior-set-wait)
("u" "until" taskwarrior-set-untl)]])