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
- snapshot
steps:
- name: Set up Emacs
uses: purcell/setup-emacs@v1.0
with:
version: ${{ matrix.emacs_version }}
- uses: actions/checkout@v1
- name: Install taskwarrior package
run: sudo apt-get install -y taskwarrior
- name: Run ERT test suite
run: make test
- name: Set up Emacs
uses: purcell/setup-emacs@v1.0
with:
version: ${{ matrix.emacs_version }}
- uses: actions/checkout@v1
- 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
dash.el
transient.el
makel.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
test-data: ## Generate some example tasks
for n in $$(seq 10); do task add "Example task $${n}"; done
.PHONY: test
test: fetch-deps ## Run ERT test suite
emacs -batch -l dash.el -l transient.el -l taskwarrior.el -l taskwarrior-test.el -f ert-run-tests-batch-and-exit
# .PHONY: fetch-deps
# fetch-deps: ## Fetch required dependencies
# 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
fetch-deps: ## Fetch required dependencies
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"
# Include makel.mk if present
-include makel.mk

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: Update buffer if command modifies the state
;; TODO: Extract "1-1000" id filter into variable
@ -7,6 +31,8 @@
;; (and why alist-get does not work with strings)
;; TODO: Restore position after taskwarrior-update-buffer is called
;;; Code:
(require 'json)
(require 'dash)
(require 'transient)
@ -72,7 +98,7 @@
(" M " . taskwarrior-priority-medium-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
(setq taskwarrior-mode-map (make-sparse-keymap))
(define-key taskwarrior-mode-map (kbd "a") 'taskwarrior-add)
@ -96,6 +122,7 @@
(define-key taskwarrior-mode-map (kbd "P") 'taskwarrior-edit-project))
(defun taskwarrior-load-profile (profile)
"Load a predefined taskwarrior PROFILE."
(interactive
(list (completing-read "Profile: " (-map 'car taskwarrior-profile-alist))))
(let ((filter (cdr (assoc-string profile taskwarrior-profile-alist))))
@ -105,6 +132,7 @@
(taskwarrior-update-buffer))))
(defun taskwarrior-unmark-task ()
"Unmark task."
(interactive)
(let ((id (taskwarrior-id-at-point)))
(if (local-variable-p 'taskwarrior-marks)
@ -119,6 +147,7 @@
(next-line)))
(defun taskwarrior-mark-task ()
"Mark current task."
(interactive)
(let ((id (taskwarrior-id-at-point)))
(if (local-variable-p 'taskwarrior-marks)
@ -134,32 +163,38 @@
(next-line))))
(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)) (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)))
(org-open-link-from-string choice)))
(defun taskwarrior-info ()
"Display detailed information about task."
(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)))))
(erase-buffer)
(insert (taskwarrior--shell-command "info" id)))))
(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: "))
@ -168,17 +203,20 @@
(shell-command-to-string (format "task %s annotate %s" id link))))
(defun taskwarrior-id-at-point ()
"Get id of task 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 ()
"Reset the currently set filter."
(interactive)
(progn
(setq-local taskwarrior-active-filter nil)
(taskwarrior-update-buffer)))
(defun taskwarrior-set-filter ()
"Set or edit the current filter."
(interactive)
(let ((new-filter (read-from-minibuffer "Filter: " taskwarrior-active-filter)))
(progn
@ -186,6 +224,7 @@
(taskwarrior-update-buffer))))
(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 " |") ""))
(cmd (format "%s task %s %s %s %s"
(or confirmation "")
@ -197,18 +236,19 @@
(message cmd)
(shell-command-to-string cmd))))
(defun vector-to-list (vector)
"Convert a vector to a list"
(defun taskwarrior-vector-to-list (vector)
"Convert a VECTOR to a list."
(append vector nil))
(defun taskwarrior--concat-tag-list (tags)
"Concat a list of TAGS in to readable format."
(mapconcat
(function (lambda (x) (format "+%s" x)))
(vector-to-list tags)
(taskwarrior-vector-to-list tags)
" "))
(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")))
(mapcar
(lambda (entry)
@ -220,11 +260,12 @@
(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
(taskwarrior-vector-to-list
(json-read-from-string
(taskwarrior--shell-command "export" filter))))))
(defun taskwarrior-update-buffer ()
"Update the taskwarrior buffer."
(interactive)
(progn
(setq tabulated-list-entries
@ -234,15 +275,16 @@
(tabulated-list-print t)))
(defun taskwarrior-export-task (id)
(let ((task (vector-to-list
"Export task with ID."
(let ((task (taskwarrior-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.")
(error "Seems like two task have the same id")
(car task))))
(defun taskwarrior--change-attribute (attribute)
"Change an attribute of a task"
"Change an ATTRIBUTE of a task."
(let* ((prefix (concat attribute ":"))
(id (taskwarrior-id-at-point))
(task (taskwarrior-export-task id))
@ -252,11 +294,12 @@
(taskwarrior--mutable-shell-command "modify" id (concat prefix quoted-value))))
(defun taskwarrior-edit-tags ()
"Edit tags on task."
(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)))
(old (taskwarrior-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
@ -268,7 +311,7 @@
(taskwarrior--mutable-shell-command "modify" id (concat added-tags " " removed-tags))))
(defun taskwarrior-edit-project ()
"Change the project of a task"
"Change the project of a task."
(interactive)
(let* ((id (taskwarrior-id-at-point))
(task (taskwarrior-export-task id))
@ -278,48 +321,55 @@
(taskwarrior--mutable-shell-command "modify" id (concat "project:" new))))
(defun taskwarrior-change-description ()
"Change the description of a task"
"Change the description of a task."
(interactive)
(taskwarrior--change-attribute "description"))
(defun taskwarrior-edit-priority ()
"Change the priority of a task"
"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)
"Add new task with DESCRIPTION."
(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))))
(defun taskwarrior-mark-p ()
"Whether there are any marked tasks"
"Whether there are any marked tasks."
(and
(boundp 'taskwarrior-marks)
(> (length taskwarrior-marks) 0)))
(defun taskwarrior-delete ()
"Delete task at point."
(interactive)
(taskwarrior-multi-action 'taskwarrior--delete "Delete?"))
(defun taskwarrior--delete (id)
"Delete task with id."
"Delete task with ID."
(taskwarrior--mutable-shell-command "delete" id "" "" "yes"))
(defun taskwarrior-done ()
"Mark task at point as done."
(interactive)
(taskwarrior-multi-action 'taskwarrior--done "Done?"))
(defun taskwarrior-multi-action (action confirmation-text)
"Run a ACTION after processing the CONFIRMATION-TEXT."
(when (yes-or-no-p confirmation-text)
(if (taskwarrior-mark-p)
(dolist (id taskwarrior-marks)
@ -328,11 +378,11 @@
(funcall action id)))))
(defun taskwarrior--done (id)
"Mark task as done."
"Mark task with ID as done."
(taskwarrior--mutable-shell-command "done" id))
(defun taskwarrior-annotate (annotation)
"Delete current task."
"Add ANNOTATION to task at point."
(interactive "sAnnotation: ")
(let ((id (taskwarrior-id-at-point)))
(taskwarrior--mutable-shell-command "annotate" id annotation)))
@ -344,13 +394,14 @@
(goto-line line-number)))
(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)))
(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)))
(>
@ -377,8 +428,7 @@
;;; 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."
"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
@ -389,18 +439,22 @@ the front and focus it. Otherwise, create one and load the data."
(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"))
@ -411,3 +465,6 @@ the front and focus it. Otherwise, create one and load the data."
("s" "scheduled" taskwarrior-set-scheduled)
("w" "wait" taskwarrior-set-wait)
("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:
;; Run through make target `m̀ake test`
;;; 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"
(let* ((task-id (taskwarrior--add "project:ert +emacs \"Write test suite for taskwarrior.el (using ERT)\""))
(task (taskwarrior-export-task task-id)))
(should (string= (alist-get 'project task) "ert"))
(should (string= (aref (alist-get 'tags task) 0) "emacs"))
(should (string= (alist-get 'description task) "Write test suite for taskwarrior.el (using ERT)"))))
(provide 'taskwarrior-test)
;;; taskwarrior-test.el ends here