taskwarrior.el/taskwarrior.el

251 lines
8.7 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*")
(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-01-11 20:42:20 +01:00
`(("^[0-9]*" . font-lock-variable-name-face)
("([0-9.]*?)" . font-lock-builtin-face)
("\\[.*\\]" . font-lock-preprocessor-face)
2018-10-02 17:12:10 +02:00
("[:space:].*:" . font-lock-function-name-face)))
(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)
(define-key taskwarrior-mode-map (kbd "u") '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)
(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)
2018-10-31 20:38:57 +01:00
(define-key taskwarrior-mode-map (kbd "P") 'taskwarrior-change-project))
(defun taskwarrior--display-task-details-in-echo-area ()
(let* ((id (taskwarrior-id-at-point))
(task (taskwarrior-export-task id))
(due (taskwarrior--parse-timestamp (alist-get 'due task))))
(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
(defun taskwarrior-id-at-point ()
(let ((line (thing-at-point 'line t)))
(string-match "^[0-9]*" line)
(match-string 0 line)))
(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))))
2018-10-02 17:12:10 +02:00
(defun taskwarrior--shell-command (command &optional filter modifications miscellaneous)
(let ((cmd (format "task %s %s %s %s"
(or filter "")
command
(or modifications "")
(or miscellaneous ""))))
(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"
(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))))
(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-01-14 11:01:37 +01:00
(new-value (read-from-minibuffer (concat prefix " ") old-value)))
(taskwarrior--shell-command "modify" id (concat prefix new-value))
(taskwarrior-update-buffer)))
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-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: ")
(message (taskwarrior--shell-command "add" "" description))
2018-12-20 21:04:56 +01:00
(when (eq (buffer-name) taskwarrior-buffer-name)
(taskwarrior-update-buffer)))
2018-11-13 21:17:43 +01:00
(defun taskwarrior-done ()
"Mark current task as done."
(interactive)
(let ((id (taskwarrior-id-at-point))
(confirmation (read-from-minibuffer "Done [y/n]?: ")))
(when (string= confirmation "y")
(message (taskwarrior--shell-command "done" id))
(taskwarrior-update-buffer))))
2018-11-13 21:17:43 +01:00
(defun taskwarrior-delete ()
"Delete current task."
(interactive)
(let ((id (taskwarrior-id-at-point))
(confirmation (read-from-minibuffer "Delete [y/n]?: ")))
(when (string= confirmation "y")
(taskwarrior--shell-command "config" "" "confirmation off")
(message (taskwarrior--shell-command "delete" id))
(taskwarrior--shell-command "config" "" "confirmation on")
(taskwarrior-update-buffer))))
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))))
(defun taskwarrior-update-buffer (&optional filter)
(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
(goto-char (point-min))
2019-01-11 21:06:46 +01:00
(read-only-mode -1)
2018-10-02 17:12:10 +02:00
(erase-buffer)
(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)
(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
(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.
(let ((cmp (if asc '< '>)))
(sort entries #'(lambda (x y)
(> (alist-get 'urgency x)
(alist-get 'urgency y))))))
(defun vector-to-list (vector)
"Convert a vector to a list"
(append vector nil))
(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)
(let* ((id (alist-get 'id entry))
(urgency (alist-get 'urgency entry))
(project (alist-get 'project entry))
(project-max-length (taskwarrior--get-max-length 'project entries))
(project-spacing (- project-max-length (length project)))
(description (alist-get 'description entry)))
(insert (if project
(format (concat "%-2d (%05.2f) [%s]%-" (number-to-string project-spacing) "s %s\n") id urgency project "" description)
(format
(concat "%-2d (%05.2f) %-" (number-to-string (+ 3 project-max-length)) "s%s\n")
id urgency "" description)))))))
(defun taskwarrior--parse-timestamp (ts)
"Turn the taskwarrior timestamp into a readable format"
(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))
(minute (substring ts 11 13))
(second (substring ts 13 15)))
(format "%s-%s-%s %s:%s:%s" year month day hour minute second))))
(global-set-key (kbd "C-x t") 'taskwarrior)
(taskwarrior--set-filter "project:pro5")
(message (car taskwarrior-active-filters))