taskwarrior.el/taskwarrior.el

380 lines
14 KiB
EmacsLisp
Raw Normal View History

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
;; 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-02 17:12:10 +02:00
(defvar taskwarrior-description 'taskwarrior-description
"Taskwarrior mode face used for tasks with a priority of C.")
(defvar taskwarrior-profile-alist 'taskwarrior-profile-alist
"A list of named filters in the form an associative list.")
2019-05-21 10:57:30 +02:00
(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.")
2018-10-02 17:12:10 +02:00
(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)
2019-05-21 10:57:30 +02:00
("[:space:].*:" . font-lock-function-name-face)
(" L " . taskwarrior-priority-low-face)
(" M " . taskwarrior-priority-medium-face)
(" H " . taskwarrior-priority-high-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))
(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)
(define-key taskwarrior-mode-map (kbd "U") 'taskwarrior-edit-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)
(define-key taskwarrior-mode-map (kbd "A") 'taskwarrior-annotate)
2018-11-13 21:17:43 +01:00
(define-key taskwarrior-mode-map (kbd "d") 'taskwarrior-done)
(define-key taskwarrior-mode-map (kbd "l") 'taskwarrior-load-profile)
(define-key taskwarrior-mode-map (kbd "o") 'taskwarrior-open-annotation)
2018-11-13 21:17:43 +01:00
(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)
(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-05-21 09:37:48 +02:00
(define-key taskwarrior-mode-map (kbd "t") 'taskwarrior-edit-tags)
2019-04-09 14:42:11 +02:00
(define-key taskwarrior-mode-map (kbd "RET") 'taskwarrior-info)
(define-key taskwarrior-mode-map (kbd "P") 'taskwarrior-edit-project))
2018-10-31 20:38:57 +01:00
(defun taskwarrior-load-profile ()
(interactive)
(let* ((profiles (-map 'car taskwarrior-profile-alist))
(profile (completing-read "Profile: " profiles))
(filter (cdr (assoc-string profile taskwarrior-profile-alist))))
(progn
(taskwarrior--set-filter filter)
(taskwarrior-update-buffer filter))))
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)
2019-09-02 22:14:28 +02:00
(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)))
2019-01-22 21:07:46 +01:00
(defun taskwarrior-mark-task ()
(interactive)
(let ((id (taskwarrior-id-at-point)))
2019-09-02 22:14:28 +02:00
(if (local-variable-p 'taskwarrior-marks)
(setq-local taskwarrior-marks (delete-dups (cons id taskwarrior-marks)))
(setq-local taskwarrior-marks (list id)))
2019-05-08 20:06:13 +02:00
(progn
2019-09-02 22:14:28 +02:00
(save-excursion
(read-only-mode -1)
(beginning-of-line)
(delete-forward-char 1)
(insert "*")
(read-only-mode 1))
(next-line))))
2019-05-08 20:06:13 +02:00
(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)))
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)))))
(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))))
2018-10-02 17:12:10 +02:00
(defun taskwarrior-id-at-point ()
(let ((line (thing-at-point 'line t)))
2019-09-02 22:14:28 +02:00
(string-match "^ [0-9]*" line)
2019-05-08 20:06:13 +02:00
(string-trim-left (match-string 0 line))))
2018-10-02 17:12:10 +02: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
""))
(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."))))
2019-01-14 11:00:15 +01:00
(defun taskwarrior-reset-filter ()
(interactive)
(progn
(taskwarrior--set-filter "")
(taskwarrior-update-buffer "")))
(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))))
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 ""))))
(progn
(message cmd)
(shell-command-to-string cmd))))
2018-10-02 17:12:10 +02:00
(defun vector-to-list (vector)
"Convert a vector to a list"
(append vector nil))
2018-10-02 17:12:10 +02:00
(defun taskwarrior--concat-tag-list (tags)
(mapconcat
(function (lambda (x) (format "+%s" x)))
(vector-to-list tags)
" "))
(defun taskwarrior-export ()
"Turn task export into the tabulated list entry form"
(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) "")))
2019-09-04 21:27:21 +02:00
(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))))
2019-09-04 21:27:21 +02:00
`(,id [,id ,urgency ,priority ,annotations ,project ,tags ,description])))
(vector-to-list
(json-read-from-string
(taskwarrior--shell-command "export" "id.not:0")))))
(defun taskwarrior-update-buffer (&optional filter)
(interactive)
(let* ((filter (taskwarrior--get-filter-as-string)))
(progn
(setq tabulated-list-entries (taskwarrior-export))
(tabulated-list-print t)
(goto-char (point-min))
(while (not (equal (overlays-at (point)) nil))
(forward-char))
(taskwarrior--set-filter filter))))
2018-10-24 21:11:46 +02:00
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))))
(defun taskwarrior--change-attribute (attribute)
"Change an attribute of a task"
2019-01-14 11:01:37 +01:00
(let* ((prefix (concat attribute ":"))
(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))))
2019-05-21 09:37:48 +02:00
(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))))
2018-10-31 20:13:24 +01:00
(defun taskwarrior-change-description ()
"Change the description of a task"
2018-10-31 20:13:24 +01:00
(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))))
2018-10-02 17:12:10 +02:00
(defun taskwarrior-add (description)
(interactive "sDescription: ")
(taskwarrior--mutable-shell-command "add" "" description))
2018-11-13 21:17:43 +01:00
2019-09-02 22:14:28 +02:00
(defun taskwarrior-mark-p ()
"Whether there are any marked tasks"
(and
(boundp 'taskwarrior-marks)
(> (length taskwarrior-marks) 0)))
2018-11-13 21:17:43 +01:00
(defun taskwarrior-done ()
(interactive)
2019-09-02 22:14:28 +02:00
(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))
2018-11-13 21:17:43 +01:00
(defun taskwarrior-delete ()
"Delete current task."
(interactive)
(let ((id (taskwarrior-id-at-point))
(confirmation (yes-or-no-p "Delete?")))
(when confirmation
(taskwarrior--mutable-shell-command "delete" id "" "" "yes"))))
(defun taskwarrior-annotate (annotation)
"Delete current task."
(interactive "sAnnotation: ")
(let ((id (taskwarrior-id-at-point)))
(taskwarrior--mutable-shell-command "annotate" id annotation)))
(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)))
2019-09-06 21:53:51 +02:00
(>
(string-to-number a)
(string-to-number b))))
2018-10-02 17:12:10 +02:00
;; Setup a major mode for taskwarrior
;;;###autoload
(define-derived-mode taskwarrior-mode tabulated-list-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 tabulated-list-format
`[("Id" 3 nil)
("Urg" 6 taskwarrior--urgency-predicate)
("Pri" 3 nil)
2019-09-04 21:27:21 +02:00
("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 ""))
2018-10-02 17:12:10 +02:00
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)
2019-01-11 21:06:46 +01:00
(setq font-lock-defaults '(taskwarrior-highlight-regexps))
(taskwarrior-mode)
(hl-line-mode))))