Introduce makel.el to make future MELPA packaging easier

This commit is contained in:
Patrick Winter 2019-11-04 12:46:43 +01:00
parent 1afcb6f37a
commit d277e955b9
6 changed files with 294 additions and 47 deletions

View file

@ -17,12 +17,27 @@ jobs:
- 26.3 - 26.3
- snapshot - snapshot
steps: steps:
- name: Set up Emacs
uses: purcell/setup-emacs@v1.0 - name: Set up Emacs
with: uses: purcell/setup-emacs@v1.0
version: ${{ matrix.emacs_version }} with:
- uses: actions/checkout@v1 version: ${{ matrix.emacs_version }}
- name: Install taskwarrior package
run: sudo apt-get install -y taskwarrior - uses: actions/checkout@v1
- name: Run ERT test suite
run: make test - name: Install taskwarrior package
run: sudo apt-get install -y taskwarrior
- name: Create taskwarrior config
run: echo -e 'data.location=~/.task\nverbose=no' > ~/.taskrc
- name: Install elisp dependencies
run: make ci-dependencies
# TODO: Replace this with `make check` when packaging for MELPA
- name: Run checkdoc
run: make lint-checkdoc
- name: Run ERT test suite
run: make test

3
.gitignore vendored
View file

@ -1,3 +1,2 @@
.task .task
dash.el makel.el
transient.el

View file

@ -1,13 +1,30 @@
ELPA_DEPENDENCIES=package-lint transient dash
ELPA_ARCHIVES=melpa-stable gnu
TEST_ERT_FILES = $(wildcard test/*.el)
LINT_CHECKDOC_FILES = "taskwarrior.el" ${TEST_ERT_FILES}
LINT_PACKAGE_LINT_FILES = ${LINT_CHECKDOC_FILES}
LINT_COMPILE_FILES = ${LINT_CHECKDOC_FILES}
makel.mk: # Download makel
@if [ -f ../makel/makel.mk ]; then \
ln -s ../makel/makel.mk .; \
else \
curl \
--fail --silent --show-error --insecure --location \
--retry 9 --retry-delay 9 \
-O https://gitlab.petton.fr/DamienCassou/makel/raw/v0.5.3/makel.mk; \
fi
.PHONY: test-data .PHONY: test-data
test-data: ## Generate some example tasks test-data: ## Generate some example tasks
for n in $$(seq 10); do task add "Example task $${n}"; done for n in $$(seq 10); do task add "Example task $${n}"; done
.PHONY: test # .PHONY: fetch-deps
test: fetch-deps ## Run ERT test suite # fetch-deps: ## Fetch required dependencies
emacs -batch -l dash.el -l transient.el -l taskwarrior.el -l taskwarrior-test.el -f ert-run-tests-batch-and-exit # curl -fsSkL --retry 9 --retry-delay 9 -O "https://raw.githubusercontent.com/magit/transient/master/lisp/transient.el"
# curl -fsSkL --retry 9 --retry-delay 9 -O "https://raw.githubusercontent.com/magnars/dash.el/master/dash.el"
.PHONY: fetch-deps # Include makel.mk if present
fetch-deps: ## Fetch required dependencies -include makel.mk
curl -fsSkL --retry 9 --retry-delay 9 -O "https://raw.githubusercontent.com/magit/transient/master/lisp/transient.el"
curl -fsSkL --retry 9 --retry-delay 9 -O "https://raw.githubusercontent.com/magnars/dash.el/master/dash.el"

154
makel.mk Normal file
View file

@ -0,0 +1,154 @@
MAKEL_VERSION=0.5.3
MAKEL_LOAD_PATH=-L . $(patsubst %,-L ../%,$(ELPA_DEPENDENCIES))
MAKEL_SET_ARCHIVES0=${ELPA_ARCHIVES}
MAKEL_SET_ARCHIVES1=$(patsubst gnu,(cons \"gnu\" \"https://elpa.gnu.org/packages/\"),${MAKEL_SET_ARCHIVES0})
MAKEL_SET_ARCHIVES2=$(patsubst melpa,(cons \"melpa\" \"https://melpa.org/packages/\"),${MAKEL_SET_ARCHIVES1})
MAKEL_SET_ARCHIVES3=$(patsubst melpa-stable,(cons \"melpa-stable\" \"https://stable.melpa.org/packages/\"),${MAKEL_SET_ARCHIVES2})
MAKEL_SET_ARCHIVES4=$(patsubst org,(cons \"org\" \"https://orgmode.org/elpa/\"),${MAKEL_SET_ARCHIVES3})
MAKEL_SET_ARCHIVES=(setq package-archives (list ${MAKEL_SET_ARCHIVES4}))
EMACSBIN?=emacs
BATCH=$(EMACSBIN) -Q --batch $(MAKEL_LOAD_PATH) \
--eval "(setq load-prefer-newer t)" \
--eval "(require 'package)" \
--eval "${MAKEL_SET_ARCHIVES}" \
--eval "(setq enable-dir-local-variables nil)" \
--funcall package-initialize
CURL = curl --fail --silent --show-error --insecure \
--location --retry 9 --retry-delay 9 \
--remote-name-all
# Definition of a utility function `split_with_commas`.
# Argument 1: a space-separated list of filenames
# Return: a comma+space-separated list of filenames
comma:=,
empty:=
space:=$(empty) $(empty)
split_with_commas=$(subst ${space},${comma}${space},$(1))
.PHONY: debug install-elpa-dependencies download-non-elpa-dependencies ci-dependencies check test test-ert test-buttercup lint lint-checkdoc lint-package-lint lint-compile
makel-version:
@echo "makel v${MAKEL_VERSION}"
debug:
@echo "MAKEL_LOAD_PATH=${MAKEL_LOAD_PATH}"
@echo "MAKEL_SET_ARCHIVES=${MAKEL_SET_ARCHIVES}"
@${BATCH} --eval "(message \"%S\" package-archives)"
install-elpa-dependencies:
@if [ -n "${ELPA_DEPENDENCIES}" ]; then \
echo "# Install ELPA dependencies: $(call split_with_commas,${ELPA_DEPENDENCIES})"; \
output=$$(mktemp --tmpdir "makel-ci-dependencies-XXXXX"); \
$(BATCH) \
--funcall package-refresh-contents \
${patsubst %,--eval "(package-install (quote %))",${ELPA_DEPENDENCIES}} \
> $${output} 2>&1 || ( cat $${output} && exit 1 ); \
fi
download-non-elpa-dependencies:
@if [ -n "${DOWNLOAD_DEPENDENCIES}" ]; then \
echo "# Download non-ELPA dependencies: $(call split_with_commas,${DOWNLOAD_DEPENDENCIES})"; \
$(CURL) $(patsubst %,"%",${DOWNLOAD_DEPENDENCIES}); \
fi
ci-dependencies: install-elpa-dependencies download-non-elpa-dependencies
check: test lint
####################################
# Tests
####################################
test: test-ert test-buttercup
####################################
# Tests - ERT
####################################
MAKEL_TEST_ERT_FILES0=$(filter-out %-autoloads.el,${TEST_ERT_FILES})
MAKEL_TEST_ERT_FILES=$(patsubst %,(load-file \"%\"),${MAKEL_TEST_ERT_FILES0})
test-ert:
# Run ert tests from $(call split_with_commas,${MAKEL_TEST_ERT_FILES0})…
@output=$$(mktemp --tmpdir "makel-test-ert-XXXXX"); \
${BATCH} \
$(if ${TEST_ERT_OPTIONS},${TEST_ERT_OPTIONS}) \
--eval "(progn ${MAKEL_TEST_ERT_FILES} (ert-run-tests-batch-and-exit))" \
> $${output} 2>&1 || ( cat $${output} && exit 1 )
####################################
# Tests - Buttercup
####################################
test-buttercup:
@if [ -n "${TEST_BUTTERCUP_OPTIONS}" ]; then \
echo "# Run buttercup tests on $(call split_with_commas,${TEST_BUTTERCUP_OPTIONS})"; \
output=$$(mktemp --tmpdir "makel-test-buttercup-XXXXX"); \
${BATCH} \
--eval "(require 'buttercup)" \
-f buttercup-run-discover ${TEST_BUTTERCUP_OPTIONS} \
> $${output} 2>&1 || ( cat $${output} && exit 1 ); \
fi;
####################################
# Lint
####################################
lint: lint-checkdoc lint-package-lint lint-compile
####################################
# Lint - Checkdoc
####################################
MAKEL_LINT_CHECKDOC_FILES0=$(filter-out %-autoloads.el,${LINT_CHECKDOC_FILES})
MAKEL_LINT_CHECKDOC_FILES=$(patsubst %,\"%\",${MAKEL_LINT_CHECKDOC_FILES0})
# This rule has to work around the fact that checkdoc doesn't throw
# errors, it always succeeds. We thus have to check if checkdoc
# printed anything to decide the exit status of the rule.
lint-checkdoc:
# Run checkdoc on $(call split_with_commas,${MAKEL_LINT_CHECKDOC_FILES0})…
@output=$$(mktemp --tmpdir "makel-lint-checkdoc-XXXXX"); \
${BATCH} \
$(if ${LINT_CHECKDOC_OPTIONS},${LINT_CHECKDOC_OPTIONS}) \
--eval "(mapcar #'checkdoc-file (list ${MAKEL_LINT_CHECKDOC_FILES}))" \
> $${output} 2>&1; \
if [ "$$(stat --printf='%s' $${output})" -eq 0 ]; then \
exit 0; \
else \
cat $${output}; \
exit 1; \
fi
####################################
# Lint - Package-lint
####################################
MAKEL_LINT_PACKAGE_LINT_FILES=$(filter-out %-autoloads.el,${LINT_PACKAGE_LINT_FILES})
lint-package-lint:
# Run package-lint on $(call split_with_commas,${MAKEL_LINT_PACKAGE_LINT_FILES})…
@${BATCH} \
--eval "(require 'package-lint)" \
$(if ${LINT_PACKAGE_LINT_OPTIONS},${LINT_PACKAGE_LINT_OPTIONS}) \
--funcall package-lint-batch-and-exit \
${MAKEL_LINT_PACKAGE_LINT_FILES}
####################################
# Lint - Compilation
####################################
MAKEL_LINT_COMPILE_FILES=$(filter-out %-autoloads.el,${LINT_COMPILE_FILES})
lint-compile:
# Run byte compilation on $(call split_with_commas,${MAKEL_LINT_COMPILE_FILES})…
@${BATCH} \
--eval "(setq byte-compile-error-on-warn t)" \
$(if ${LINT_COMPILE_OPTIONS},${LINT_COMPILE_OPTIONS}) \
--funcall batch-byte-compile \
${MAKEL_LINT_COMPILE_FILES}

View file

@ -1,5 +1,29 @@
;; Frontend for taskwarrior ;;; 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:
;; TODO: Implement modeline indicator for deadline and entries ;; TODO: Implement modeline indicator for deadline and entries
;; TODO: Update buffer if command modifies the state ;; TODO: Update buffer if command modifies the state
;; TODO: Extract "1-1000" id filter into variable ;; TODO: Extract "1-1000" id filter into variable
@ -7,6 +31,8 @@
;; (and why alist-get does not work with strings) ;; (and why alist-get does not work with strings)
;; TODO: Restore position after taskwarrior-update-buffer is called ;; TODO: Restore position after taskwarrior-update-buffer is called
;;; Code:
(require 'json) (require 'json)
(require 'dash) (require 'dash)
(require 'transient) (require 'transient)
@ -72,7 +98,7 @@
(" M " . taskwarrior-priority-medium-face) (" M " . taskwarrior-priority-medium-face)
(" H " . taskwarrior-priority-high-face))) (" H " . taskwarrior-priority-high-face)))
(defvar taskwarrior-mode-map nil "Keymap for `taskwarrior-mode'") (defvar taskwarrior-mode-map nil "Keymap for `taskwarrior-mode'.")
(progn (progn
(setq taskwarrior-mode-map (make-sparse-keymap)) (setq taskwarrior-mode-map (make-sparse-keymap))
(define-key taskwarrior-mode-map (kbd "a") 'taskwarrior-add) (define-key taskwarrior-mode-map (kbd "a") 'taskwarrior-add)
@ -96,6 +122,7 @@
(define-key taskwarrior-mode-map (kbd "P") 'taskwarrior-edit-project)) (define-key taskwarrior-mode-map (kbd "P") 'taskwarrior-edit-project))
(defun taskwarrior-load-profile (profile) (defun taskwarrior-load-profile (profile)
"Load a predefined taskwarrior PROFILE."
(interactive (interactive
(list (completing-read "Profile: " (-map 'car taskwarrior-profile-alist)))) (list (completing-read "Profile: " (-map 'car taskwarrior-profile-alist))))
(let ((filter (cdr (assoc-string profile taskwarrior-profile-alist)))) (let ((filter (cdr (assoc-string profile taskwarrior-profile-alist))))
@ -105,6 +132,7 @@
(taskwarrior-update-buffer)))) (taskwarrior-update-buffer))))
(defun taskwarrior-unmark-task () (defun taskwarrior-unmark-task ()
"Unmark task."
(interactive) (interactive)
(let ((id (taskwarrior-id-at-point))) (let ((id (taskwarrior-id-at-point)))
(if (local-variable-p 'taskwarrior-marks) (if (local-variable-p 'taskwarrior-marks)
@ -119,6 +147,7 @@
(next-line))) (next-line)))
(defun taskwarrior-mark-task () (defun taskwarrior-mark-task ()
"Mark current task."
(interactive) (interactive)
(let ((id (taskwarrior-id-at-point))) (let ((id (taskwarrior-id-at-point)))
(if (local-variable-p 'taskwarrior-marks) (if (local-variable-p 'taskwarrior-marks)
@ -134,32 +163,38 @@
(next-line)))) (next-line))))
(defun taskwarrior-open-annotation () (defun taskwarrior-open-annotation ()
"Open annotation on task."
(interactive) (interactive)
(let* ((id (taskwarrior-id-at-point)) (let* ((id (taskwarrior-id-at-point))
(task (taskwarrior-export-task id)) (task (taskwarrior-export-task id))
(annotations (-map (lambda (x) (alist-get 'description x)) (vector-to-list (alist-get 'annotations task)))) (annotations (-map (lambda (x) (alist-get 'description x)) (taskwarrior-vector-to-list (alist-get 'annotations task))))
(choice (completing-read "Tags: " annotations))) (choice (completing-read "Tags: " annotations)))
(org-open-link-from-string choice))) (org-open-link-from-string choice)))
(defun taskwarrior-info () (defun taskwarrior-info ()
"Display detailed information about task."
(interactive) (interactive)
(let* ((id (taskwarrior-id-at-point)) (let* ((id (taskwarrior-id-at-point))
(buf (get-buffer-create "*taskwarrior info*"))) (buf (get-buffer-create "*taskwarrior info*")))
(progn (progn
(switch-to-buffer-other-window buf) (switch-to-buffer-other-window buf)
(insert (taskwarrior--shell-command "info" "" id))))) (erase-buffer)
(insert (taskwarrior--shell-command "info" id)))))
(defun taskwarrior--parse-created-task-id (output) (defun taskwarrior--parse-created-task-id (output)
"Extract task id from shell OUTPUT of `task add`."
(when (string-match "^.*Created task \\([0-9]+\\)\\.*$" output) (when (string-match "^.*Created task \\([0-9]+\\)\\.*$" output)
(message (match-string 1 output)))) (message (match-string 1 output))))
(defun taskwarrior--parse-org-link (link) (defun taskwarrior--parse-org-link (link)
"Extract 'org-mode' link from LINK."
(string-match org-bracket-link-regexp link) (string-match org-bracket-link-regexp link)
(list (list
(match-string 1 link) (match-string 1 link)
(match-string 3 link))) (match-string 3 link)))
(defun taskwarrior-capture (arg) (defun taskwarrior-capture (arg)
"Capture a taskwarrior task with content ARG."
(interactive "P") (interactive "P")
(let* ((link (car (cdr (taskwarrior--parse-org-link (org-store-link arg))))) (let* ((link (car (cdr (taskwarrior--parse-org-link (org-store-link arg)))))
(description (read-from-minibuffer "Description: ")) (description (read-from-minibuffer "Description: "))
@ -168,17 +203,20 @@
(shell-command-to-string (format "task %s annotate %s" id link)))) (shell-command-to-string (format "task %s annotate %s" id link))))
(defun taskwarrior-id-at-point () (defun taskwarrior-id-at-point ()
"Get id of task at point."
(let ((line (thing-at-point 'line t))) (let ((line (thing-at-point 'line t)))
(string-match "^ [0-9]*" line) (string-match "^ [0-9]*" line)
(string-trim-left (match-string 0 line)))) (string-trim-left (match-string 0 line))))
(defun taskwarrior-reset-filter () (defun taskwarrior-reset-filter ()
"Reset the currently set filter."
(interactive) (interactive)
(progn (progn
(setq-local taskwarrior-active-filter nil) (setq-local taskwarrior-active-filter nil)
(taskwarrior-update-buffer))) (taskwarrior-update-buffer)))
(defun taskwarrior-set-filter () (defun taskwarrior-set-filter ()
"Set or edit the current filter."
(interactive) (interactive)
(let ((new-filter (read-from-minibuffer "Filter: " taskwarrior-active-filter))) (let ((new-filter (read-from-minibuffer "Filter: " taskwarrior-active-filter)))
(progn (progn
@ -186,6 +224,7 @@
(taskwarrior-update-buffer)))) (taskwarrior-update-buffer))))
(defun taskwarrior--shell-command (command &optional filter modifications miscellaneous confirm) (defun taskwarrior--shell-command (command &optional filter modifications miscellaneous confirm)
"Run a taskwarrior COMMAND with specified FILTER MODIFICATIONS MISCELLANEOUS CONFIRM."
(let* ((confirmation (if confirm (concat "echo " confirm " |") "")) (let* ((confirmation (if confirm (concat "echo " confirm " |") ""))
(cmd (format "%s task %s %s %s %s" (cmd (format "%s task %s %s %s %s"
(or confirmation "") (or confirmation "")
@ -197,18 +236,19 @@
(message cmd) (message cmd)
(shell-command-to-string cmd)))) (shell-command-to-string cmd))))
(defun vector-to-list (vector) (defun taskwarrior-vector-to-list (vector)
"Convert a vector to a list" "Convert a VECTOR to a list."
(append vector nil)) (append vector nil))
(defun taskwarrior--concat-tag-list (tags) (defun taskwarrior--concat-tag-list (tags)
"Concat a list of TAGS in to readable format."
(mapconcat (mapconcat
(function (lambda (x) (format "+%s" x))) (function (lambda (x) (format "+%s" x)))
(vector-to-list tags) (taskwarrior-vector-to-list tags)
" ")) " "))
(defun taskwarrior-export (&optional filter) (defun taskwarrior-export (&optional filter)
"Turn task export into the tabulated list entry form" "Turn task export into the tabulated list entry form filted by FILTER."
(let ((filter (concat filter " id.not:0"))) (let ((filter (concat filter " id.not:0")))
(mapcar (mapcar
(lambda (entry) (lambda (entry)
@ -220,11 +260,12 @@
(tags (or (taskwarrior--concat-tag-list (alist-get 'tags entry)) "")) (tags (or (taskwarrior--concat-tag-list (alist-get 'tags entry)) ""))
(description (format "%s" (alist-get 'description entry)))) (description (format "%s" (alist-get 'description entry))))
`(,id [,id ,urgency ,priority ,annotations ,project ,tags ,description]))) `(,id [,id ,urgency ,priority ,annotations ,project ,tags ,description])))
(vector-to-list (taskwarrior-vector-to-list
(json-read-from-string (json-read-from-string
(taskwarrior--shell-command "export" filter)))))) (taskwarrior--shell-command "export" filter))))))
(defun taskwarrior-update-buffer () (defun taskwarrior-update-buffer ()
"Update the taskwarrior buffer."
(interactive) (interactive)
(progn (progn
(setq tabulated-list-entries (setq tabulated-list-entries
@ -234,15 +275,16 @@
(tabulated-list-print t))) (tabulated-list-print t)))
(defun taskwarrior-export-task (id) (defun taskwarrior-export-task (id)
(let ((task (vector-to-list "Export task with ID."
(let ((task (taskwarrior-vector-to-list
(json-read-from-string (json-read-from-string
(taskwarrior--shell-command "export" (concat "id:" id)))))) (taskwarrior--shell-command "export" (concat "id:" id))))))
(if (< (length task) 1) (if (< (length task) 1)
(error "Seems like two task have the same id.") (error "Seems like two task have the same id")
(car task)))) (car task))))
(defun taskwarrior--change-attribute (attribute) (defun taskwarrior--change-attribute (attribute)
"Change an attribute of a task" "Change an ATTRIBUTE of a task."
(let* ((prefix (concat attribute ":")) (let* ((prefix (concat attribute ":"))
(id (taskwarrior-id-at-point)) (id (taskwarrior-id-at-point))
(task (taskwarrior-export-task id)) (task (taskwarrior-export-task id))
@ -252,11 +294,12 @@
(taskwarrior--mutable-shell-command "modify" id (concat prefix quoted-value)))) (taskwarrior--mutable-shell-command "modify" id (concat prefix quoted-value))))
(defun taskwarrior-edit-tags () (defun taskwarrior-edit-tags ()
"Edit tags on task."
(interactive) (interactive)
(let* ((id (taskwarrior-id-at-point)) (let* ((id (taskwarrior-id-at-point))
(task (taskwarrior-export-task id)) (task (taskwarrior-export-task id))
(options (split-string (shell-command-to-string "task _tags") "\n")) (options (split-string (shell-command-to-string "task _tags") "\n"))
(old (vector-to-list (alist-get 'tags task))) (old (taskwarrior-vector-to-list (alist-get 'tags task)))
(current-tags (mapconcat 'identity old " ")) (current-tags (mapconcat 'identity old " "))
(new (split-string (completing-read "Tags: " options nil nil current-tags) " ")) (new (split-string (completing-read "Tags: " options nil nil current-tags) " "))
(added-tags (mapconcat (added-tags (mapconcat
@ -268,7 +311,7 @@
(taskwarrior--mutable-shell-command "modify" id (concat added-tags " " removed-tags)))) (taskwarrior--mutable-shell-command "modify" id (concat added-tags " " removed-tags))))
(defun taskwarrior-edit-project () (defun taskwarrior-edit-project ()
"Change the project of a task" "Change the project of a task."
(interactive) (interactive)
(let* ((id (taskwarrior-id-at-point)) (let* ((id (taskwarrior-id-at-point))
(task (taskwarrior-export-task id)) (task (taskwarrior-export-task id))
@ -278,48 +321,55 @@
(taskwarrior--mutable-shell-command "modify" id (concat "project:" new)))) (taskwarrior--mutable-shell-command "modify" id (concat "project:" new))))
(defun taskwarrior-change-description () (defun taskwarrior-change-description ()
"Change the description of a task" "Change the description of a task."
(interactive) (interactive)
(taskwarrior--change-attribute "description")) (taskwarrior--change-attribute "description"))
(defun taskwarrior-edit-priority () (defun taskwarrior-edit-priority ()
"Change the priority of a task" "Change the priority of a task."
(interactive) (interactive)
(let* ((id (taskwarrior-id-at-point)) (let* ((id (taskwarrior-id-at-point))
(options '("" "H" "M" "L")) (options '("" "H" "M" "L"))
(new (completing-read "Priority: " options))) (new (completing-read "Priority: " options)))
(taskwarrior--mutable-shell-command "modify" id (concat "priority:" new)))) (taskwarrior--mutable-shell-command "modify" id (concat "priority:" new))))
(defun taskwarrior-add (description) (defun taskwarrior-add (description)
"Add new task with DESCRIPTION."
(interactive "sDescription: ") (interactive "sDescription: ")
(progn (progn
(taskwarrior--add description) (taskwarrior--add description)
(taskwarrior--revert-buffer))) (taskwarrior--revert-buffer)))
(defun taskwarrior--add (description) (defun taskwarrior--add (description)
"Add new task with DESCRIPTION."
(let ((output (taskwarrior--shell-command "add" "" description))) (let ((output (taskwarrior--shell-command "add" "" description)))
(when (string-match "Created task \\([[:digit:]]+\\)." output) (when (string-match "Created task \\([[:digit:]]+\\)." output)
(match-string 1 output)))) (match-string 1 output))))
(defun taskwarrior-mark-p () (defun taskwarrior-mark-p ()
"Whether there are any marked tasks" "Whether there are any marked tasks."
(and (and
(boundp 'taskwarrior-marks) (boundp 'taskwarrior-marks)
(> (length taskwarrior-marks) 0))) (> (length taskwarrior-marks) 0)))
(defun taskwarrior-delete () (defun taskwarrior-delete ()
"Delete task at point."
(interactive) (interactive)
(taskwarrior-multi-action 'taskwarrior--delete "Delete?")) (taskwarrior-multi-action 'taskwarrior--delete "Delete?"))
(defun taskwarrior--delete (id) (defun taskwarrior--delete (id)
"Delete task with id." "Delete task with ID."
(taskwarrior--mutable-shell-command "delete" id "" "" "yes")) (taskwarrior--mutable-shell-command "delete" id "" "" "yes"))
(defun taskwarrior-done () (defun taskwarrior-done ()
"Mark task at point as done."
(interactive) (interactive)
(taskwarrior-multi-action 'taskwarrior--done "Done?")) (taskwarrior-multi-action 'taskwarrior--done "Done?"))
(defun taskwarrior-multi-action (action confirmation-text) (defun taskwarrior-multi-action (action confirmation-text)
"Run a ACTION after processing the CONFIRMATION-TEXT."
(when (yes-or-no-p confirmation-text) (when (yes-or-no-p confirmation-text)
(if (taskwarrior-mark-p) (if (taskwarrior-mark-p)
(dolist (id taskwarrior-marks) (dolist (id taskwarrior-marks)
@ -328,11 +378,11 @@
(funcall action id))))) (funcall action id)))))
(defun taskwarrior--done (id) (defun taskwarrior--done (id)
"Mark task as done." "Mark task with ID as done."
(taskwarrior--mutable-shell-command "done" id)) (taskwarrior--mutable-shell-command "done" id))
(defun taskwarrior-annotate (annotation) (defun taskwarrior-annotate (annotation)
"Delete current task." "Add ANNOTATION to task at point."
(interactive "sAnnotation: ") (interactive "sAnnotation: ")
(let ((id (taskwarrior-id-at-point))) (let ((id (taskwarrior-id-at-point)))
(taskwarrior--mutable-shell-command "annotate" id annotation))) (taskwarrior--mutable-shell-command "annotate" id annotation)))
@ -344,13 +394,14 @@
(goto-line line-number))) (goto-line line-number)))
(defun taskwarrior--mutable-shell-command (command &optional filter modifications misc confirm) (defun taskwarrior--mutable-shell-command (command &optional filter modifications misc confirm)
"Run shell command and restore taskwarrior buffer." "Run shell COMMAND with FILTER MODIFICATIONS MISC and CONFIRM."
(let ((line-number (line-number-at-pos))) (let ((line-number (line-number-at-pos)))
(taskwarrior--shell-command command filter modifications misc confirm) (taskwarrior--shell-command command filter modifications misc confirm)
(taskwarrior-update-buffer) (taskwarrior-update-buffer)
(goto-line line-number))) (goto-line line-number)))
(defun taskwarrior--urgency-predicate (A B) (defun taskwarrior--urgency-predicate (A B)
"Compare urgency of task A to task B."
(let ((a (aref (cadr A) 1)) (let ((a (aref (cadr A) 1))
(b (aref (cadr B) 1))) (b (aref (cadr B) 1)))
(> (>
@ -377,8 +428,7 @@
;;; Externally visible functions ;;; Externally visible functions
;;;###autoload ;;;###autoload
(defun taskwarrior () (defun taskwarrior ()
"Open the taskwarrior buffer. If one already exists, bring it to "Open the taskwarrior buffer. If one already exists, bring it to the front and focus it. Otherwise, create one and load the data."
the front and focus it. Otherwise, create one and load the data."
(interactive) (interactive)
(let* ((buf (get-buffer-create taskwarrior-buffer-name))) (let* ((buf (get-buffer-create taskwarrior-buffer-name)))
(progn (progn
@ -389,18 +439,22 @@ the front and focus it. Otherwise, create one and load the data."
(hl-line-mode)))) (hl-line-mode))))
(defun taskwarrior-set-due () (defun taskwarrior-set-due ()
"Set due date on task."
(interactive) (interactive)
(taskwarrior--change-attribute "due")) (taskwarrior--change-attribute "due"))
(defun taskwarrior-set-scheduled () (defun taskwarrior-set-scheduled ()
"Set schedule date on task at point."
(interactive) (interactive)
(taskwarrior--change-attribute "scheduled")) (taskwarrior--change-attribute "scheduled"))
(defun taskwarrior-set-wait () (defun taskwarrior-set-wait ()
"Set wait date on task at point."
(interactive) (interactive)
(taskwarrior--change-attribute "wait")) (taskwarrior--change-attribute "wait"))
(defun taskwarrior-set-untl () (defun taskwarrior-set-untl ()
"Set until date on task at point."
(interactive) (interactive)
(taskwarrior--change-attribute "until")) (taskwarrior--change-attribute "until"))
@ -411,3 +465,6 @@ the front and focus it. Otherwise, create one and load the data."
("s" "scheduled" taskwarrior-set-scheduled) ("s" "scheduled" taskwarrior-set-scheduled)
("w" "wait" taskwarrior-set-wait) ("w" "wait" taskwarrior-set-wait)
("u" "until" taskwarrior-set-untl)]]) ("u" "until" taskwarrior-set-untl)]])
(provide 'taskwarrior)
;;; taskwarrior.el ends here

View file

@ -1,17 +1,22 @@
;;; taskwarrior-test.el --- taskwarrior unit tests ;;; taskwarrior-test.el --- Taskwarrior test suite -*- lexical-binding: t; -*-
;; Copyright (C) 2019-2020 Patrick Winter
;;
;; License: GPLv3
;;; Commentary: ;;; Commentary:
;; Run through make target `m̀ake test`
;;; Code: ;;; Code:
(require 'ert) (require 'taskwarrior)
(ert-deftest taskwarrior-add-task-test () (ert-deftest taskwarrior-add-task ()
"Ensure that special characters such as quotes and parens are properly escaped when adding new tasks" "Ensure that special characters such as quotes and parens are properly escaped when adding new tasks"
(let* ((task-id (taskwarrior--add "project:ert +emacs \"Write test suite for taskwarrior.el (using ERT)\"")) (let* ((task-id (taskwarrior--add "project:ert +emacs \"Write test suite for taskwarrior.el (using ERT)\""))
(task (taskwarrior-export-task task-id))) (task (taskwarrior-export-task task-id)))
(should (string= (alist-get 'project task) "ert")) (should (string= (alist-get 'project task) "ert"))
(should (string= (aref (alist-get 'tags task) 0) "emacs")) (should (string= (aref (alist-get 'tags task) 0) "emacs"))
(should (string= (alist-get 'description task) "Write test suite for taskwarrior.el (using ERT)")))) (should (string= (alist-get 'description task) "Write test suite for taskwarrior.el (using ERT)"))))
(provide 'taskwarrior-test)
;;; taskwarrior-test.el ends here