aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modules/dashboard-config.el131
-rw-r--r--tests/test-dashboard-config-launchers.el97
2 files changed, 147 insertions, 81 deletions
diff --git a/modules/dashboard-config.el b/modules/dashboard-config.el
index 803c249d..74c32bd5 100644
--- a/modules/dashboard-config.el
+++ b/modules/dashboard-config.el
@@ -56,6 +56,52 @@
Positive values shift left, negative values shift right.
Adjust this if the title doesn't appear centered under the banner image.")
+;; --------------------------- Launcher Definitions ----------------------------
+;; Single source of truth for the dashboard launchers. Both the navigator
+;; icon rows and the dashboard-mode-map keybindings derive from this table, so
+;; a launcher is added or reordered in exactly one place (no icon-row/keymap
+;; drift). Each entry: (KEY ICON-FN ICON-NAME LABEL TOOLTIP ACTION); ACTION is
+;; a zero-argument function run by both the icon button and the key.
+
+(defconst cj/dashboard--launchers
+ (list
+ (list "c" #'nerd-icons-faicon "nf-fa-code" "Code" "Switch Project" (lambda () (projectile-switch-project)))
+ (list "d" #'nerd-icons-faicon "nf-fa-folder_o" "Files" "Dirvish File Manager" (lambda () (dirvish user-home-dir)))
+ (list "t" #'nerd-icons-devicon "nf-dev-terminal" "Terminal" "Launch VTerm" (lambda () (vterm)))
+ (list "a" #'nerd-icons-mdicon "nf-md-calendar" "Agenda" "Main Org Agenda" (lambda () (cj/main-agenda-display)))
+ (list "r" #'nerd-icons-faicon "nf-fa-rss_square" "Feeds" "Elfeed Feed Reader" (lambda () (cj/elfeed-open)))
+ (list "b" #'nerd-icons-faicon "nf-fae-book_open_o" "Books" "Calibre Ebook Reader" (lambda () (calibredb)))
+ (list "f" #'nerd-icons-mdicon "nf-md-school" "Flashcards" "Org-Drill" (lambda () (cj/drill-start)))
+ (list "m" #'nerd-icons-mdicon "nf-md-music" "Music" "EMMS Music Player" (lambda () (cj/music-playlist-toggle) (cj/music-playlist-load)))
+ (list "e" #'nerd-icons-faicon "nf-fa-envelope" "Email" "Mu4e Email Client" (lambda () (mu4e)))
+ (list "i" #'nerd-icons-faicon "nf-fa-comments" "IRC" "Emacs Relay Chat" (lambda () (cj/erc-switch-to-buffer-with-completion)))
+ (list "s" #'nerd-icons-faicon "nf-fa-slack" "Slack" "Slack Client" (lambda () (cj/slack-start)))
+ (list "g" #'nerd-icons-faicon "nf-fa-telegram" "Telegram" "Telega Telegram Client" (lambda () (cj/telega))))
+ "Dashboard launcher table: (KEY ICON-FN ICON-NAME LABEL TOOLTIP ACTION).
+Drives both `dashboard-navigator-buttons' and the dashboard-mode-map keys.")
+
+(defun cj/dashboard--navigator-rows ()
+ "Build `dashboard-navigator-buttons' rows from `cj/dashboard--launchers'.
+Chunks the launchers four per row and maps each to a navigator button."
+ (let (rows row)
+ (dolist (l cj/dashboard--launchers)
+ (let ((icon-fn (nth 1 l)) (icon-name (nth 2 l))
+ (label (nth 3 l)) (tooltip (nth 4 l)) (action (nth 5 l)))
+ (push (list (funcall icon-fn icon-name) label tooltip
+ (lambda (&rest _) (funcall action)) nil " " "")
+ row))
+ (when (= (length row) 4)
+ (push (nreverse row) rows)
+ (setq row nil)))
+ (when row (push (nreverse row) rows))
+ (nreverse rows)))
+
+(defun cj/dashboard--bind-launchers (map)
+ "Bind each launcher KEY in MAP to a command that runs its ACTION."
+ (dolist (l cj/dashboard--launchers)
+ (let ((key (nth 0 l)) (action (nth 5 l)))
+ (define-key map (kbd key) (lambda () (interactive) (funcall action))))))
+
;; ----------------------------- Display Dashboard -----------------------------
;; convenience function to redisplay dashboard and kill all other windows
@@ -124,69 +170,7 @@ window."
;; == navigation
(setq dashboard-set-navigator t)
- (setq dashboard-navigator-buttons
- `(;; Row 1 — Work tools
- ((,(nerd-icons-faicon "nf-fa-code")
- "Code" "Switch Project"
- (lambda (&rest _) (projectile-switch-project))
- nil " " "")
-
- (,(nerd-icons-faicon "nf-fa-folder_o")
- "Files" "Dirvish File Manager"
- (lambda (&rest _) (dirvish user-home-dir))
- nil " " "")
-
- (,(nerd-icons-devicon "nf-dev-terminal")
- "Terminal" "Launch VTerm"
- (lambda (&rest _) (vterm))
- nil " " "")
-
- (,(nerd-icons-mdicon "nf-md-calendar")
- "Agenda" "Main Org Agenda"
- (lambda (&rest _) (cj/main-agenda-display))
- nil " " ""))
-
- ;; Row 2 — Read & Learn
- ((,(nerd-icons-faicon "nf-fa-rss_square")
- "Feeds" "Elfeed Feed Reader"
- (lambda (&rest _) (cj/elfeed-open))
- nil " " "")
-
- (,(nerd-icons-faicon "nf-fae-book_open_o")
- "Books" "Calibre Ebook Reader"
- (lambda (&rest _) (calibredb))
- nil " " "")
-
- (,(nerd-icons-mdicon "nf-md-school")
- "Flashcards" "Org-Drill"
- (lambda (&rest _) (cj/drill-start))
- nil " " "")
-
- (,(nerd-icons-mdicon "nf-md-music")
- "Music" "EMMS Music Player"
- (lambda (&rest _) (cj/music-playlist-toggle) (cj/music-playlist-load))
- nil " " ""))
-
- ;; Row 3 — Communication
- ((,(nerd-icons-faicon "nf-fa-envelope")
- "Email" "Mu4e Email Client"
- (lambda (&rest _) (mu4e))
- nil " " "")
-
- (,(nerd-icons-faicon "nf-fa-comments")
- "IRC" "Emacs Relay Chat"
- (lambda (&rest _) (cj/erc-switch-to-buffer-with-completion))
- nil " " "")
-
- (,(nerd-icons-faicon "nf-fa-slack")
- "Slack" "Slack Client"
- (lambda (&rest _) (cj/slack-start))
- nil " " "")
-
- (,(nerd-icons-faicon "nf-fa-telegram")
- "Telegram" "Telega Telegram Client"
- (lambda (&rest _) (cj/telega))
- nil " " ""))))
+ (setq dashboard-navigator-buttons (cj/dashboard--navigator-rows))
;; == content
(setq dashboard-show-shortcuts nil) ;; don't show dashboard item abbreviations
@@ -198,24 +182,9 @@ window."
;; Disable 'q' to quit dashboard
(define-key dashboard-mode-map (kbd "q") nil)
- ;; Dashboard launcher keybindings, ordered to mirror the icon rows.
- ;; Row 1 — Work tools
- (define-key dashboard-mode-map (kbd "c") (lambda () (interactive) (projectile-switch-project)))
- (define-key dashboard-mode-map (kbd "d") (lambda () (interactive) (dirvish user-home-dir)))
- (define-key dashboard-mode-map (kbd "t") (lambda () (interactive) (vterm)))
- (define-key dashboard-mode-map (kbd "a") (lambda () (interactive) (cj/main-agenda-display)))
- ;; Row 2 — Read & Learn
- (define-key dashboard-mode-map (kbd "r") (lambda () (interactive) (cj/elfeed-open)))
- (define-key dashboard-mode-map (kbd "b") (lambda () (interactive) (calibredb)))
- (define-key dashboard-mode-map (kbd "f") (lambda () (interactive) (cj/drill-start)))
- (define-key dashboard-mode-map (kbd "m") (lambda () (interactive)
- (cj/music-playlist-toggle)
- (cj/music-playlist-load)))
- ;; Row 3 — Communication
- (define-key dashboard-mode-map (kbd "e") (lambda () (interactive) (mu4e)))
- (define-key dashboard-mode-map (kbd "i") (lambda () (interactive) (cj/erc-switch-to-buffer-with-completion)))
- (define-key dashboard-mode-map (kbd "s") (lambda () (interactive) (cj/slack-start)))
- (define-key dashboard-mode-map (kbd "g") (lambda () (interactive) (cj/telega))))
+ ;; Launcher keys, derived from `cj/dashboard--launchers' (same source as the
+ ;; navigator icons, so key order can't drift from the icon-row order).
+ (cj/dashboard--bind-launchers dashboard-mode-map))
;; Override banner title centering (must be after dashboard-widgets loads)
(with-eval-after-load 'dashboard-widgets
diff --git a/tests/test-dashboard-config-launchers.el b/tests/test-dashboard-config-launchers.el
new file mode 100644
index 00000000..633d6612
--- /dev/null
+++ b/tests/test-dashboard-config-launchers.el
@@ -0,0 +1,97 @@
+;;; test-dashboard-config-launchers.el --- Tests for the dashboard launcher table -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Locks the single-source launcher table that drives both the dashboard
+;; navigator icon rows and the dashboard-mode-map keybindings. The bug this
+;; guards against: the icon row and the keymap were maintained as two separate
+;; inline lists that could drift in order or wiring. These tests assert the
+;; table, the derived 3x4 navigator rows, and that every key binds to the
+;; right command (including Music's two-call action).
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
+(setq load-prefer-newer t)
+
+;; deps dashboard-config pulls transitively / actions reference
+(unless (fboundp 'cj/kill-all-other-buffers-and-windows)
+ (defun cj/kill-all-other-buffers-and-windows () nil))
+(unless (fboundp 'cj/make-buffer-undead) (defun cj/make-buffer-undead (_n) nil))
+(defvar user-home-dir "/tmp/")
+
+(require 'dashboard-config)
+
+(defconst test-dash--keys '("c" "d" "t" "a" "r" "b" "f" "m" "e" "i" "s" "g"))
+
+;; ----------------------------- launcher table --------------------------------
+
+(ert-deftest test-dashboard-launchers-keys-in-order ()
+ "Normal: 12 launchers with the expected keys in display order."
+ (should (= 12 (length cj/dashboard--launchers)))
+ (should (equal test-dash--keys (mapcar (lambda (l) (nth 0 l)) cj/dashboard--launchers))))
+
+(ert-deftest test-dashboard-launchers-labels-in-order ()
+ "Normal: labels in display order."
+ (should (equal '("Code" "Files" "Terminal" "Agenda" "Feeds" "Books"
+ "Flashcards" "Music" "Email" "IRC" "Slack" "Telegram")
+ (mapcar (lambda (l) (nth 3 l)) cj/dashboard--launchers))))
+
+;; --------------------------- navigator rows ----------------------------------
+
+(ert-deftest test-dashboard-navigator-rows-three-rows-of-four ()
+ "Normal: navigator derives 3 rows of 4, with the right labels and button shape."
+ (cl-letf (((symbol-function 'nerd-icons-faicon) (lambda (n &rest _) (concat "I:" n)))
+ ((symbol-function 'nerd-icons-devicon) (lambda (n &rest _) (concat "I:" n)))
+ ((symbol-function 'nerd-icons-mdicon) (lambda (n &rest _) (concat "I:" n))))
+ (let ((rows (cj/dashboard--navigator-rows)))
+ (should (= 3 (length rows)))
+ (should (cl-every (lambda (r) (= 4 (length r))) rows))
+ (should (equal '("Code" "Files" "Terminal" "Agenda")
+ (mapcar (lambda (b) (nth 1 b)) (nth 0 rows))))
+ (let ((btn (car (car rows)))) ; (icon label tooltip action nil " " "")
+ (should (string= "I:nf-fa-code" (nth 0 btn)))
+ (should (string= "Code" (nth 1 btn)))
+ (should (functionp (nth 3 btn)))
+ (should (null (nth 4 btn)))))))
+
+;; ---------------------------- keymap binding ---------------------------------
+
+(ert-deftest test-dashboard-bind-launchers-binds-every-key ()
+ "Normal: every launcher key binds to a command; q is left to the caller."
+ (let ((map (make-sparse-keymap)))
+ (cj/dashboard--bind-launchers map)
+ (dolist (key test-dash--keys)
+ (should (commandp (keymap-lookup map key))))
+ (should-not (keymap-lookup map "q"))))
+
+(ert-deftest test-dashboard-bind-launchers-each-key-runs-its-command ()
+ "Behavior: invoking each key runs its launcher's command(s) — Music runs two."
+ (let ((map (make-sparse-keymap)) (calls nil))
+ (cl-letf (((symbol-function 'projectile-switch-project) (lambda (&rest _) (push 'code calls)))
+ ((symbol-function 'dirvish) (lambda (&rest _) (push 'files calls)))
+ ((symbol-function 'vterm) (lambda (&rest _) (push 'term calls)))
+ ((symbol-function 'cj/main-agenda-display) (lambda (&rest _) (push 'agenda calls)))
+ ((symbol-function 'cj/elfeed-open) (lambda (&rest _) (push 'feeds calls)))
+ ((symbol-function 'calibredb) (lambda (&rest _) (push 'books calls)))
+ ((symbol-function 'cj/drill-start) (lambda (&rest _) (push 'cards calls)))
+ ((symbol-function 'cj/music-playlist-toggle) (lambda (&rest _) (push 'm-toggle calls)))
+ ((symbol-function 'cj/music-playlist-load) (lambda (&rest _) (push 'm-load calls)))
+ ((symbol-function 'mu4e) (lambda (&rest _) (push 'email calls)))
+ ((symbol-function 'cj/erc-switch-to-buffer-with-completion) (lambda (&rest _) (push 'irc calls)))
+ ((symbol-function 'cj/slack-start) (lambda (&rest _) (push 'slack calls)))
+ ((symbol-function 'cj/telega) (lambda (&rest _) (push 'tg calls))))
+ (cj/dashboard--bind-launchers map)
+ (dolist (key test-dash--keys)
+ (call-interactively (keymap-lookup map key)))
+ (should (memq 'code calls))
+ (should (memq 'tg calls))
+ (should (memq 'm-toggle calls))
+ (should (memq 'm-load calls))
+ (should (= 13 (length calls)))))) ; 12 keys, Music fires two
+
+(provide 'test-dashboard-config-launchers)
+;;; test-dashboard-config-launchers.el ends here