aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--init.el1
-rw-r--r--modules/linear-config.el100
-rw-r--r--tests/test-linear-config.el52
3 files changed, 153 insertions, 0 deletions
diff --git a/init.el b/init.el
index 4c522362..19d8e269 100644
--- a/init.el
+++ b/init.el
@@ -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