diff options
| -rw-r--r-- | init.el | 1 | ||||
| -rw-r--r-- | modules/linear-config.el | 100 | ||||
| -rw-r--r-- | tests/test-linear-config.el | 52 |
3 files changed, 153 insertions, 0 deletions
@@ -71,6 +71,7 @@ (require 'diff-config) ;; diff and merge functionality w/in Emacs (require 'erc-config) ;; seamless IRC client (require 'slack-config) ;; slack client via emacs-slack +(require 'linear-config) ;; Linear.app issue tracking (deepsat workspace) (require 'telega-config) ;; telegram client via telega.el (TDLib in docker) (require 'eshell-config) ;; emacs shell configuration (require 'vterm-config) ;; vterm + F12 toggle + tmux history copy diff --git a/modules/linear-config.el b/modules/linear-config.el new file mode 100644 index 00000000..f92c13ce --- /dev/null +++ b/modules/linear-config.el @@ -0,0 +1,100 @@ +;;; linear-config.el --- Linear.app integration -*- lexical-binding: t; -*- +;; author: Craig Jennings <c@cjennings.net> + +;;; Commentary: + +;; Wires the local linear-emacs checkout (~/code/linear-emacs) into the config, +;; pointed at DeepSat's Linear workspace. +;; +;; Authentication: +;; The Linear personal API key is read from authinfo.gpg, never plaintext. +;; Add an entry like: +;; machine api.linear.app login apikey password lin_api_YOURKEYHERE +;; Generate the key in Linear: Settings -> Security & access -> Personal API +;; keys. The key is loaded lazily on first use, so there is no GPG prompt at +;; startup. +;; +;; The default team is DeepSat's Software Engineering team (the SE-* issues), so +;; new issues land there unless another team is chosen. +;; +;; Keybindings (C-; L prefix): +;; C-; L l — list issues +;; C-; L p — list issues by project +;; C-; L n — new issue +;; C-; L s — enable org sync +;; C-; L S — disable org sync +;; C-; L t — test connection +;; C-; L ? — check setup + +;;; Code: + +(require 'system-lib) ;; provides cj/auth-source-secret-value + +;; Owned by linear-emacs, which loads lazily via :load-path below. +(defvar linear-emacs-api-key) +(defvar linear-emacs-default-team-id) +(declare-function linear-emacs--graphql-request-async "linear-emacs") + +(defconst cj/linear-team-id "9fca2cf6-390c-4102-a9ff-f94a4ed823c5" + "Linear team id for DeepSat's Software Engineering team (the SE-* issues).") + +(defun cj/linear--ensure-api-key () + "Load the Linear API key from authinfo.gpg into `linear-emacs-api-key' if unset. +Looks up host \"api.linear.app\". This is a no-op once the key is set, so the +GPG prompt fires at most once per session and only when Linear is actually used." + (unless linear-emacs-api-key + (setq linear-emacs-api-key (cj/auth-source-secret-value "api.linear.app")))) + +(defun cj/linear--ensure-key-before (&rest _) + "Advice: load the Linear API key before a GraphQL request runs. +Named (not a lambda) so the advice is idempotent across reloads and removable." + (cj/linear--ensure-api-key)) + +(use-package linear-emacs + :ensure nil ;; local checkout, not from an archive + :load-path "~/code/linear-emacs" + :defer t + :commands (linear-emacs-list-issues + linear-emacs-list-issues-by-project + linear-emacs-new-issue + linear-emacs-enable-org-sync + linear-emacs-disable-org-sync + linear-emacs-test-connection + linear-emacs-check-setup) + :config + (setq linear-emacs-default-team-id cj/linear-team-id) + ;; Load the key before any GraphQL request — lazy, and it retries if the key + ;; was added to authinfo after a first (failed) attempt this session. + (advice-add 'linear-emacs--graphql-request-async :before + #'cj/linear--ensure-key-before)) + +;; ------------------------------ Keybindings ---------------------------------- + +(defvar cj/linear-keymap (make-sparse-keymap) + "Keymap for Linear commands under C-; L.") + +(global-set-key (kbd "C-; L") cj/linear-keymap) + +(define-key cj/linear-keymap (kbd "l") #'linear-emacs-list-issues) +(define-key cj/linear-keymap (kbd "p") #'linear-emacs-list-issues-by-project) +(define-key cj/linear-keymap (kbd "n") #'linear-emacs-new-issue) +(define-key cj/linear-keymap (kbd "s") #'linear-emacs-enable-org-sync) +(define-key cj/linear-keymap (kbd "S") #'linear-emacs-disable-org-sync) +(define-key cj/linear-keymap (kbd "t") #'linear-emacs-test-connection) +(define-key cj/linear-keymap (kbd "?") #'linear-emacs-check-setup) + +;; Register which-key labels lazily so this module's require doesn't depend on +;; which-key being loaded. Same pattern as the other client modules. +(with-eval-after-load 'which-key + (which-key-add-keymap-based-replacements cj/linear-keymap + "" "linear menu" + "l" "list issues" + "p" "issues by project" + "n" "new issue" + "s" "enable org sync" + "S" "disable org sync" + "t" "test connection" + "?" "check setup")) + +(provide 'linear-config) +;;; linear-config.el ends here
\ No newline at end of file diff --git a/tests/test-linear-config.el b/tests/test-linear-config.el new file mode 100644 index 00000000..c3e702d8 --- /dev/null +++ b/tests/test-linear-config.el @@ -0,0 +1,52 @@ +;;; test-linear-config.el --- Tests for linear-config.el -*- lexical-binding: t; -*- + +;;; Commentary: +;; Covers the lazy API-key loader and the keybinding wiring. linear-emacs +;; itself is never loaded here (it's a deferred :load-path package), so +;; `linear-emacs-api-key' is declared special below to make the dynamic +;; let-bindings reach `cj/linear--ensure-api-key'. `cj/auth-source-secret-value' +;; is stubbed — no authinfo.gpg / GPG access in the tests. + +;;; Code: + +(require 'ert) +(require 'cl-lib) +(require 'linear-config) + +;; linear-config declares this with a bare (defvar linear-emacs-api-key), which +;; is only file-local; declare it special here so the let-bindings are dynamic. +(defvar linear-emacs-api-key nil) + +(ert-deftest test-linear-ensure-api-key-loads-when-unset () + "Normal: an unset key is loaded from auth-source." + (let ((linear-emacs-api-key nil)) + (cl-letf (((symbol-function 'cj/auth-source-secret-value) + (lambda (&rest _) "lin_api_test"))) + (cj/linear--ensure-api-key) + (should (equal linear-emacs-api-key "lin_api_test"))))) + +(ert-deftest test-linear-ensure-api-key-keeps-existing () + "Boundary: an already-set key is neither overwritten nor re-fetched." + (let ((linear-emacs-api-key "already-set") (fetched nil)) + (cl-letf (((symbol-function 'cj/auth-source-secret-value) + (lambda (&rest _) (setq fetched t) "other"))) + (cj/linear--ensure-api-key) + (should (equal linear-emacs-api-key "already-set")) + (should-not fetched)))) + +(ert-deftest test-linear-ensure-api-key-nil-when-absent () + "Boundary: a missing authinfo entry leaves the key nil without error." + (let ((linear-emacs-api-key nil)) + (cl-letf (((symbol-function 'cj/auth-source-secret-value) (lambda (&rest _) nil))) + (cj/linear--ensure-api-key) + (should-not linear-emacs-api-key)))) + +(ert-deftest test-linear-keymap-bound-under-prefix () + "Smoke: C-; L holds the linear keymap and the entry commands are bound." + (should (keymapp cj/linear-keymap)) + (should (eq (keymap-lookup (current-global-map) "C-; L") cj/linear-keymap)) + (should (eq (keymap-lookup cj/linear-keymap "l") #'linear-emacs-list-issues)) + (should (eq (keymap-lookup cj/linear-keymap "n") #'linear-emacs-new-issue))) + +(provide 'test-linear-config) +;;; test-linear-config.el ends here |
