taskwarrior.el/taskwarrior.el

474 lines
17 KiB
EmacsLisp
Raw Normal View History

;;; taskwarrior.el --- An interactive taskwarrior interface -*- lexical-binding: t; -*-
;; Copyright (C) 2019-2020 Patrick Winter
;; Author: Patrick Winter <patrickwinter@posteo.ch>
;; Keywords: Tools
;; Url: https://gitlab/winpat/taskwarrior.el
;; Package-requires: ((emacs "26.3"))
;; Version: 0.0.1
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; The following todo exist:
2018-10-02 17:12:10 +02:00
;; 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
;;; Code:
2018-10-02 17:12:10 +02:00
(require 'json)
2019-10-23 11:38:57 +02:00
(require 'dash)
(require 'transient)
2018-10-02 17:12:10 +02:00
(defgroup taskwarrior nil "An emacs frontend to taskwarrior.")
2018-12-20 21:04:56 +01:00
(defvar taskwarrior-buffer-name "*taskwarrior*")
2019-09-07 20:11:07 +02:00
(defvar taskwarrior-active-filter nil "Currently active filter.")
(defvar taskwarrior-active-profile nil "Name of the currently active profile.")
2018-10-02 17:12:10 +02:00
(defvar taskwarrior-description 'taskwarrior-description
"Taskwarrior mode face used for tasks with a priority of C.")
2019-09-07 20:11:07 +02:00
(defvar taskwarrior-profile-alist nil
"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'.")
2018-10-02 17:12:10 +02:00
(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)
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)
2019-01-14 11:00:15 +01:00
(define-key taskwarrior-mode-map (kbd "r") 'taskwarrior-reset-filter)
(define-key taskwarrior-mode-map (kbd "f") 'taskwarrior-set-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
2019-09-07 20:11:07 +02:00
(defun taskwarrior-load-profile (profile)
"Load a predefined taskwarrior PROFILE."
2019-09-07 20:11:07 +02:00
(interactive
(list (completing-read "Profile: " (-map 'car taskwarrior-profile-alist))))
(let ((filter (cdr (assoc-string profile taskwarrior-profile-alist))))
(progn
2019-09-07 20:11:07 +02:00
(setq taskwarrior-active-profile profile)
(setq taskwarrior-active-filter filter)
(taskwarrior-update-buffer))))
2019-01-22 21:07:46 +01:00
(defun taskwarrior-unmark-task ()
"Unmark task."
2019-01-22 21:07:46 +01:00
(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 ()
"Mark current task."
2019-01-22 21:07:46 +01:00
(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 ()
"Open annotation on task."
(interactive)
(let* ((id (taskwarrior-id-at-point))
(task (taskwarrior-export-task id))
(annotations (-map (lambda (x) (alist-get 'description x)) (taskwarrior-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 ()
"Display detailed information about task."
2019-04-09 14:42:11 +02:00
(interactive)
(let* ((id (taskwarrior-id-at-point))
(buf (get-buffer-create "*taskwarrior info*")))
(progn
(switch-to-buffer-other-window buf)
(erase-buffer)
(insert (taskwarrior--shell-command "info" id)))))
2019-04-09 14:42:11 +02:00
(defun taskwarrior--parse-created-task-id (output)
"Extract task id from shell OUTPUT of `task add`."
(when (string-match "^.*Created task \\([0-9]+\\)\\.*$" output)
(message (match-string 1 output))))
(defun taskwarrior--parse-org-link (link)
"Extract 'org-mode' link from LINK."
(string-match org-bracket-link-regexp link)
(list
(match-string 1 link)
(match-string 3 link)))
(defun taskwarrior-capture (arg)
"Capture a taskwarrior task with content 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 ()
"Get id of task at point."
(let* ((line (thing-at-point 'line t))
(parse-result (string-match "^ [0-9]*" line))
(id (string-trim-left (match-string 0 line))))
(if (= id nil)
(error "Unable to extract id from line '%s'" line)
id)))
2018-10-02 17:12:10 +02:00
2019-01-14 11:00:15 +01:00
(defun taskwarrior-reset-filter ()
"Reset the currently set filter."
2019-01-14 11:00:15 +01:00
(interactive)
(progn
2019-09-07 20:11:07 +02:00
(setq-local taskwarrior-active-filter nil)
2019-09-07 17:27:23 +02:00
(taskwarrior-update-buffer)))
2019-01-14 11:00:15 +01:00
2019-09-07 17:27:23 +02:00
(defun taskwarrior-set-filter ()
"Set or edit the current filter."
(interactive)
2019-09-07 20:11:07 +02:00
(let ((new-filter (read-from-minibuffer "Filter: " taskwarrior-active-filter)))
(progn
2019-09-07 20:11:07 +02:00
(setq-local taskwarrior-active-filter new-filter)
2019-09-07 17:27:23 +02:00
(taskwarrior-update-buffer))))
2019-01-16 19:46:37 +01:00
(defun taskwarrior--shell-command (command &optional filter modifications miscellaneous confirm)
"Run a taskwarrior COMMAND with specified FILTER MODIFICATIONS MISCELLANEOUS CONFIRM."
2019-01-16 19:46:37 +01:00
(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 taskwarrior-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)
"Concat a list of TAGS in to readable format."
(mapconcat
(function (lambda (x) (format "+%s" x)))
(taskwarrior-vector-to-list tags)
" "))
2019-09-07 17:27:23 +02:00
(defun taskwarrior-export (&optional filter)
"Turn task export into the tabulated list entry form filted by FILTER."
2019-09-07 17:27:23 +02:00
(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])))
(taskwarrior-vector-to-list
2019-09-07 17:27:23 +02:00
(json-read-from-string
(taskwarrior--shell-command "export" filter))))))
(defun taskwarrior-update-buffer ()
"Update the taskwarrior buffer."
(interactive)
(progn
2019-09-07 17:27:23 +02:00
(setq tabulated-list-entries
2019-09-07 20:11:07 +02:00
(if taskwarrior-active-filter
(taskwarrior-export taskwarrior-active-filter)
2019-09-07 17:27:23 +02:00
(taskwarrior-export)))
(tabulated-list-print t)))
2018-10-24 21:11:46 +02:00
2018-10-31 20:13:24 +01:00
(defun taskwarrior-export-task (id)
"Export task with ID."
(let ((task (taskwarrior-vector-to-list
2018-10-31 20:13:24 +01:00
(json-read-from-string
(taskwarrior--shell-command "export" (concat "id:" id))))))
(if (< (length task) 1)
(error "Seems like two task have the same id")
2018-10-31 20:13:24 +01:00
(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 ()
"Edit tags on task."
2019-05-21 09:37:48 +02:00
(interactive)
(let* ((id (taskwarrior-id-at-point))
(task (taskwarrior-export-task id))
(options (split-string (shell-command-to-string "task _tags") "\n"))
(old (taskwarrior-vector-to-list (alist-get 'tags task)))
2019-05-21 09:37:48 +02:00
(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
2018-10-02 17:12:10 +02:00
(defun taskwarrior-add (description)
"Add new task with DESCRIPTION."
2018-10-02 17:12:10 +02:00
(interactive "sDescription: ")
(progn
(taskwarrior--add description)
(taskwarrior--revert-buffer)))
(defun taskwarrior--add (description)
"Add new task with DESCRIPTION."
(let ((output (taskwarrior--shell-command "add" "" description)))
(when (string-match "Created task \\([[:digit:]]+\\)." output)
(match-string 1 output))))
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."
2019-09-02 22:14:28 +02:00
(and
(boundp 'taskwarrior-marks)
(> (length taskwarrior-marks) 0)))
2019-09-06 21:58:47 +02:00
(defun taskwarrior-delete ()
"Delete task at point."
2019-09-06 21:58:47 +02:00
(interactive)
(taskwarrior-multi-action 'taskwarrior--delete "Delete?"))
(defun taskwarrior--delete (id)
"Delete task with ID."
2019-09-06 21:58:47 +02:00
(taskwarrior--mutable-shell-command "delete" id "" "" "yes"))
2018-11-13 21:17:43 +01:00
(defun taskwarrior-done ()
"Mark task at point as done."
2018-11-13 21:17:43 +01:00
(interactive)
2019-09-02 22:14:28 +02:00
(taskwarrior-multi-action 'taskwarrior--done "Done?"))
(defun taskwarrior-multi-action (action confirmation-text)
"Run a ACTION after processing the CONFIRMATION-TEXT."
2019-09-02 22:14:28 +02:00
(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 with ID as done."
2019-09-02 22:14:28 +02:00
(taskwarrior--mutable-shell-command "done" id))
2018-11-13 21:17:43 +01:00
(defun taskwarrior-annotate (annotation)
"Add ANNOTATION to task at point."
(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 with FILTER MODIFICATIONS MISC and CONFIRM."
(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)
"Compare urgency of task A to task 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)
2019-09-07 17:27:23 +02:00
(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."
2018-10-02 17:12:10 +02:00
(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))))
(defun taskwarrior-set-due ()
"Set due date on task."
(interactive)
(taskwarrior--change-attribute "due"))
(defun taskwarrior-set-scheduled ()
"Set schedule date on task at point."
(interactive)
(taskwarrior--change-attribute "scheduled"))
(defun taskwarrior-set-wait ()
"Set wait date on task at point."
(interactive)
(taskwarrior--change-attribute "wait"))
(defun taskwarrior-set-untl ()
"Set until date on task at point."
(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)]])
(provide 'taskwarrior)
;;; taskwarrior.el ends here