summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-08-23 19:19:17 +0200
committercashmere <cashmere@autistici.org>2026-08-23 19:19:17 +0200
commit15def4cc1edbd5a80973fa9c5bba35d5eb32c340 (patch)
treea9172e36e74a6691738b7d5012289cec1767241d
parente8b0abed0711cefcd0c06b8e12e3a7a9224fb329 (diff)
downloademacs-15def4cc1edbd5a80973fa9c5bba35d5eb32c340.tar.gz
feat: refresh Emacs UI and lock Elpaca packages
-rw-r--r--.gitignore1
-rw-r--r--config.el1190
-rw-r--r--config.org1305
-rw-r--r--elpaca-lock.eld1602
-rw-r--r--init.el6
-rw-r--r--lisp/denote-menu.el297
-rw-r--r--lisp/garden-connect.el365
-rw-r--r--lisp/garden-core.el372
-rw-r--r--lisp/garden-dashboard.el461
-rw-r--r--lisp/garden-fleet.el464
-rw-r--r--lisp/garden-icons.el64
-rw-r--r--lisp/garden-links.el29
-rw-r--r--lisp/garden-preview.el111
-rw-r--r--lisp/garden-publish.el450
-rw-r--r--lisp/my-mode-line.el99
-rw-r--r--lisp/rsync-ui.el142
-rw-r--r--lisp/zfs.el292
-rw-r--r--local.el.example3
-rw-r--r--themes/tinty-theme.el134
19 files changed, 5943 insertions, 1444 deletions
diff --git a/.gitignore b/.gitignore
index b064cdf..13ce890 100644
--- a/.gitignore
+++ b/.gitignore
@@ -100,5 +100,6 @@ dirvish/thumbnails
/elfeed/index
/org-persist/
*.eld
+!/elpaca-lock.eld
/elfeed/
/cache/
diff --git a/config.el b/config.el
index c853b14..a533040 100644
--- a/config.el
+++ b/config.el
@@ -59,6 +59,25 @@ Set per-host in the gitignored `local.el'.")
(funcall orig name _keyword args)))
(advice-add 'use-package-normalize/:ensure :around #'my/elpaca-skip-local)
+(defun my/elpaca-write-lock-file ()
+ "Write exact revisions of all queued packages to `elpaca-lock-file'."
+ (interactive)
+ (unless elpaca-lock-file
+ (user-error "`elpaca-lock-file' is not configured"))
+ (elpaca-wait)
+ (elpaca-write-lock-file elpaca-lock-file)
+ ;; Make the newly written revisions visible to this Emacs session too.
+ (elpaca-menu-lock-file 'update)
+ (message "Elpaca lock file updated: %s"
+ (abbreviate-file-name elpaca-lock-file)))
+
+(defun my/elpaca-visit-lock-file ()
+ "Visit the configured `elpaca-lock-file'."
+ (interactive)
+ (unless elpaca-lock-file
+ (user-error "`elpaca-lock-file' is not configured"))
+ (find-file elpaca-lock-file))
+
(use-package emacs
:ensure nil
:custom
@@ -213,8 +232,7 @@ Selects the compilation window so the cursor lands in it."
"Focus the async-shell window and make q bury it without killing the process."
(select-window window)
(with-current-buffer (window-buffer window)
- (evil-local-set-key 'normal (kbd "q") #'quit-window)
- (evil-local-set-key 'motion (kbd "q") #'quit-window)
+ (my/async-shell-command-bindings)
(evil-normal-state)))
(use-package window
@@ -276,11 +294,6 @@ Selects the compilation window so the cursor lands in it."
(mailcap-add "image/jpg" "kitty --hold kitty +kitten icat %s")
(mailcap-add "image/gif" "kitty --hold kitty +kitten icat %s")
- (define-key dired-mode-map (kbd "E")
- (lambda ()
- (interactive)
- (mailcap-view-file (dired-get-filename))))
-
(advice-add 'dired-create-directory :around
(lambda (orig-fun dirname)
(let ((current-buffer (current-buffer)))
@@ -295,25 +308,11 @@ Selects the compilation window so the cursor lands in it."
(use-package async :ensure t)
-(use-package general
- :ensure (:wait t)
- :demand t
- :config
- (general-evil-setup))
-
-(eval-and-compile
- (require 'general)
-
- (general-create-definer my-leader
- :states '(normal visual insert)
- :keymaps 'override
- :prefix "SPC"
- :non-normal-prefix "M-SPC")
+(defvar-keymap my/leader-map
+ :doc "Global leader map used from Evil states.")
- (general-create-definer my-local-leader
- :states '(normal visual)
- :keymaps 'override
- :prefix ","))
+(defvar-keymap my/local-leader-map
+ :doc "Global local-leader map used from Evil states.")
;; Always-available IRC entry points so `M-x run-irc` works from any daemon
;; (hub, work, standalone). Calling `erc-tls` autoloads ERC on demand.
@@ -507,24 +506,7 @@ Temporarily disables notifications during the fetch."
(yes-or-no-p
(format "ERC buffer %s is connected. Really kill (will part channel)? "
(buffer-name)))
- t)))
-
- (general-def 'normal erc-mode-map
- "q" 'quit-window
- "Q" 'my/erc-quit-all
- "gb" 'my/erc-switch-channel
- "gn" 'erc-track-switch-buffer
- "gH" 'my/erc-fetch-history
- "go" 'erc-channel-names
- "gj" 'erc-join-channel
- "gl" 'my/erc-list-channels
- "gr" 'my/erc-reconnect
- "RET" 'erc-send-current-line)
-
- (general-def '(normal insert) erc-mode-map
- "M-n" 'erc-track-switch-buffer
- "C-k" 'erc-previous-command
- "C-j" 'erc-next-command))
+ t))))
(defvar my/centered-cursor-enabled nil)
@@ -591,18 +573,11 @@ Temporarily disables notifications during the fetch."
(setq isearch-lazy-count t) ;; Enable lazy counting to show current match information.
(setq lazy-count-prefix-format "(%s/%s) ") ;; Format for displaying current match count.
(setq lazy-count-suffix-format nil) ;; Disable suffix formatting for match count.
- (setq search-whitespace-regexp ".*?") ;; Allow searching across whitespace.
- :bind (("C-s" . isearch-forward) ;; Bind C-s to forward isearch.
- ("C-r" . isearch-backward))) ;; Bind C-r to backward isearch.
+ (setq search-whitespace-regexp ".*?")) ;; Allow searching across whitespace.
(use-package vc
:ensure nil ;; This is built-in, no need to fetch it.
:defer t
- :bind
- (("C-x v d" . vc-dir) ;; Open VC directory for version control status.
- ("C-x v =" . vc-diff) ;; Show differences for the current file.
- ("C-x v D" . vc-root-diff) ;; Show differences for the entire repository.
- ("C-x v v" . vc-next-action)) ;; Perform the next version control action.
:config
;; Better colors for <leader> g b (blame file)
(setq vc-annotate-color-map
@@ -744,12 +719,7 @@ Temporarily disables notifications during the fetch."
(use-package smerge-mode
:ensure nil ;; This is built-in, no need to fetch it.
- :defer t
- :bind (:map smerge-mode-map
- ("C-c ^ u" . smerge-keep-upper) ;; Keep the changes from the upper version.
- ("C-c ^ l" . smerge-keep-lower) ;; Keep the changes from the lower version.
- ("C-c ^ n" . smerge-next) ;; Move to the next conflict.
- ("C-c ^ p" . smerge-previous))) ;; Move to the previous conflict.
+ :defer t)
(use-package markdown-mode
:ensure t
@@ -869,22 +839,6 @@ Temporarily disables notifications during the fetch."
'("delta" "--no-gitconfig" "--color-only")))
(setq agent-shell-activity-group-header-label-function
#'agent-shell-activity-group-descriptive-label)
- (define-key agent-shell-mode-map (kbd "RET") #'my/agent-shell-ret)
- (define-key agent-shell-mode-map (kbd "<return>") #'my/agent-shell-ret)
- (define-key agent-shell-viewport-view-mode-map (kbd "RET") #'my/agent-shell-ret)
- (define-key agent-shell-viewport-view-mode-map (kbd "<return>") #'my/agent-shell-ret)
- (with-eval-after-load 'evil
- (evil-define-key '(normal motion) agent-shell-mode-map
- (kbd "RET") #'my/agent-shell-ret
- (kbd "<return>") #'my/agent-shell-ret)
- (evil-define-key 'insert agent-shell-mode-map
- (kbd "RET") #'agent-shell-submit
- (kbd "<return>") #'agent-shell-submit)
- (evil-define-key '(normal motion) agent-shell-viewport-view-mode-map
- (kbd "RET") #'my/agent-shell-ret
- (kbd "<return>") #'my/agent-shell-ret))
-
-
;; Work around agent-shell's `window-system' guard so clipboard images
;; work in terminal Emacs (`emacs -nw'). The underlying save routine
;; already shells out to wl-paste/xclip/pngpaste/powershell, none of
@@ -1358,11 +1312,7 @@ Skips capture tasks and projects."
;; Change ACTIVE projects back to TODO (projects shouldn't be clocked directly)
((and (member (org-get-todo-state) (list "ACTIVE"))
(my/is-project-p))
- "TODO"))))
-
- :bind (("<f11>" . org-clock-goto)
- ("C-c C-x C-i" . org-clock-in)
- ("C-c C-x C-o" . org-clock-out)))
+ "TODO")))))
(with-eval-after-load 'org
(setq org-global-properties
@@ -1384,10 +1334,7 @@ Skips capture tasks and projects."
(cond
((string= complexity "low") (org-set-property "EFFORT" "0:30"))
((string= complexity "medium") (org-set-property "EFFORT" "2:00"))
- ((string= complexity "high") (org-set-property "EFFORT" "5:00"))))))
-
- ;; Keybinding for manual effort setting
- (define-key org-mode-map (kbd "C-c e") 'my/org-set-effort-from-complexity))
+ ((string= complexity "high") (org-set-property "EFFORT" "5:00")))))))
(defun my/create-project ()
"Create a new project file with denote and comprehensive project structure."
@@ -1915,6 +1862,14 @@ Skips capture tasks and projects."
:ensure (:wait t)
:after org)
+(use-package vui
+ :ensure (:host github :repo "d12frosted/vui.el"
+ :ref "v1.4.0"
+ :files ("*.el"))
+ :demand t
+ :custom
+ (vui-width-mode 'pixel))
+
(use-package denote
:ensure (:wait t)
:hook (dired-mode . denote-dired-mode)
@@ -2094,8 +2049,7 @@ Works on the base filename (without extension), e.g. matches \"-agenda\", \":age
(kill-buffer)))))
(use-package ox-json
- :ensure t
- )
+ :ensure t)
(use-package ox-gfm
:ensure t)
@@ -2119,8 +2073,6 @@ Works on the base filename (without extension), e.g. matches \"-agenda\", \":age
(setq tmr-sound-file "/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga"
tmr-notification-urgency 'normal
tmr-description-list 'tmr-description-history)
- (define-key global-map (kbd "C-c t") #'tmr-prefix-map)
-
;; ── Timer persistence across Emacs restarts ──────────────
(defvar my/tmr-save-file
(expand-file-name "tmr-timers.eld" user-emacs-directory)
@@ -2284,12 +2236,8 @@ Timers that expired while Emacs was closed fire immediately."
"\\`\\*eldoc"
"\\`\\*elpaca-log\\*\\'"
"\\`\\*Native-compile-Log\\*\\'"
- "\\`\\*Async-native-compile-log\\*\\'"))
+ "\\`\\*Async-native-compile-log\\*\\'")))
;; (helm-display-function #'helm-display-buffer-in-own-frame)
- :bind (:map helm-map
- ("C-j" . helm-next-line)
- ("C-k" . helm-previous-line)))
-(global-set-key (kbd "M-x") 'helm-M-x)
(use-package helm-xref
:ensure t
@@ -2362,15 +2310,7 @@ Timers that expired while Emacs was closed fire immediately."
(defun my/project-ctags-setup ()
(interactive)
(my/project-ctags-start (my/project-ctags-root)
- my/project-ctags-arguments))
-
- (with-eval-after-load 'evil
- (evil-define-key '(normal motion) 'global
- (kbd "g d") #'citre-jump
- (kbd "g D") #'citre-query-jump
- (kbd "g p") #'citre-peek
- (kbd "g r") #'citre-jump-to-reference
- (kbd "g b") #'citre-jump-back)))
+ my/project-ctags-arguments)))
(use-package nov
:ensure t
@@ -2426,8 +2366,6 @@ Timers that expired while Emacs was closed fire immediately."
(goto-char (point-min))
(search-forward (car (last (split-string option "\\." t))))))
-(keymap-set compilation-mode-map "C-c o" #'nix-compile-goto-option)
-
(use-package c-ts-mode
:ensure nil
:mode (("\\.c\\'" . c-ts-mode)
@@ -2647,13 +2585,7 @@ so the working-tree diff stays visible until the user explicitly stages."
(setq evil-vsplit-window-right t)
(setq evil-paste-from-register nil)
:config
- (evil-mode 1)
- (evil-set-initial-state 'help-mode 'emacs)
- (evil-set-initial-state 'messages-buffer-mode 'normal)
- (evil-set-initial-state 'dired-mode 'normal)
- (evil-set-initial-state 'ibuffer-mode 'normal)
- (evil-set-initial-state 'erc-mode 'normal)
- (define-key evil-insert-state-map (kbd "C-w") 'evil-window-map))
+ (evil-mode 1))
(modify-syntax-entry ?_ "w")
@@ -2665,8 +2597,6 @@ so the working-tree diff stays visible until the user explicitly stages."
;; (eldoc-doc-buffer t))
(eldoc-doc-buffer t))
-(define-key evil-normal-state-map (kbd "K") #'my/eldoc-and-jump)
-
(use-package smartparens
:ensure t
:init
@@ -2701,9 +2631,6 @@ so the working-tree diff stays visible until the user explicitly stages."
:ensure t
:after evil
:config
- (define-key evil-inner-text-objects-map "b" 'evil-textobj-anyblock-inner-block)
- (define-key evil-outer-text-objects-map "b" 'evil-textobj-anyblock-a-block)
-
(setq evil-textobj-anyblock-blocks
'(("(" . ")")
("{" . "}")
@@ -2730,9 +2657,6 @@ so the working-tree diff stays visible until the user explicitly stages."
("“" . "”"))))
(evil-textobj-anyblock--make-textobj beg end type count t)))
-(define-key evil-inner-text-objects-map "q" 'my-evil-textobj-anyblock-inner-quote)
-(define-key evil-outer-text-objects-map "q" 'my-evil-textobj-anyblock-a-quote)
-
(use-package evil-mc
:ensure t
:after evil
@@ -2760,10 +2684,7 @@ so the working-tree diff stays visible until the user explicitly stages."
(evil-mc-make-cursor-in-visual-selection-end)
(evil-append 1))
;; Fallback: normales Verhalten
- (call-interactively 'evil-append)))
-
- (define-key evil-visual-state-map (kbd "I") 'my/evil-mc-visual-block-insert)
- (define-key evil-visual-state-map (kbd "A") 'my/evil-mc-visual-block-append))
+ (call-interactively 'evil-append))))
(use-package flash
:ensure (:host github :repo "Prgebish/flash")
@@ -2787,13 +2708,6 @@ so the working-tree diff stays visible until the user explicitly stages."
(require 'flash-evil)
(flash-evil-setup t)
- ;; Restore ; and , after flash-char overwrites them
- ;; ; is used as prefix for prev-navigation (;b, ;d)
- ;; , is the local leader
- (evil-define-key* '(normal visual motion) 'global
- (kbd ";") nil
- (kbd ",") nil)
-
;; Search integration: labels during C-s, /, ?
(require 'flash-isearch)
(flash-isearch-mode 1))
@@ -2903,7 +2817,7 @@ so the working-tree diff stays visible until the user explicitly stages."
:mode-line-active-height 0.92
:mode-line-inactive-family "SFProText Nerd Font"
:mode-line-inactive-height 0.92
- :header-line-family "SFProText Nerd Font"
+ :header-line-family ,cashmere/font-family
:header-line-weight regular
:line-number-family ,cashmere/font-family
:line-number-height 0.9
@@ -2987,10 +2901,7 @@ so the working-tree diff stays visible until the user explicitly stages."
(use-package croc-ui
:ensure nil
:commands (croc-ui croc-ui-send-files croc-ui-send-directory
- croc-ui-send-text croc-ui-receive)
- :init
- (with-eval-after-load 'general
- (my-leader "oc" '(croc-ui :wk "croc"))))
+ croc-ui-send-text croc-ui-receive))
(use-package textui
:ensure (:host github :repo "yibie/textui" :files ("*.el")))
@@ -3003,17 +2914,11 @@ so the working-tree diff stays visible until the user explicitly stages."
(use-package yggdrasil-ui
:ensure nil
:after textui
- :commands (yggdrasil-ui)
- :init
- (with-eval-after-load 'general
- (my-leader "ov" '(yggdrasil-ui :wk "yggdrasil"))))
+ :commands (yggdrasil-ui))
(use-package sync-ui
:ensure nil
:commands (sync-ui))
- :init
- (with-eval-after-load 'general
- (my-leader "oy" '(sync-ui :wk "sync")))
(defun my/reload-config ()
"Re-tangle config.org and reload the generated config.el in place.
@@ -3057,8 +2962,6 @@ still require a restart since elpaca queues run at init time."
(use-package projectile
:ensure (:wait t)
:demand t
- :bind-keymap
- (("C-x p" . projectile-command-map))
:config
(projectile-mode +1)
(setq projectile-completion-system 'default
@@ -3330,12 +3233,6 @@ place. `C-c C-c' commits, `C-c C-k' aborts."
(with-eval-after-load 'projectile
(setq projectile-switch-project-action #'my/workspace-switch-to-project))
- (define-key persp-mode-map [remap delete-window]
- #'my/workspace-close-window-or-workspace)
- (with-eval-after-load 'evil
- (define-key persp-mode-map [remap evil-window-delete]
- #'my/workspace-close-window-or-workspace))
-
(add-hook 'persp-filter-save-buffers-functions
(lambda (buf)
(with-current-buffer buf (derived-mode-p 'erc-mode))))
@@ -3364,47 +3261,6 @@ place. `C-c C-c' commits, `C-c C-k' aborts."
(ibuffer nil (format "*ibuffer: %s*" name)
(list (cons 'persp t))))))
-(my-leader
- "TAB" '(:ignore t :wk "workspace")
- "TAB TAB" '(my/workspace-display :wk "display")
- "TAB ." '(my/workspace-switch-to :wk "switch to…")
- "TAB `" '(my/workspace-other :wk "last")
- "TAB n" '(my/workspace-new :wk "new")
- "TAB r" '(my/workspace-rename :wk "rename")
- "TAB d" '(my/workspace-kill :wk "delete")
- "TAB x" '(my/workspace-kill-session :wk "kill session")
- "TAB s" '(my/workspace-save-session :wk "save session")
- "TAB l" '(my/workspace-load-session :wk "load session")
- "TAB [" '(my/workspace-switch-left :wk "prev")
- "TAB ]" '(my/workspace-switch-right :wk "next")
- "TAB 1" '((lambda () (interactive) (my/workspace-switch-to-index 0)) :wk "1")
- "TAB 2" '((lambda () (interactive) (my/workspace-switch-to-index 1)) :wk "2")
- "TAB 3" '((lambda () (interactive) (my/workspace-switch-to-index 2)) :wk "3")
- "TAB 4" '((lambda () (interactive) (my/workspace-switch-to-index 3)) :wk "4")
- "TAB 5" '((lambda () (interactive) (my/workspace-switch-to-index 4)) :wk "5")
- "TAB 6" '((lambda () (interactive) (my/workspace-switch-to-index 5)) :wk "6")
- "TAB 7" '((lambda () (interactive) (my/workspace-switch-to-index 6)) :wk "7")
- "TAB 8" '((lambda () (interactive) (my/workspace-switch-to-index 7)) :wk "8")
- "TAB 9" '((lambda () (interactive) (my/workspace-switch-to-index 8)) :wk "9")
- "TAB 0" '(my/workspace-switch-to-final :wk "last"))
-
-(general-def '(normal motion)
- "gt" 'my/workspace-switch-right
- "gT" 'my/workspace-switch-left)
-
-(dotimes (i 9)
- (general-define-key
- :states '(normal visual)
- :keymaps 'override
- :prefix ","
- (number-to-string (1+ i))
- `(lambda () (interactive) (my/workspace-switch-to-index ,i))))
-(general-define-key
- :states '(normal visual)
- :keymaps 'override
- :prefix ","
- "0" #'my/workspace-switch-to-final)
-
(use-package pass
:ensure t
:defer t
@@ -3415,29 +3271,7 @@ place. `C-c C-c' commits, `C-c C-k' aborts."
(add-to-list 'display-buffer-alist
'("\\*Pass.*\\*"
- (display-buffer-full-frame)))
-
- (with-eval-after-load 'evil
- (general-def 'normal pass-mode-map
- "q" 'quit-window
- "j" 'pass-next-entry
- "k" 'pass-prev-entry
- "RET" 'pass-view
- "d" 'pass-kill
- "y" 'pass-copy
- "Y" 'pass-copy-field
- "e" 'pass-edit
- "a" 'pass-insert
- "G" 'pass-insert-generated
- "o" 'pass-otp-options
- "r" 'pass-rename
- "/" 'isearch-forward)
-
- (general-def 'normal pass-view-mode-map
- "q" 'quit-window
- "t" 'pass-view-toggle-password
- "y" 'pass-copy
- "Y" 'pass-copy-field)))
+ (display-buffer-full-frame))))
(use-package auth-source
:ensure nil ;; This is built-in, no need to fetch it.
@@ -3467,300 +3301,448 @@ place. `C-c C-c' commits, `C-c C-k' aborts."
(defun my/format-buffer ()
"Format the current buffer with its mode-specific formatter."
(interactive)
- (cond
- ((derived-mode-p 'org-mode)
- (org-fmt-buffer))
- (t
- (user-error "No formatter configured for %s" major-mode))))
+ (if (derived-mode-p 'org-mode)
+ (org-fmt-buffer)
+ (user-error "No formatter configured for %s" major-mode)))
(defun my/find-file-or-switch-project ()
- "Find a file in the current project; outside one, pick a project first.
-Uses the native Helm sources from `helm-projectile'. Picking a project
-switches to its workspace and shows its files via
-`projectile-switch-project-action', so this also works from the master
-workspace (e.g. *scratch*)."
+ "Find a file in the current project, or select a project first."
(interactive)
(if (projectile-project-p)
(helm-projectile-find-file)
(helm-projectile-switch-project)))
-(my-leader
- "SPC" '(my/find-file-or-switch-project :wk "find file/switch project")
- "sp" '(helm-projectile :wk "search project")
- "ss" '(helm-occur :wk "search line")
- "sg" '(my/helm-rg-in :wk "rg in dir")
- "sf" '(my/helm-find-in :wk "find file in dir")
- "/" '(helm-projectile-rg :wk "search project")
- "." '(helm-find-files :wk "find file")
- "," '(helm-mini :wk "switch buffer")
- ":" (lambda () (interactive) (execute-extended-command nil))
- "u" '(universal-argument :wk "universal argument")
+(defun my/xref-find-definitions-other-window ()
+ "Find the definition at point in another window."
+ (interactive)
+ (let ((current-prefix-arg 4))
+ (call-interactively #'xref-find-definitions)))
+
+(defun my/comment-line ()
+ "Toggle the comment on the current line."
+ (interactive)
+ (comment-or-uncomment-region
+ (line-beginning-position)
+ (line-end-position)))
+
+(defun my/comment-region (beg end)
+ "Toggle comments in the active region from BEG to END."
+ (interactive "r")
+ (comment-or-uncomment-region beg end))
+
+(defun my/workspace-switch-by-key ()
+ "Switch to workspace 1–9 using the invoking digit."
+ (interactive)
+ (my/workspace-switch-to-index (- last-command-event ?1)))
+
+(defvar my/leader-search-map nil)
+(defvar my/leader-denote-map nil)
+(defvar my/leader-denote-merge-map nil)
+(defvar my/leader-denote-timer-map nil)
+(defvar my/leader-file-map nil)
+(defvar my/leader-buffer-map nil)
+(defvar my/leader-project-map nil)
+(defvar my/leader-git-map nil)
+(defvar my/leader-agent-map nil)
+(defvar my/leader-open-map nil)
+(defvar my/leader-help-map nil)
+(defvar my/leader-window-map nil)
+(defvar my/leader-code-map nil)
+(defvar my/leader-quit-map nil)
+(defvar my/leader-web-map nil)
+(defvar my/leader-workspace-map nil)
+
+;; Recreate the maps on reload so removed bindings cannot linger.
+(setq my/leader-map (make-sparse-keymap)
+ my/local-leader-map (make-sparse-keymap)
+ my/leader-search-map (make-sparse-keymap)
+ my/leader-denote-map (make-sparse-keymap)
+ my/leader-denote-merge-map (make-sparse-keymap)
+ my/leader-denote-timer-map (make-sparse-keymap)
+ my/leader-file-map (make-sparse-keymap)
+ my/leader-buffer-map (make-sparse-keymap)
+ my/leader-project-map (make-sparse-keymap)
+ my/leader-git-map (make-sparse-keymap)
+ my/leader-agent-map (make-sparse-keymap)
+ my/leader-open-map (make-sparse-keymap)
+ my/leader-help-map (make-sparse-keymap)
+ my/leader-window-map (make-sparse-keymap)
+ my/leader-code-map (make-sparse-keymap)
+ my/leader-quit-map (make-sparse-keymap)
+ my/leader-web-map (make-sparse-keymap)
+ my/leader-workspace-map (make-sparse-keymap))
+
+(keymap-set my/leader-map "SPC" #'my/find-file-or-switch-project)
+(keymap-set my/leader-map "/" #'helm-projectile-rg)
+(keymap-set my/leader-map "." #'helm-find-files)
+(keymap-set my/leader-map "," #'helm-mini)
+(keymap-set my/leader-map ":" #'execute-extended-command)
+(keymap-set my/leader-map "u" #'universal-argument)
+(keymap-set my/leader-map "x" #'org-capture)
+(keymap-set my/leader-map ";" #'embark-act)
+(keymap-set my/leader-map "P" #'helm-show-kill-ring)
+
+(keymap-set my/leader-map "s" my/leader-search-map)
+(keymap-set my/leader-search-map "p" #'helm-projectile)
+(keymap-set my/leader-search-map "s" #'helm-occur)
+(keymap-set my/leader-search-map "g" #'my/helm-rg-in)
+(keymap-set my/leader-search-map "f" #'my/helm-find-in)
+
+(keymap-set my/leader-map "d" my/leader-denote-map)
+(keymap-set my/leader-denote-map "a" #'my/denote-toggle-agenda-keyword)
+(keymap-set my/leader-denote-map "j" #'denote-journal-new-or-existing-entry)
+(keymap-set my/leader-denote-map "d" #'denote-menu)
+(keymap-set my/leader-denote-map "g" #'my/denote-rg)
+(keymap-set my/leader-denote-map "l" #'denote-link-or-create)
+(keymap-set my/leader-denote-map "n" #'denote)
+(keymap-set my/leader-denote-map "r" #'denote-rename-file)
+(keymap-set my/leader-denote-map "s" #'denote-solo-switch)
+(keymap-set my/leader-denote-map "m" my/leader-denote-merge-map)
+(keymap-set my/leader-denote-merge-map "r" #'denote-merge-region)
+(keymap-set my/leader-denote-merge-map "f" #'denote-merge-file)
+(keymap-set my/leader-denote-map "t" my/leader-denote-timer-map)
+(keymap-set my/leader-denote-timer-map "l" #'tmr-list-timers)
+(keymap-set my/leader-denote-timer-map "t" #'tmr)
- "d" '(:ignore t :wk "denote")
- "da" '(my/denote-toggle-agenda-keyword :wk "toggle agenda tag")
- "dj" '(denote-journal-new-or-existing-entry :wk "journal")
- "dd" '(denote-menu t :wk "List all notes")
- "dm" '(:ignore t :wk "Merge Notes")
- "dmr" '(denote-merge-region :wk "Merge Region")
- "dmf" '(denote-merge-file :wk "Merge File")
- "dg" '(my/denote-rg :wk "grep denotes")
- "dl" '(denote-link-or-create t :wk "Link Note")
- "dn" '(denote t :wk "Create a new note")
- "dr" '(denote-rename-file t :wk "Rename Note")
- "ds" '(denote-solo-switch :wk "switch silo")
- "dtl" '(tmr-list-timers :wk "list timer")
- "dtt" '(tmr :wk "set timer")
+(keymap-set my/leader-map "f" my/leader-file-map)
+(keymap-set my/leader-file-map "d" #'dired-jump)
+(keymap-set my/leader-file-map "D" #'dired-jump)
+(keymap-set my/leader-file-map "r" #'helm-recentf)
+(keymap-set my/leader-file-map "f" #'helm-find-files)
+(keymap-set my/leader-file-map "s" #'save-buffer)
- "f" '(:ignore t :wk "files")
- "fd" '(dired-jump :wk "dired")
- "fD" '(dired-jump :wk "dired jump")
- "fr" '(helm-recentf :wk "recent files")
- "ff" '(helm-find-files :wk "find file")
- "fs" '(save-buffer :wk "save file")
- "b" '(:ignore t :wk "buffer/bookmarks")
- "bb" '(helm-filtered-bookmarks :wk "display current bookmarks")
- "bi" '(my/ibuffer-workspace :wk "ibuffer (workspace)")
- "bp" '(projectile-ibuffer :wk "ibuffer project")
- "bd" '(bookmark-delete :wk "delete bookmark")
- "bk" '(kill-current-buffer :wk "kill buffer")
- "bs" '(bookmark-set :wk "save bookmark")
- "br" '(rename-buffer :wk "rename buffer")
+(keymap-set my/leader-map "b" my/leader-buffer-map)
+(keymap-set my/leader-buffer-map "b" #'helm-filtered-bookmarks)
+(keymap-set my/leader-buffer-map "i" #'my/ibuffer-workspace)
+(keymap-set my/leader-buffer-map "p" #'projectile-ibuffer)
+(keymap-set my/leader-buffer-map "d" #'bookmark-delete)
+(keymap-set my/leader-buffer-map "k" #'kill-current-buffer)
+(keymap-set my/leader-buffer-map "s" #'bookmark-set)
+(keymap-set my/leader-buffer-map "r" #'rename-buffer)
- "p" '(:ignore t :wk "project")
- "pp" '(helm-projectile-switch-project :wk "switch project workspace")
- "pf" '(helm-projectile-find-file :wk "find file")
- "ps" '(helm-projectile-rg :wk "search")
- "pb" '(helm-projectile-switch-to-buffer :wk "buffers")
- "pk" '(projectile-kill-buffers :wk "kill buffers")
- "pd" '(projectile-remove-known-project :wk "delete project")
- "pr" '(my/project-replace :wk "project replace (wgrep)")
- "pa" '(projectile-add-known-project :wk "add project")
- "pi" '(projectile-invalidate-cache :wk "invalidate cache")
- "pt" '(projectile-run-task :wk "tasks")
- "pc" '(my/project-ctags-setup :wk "generate/update ctags")
+(keymap-set my/leader-map "p" my/leader-project-map)
+(keymap-set my/leader-project-map "p" #'helm-projectile-switch-project)
+(keymap-set my/leader-project-map "f" #'helm-projectile-find-file)
+(keymap-set my/leader-project-map "s" #'helm-projectile-rg)
+(keymap-set my/leader-project-map "b" #'helm-projectile-switch-to-buffer)
+(keymap-set my/leader-project-map "k" #'projectile-kill-buffers)
+(keymap-set my/leader-project-map "d" #'projectile-remove-known-project)
+(keymap-set my/leader-project-map "r" #'my/project-replace)
+(keymap-set my/leader-project-map "a" #'projectile-add-known-project)
+(keymap-set my/leader-project-map "i" #'projectile-invalidate-cache)
+(keymap-set my/leader-project-map "t" #'projectile-run-task)
+(keymap-set my/leader-project-map "c" #'my/project-ctags-setup)
- "g" '(:ignore t :wk "git")
- "gc" '(magit-clone :wk "clone")
- "gg" '(magit-status :wk "status")
- "gf" '(fossil-ui-status :wk "fossil status")
- "gl" '(magit-log-current :wk "log")
- "gi" '(magit-init :wk "init")
- "gd" '(xref-find-definitions :wk "go to definition")
- "gD" '((lambda () (interactive)
- (let ((current-prefix-arg 4))
- (call-interactively #'xref-find-definitions)))
- :wk "definition other window")
- "gI" '(citre-query-jump :wk "find definition")
- "gt" '(citre-peek :wk "peek definition")
- "gr" '(xref-find-references :wk "find references")
- "gs" '(magit-file-stage :wk "stage file")
- "gu" '(my/magit-uncommit :wk "uncommit (keep & unstage)")
- "gb" '(vc-annotate :wk "blame")
- "gT" '(my/code-todos-harvest :wk "harvest code TODOs")
- "aa" '(agent-shell :wk "start")
- "at" '(agent-shell-toggle :wk "toggle")
- "am" '(agent-shell-help-menu :wk "open session")
- "ar" '(agent-shell-send-region :wk "send region")
- "ac" '(agent-shell-send-clipboard-image :wk "send screenshot")
- "o" '(:ignore t :wk "open")
- "oa" '(my/app-launcher :wk "app launcher")
- "os" '(my/snip-upload :wk "snip buffer/region")
- "oS" '(my/snip-upload-file :wk "snip file")
- "op" '(pass :wk "pass")
- "ot" '(ghostel :wk "ghostel")
- "oz" '(zfs :wk "zfs")
+(keymap-set my/leader-map "g" my/leader-git-map)
+(keymap-set my/leader-git-map "c" #'magit-clone)
+(keymap-set my/leader-git-map "g" #'magit-status)
+(keymap-set my/leader-git-map "f" #'fossil-ui-status)
+(keymap-set my/leader-git-map "l" #'magit-log-current)
+(keymap-set my/leader-git-map "i" #'magit-init)
+(keymap-set my/leader-git-map "d" #'xref-find-definitions)
+(keymap-set my/leader-git-map "D" #'my/xref-find-definitions-other-window)
+(keymap-set my/leader-git-map "I" #'citre-query-jump)
+(keymap-set my/leader-git-map "t" #'citre-peek)
+(keymap-set my/leader-git-map "r" #'xref-find-references)
+(keymap-set my/leader-git-map "s" #'magit-file-stage)
+(keymap-set my/leader-git-map "u" #'my/magit-uncommit)
+(keymap-set my/leader-git-map "b" #'vc-annotate)
+(keymap-set my/leader-git-map "T" #'my/code-todos-harvest)
- "h" '(:ignore t :wk "help")
- "hm" '(describe-mode :wk "mode")
- "hf" '(describe-function :wk "function")
- "hv" '(describe-variable :wk "variable")
- "hk" '(describe-key :wk "key")
- ;; "ht" '(load-theme :wk "load theme")
+(keymap-set my/leader-map "a" my/leader-agent-map)
+(keymap-set my/leader-agent-map "a" #'agent-shell)
+(keymap-set my/leader-agent-map "t" #'agent-shell-toggle)
+(keymap-set my/leader-agent-map "m" #'agent-shell-help-menu)
+(keymap-set my/leader-agent-map "r" #'agent-shell-send-region)
+(keymap-set my/leader-agent-map "c" #'agent-shell-send-clipboard-image)
- "w w" '(evil-window-next :wk "Close window")
- "w c" '(evil-window-delete :wk "Close window")
- "w o" '(delete-other-windows :wk "Maximize window")
- "w n" '(evil-window-new :wk "New window")
- "w s" '(evil-window-split :wk "Horizontal split window")
- "w v" '(evil-window-vsplit :wk "Vertical split window")
- "w h" '(evil-window-left :wk "Window left")
- "w j" '(evil-window-down :wk "Window down")
- "w k" '(evil-window-up :wk "Window up")
- "w l" '(evil-window-right :wk "Window right")
- "w w" '(evil-window-next :wk "Goto next window")
- "w H" '(buf-move-left :wk "Buffer move left")
- "w J" '(buf-move-down :wk "Buffer move down")
- "w K" '(buf-move-up :wk "Buffer move up")
- "w L" '(buf-move-right :wk "Buffer move right")
+(keymap-set my/leader-map "o" my/leader-open-map)
+(keymap-set my/leader-open-map "a" #'my/app-launcher)
+(keymap-set my/leader-open-map "s" #'my/snip-upload)
+(keymap-set my/leader-open-map "S" #'my/snip-upload-file)
+(keymap-set my/leader-open-map "p" #'pass)
+(keymap-set my/leader-open-map "t" #'ghostel)
+(keymap-set my/leader-open-map "z" #'zfs)
+(keymap-set my/leader-open-map "c" #'croc-ui)
+(keymap-set my/leader-open-map "v" #'yggdrasil-ui)
+(keymap-set my/leader-open-map "y" #'sync-ui)
+(keymap-set my/leader-open-map "r" #'rsync-ui)
- "c" '(:ignore t :wk "code")
- "cc" '(my/compile-or-recompile :wk "compile")
- "cC" '(ghostel-compile :wk "recompile")
- "ca" '(citre-query-jump :wk "find definition")
- "cr" '(query-replace :wk "replace")
- "cf" '(my/format-buffer :wk "format buffer")
- "cs" '(yas-insert-snippet :wk "snippets")
- "cl" '(flycheck-list-errors :wk "list errors")
+(keymap-set my/leader-map "h" my/leader-help-map)
+(keymap-set my/leader-help-map "m" #'describe-mode)
+(keymap-set my/leader-help-map "f" #'describe-function)
+(keymap-set my/leader-help-map "v" #'describe-variable)
+(keymap-set my/leader-help-map "k" #'describe-key)
+(keymap-set my/leader-help-map "r" #'my/reload-config)
- "q" '(:ignore t :wk "quit")
- "qq" '(save-buffers-kill-terminal :wk "quit emacs")
- "qr" '(restart-emacs :wk "restart")
- "hr" '(my/reload-config :wk "reload config")
+(keymap-set my/leader-map "w" my/leader-window-map)
+(keymap-set my/leader-window-map "w" #'evil-window-next)
+(keymap-set my/leader-window-map "c" #'evil-window-delete)
+(keymap-set my/leader-window-map "o" #'delete-other-windows)
+(keymap-set my/leader-window-map "n" #'evil-window-new)
+(keymap-set my/leader-window-map "s" #'evil-window-split)
+(keymap-set my/leader-window-map "v" #'evil-window-vsplit)
+(keymap-set my/leader-window-map "h" #'evil-window-left)
+(keymap-set my/leader-window-map "j" #'evil-window-down)
+(keymap-set my/leader-window-map "k" #'evil-window-up)
+(keymap-set my/leader-window-map "l" #'evil-window-right)
+(keymap-set my/leader-window-map "H" #'buf-move-left)
+(keymap-set my/leader-window-map "J" #'buf-move-down)
+(keymap-set my/leader-window-map "K" #'buf-move-up)
+(keymap-set my/leader-window-map "L" #'buf-move-right)
- "x" '(org-capture :wk "capture")
+(keymap-set my/leader-map "c" my/leader-code-map)
+(keymap-set my/leader-code-map "c" #'my/compile-or-recompile)
+(keymap-set my/leader-code-map "C" #'ghostel-compile)
+(keymap-set my/leader-code-map "a" #'citre-query-jump)
+(keymap-set my/leader-code-map "r" #'query-replace)
+(keymap-set my/leader-code-map "f" #'my/format-buffer)
+(keymap-set my/leader-code-map "s" #'yas-insert-snippet)
+(keymap-set my/leader-code-map "l" #'flycheck-list-errors)
- ";" '(embark-act :wk "embark")
- "P" '(helm-show-kill-ring :wk "paste history")
+(keymap-set my/leader-map "q" my/leader-quit-map)
+(keymap-set my/leader-quit-map "q" #'save-buffers-kill-terminal)
+(keymap-set my/leader-quit-map "r" #'restart-emacs)
- ;; "t" '(:ignore t :wk "treesitter")
- ;; "ts" '(flash-treesitter :wk "flash treesitter")
+(keymap-set my/leader-map "e" my/leader-web-map)
+(keymap-set my/leader-web-map "e" #'eww)
+(keymap-set my/leader-web-map "n" #'my/eww-new-buffer)
+(keymap-set my/leader-web-map "b" #'eww-list-bookmarks)
+(keymap-set my/leader-web-map "h" #'eww-list-histories)
+(keymap-set my/leader-web-map "f" #'elfeed)
+(keymap-set my/leader-web-map "L" #'link-hint-open-link)
+(keymap-set my/leader-web-map "C" #'link-hint-copy-link)
+(keymap-set my/leader-web-map "x" #'xwidget-webkit-browse-url)
+(keymap-set my/leader-map "TAB" my/leader-workspace-map)
+(keymap-set my/leader-workspace-map "TAB" #'my/workspace-display)
+(keymap-set my/leader-workspace-map "." #'my/workspace-switch-to)
+(keymap-set my/leader-workspace-map "`" #'my/workspace-other)
+(keymap-set my/leader-workspace-map "n" #'my/workspace-new)
+(keymap-set my/leader-workspace-map "r" #'my/workspace-rename)
+(keymap-set my/leader-workspace-map "d" #'my/workspace-kill)
+(keymap-set my/leader-workspace-map "x" #'my/workspace-kill-session)
+(keymap-set my/leader-workspace-map "s" #'my/workspace-save-session)
+(keymap-set my/leader-workspace-map "l" #'my/workspace-load-session)
+(keymap-set my/leader-workspace-map "[" #'my/workspace-switch-left)
+(keymap-set my/leader-workspace-map "]" #'my/workspace-switch-right)
+(dotimes (index 9)
+ (keymap-set my/leader-workspace-map (number-to-string (1+ index))
+ #'my/workspace-switch-by-key))
+(keymap-set my/leader-workspace-map "0" #'my/workspace-switch-to-final)
- "e" '(:ignore t :wk "eww/web")
- "e e" '(eww :wk "eww browse / search")
- "e n" '(my/eww-new-buffer :wk "new eww buffer")
- "e b" '(eww-list-bookmarks :wk "bookmarks")
- "e h" '(eww-list-histories :wk "history")
- "e f" '(elfeed :wk "elfeed (rss)")
- "e L" '(link-hint-open-link :wk "hint open link")
- "e C" '(link-hint-copy-link :wk "hint copy link")
- "e x" '(xwidget-webkit-browse-url :wk "webkit browser")
- )
+(keymap-set my/local-leader-map "a" #'org-agenda)
+(keymap-set my/local-leader-map "c" #'my/centered-cursor)
+(keymap-set my/local-leader-map "f" #'dirvish)
+(keymap-set my/local-leader-map "m" #'mu4e)
+(keymap-set my/local-leader-map "i" #'clatter)
+(keymap-set my/local-leader-map "r" #'async-shell-command)
+(keymap-set my/local-leader-map "t" #'ghostel-project)
+(keymap-set my/local-leader-map "T" #'ghostel-list-buffers)
+(keymap-set my/local-leader-map "z" #'golden-ratio-mode)
+(keymap-set my/local-leader-map "o" #'my/global-olivetti-mode)
+(keymap-set my/local-leader-map "s" #'my-org-sidecar-left)
+(dotimes (index 9)
+ (keymap-set my/local-leader-map (number-to-string (1+ index))
+ #'my/workspace-switch-by-key))
+(keymap-set my/local-leader-map "0" #'my/workspace-switch-to-final)
-(defun my/flash-enabled-p ()
- (and (not (derived-mode-p 'magit-mode 'dired-mode 'ibuffer-mode))
- (not (eq major-mode 'dirvish-mode))))
+(with-eval-after-load 'which-key
+ (which-key-add-keymap-based-replacements
+ my/leader-map
+ "s" "search" "d" "denote" "f" "files" "b" "buffers/bookmarks"
+ "p" "project" "g" "git" "a" "agent" "o" "open" "h" "help"
+ "w" "windows" "c" "code" "q" "quit" "e" "web" "TAB" "workspace")
+ (which-key-add-keymap-based-replacements
+ my/leader-denote-map "m" "merge" "t" "timer"))
+
+(keymap-global-set "C-s" #'isearch-forward)
+(keymap-global-set "C-r" #'isearch-backward)
+(keymap-global-set "C-x v d" #'vc-dir)
+(keymap-global-set "C-x v =" #'vc-diff)
+(keymap-global-set "C-x v D" #'vc-root-diff)
+(keymap-global-set "C-x v v" #'vc-next-action)
+(keymap-global-set "C-x p" projectile-command-map)
+(keymap-global-set "C-c t" tmr-prefix-map)
+(keymap-global-set "<f11>" #'org-clock-goto)
+(keymap-global-set "M-x" #'helm-M-x)
+(keymap-global-set "C-=" #'text-scale-increase)
+(keymap-global-set "C--" #'text-scale-decrease)
(defun my/s-key-dispatch ()
+ "Run Flash with stable viewport margins."
(interactive)
- (if (derived-mode-p 'magit-mode)
- (call-interactively 'magit-stage)
- (when (my/flash-enabled-p)
- (let ((scroll-margin 0)
- (maximum-scroll-margin 0))
- (call-interactively 'flash-evil-jump)))))
+ (let ((scroll-margin 0)
+ (maximum-scroll-margin 0))
+ (call-interactively #'flash-evil-jump)))
+
+;; The wrapper must retain Flash's inclusive operator semantics. Without
+;; these properties `d s' becomes exclusive and stops before the target.
+(evil-set-command-properties #'my/s-key-dispatch
+ :type 'inclusive
+ :keep-visual t
+ :repeat 'motion)
-(general-def '(normal visual operator) 'override
- :predicate '(not (derived-mode-p 'mu4e-main-mode 'mu4e-headers-mode
- 'mu4e-view-mode 'mu4e-compose-mode
- 'croc-ui-mode 'sync-ui-mode
- 'fossil-ui-mode))
- "s" 'my/s-key-dispatch)
+;; Flash's enhanced character motions claim `;' and `,'. Keep `;' free for
+;; mode-local prefixes and reserve `,' for the local leader below.
+(evil-define-key 'motion 'global
+ (kbd ";") nil
+ (kbd ",") nil)
+(evil-define-key '(normal visual) 'global (kbd "SPC") my/leader-map)
+(evil-define-key 'insert 'global (kbd "M-SPC") my/leader-map)
+(evil-define-key '(normal visual) 'global (kbd ",") my/local-leader-map)
+(evil-define-key '(normal visual operator) 'global (kbd "s") #'my/s-key-dispatch)
+(evil-define-key 'insert 'global (kbd "C-w") evil-window-map)
+(evil-define-key 'normal 'global
+ (kbd "K") #'my/eldoc-and-jump
+ (kbd "] d") #'flycheck-next-error
+ (kbd "[ d") #'flycheck-previous-error
+ (kbd "] c") #'diff-hl-next-hunk
+ (kbd "[ c") #'diff-hl-previous-hunk
+ (kbd "] b") #'switch-to-next-buffer
+ (kbd "[ b") #'switch-to-prev-buffer
+ (kbd "] t") #'tab-next
+ (kbd "[ t") #'tab-previous
+ (kbd "g c c") #'my/comment-line
+ (kbd "g d") #'citre-jump
+ (kbd "g D") #'citre-query-jump
+ (kbd "g p") #'citre-peek
+ (kbd "g r") #'citre-jump-to-reference
+ (kbd "g b") #'citre-jump-back)
-(general-def 'normal 'override
- "K" 'my/eldoc-and-jump
- "]d" 'flycheck-next-error
- "[d" 'flycheck-previous-error
- "]c" 'diff-hl-next-hunk
- "[c" 'diff-hl-previous-hunk
- "]b" 'switch-to-next-buffer
- "[b" 'switch-to-prev-buffer
- "]t" 'tab-next
- "[t" 'tab-previous
- ;; "P" 'helm-show-kill-ring
- ;; "?" 'casual-avy-tmenu
- "gcc" (lambda ()
- (interactive)
- (unless (use-region-p)
- (comment-or-uncomment-region
- (line-beginning-position)
- (line-end-position)))))
+(evil-define-key 'visual 'global (kbd "g c") #'my/comment-region)
+(evil-define-key 'visual 'global
+ (kbd "I") #'my/evil-mc-visual-block-insert
+ (kbd "A") #'my/evil-mc-visual-block-append)
-(general-def 'visual 'override
- "gc" (lambda ()
- (interactive)
- (when (use-region-p)
- (comment-or-uncomment-region
- (region-beginning)
- (region-end)))))
+(define-key evil-inner-text-objects-map "b" #'evil-textobj-anyblock-inner-block)
+(define-key evil-outer-text-objects-map "b" #'evil-textobj-anyblock-a-block)
+(define-key evil-inner-text-objects-map "q" #'my-evil-textobj-anyblock-inner-quote)
+(define-key evil-outer-text-objects-map "q" #'my-evil-textobj-anyblock-a-quote)
-(my-leader
- :keymaps 'org-mode-map
- "m" '(:ignore :wk "org")
+(with-eval-after-load 'agent-shell
+ (keymap-set agent-shell-mode-map "RET" #'my/agent-shell-ret)
+ (keymap-set agent-shell-mode-map "<return>" #'my/agent-shell-ret)
+ (evil-define-key '(normal motion) agent-shell-mode-map
+ (kbd "RET") #'my/agent-shell-ret
+ (kbd "<return>") #'my/agent-shell-ret)
+ (evil-define-key 'insert agent-shell-mode-map
+ (kbd "RET") #'agent-shell-submit
+ (kbd "<return>") #'agent-shell-submit))
- "mt" '(org-todo :wk "TODO")
- "ma" '(org-add-note :wk "add note")
- "mC" '(org-capture :wk "capture")
- "mf" '(org-fmt-buffer :wk "format buffer")
+(with-eval-after-load 'agent-shell
+ (keymap-set agent-shell-viewport-view-mode-map "RET" #'my/agent-shell-ret)
+ (keymap-set agent-shell-viewport-view-mode-map "<return>" #'my/agent-shell-ret)
+ (evil-define-key '(normal motion) agent-shell-viewport-view-mode-map
+ (kbd "RET") #'my/agent-shell-ret
+ (kbd "<return>") #'my/agent-shell-ret))
- "mc" '(:wk "set" :ignore)
- "mk" '(kitty-graphics-org-heading-sizes :wk "kgfx headlines")
- "mcd" '(org-deadline :wk "deadline")
- "mcs" '(org-schedule :wk "schedule")
- "mce" '(org-set-effort :wk "effort")
- "mcr" '(org-clock-report :wk "clock report")
- "m," '(org-priority :wk "priority")
- "mI" '(org-clock-in :wk "clock in")
- "mO" '(org-clock-out :wk "clock out")
+(defun my/async-shell-command-bindings ()
+ "Install bindings local to the displayed async shell buffer."
+ (evil-local-set-key 'normal (kbd "q") #'quit-window)
+ (evil-local-set-key 'motion (kbd "q") #'quit-window))
+
+(with-eval-after-load 'compile
+ (keymap-set compilation-mode-map "C-c o" #'nix-compile-goto-option))
- "l" '(:ignore :wk "link")
- "lc" '(org-cliplink :wk "cliplink")
- "li" '(org-download-clipboard :wk "image")
- "ll" '(org-insert-link :wk "link various things")
+(defun my/dired-view-file-externally ()
+ "Open the current Dired file through mailcap."
+ (interactive)
+ (mailcap-view-file (dired-get-filename)))
- "n" '(org-toggle-narrow-to-subtree :wk "narrow"))
+(with-eval-after-load 'dired
+ (evil-set-initial-state 'dired-mode 'normal)
+ (keymap-set dired-mode-map "E" #'my/dired-view-file-externally)
+ (keymap-set dired-mode-map "C-c r" #'rsync-ui-dired)
+ (evil-define-key 'normal dired-mode-map
+ (kbd "h") #'dired-up-directory
+ (kbd "l") #'dired-find-file))
-(evil-define-key 'normal org-mode-map (kbd "RET") 'org-open-at-point)
+(defvar my/dirvish-yank-map (make-sparse-keymap))
+(defvar my/dirvish-symlink-map (make-sparse-keymap))
(defun dirvish-next-file (arg)
- "Move down ARG lines, landing on the filename column.
-Uses raw line motion so hidden detail lines (permissions, owner,
-date) and the dired header are never skipped."
+ "Move down ARG raw lines and land on the filename column."
(interactive "^p")
(forward-line arg)
(dired-move-to-filename))
(defun dirvish-prev-file (arg)
- "Move up ARG lines, landing on the filename column."
+ "Move up ARG raw lines and land on the filename column."
(interactive "^p")
(forward-line (- arg))
(dired-move-to-filename))
-(general-def 'normal dired-mode-map
- "h" 'dired-up-directory
- "l" 'dired-find-file)
-
-(general-def 'normal dirvish-mode-map
- "?" 'dirvish-dispatch
- "q" 'dirvish-quit
- "b" 'dirvish-quick-access
- "f" 'dirvish-file-info-menu
- "p" 'dirvish-yank
- "S" 'dirvish-quicksort
- "F" 'dirvish-layout-toggle
- "z" 'zoxide-travel
- "j" 'dirvish-next-file
- "k" 'dirvish-prev-file
- "gh" 'dirvish-subtree-up
- "gl" 'dirvish-subtree-toggle
- "h" 'dired-up-directory
- "l" 'dired-find-file
- "TAB" 'dirvish-subtree-toggle
- "[h" 'dirvish-history-go-backward
- "]h" 'dirvish-history-go-forward)
-
-(general-def '(normal visual) dirvish-mode-map
- :prefix "y"
- "l" 'dirvish-copy-file-true-path
- "n" 'dirvish-copy-file-name
- "p" 'dirvish-copy-file-path
- "y" 'dired-do-copy)
-
-(general-def 'normal dirvish-mode-map
- :prefix "s"
- "s" 'dirvish-symlink
- "S" 'dirvish-relative-symlink
- "h" 'dirvish-hardlink)
-
(defun my/dirvish-copy-to-clipboard (&rest _)
(my/send-to-clipboard (car kill-ring)))
+(setq my/dirvish-yank-map (make-sparse-keymap)
+ my/dirvish-symlink-map (make-sparse-keymap))
+(keymap-set my/dirvish-yank-map "l" #'dirvish-copy-file-true-path)
+(keymap-set my/dirvish-yank-map "n" #'dirvish-copy-file-name)
+(keymap-set my/dirvish-yank-map "p" #'dirvish-copy-file-path)
+(keymap-set my/dirvish-yank-map "y" #'dired-do-copy)
+(keymap-set my/dirvish-symlink-map "s" #'dirvish-symlink)
+(keymap-set my/dirvish-symlink-map "S" #'dirvish-relative-symlink)
+(keymap-set my/dirvish-symlink-map "h" #'dirvish-hardlink)
+
+(with-eval-after-load 'dirvish
+ (evil-define-key 'normal dirvish-mode-map
+ (kbd "?") #'dirvish-dispatch
+ (kbd "q") #'dirvish-quit
+ (kbd "b") #'dirvish-quick-access
+ (kbd "f") #'dirvish-file-info-menu
+ (kbd "p") #'dirvish-yank
+ (kbd "S") #'dirvish-quicksort
+ (kbd "F") #'dirvish-layout-toggle
+ (kbd "z") #'zoxide-travel
+ (kbd "j") #'dirvish-next-file
+ (kbd "k") #'dirvish-prev-file
+ (kbd "g h") #'dirvish-subtree-up
+ (kbd "g l") #'dirvish-subtree-toggle
+ (kbd "h") #'dired-up-directory
+ (kbd "l") #'dired-find-file
+ (kbd "TAB") #'dirvish-subtree-toggle
+ (kbd "[ h") #'dirvish-history-go-backward
+ (kbd "] h") #'dirvish-history-go-forward
+ (kbd "s") my/dirvish-symlink-map)
+ (evil-define-key '(normal visual) dirvish-mode-map
+ (kbd "y") my/dirvish-yank-map))
+
(dolist (fn '(dirvish-copy-file-path
dirvish-copy-file-name
dirvish-copy-file-true-path))
(advice-add fn :after #'my/dirvish-copy-to-clipboard))
+(with-eval-after-load 'elfeed
+ (evil-define-key '(normal visual) elfeed-search-mode-map
+ (kbd "f") #'link-hint-open-link
+ (kbd "F") #'link-hint-copy-link))
+
+(with-eval-after-load 'elfeed
+ (evil-define-key '(normal visual) elfeed-show-mode-map
+ (kbd "f") #'link-hint-open-link
+ (kbd "F") #'link-hint-copy-link))
+
+(with-eval-after-load 'erc
+ (evil-set-initial-state 'erc-mode 'normal)
+ (evil-define-key 'normal erc-mode-map
+ (kbd "q") #'quit-window
+ (kbd "Q") #'my/erc-quit-all
+ (kbd "g b") #'my/erc-switch-channel
+ (kbd "g n") #'erc-track-switch-buffer
+ (kbd "g H") #'my/erc-fetch-history
+ (kbd "g o") #'erc-channel-names
+ (kbd "g j") #'erc-join-channel
+ (kbd "g l") #'my/erc-list-channels
+ (kbd "g r") #'my/erc-reconnect
+ (kbd "RET") #'erc-send-current-line)
+ (evil-define-key '(normal insert) erc-mode-map
+ (kbd "M-n") #'erc-track-switch-buffer
+ (kbd "C-k") #'erc-previous-command
+ (kbd "C-j") #'erc-next-command))
+
(defun my/eshell-clear ()
(interactive)
(eshell/clear-scrollback))
@@ -3769,33 +3751,138 @@ date) and the dired header are never skipped."
(interactive)
(eshell-atuin-history))
+(defun my/setup-eshell-bindings ()
+ "Apply native and Evil bindings to `eshell-mode-map'."
+ (keymap-set eshell-mode-map "C-l" #'my/eshell-clear)
+ (keymap-set eshell-mode-map "C-r" #'my/atuin-history)
+ (evil-define-key '(normal insert) eshell-mode-map
+ (kbd "C-r") #'my/atuin-history
+ (kbd "C-l") #'my/eshell-clear))
+
+(defun my/eshell-bindings-after-evil-collection (mode _maps &rest _)
+ "Reapply Eshell bindings after Evil Collection configures MODE."
+ (when (eq mode 'eshell)
+ (my/setup-eshell-bindings)))
+
(with-eval-after-load 'eshell
- (add-hook 'eshell-mode-hook
- (lambda ()
- (local-set-key (kbd "C-l") 'my/eshell-clear)
- (local-set-key (kbd "C-r") 'my/atuin-history))))
+ (my/setup-eshell-bindings))
+(add-hook 'evil-collection-setup-hook
+ #'my/eshell-bindings-after-evil-collection)
-(with-eval-after-load 'evil
- (with-eval-after-load 'eshell
- (evil-define-key '(normal insert) eshell-mode-map
- (kbd "C-r") 'my/atuin-history
- (kbd "C-l") 'my/eshell-clear)))
+(defvar my/eww-local-leader-map (make-sparse-keymap))
+(setq my/eww-local-leader-map (make-sparse-keymap))
+(set-keymap-parent my/eww-local-leader-map my/local-leader-map)
+(keymap-set my/eww-local-leader-map "B" #'eww-add-bookmark)
+(keymap-set my/eww-local-leader-map "r" #'eww-readable)
+(keymap-set my/eww-local-leader-map "i" #'my/eww-toggle-images)
+(keymap-set my/eww-local-leader-map "y" #'my/eww-copy-as-org)
+(keymap-set my/eww-local-leader-map "d" #'eww-download)
-(my-local-leader
- "a" '(org-agenda :wk "org agenda")
- "c" '(my/centered-cursor :wk "center cursor")
- "f" '(dirvish :wk "file manager")
- "m" '(mu4e :wk "mu4e")
- "i" '(clatter :wk "irc")
- "r" '(async-shell-command :wk "run async")
- "t" '(ghostel-project :wk "terminal (project)")
- "T" '(ghostel-list-buffers :wk "terminal (switch)")
- "z" '(golden-ratio-mode :wk "zoom/golden ratio")
- "o" '(my/global-olivetti-mode :wk "center buffer")
- "s" '(my-org-sidecar-left :wk "org sidecar"))
+(with-eval-after-load 'eww
+ (evil-define-key '(normal visual) eww-mode-map
+ (kbd ",") my/eww-local-leader-map
+ (kbd "f") #'link-hint-open-link
+ (kbd "F") #'link-hint-copy-link))
-(global-set-key (kbd "C-=") 'text-scale-increase)
-(global-set-key (kbd "C--") 'text-scale-decrease)
+(with-eval-after-load 'ghostel
+ (evil-define-key 'normal ghostel-mode-map
+ (kbd "g t") #'ghostel-next
+ (kbd "g T") #'ghostel-previous
+ (kbd "g n") #'ghostel
+ (kbd "g b") #'ghostel-list-buffers))
+
+(with-eval-after-load 'helm
+ (keymap-set helm-map "C-j" #'helm-next-line)
+ (keymap-set helm-map "C-k" #'helm-previous-line))
+
+(evil-set-initial-state 'help-mode 'emacs)
+
+(evil-set-initial-state 'ibuffer-mode 'normal)
+
+(evil-set-initial-state 'messages-buffer-mode 'normal)
+
+(defvar my/org-leader-map (make-sparse-keymap))
+(defvar my/org-command-map (make-sparse-keymap))
+(defvar my/org-set-map (make-sparse-keymap))
+(defvar my/org-link-map (make-sparse-keymap))
+
+(setq my/org-leader-map (make-sparse-keymap)
+ my/org-command-map (make-sparse-keymap)
+ my/org-set-map (make-sparse-keymap)
+ my/org-link-map (make-sparse-keymap))
+(set-keymap-parent my/org-leader-map my/leader-map)
+(keymap-set my/org-leader-map "m" my/org-command-map)
+(keymap-set my/org-command-map "t" #'org-todo)
+(keymap-set my/org-command-map "a" #'org-add-note)
+(keymap-set my/org-command-map "C" #'org-capture)
+(keymap-set my/org-command-map "f" #'org-fmt-buffer)
+(keymap-set my/org-command-map "k" #'kitty-graphics-org-heading-sizes)
+(keymap-set my/org-command-map "c" my/org-set-map)
+(keymap-set my/org-set-map "d" #'org-deadline)
+(keymap-set my/org-set-map "s" #'org-schedule)
+(keymap-set my/org-set-map "e" #'org-set-effort)
+(keymap-set my/org-set-map "r" #'org-clock-report)
+(keymap-set my/org-command-map "," #'org-priority)
+(keymap-set my/org-command-map "I" #'org-clock-in)
+(keymap-set my/org-command-map "O" #'org-clock-out)
+(keymap-set my/org-leader-map "l" my/org-link-map)
+(keymap-set my/org-link-map "c" #'org-cliplink)
+(keymap-set my/org-link-map "i" #'org-download-clipboard)
+(keymap-set my/org-link-map "l" #'org-insert-link)
+(keymap-set my/org-leader-map "n" #'org-toggle-narrow-to-subtree)
+
+(with-eval-after-load 'org
+ (keymap-set org-mode-map "C-c e" #'my/org-set-effort-from-complexity)
+ (keymap-set org-mode-map "C-c C-x C-i" #'org-clock-in)
+ (keymap-set org-mode-map "C-c C-x C-o" #'org-clock-out)
+ (evil-define-key '(normal visual) org-mode-map
+ (kbd "SPC") my/org-leader-map)
+ (evil-define-key 'insert org-mode-map
+ (kbd "M-SPC") my/org-leader-map
+ (kbd "TAB") #'indent-for-tab-command)
+ (evil-define-key 'normal org-mode-map
+ (kbd "RET") #'org-open-at-point))
+
+(with-eval-after-load 'which-key
+ (which-key-add-keymap-based-replacements
+ my/org-leader-map "m" "org" "l" "link")
+ (which-key-add-keymap-based-replacements
+ my/org-command-map "c" "set"))
+
+(with-eval-after-load 'pass
+ (evil-define-key 'normal pass-mode-map
+ (kbd "q") #'quit-window
+ (kbd "j") #'pass-next-entry
+ (kbd "k") #'pass-prev-entry
+ (kbd "RET") #'pass-view
+ (kbd "d") #'pass-kill
+ (kbd "y") #'pass-copy
+ (kbd "Y") #'pass-copy-field
+ (kbd "e") #'pass-edit
+ (kbd "a") #'pass-insert
+ (kbd "G") #'pass-insert-generated
+ (kbd "o") #'pass-otp-options
+ (kbd "r") #'pass-rename
+ (kbd "/") #'isearch-forward))
+
+(with-eval-after-load 'pass
+ (evil-define-key 'normal pass-view-mode-map
+ (kbd "q") #'quit-window
+ (kbd "t") #'pass-view-toggle-password
+ (kbd "y") #'pass-copy
+ (kbd "Y") #'pass-copy-field))
+
+(with-eval-after-load 'persp-mode
+ (define-key persp-mode-map [remap delete-window]
+ #'my/workspace-close-window-or-workspace)
+ (define-key persp-mode-map [remap evil-window-delete]
+ #'my/workspace-close-window-or-workspace))
+
+(with-eval-after-load 'smerge-mode
+ (keymap-set smerge-mode-map "C-c ^ u" #'smerge-keep-upper)
+ (keymap-set smerge-mode-map "C-c ^ l" #'smerge-keep-lower)
+ (keymap-set smerge-mode-map "C-c ^ n" #'smerge-next)
+ (keymap-set smerge-mode-map "C-c ^ p" #'smerge-previous))
(use-package elfeed
:ensure (:host github :repo "emacs-elfeed/elfeed" :branch "main")
@@ -3924,16 +4011,7 @@ reset is unnecessary, so do the handler resolution ourselves and skip
shr-max-image-proportion 0.6
shr-width nil
shr-cookie-policy nil)
- (make-directory eww-bookmarks-directory t)
-
- (with-eval-after-load 'general
- (my-local-leader
- :keymaps 'eww-mode-map
- "B" '(eww-add-bookmark :wk "add bookmark")
- "r" '(eww-readable :wk "reader mode")
- "i" '(my/eww-toggle-images :wk "toggle images")
- "y" '(my/eww-copy-as-org :wk "copy page as org")
- "d" '(eww-download :wk "download"))))
+ (make-directory eww-bookmarks-directory t))
(defun my/eww-new-buffer (url)
"Open URL in a fresh eww buffer (keep current)."
@@ -3980,12 +4058,6 @@ reset is unnecessary, so do the handler resolution ourselves and skip
:ensure t
:commands (link-hint-open-link link-hint-copy-link))
-(with-eval-after-load 'general
- (general-def :states '(normal visual)
- :keymaps '(eww-mode-map elfeed-show-mode-map elfeed-search-mode-map)
- "f" #'link-hint-open-link
- "F" #'link-hint-copy-link))
-
(use-package mu4e
:ensure nil
:defer t
@@ -4182,11 +4254,6 @@ opening another file in same project does not re-notify."
(ghostel-compile-global-mode 1)
(setq-default window-adjust-process-window-size-function
#'window-adjust-process-window-size-largest)
- (evil-define-key 'normal ghostel-mode-map
- (kbd "g t") #'ghostel-next
- (kbd "g T") #'ghostel-previous
- (kbd "g n") #'ghostel
- (kbd "g b") #'ghostel-list-buffers)
(defun my/ghostel-transparent-buffer-face (fg _bg)
(unless (equal fg ghostel--face-cookie-fg-bg)
(when ghostel--face-cookie
@@ -4216,36 +4283,45 @@ opening another file in same project does not re-notify."
(inheritenv-add-advice 'ghostel-compile)
(inheritenv-add-advice 'ghostel-recompile))
-(when-let* ((garden-dir (expand-file-name
- (or (alist-get 'garden my/local-packages) "~/garden")))
- ((file-directory-p garden-dir)))
- (use-package vui
- :ensure (:host github :repo "d12frosted/vui.el" :files ("*.el")))
-
- (add-to-list 'load-path (expand-file-name "lisp/" garden-dir))
- (setq garden-directory (file-name-as-directory garden-dir))
- (require 'garden-core)
+(use-package garden-core
+ :ensure nil
+ :demand t
+ :custom
+ (garden-directory (file-name-as-directory (expand-file-name "~/garden")))
+ :config
(garden-auto-index-mode 1)
- (autoload 'garden "garden-dashboard" nil t)
- (autoload 'garden-sidecar "garden-dashboard" nil t)
- (autoload 'garden-search "garden-dashboard" nil t)
- (autoload 'garden-fleet "garden-fleet" nil t)
- (autoload 'garden-connect "garden-connect" nil t)
- (autoload 'garden-connect-pick "garden-connect" nil t)
- (autoload 'garden-publish "garden-publish" nil t)
- (autoload 'garden-publish-all "garden-publish" nil t)
- (autoload 'garden-publish-wiki "garden-publish" nil t)
- (autoload 'garden-publish-blog "garden-publish" nil t)
- (autoload 'garden-publish-deploy "garden-publish" nil t)
- (autoload 'denote-capf-setup "denote-capf" nil t)
- (with-eval-after-load 'denote-capf
- (setq denote-capf-directories '("~/org/")))
- (add-hook 'org-mode-hook #'denote-capf-setup)
+)
+
+(use-package garden-dashboard
+ :ensure nil
+ :commands (garden garden-sidecar garden-search garden-search-text
+ garden-visit-random garden-reclassify-tag))
+
+(use-package garden-fleet
+ :ensure nil
+ :commands (garden-fleet garden-fleet-import-all))
+
+(use-package garden-connect
+ :ensure nil
+ :commands (garden-connect garden-connect-pick garden-connect-all))
+
+(use-package garden-publish
+ :ensure nil
+ :commands (garden-publish garden-publish-one garden-publish-all
+ garden-publish-wiki garden-publish-blog garden-publish-pages
+ garden-publish-deploy))
- (defun my/denote-capf-tab-in-insert ()
- (when (featurep 'evil)
- (evil-local-set-key 'insert (kbd "TAB") #'indent-for-tab-command)))
- (add-hook 'org-mode-hook #'my/denote-capf-tab-in-insert))
+(use-package garden-links
+ :ensure nil
+ :commands (garden-insert-denote-links garden-update-denote-links))
+
+(use-package denote-capf
+ :ensure nil
+ :commands (denote-capf-setup denote-capf-teardown denote-capf-refresh)
+ :custom
+ (denote-capf-directories '("~/org/"))
+ :hook
+ (org-mode . denote-capf-setup))
(use-package sops
:ensure (:type git :host github :repo "djgoku/sops")
@@ -4357,12 +4433,7 @@ opening another file in same project does not re-notify."
(use-package rsync-ui
:ensure nil
- :commands (rsync-ui rsync-ui-dired)
- :bind (:map dired-mode-map
- ("C-c r" . rsync-ui-dired))
- :init
- (with-eval-after-load 'general
- (my-leader "or" '(rsync-ui :wk "rsync"))))
+ :commands (rsync-ui rsync-ui-dired))
(defun rlr/org-export-html-to-browser ()
"Export the current Org buffer to a temporary HTML file in the system temp directory, open it, and then delete it when Emacs is killed."
@@ -4477,6 +4548,63 @@ opening another file in same project does not re-notify."
(require 'clatter-org)
(clatter-org-setup)))
+(defvar my/bluesky-feeds
+ '(("❄ Nix and NixOS · @alesya.social"
+ . "at://did:plc:s44h5nbko454vhpa3oyza5lf/app.bsky.feed.generator/aaah6ge3eqpck")
+ ("λ Emacs · @spathi.bsky.social"
+ . "at://did:plc:f6p7kxtbvmsujg67gayjux7m/app.bsky.feed.generator/aaabl5c2jmqaw"))
+ "Named custom feeds offered by `my/bluesky-feed'.")
+
+(defun my/bluesky-feed ()
+ "Select and open one of `my/bluesky-feeds' with Helm."
+ (interactive)
+ (require 'helm)
+ (bluesky-feed
+ (helm-comp-read "Feed: " my/bluesky-feeds
+ :alistp t
+ :buffer "*helm bluesky feeds*"
+ :fuzzy t
+ :must-match t
+ :name "🦋 Bluesky feeds")))
+
+(use-package bluesky
+ :ensure (:host github :repo "ahyatt/emacs-bluesky")
+ :commands (bluesky
+ bluesky-author
+ bluesky-search
+ bluesky-tag
+ bluesky-feed
+ bluesky-notifications
+ bluesky-likes
+ bluesky-replies)
+ :config
+ (require 'password-store)
+
+ ;; Feed, thread, search, author, and notification buffers all use
+ ;; `bluesky-mode'; composers use `bluesky-post-mode'.
+ (with-eval-after-load 'evil
+ (evil-set-initial-state 'bluesky-mode 'emacs)
+ (evil-set-initial-state 'bluesky-post-mode 'emacs))
+
+ ;; Thread views are opened with `pop-to-buffer', so quitting the window
+ ;; naturally reveals the timeline that was underneath.
+ (keymap-set bluesky-mode-map "q" #'quit-window)
+
+ (defun my/bluesky-password-store-auth
+ (orig callback &optional username password host discard-result)
+ "Authenticate Bluesky with the password-store entry `bsky.app'."
+ (funcall orig
+ callback
+ (or username (password-store-get-field "bsky.app" "username"))
+ (or password (password-store-get "bsky.app"))
+ host
+ discard-result))
+
+ (unless (advice-member-p #'my/bluesky-password-store-auth
+ #'bluesky--authenticate)
+ (advice-add 'bluesky--authenticate
+ :around #'my/bluesky-password-store-auth)))
+
(use-package org-other-agenda
:ensure (:host github
:repo "yibie/org-other-agenda"
diff --git a/config.org b/config.org
index a4002b1..3c350fc 100644
--- a/config.org
+++ b/config.org
@@ -78,6 +78,33 @@ Set per-host in the gitignored `local.el'.")
(advice-add 'use-package-normalize/:ensure :around #'my/elpaca-skip-local)
#+end_src
+* elpaca
+
+The lock file is activated in =init.el= before the first package order is
+declared. It is deliberately refreshed only through an explicit command, so
+normal package updates cannot silently rewrite pinned revisions.
+
+#+begin_src emacs-lisp
+(defun my/elpaca-write-lock-file ()
+ "Write exact revisions of all queued packages to `elpaca-lock-file'."
+ (interactive)
+ (unless elpaca-lock-file
+ (user-error "`elpaca-lock-file' is not configured"))
+ (elpaca-wait)
+ (elpaca-write-lock-file elpaca-lock-file)
+ ;; Make the newly written revisions visible to this Emacs session too.
+ (elpaca-menu-lock-file 'update)
+ (message "Elpaca lock file updated: %s"
+ (abbreviate-file-name elpaca-lock-file)))
+
+(defun my/elpaca-visit-lock-file ()
+ "Visit the configured `elpaca-lock-file'."
+ (interactive)
+ (unless elpaca-lock-file
+ (user-error "`elpaca-lock-file' is not configured"))
+ (find-file elpaca-lock-file))
+#+end_src
+
* emacs
#+begin_src emacs-lisp
@@ -246,8 +273,7 @@ Selects the compilation window so the cursor lands in it."
"Focus the async-shell window and make q bury it without killing the process."
(select-window window)
(with-current-buffer (window-buffer window)
- (evil-local-set-key 'normal (kbd "q") #'quit-window)
- (evil-local-set-key 'motion (kbd "q") #'quit-window)
+ (my/async-shell-command-bindings)
(evil-normal-state)))
(use-package window
@@ -313,11 +339,6 @@ Selects the compilation window so the cursor lands in it."
(mailcap-add "image/jpg" "kitty --hold kitty +kitten icat %s")
(mailcap-add "image/gif" "kitty --hold kitty +kitten icat %s")
- (define-key dired-mode-map (kbd "E")
- (lambda ()
- (interactive)
- (mailcap-view-file (dired-get-filename))))
-
(advice-add 'dired-create-directory :around
(lambda (orig-fun dirname)
(let ((current-buffer (current-buffer)))
@@ -337,28 +358,14 @@ Selects the compilation window so the cursor lands in it."
(use-package async :ensure t)
#+end_src
-* general (leader keys)
+* keybinding infrastructure
#+begin_src emacs-lisp
-(use-package general
- :ensure (:wait t)
- :demand t
- :config
- (general-evil-setup))
-
-(eval-and-compile
- (require 'general)
+(defvar-keymap my/leader-map
+ :doc "Global leader map used from Evil states.")
- (general-create-definer my-leader
- :states '(normal visual insert)
- :keymaps 'override
- :prefix "SPC"
- :non-normal-prefix "M-SPC")
-
- (general-create-definer my-local-leader
- :states '(normal visual)
- :keymaps 'override
- :prefix ","))
+(defvar-keymap my/local-leader-map
+ :doc "Global local-leader map used from Evil states.")
#+end_src
* erc
@@ -556,24 +563,7 @@ Temporarily disables notifications during the fetch."
(yes-or-no-p
(format "ERC buffer %s is connected. Really kill (will part channel)? "
(buffer-name)))
- t)))
-
- (general-def 'normal erc-mode-map
- "q" 'quit-window
- "Q" 'my/erc-quit-all
- "gb" 'my/erc-switch-channel
- "gn" 'erc-track-switch-buffer
- "gH" 'my/erc-fetch-history
- "go" 'erc-channel-names
- "gj" 'erc-join-channel
- "gl" 'my/erc-list-channels
- "gr" 'my/erc-reconnect
- "RET" 'erc-send-current-line)
-
- (general-def '(normal insert) erc-mode-map
- "M-n" 'erc-track-switch-buffer
- "C-k" 'erc-previous-command
- "C-j" 'erc-next-command))
+ t))))
#+end_src
* centered cursors
@@ -652,9 +642,7 @@ Temporarily disables notifications during the fetch."
(setq isearch-lazy-count t) ;; Enable lazy counting to show current match information.
(setq lazy-count-prefix-format "(%s/%s) ") ;; Format for displaying current match count.
(setq lazy-count-suffix-format nil) ;; Disable suffix formatting for match count.
- (setq search-whitespace-regexp ".*?") ;; Allow searching across whitespace.
- :bind (("C-s" . isearch-forward) ;; Bind C-s to forward isearch.
- ("C-r" . isearch-backward))) ;; Bind C-r to backward isearch.
+ (setq search-whitespace-regexp ".*?")) ;; Allow searching across whitespace.
#+end_src
* vc
@@ -663,11 +651,6 @@ Temporarily disables notifications during the fetch."
(use-package vc
:ensure nil ;; This is built-in, no need to fetch it.
:defer t
- :bind
- (("C-x v d" . vc-dir) ;; Open VC directory for version control status.
- ("C-x v =" . vc-diff) ;; Show differences for the current file.
- ("C-x v D" . vc-root-diff) ;; Show differences for the entire repository.
- ("C-x v v" . vc-next-action)) ;; Perform the next version control action.
:config
;; Better colors for <leader> g b (blame file)
(setq vc-annotate-color-map
@@ -833,12 +816,7 @@ Not necessary but an eyecandy to look at!
#+begin_src emacs-lisp
(use-package smerge-mode
:ensure nil ;; This is built-in, no need to fetch it.
- :defer t
- :bind (:map smerge-mode-map
- ("C-c ^ u" . smerge-keep-upper) ;; Keep the changes from the upper version.
- ("C-c ^ l" . smerge-keep-lower) ;; Keep the changes from the lower version.
- ("C-c ^ n" . smerge-next) ;; Move to the next conflict.
- ("C-c ^ p" . smerge-previous))) ;; Move to the previous conflict.
+ :defer t)
#+end_src
* ai
@@ -969,22 +947,6 @@ let's give agent-shell another try
'("delta" "--no-gitconfig" "--color-only")))
(setq agent-shell-activity-group-header-label-function
#'agent-shell-activity-group-descriptive-label)
- (define-key agent-shell-mode-map (kbd "RET") #'my/agent-shell-ret)
- (define-key agent-shell-mode-map (kbd "<return>") #'my/agent-shell-ret)
- (define-key agent-shell-viewport-view-mode-map (kbd "RET") #'my/agent-shell-ret)
- (define-key agent-shell-viewport-view-mode-map (kbd "<return>") #'my/agent-shell-ret)
- (with-eval-after-load 'evil
- (evil-define-key '(normal motion) agent-shell-mode-map
- (kbd "RET") #'my/agent-shell-ret
- (kbd "<return>") #'my/agent-shell-ret)
- (evil-define-key 'insert agent-shell-mode-map
- (kbd "RET") #'agent-shell-submit
- (kbd "<return>") #'agent-shell-submit)
- (evil-define-key '(normal motion) agent-shell-viewport-view-mode-map
- (kbd "RET") #'my/agent-shell-ret
- (kbd "<return>") #'my/agent-shell-ret))
-
-
;; Work around agent-shell's `window-system' guard so clipboard images
;; work in terminal Emacs (`emacs -nw'). The underlying save routine
;; already shells out to wl-paste/xclip/pngpaste/powershell, none of
@@ -1570,11 +1532,7 @@ Skips capture tasks and projects."
;; Change ACTIVE projects back to TODO (projects shouldn't be clocked directly)
((and (member (org-get-todo-state) (list "ACTIVE"))
(my/is-project-p))
- "TODO"))))
-
- :bind (("<f11>" . org-clock-goto)
- ("C-c C-x C-i" . org-clock-in)
- ("C-c C-x C-o" . org-clock-out)))
+ "TODO")))))
#+end_src
*** effort and complexity management
@@ -1600,10 +1558,7 @@ Skips capture tasks and projects."
(cond
((string= complexity "low") (org-set-property "EFFORT" "0:30"))
((string= complexity "medium") (org-set-property "EFFORT" "2:00"))
- ((string= complexity "high") (org-set-property "EFFORT" "5:00"))))))
-
- ;; Keybinding for manual effort setting
- (define-key org-mode-map (kbd "C-c e") 'my/org-set-effort-from-complexity))
+ ((string= complexity "high") (org-set-property "EFFORT" "5:00")))))))
#+end_src
*** my/create-project
@@ -2186,6 +2141,22 @@ Org Backend exporter for typst templates written and configured in org mode.
:after org)
#+end_src
+* vui
+
+All buffer UIs share the v1.4 pixel-aware layout engine. Pixel mode keeps
+tables, flex rows, boxes, buttons, and fields aligned when Nerd Font glyphs,
+emoji, CJK, scaled text, or variable-pitch faces are mixed.
+
+#+begin_src emacs-lisp
+(use-package vui
+ :ensure (:host github :repo "d12frosted/vui.el"
+ :ref "v1.4.0"
+ :files ("*.el"))
+ :demand t
+ :custom
+ (vui-width-mode 'pixel))
+#+end_src
+
* denote
Denote is objectively the best note taking extension, which you could install. It has a very easy to parse file naming scheme and suports not only org files. It has also transclusion inclusive! if you are interested, then check it out yourself.
@@ -2408,8 +2379,7 @@ Some goodies for better integration with org files.
#+begin_src emacs-lisp
(use-package ox-json
- :ensure t
- )
+ :ensure t)
#+end_src
** denote-publish
@@ -2441,8 +2411,6 @@ Some goodies for better integration with org files.
(setq tmr-sound-file "/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga"
tmr-notification-urgency 'normal
tmr-description-list 'tmr-description-history)
- (define-key global-map (kbd "C-c t") #'tmr-prefix-map)
-
;; ── Timer persistence across Emacs restarts ──────────────
(defvar my/tmr-save-file
(expand-file-name "tmr-timers.eld" user-emacs-directory)
@@ -2651,12 +2619,8 @@ Auto switch silos (different workspaces/folders) based on preconfigured director
"\\`\\*eldoc"
"\\`\\*elpaca-log\\*\\'"
"\\`\\*Native-compile-Log\\*\\'"
- "\\`\\*Async-native-compile-log\\*\\'"))
+ "\\`\\*Async-native-compile-log\\*\\'")))
;; (helm-display-function #'helm-display-buffer-in-own-frame)
- :bind (:map helm-map
- ("C-j" . helm-next-line)
- ("C-k" . helm-previous-line)))
-(global-set-key (kbd "M-x") 'helm-M-x)
#+end_src
* helm-xref
@@ -2737,15 +2701,7 @@ Auto switch silos (different workspaces/folders) based on preconfigured director
(defun my/project-ctags-setup ()
(interactive)
(my/project-ctags-start (my/project-ctags-root)
- my/project-ctags-arguments))
-
- (with-eval-after-load 'evil
- (evil-define-key '(normal motion) 'global
- (kbd "g d") #'citre-jump
- (kbd "g D") #'citre-query-jump
- (kbd "g p") #'citre-peek
- (kbd "g r") #'citre-jump-to-reference
- (kbd "g b") #'citre-jump-back)))
+ my/project-ctags-arguments)))
#+end_src
* nov
@@ -2822,7 +2778,6 @@ Auto switch silos (different workspaces/folders) based on preconfigured director
(goto-char (point-min))
(search-forward (car (last (split-string option "\\." t))))))
-(keymap-set compilation-mode-map "C-c o" #'nix-compile-goto-option)
#+end_src
** c-ts-mode
@@ -3128,8 +3083,8 @@ so the working-tree diff stays visible until the user explicitly stages."
(funcall orig))))
#+end_src
-* keybindings
-** evil-mode
+* evil
+** core
#+begin_src emacs-lisp
(use-package evil
@@ -3144,13 +3099,7 @@ so the working-tree diff stays visible until the user explicitly stages."
(setq evil-vsplit-window-right t)
(setq evil-paste-from-register nil)
:config
- (evil-mode 1)
- (evil-set-initial-state 'help-mode 'emacs)
- (evil-set-initial-state 'messages-buffer-mode 'normal)
- (evil-set-initial-state 'dired-mode 'normal)
- (evil-set-initial-state 'ibuffer-mode 'normal)
- (evil-set-initial-state 'erc-mode 'normal)
- (define-key evil-insert-state-map (kbd "C-w") 'evil-window-map))
+ (evil-mode 1))
(modify-syntax-entry ?_ "w")
@@ -3162,7 +3111,6 @@ so the working-tree diff stays visible until the user explicitly stages."
;; (eldoc-doc-buffer t))
(eldoc-doc-buffer t))
-(define-key evil-normal-state-map (kbd "K") #'my/eldoc-and-jump)
#+end_src
** smart-parens
@@ -3218,9 +3166,6 @@ so the working-tree diff stays visible until the user explicitly stages."
:ensure t
:after evil
:config
- (define-key evil-inner-text-objects-map "b" 'evil-textobj-anyblock-inner-block)
- (define-key evil-outer-text-objects-map "b" 'evil-textobj-anyblock-a-block)
-
(setq evil-textobj-anyblock-blocks
'(("(" . ")")
("{" . "}")
@@ -3247,8 +3192,6 @@ so the working-tree diff stays visible until the user explicitly stages."
("“" . "”"))))
(evil-textobj-anyblock--make-textobj beg end type count t)))
-(define-key evil-inner-text-objects-map "q" 'my-evil-textobj-anyblock-inner-quote)
-(define-key evil-outer-text-objects-map "q" 'my-evil-textobj-anyblock-a-quote)
#+end_src
** evil multiple cursor
@@ -3281,10 +3224,7 @@ so the working-tree diff stays visible until the user explicitly stages."
(evil-mc-make-cursor-in-visual-selection-end)
(evil-append 1))
;; Fallback: normales Verhalten
- (call-interactively 'evil-append)))
-
- (define-key evil-visual-state-map (kbd "I") 'my/evil-mc-visual-block-insert)
- (define-key evil-visual-state-map (kbd "A") 'my/evil-mc-visual-block-append))
+ (call-interactively 'evil-append))))
#+end_src
* flash
@@ -3312,13 +3252,6 @@ so the working-tree diff stays visible until the user explicitly stages."
(require 'flash-evil)
(flash-evil-setup t)
- ;; Restore ; and , after flash-char overwrites them
- ;; ; is used as prefix for prev-navigation (;b, ;d)
- ;; , is the local leader
- (evil-define-key* '(normal visual motion) 'global
- (kbd ";") nil
- (kbd ",") nil)
-
;; Search integration: labels during C-s, /, ?
(require 'flash-isearch)
(flash-isearch-mode 1))
@@ -3473,7 +3406,7 @@ Native =undo-redo= (via =evil-undo-system='undo-redo=); just raise the undo limi
:mode-line-active-height 0.92
:mode-line-inactive-family "SFProText Nerd Font"
:mode-line-inactive-height 0.92
- :header-line-family "SFProText Nerd Font"
+ :header-line-family ,cashmere/font-family
:header-line-weight regular
:line-number-family ,cashmere/font-family
:line-number-height 0.9
@@ -3598,10 +3531,7 @@ moved to local.el
(use-package croc-ui
:ensure nil
:commands (croc-ui croc-ui-send-files croc-ui-send-directory
- croc-ui-send-text croc-ui-receive)
- :init
- (with-eval-after-load 'general
- (my-leader "oc" '(croc-ui :wk "croc"))))
+ croc-ui-send-text croc-ui-receive))
#+end_src
* yggdrasil
@@ -3618,10 +3548,7 @@ moved to local.el
(use-package yggdrasil-ui
:ensure nil
:after textui
- :commands (yggdrasil-ui)
- :init
- (with-eval-after-load 'general
- (my-leader "ov" '(yggdrasil-ui :wk "yggdrasil"))))
+ :commands (yggdrasil-ui))
#+end_src
* sync
@@ -3630,9 +3557,6 @@ moved to local.el
(use-package sync-ui
:ensure nil
:commands (sync-ui))
- :init
- (with-eval-after-load 'general
- (my-leader "oy" '(sync-ui :wk "sync")))
#+end_src
* my/fns
@@ -3690,8 +3614,6 @@ still require a restart since elpaca queues run at init time."
(use-package projectile
:ensure (:wait t)
:demand t
- :bind-keymap
- (("C-x p" . projectile-command-map))
:config
(projectile-mode +1)
(setq projectile-completion-system 'default
@@ -3979,12 +3901,6 @@ place. `C-c C-c' commits, `C-c C-k' aborts."
(with-eval-after-load 'projectile
(setq projectile-switch-project-action #'my/workspace-switch-to-project))
- (define-key persp-mode-map [remap delete-window]
- #'my/workspace-close-window-or-workspace)
- (with-eval-after-load 'evil
- (define-key persp-mode-map [remap evil-window-delete]
- #'my/workspace-close-window-or-workspace))
-
(add-hook 'persp-filter-save-buffers-functions
(lambda (buf)
(with-current-buffer buf (derived-mode-p 'erc-mode))))
@@ -4016,49 +3932,6 @@ place. `C-c C-c' commits, `C-c C-k' aborts."
(list (cons 'persp t))))))
#+end_src
-#+begin_src emacs-lisp
-(my-leader
- "TAB" '(:ignore t :wk "workspace")
- "TAB TAB" '(my/workspace-display :wk "display")
- "TAB ." '(my/workspace-switch-to :wk "switch to…")
- "TAB `" '(my/workspace-other :wk "last")
- "TAB n" '(my/workspace-new :wk "new")
- "TAB r" '(my/workspace-rename :wk "rename")
- "TAB d" '(my/workspace-kill :wk "delete")
- "TAB x" '(my/workspace-kill-session :wk "kill session")
- "TAB s" '(my/workspace-save-session :wk "save session")
- "TAB l" '(my/workspace-load-session :wk "load session")
- "TAB [" '(my/workspace-switch-left :wk "prev")
- "TAB ]" '(my/workspace-switch-right :wk "next")
- "TAB 1" '((lambda () (interactive) (my/workspace-switch-to-index 0)) :wk "1")
- "TAB 2" '((lambda () (interactive) (my/workspace-switch-to-index 1)) :wk "2")
- "TAB 3" '((lambda () (interactive) (my/workspace-switch-to-index 2)) :wk "3")
- "TAB 4" '((lambda () (interactive) (my/workspace-switch-to-index 3)) :wk "4")
- "TAB 5" '((lambda () (interactive) (my/workspace-switch-to-index 4)) :wk "5")
- "TAB 6" '((lambda () (interactive) (my/workspace-switch-to-index 5)) :wk "6")
- "TAB 7" '((lambda () (interactive) (my/workspace-switch-to-index 6)) :wk "7")
- "TAB 8" '((lambda () (interactive) (my/workspace-switch-to-index 7)) :wk "8")
- "TAB 9" '((lambda () (interactive) (my/workspace-switch-to-index 8)) :wk "9")
- "TAB 0" '(my/workspace-switch-to-final :wk "last"))
-
-(general-def '(normal motion)
- "gt" 'my/workspace-switch-right
- "gT" 'my/workspace-switch-left)
-
-(dotimes (i 9)
- (general-define-key
- :states '(normal visual)
- :keymaps 'override
- :prefix ","
- (number-to-string (1+ i))
- `(lambda () (interactive) (my/workspace-switch-to-index ,i))))
-(general-define-key
- :states '(normal visual)
- :keymaps 'override
- :prefix ","
- "0" #'my/workspace-switch-to-final)
-#+end_src
-
* pass
#+begin_src emacs-lisp
@@ -4072,29 +3945,7 @@ place. `C-c C-c' commits, `C-c C-k' aborts."
(add-to-list 'display-buffer-alist
'("\\*Pass.*\\*"
- (display-buffer-full-frame)))
-
- (with-eval-after-load 'evil
- (general-def 'normal pass-mode-map
- "q" 'quit-window
- "j" 'pass-next-entry
- "k" 'pass-prev-entry
- "RET" 'pass-view
- "d" 'pass-kill
- "y" 'pass-copy
- "Y" 'pass-copy-field
- "e" 'pass-edit
- "a" 'pass-insert
- "G" 'pass-insert-generated
- "o" 'pass-otp-options
- "r" 'pass-rename
- "/" 'isearch-forward)
-
- (general-def 'normal pass-view-mode-map
- "q" 'quit-window
- "t" 'pass-view-toggle-password
- "y" 'pass-copy
- "Y" 'pass-copy-field)))
+ (display-buffer-full-frame))))
#+end_src
** auth-source
@@ -4134,316 +3985,506 @@ place. `C-c C-c' commits, `C-c C-k' aborts."
(setq golden-ratio-exclude-buffer-regexp '("\\`\\*eldoc")))
#+end_src
-* keymaps
+* keybindings
+
+** command helpers
#+begin_src emacs-lisp
(defun my/format-buffer ()
"Format the current buffer with its mode-specific formatter."
(interactive)
- (cond
- ((derived-mode-p 'org-mode)
- (org-fmt-buffer))
- (t
- (user-error "No formatter configured for %s" major-mode))))
+ (if (derived-mode-p 'org-mode)
+ (org-fmt-buffer)
+ (user-error "No formatter configured for %s" major-mode)))
(defun my/find-file-or-switch-project ()
- "Find a file in the current project; outside one, pick a project first.
-Uses the native Helm sources from `helm-projectile'. Picking a project
-switches to its workspace and shows its files via
-`projectile-switch-project-action', so this also works from the master
-workspace (e.g. *scratch*)."
+ "Find a file in the current project, or select a project first."
(interactive)
(if (projectile-project-p)
(helm-projectile-find-file)
(helm-projectile-switch-project)))
-(my-leader
- "SPC" '(my/find-file-or-switch-project :wk "find file/switch project")
- "sp" '(helm-projectile :wk "search project")
- "ss" '(helm-occur :wk "search line")
- "sg" '(my/helm-rg-in :wk "rg in dir")
- "sf" '(my/helm-find-in :wk "find file in dir")
- "/" '(helm-projectile-rg :wk "search project")
- "." '(helm-find-files :wk "find file")
- "," '(helm-mini :wk "switch buffer")
- ":" (lambda () (interactive) (execute-extended-command nil))
- "u" '(universal-argument :wk "universal argument")
+(defun my/xref-find-definitions-other-window ()
+ "Find the definition at point in another window."
+ (interactive)
+ (let ((current-prefix-arg 4))
+ (call-interactively #'xref-find-definitions)))
+
+(defun my/comment-line ()
+ "Toggle the comment on the current line."
+ (interactive)
+ (comment-or-uncomment-region
+ (line-beginning-position)
+ (line-end-position)))
+
+(defun my/comment-region (beg end)
+ "Toggle comments in the active region from BEG to END."
+ (interactive "r")
+ (comment-or-uncomment-region beg end))
+
+(defun my/workspace-switch-by-key ()
+ "Switch to workspace 1–9 using the invoking digit."
+ (interactive)
+ (my/workspace-switch-to-index (- last-command-event ?1)))
+#+end_src
+
+** leader maps
+
+#+begin_src emacs-lisp
+(defvar my/leader-search-map nil)
+(defvar my/leader-denote-map nil)
+(defvar my/leader-denote-merge-map nil)
+(defvar my/leader-denote-timer-map nil)
+(defvar my/leader-file-map nil)
+(defvar my/leader-buffer-map nil)
+(defvar my/leader-project-map nil)
+(defvar my/leader-git-map nil)
+(defvar my/leader-agent-map nil)
+(defvar my/leader-open-map nil)
+(defvar my/leader-help-map nil)
+(defvar my/leader-window-map nil)
+(defvar my/leader-code-map nil)
+(defvar my/leader-quit-map nil)
+(defvar my/leader-web-map nil)
+(defvar my/leader-workspace-map nil)
+
+;; Recreate the maps on reload so removed bindings cannot linger.
+(setq my/leader-map (make-sparse-keymap)
+ my/local-leader-map (make-sparse-keymap)
+ my/leader-search-map (make-sparse-keymap)
+ my/leader-denote-map (make-sparse-keymap)
+ my/leader-denote-merge-map (make-sparse-keymap)
+ my/leader-denote-timer-map (make-sparse-keymap)
+ my/leader-file-map (make-sparse-keymap)
+ my/leader-buffer-map (make-sparse-keymap)
+ my/leader-project-map (make-sparse-keymap)
+ my/leader-git-map (make-sparse-keymap)
+ my/leader-agent-map (make-sparse-keymap)
+ my/leader-open-map (make-sparse-keymap)
+ my/leader-help-map (make-sparse-keymap)
+ my/leader-window-map (make-sparse-keymap)
+ my/leader-code-map (make-sparse-keymap)
+ my/leader-quit-map (make-sparse-keymap)
+ my/leader-web-map (make-sparse-keymap)
+ my/leader-workspace-map (make-sparse-keymap))
+
+(keymap-set my/leader-map "SPC" #'my/find-file-or-switch-project)
+(keymap-set my/leader-map "/" #'helm-projectile-rg)
+(keymap-set my/leader-map "." #'helm-find-files)
+(keymap-set my/leader-map "," #'helm-mini)
+(keymap-set my/leader-map ":" #'execute-extended-command)
+(keymap-set my/leader-map "u" #'universal-argument)
+(keymap-set my/leader-map "x" #'org-capture)
+(keymap-set my/leader-map ";" #'embark-act)
+(keymap-set my/leader-map "P" #'helm-show-kill-ring)
- "d" '(:ignore t :wk "denote")
- "da" '(my/denote-toggle-agenda-keyword :wk "toggle agenda tag")
- "dj" '(denote-journal-new-or-existing-entry :wk "journal")
- "dd" '(denote-menu t :wk "List all notes")
- "dm" '(:ignore t :wk "Merge Notes")
- "dmr" '(denote-merge-region :wk "Merge Region")
- "dmf" '(denote-merge-file :wk "Merge File")
- "dg" '(my/denote-rg :wk "grep denotes")
- "dl" '(denote-link-or-create t :wk "Link Note")
- "dn" '(denote t :wk "Create a new note")
- "dr" '(denote-rename-file t :wk "Rename Note")
- "ds" '(denote-solo-switch :wk "switch silo")
- "dtl" '(tmr-list-timers :wk "list timer")
- "dtt" '(tmr :wk "set timer")
+(keymap-set my/leader-map "s" my/leader-search-map)
+(keymap-set my/leader-search-map "p" #'helm-projectile)
+(keymap-set my/leader-search-map "s" #'helm-occur)
+(keymap-set my/leader-search-map "g" #'my/helm-rg-in)
+(keymap-set my/leader-search-map "f" #'my/helm-find-in)
- "f" '(:ignore t :wk "files")
- "fd" '(dired-jump :wk "dired")
- "fD" '(dired-jump :wk "dired jump")
- "fr" '(helm-recentf :wk "recent files")
- "ff" '(helm-find-files :wk "find file")
- "fs" '(save-buffer :wk "save file")
- "b" '(:ignore t :wk "buffer/bookmarks")
- "bb" '(helm-filtered-bookmarks :wk "display current bookmarks")
- "bi" '(my/ibuffer-workspace :wk "ibuffer (workspace)")
- "bp" '(projectile-ibuffer :wk "ibuffer project")
- "bd" '(bookmark-delete :wk "delete bookmark")
- "bk" '(kill-current-buffer :wk "kill buffer")
- "bs" '(bookmark-set :wk "save bookmark")
- "br" '(rename-buffer :wk "rename buffer")
+(keymap-set my/leader-map "d" my/leader-denote-map)
+(keymap-set my/leader-denote-map "a" #'my/denote-toggle-agenda-keyword)
+(keymap-set my/leader-denote-map "j" #'denote-journal-new-or-existing-entry)
+(keymap-set my/leader-denote-map "d" #'denote-menu)
+(keymap-set my/leader-denote-map "g" #'my/denote-rg)
+(keymap-set my/leader-denote-map "l" #'denote-link-or-create)
+(keymap-set my/leader-denote-map "n" #'denote)
+(keymap-set my/leader-denote-map "r" #'denote-rename-file)
+(keymap-set my/leader-denote-map "s" #'denote-solo-switch)
+(keymap-set my/leader-denote-map "m" my/leader-denote-merge-map)
+(keymap-set my/leader-denote-merge-map "r" #'denote-merge-region)
+(keymap-set my/leader-denote-merge-map "f" #'denote-merge-file)
+(keymap-set my/leader-denote-map "t" my/leader-denote-timer-map)
+(keymap-set my/leader-denote-timer-map "l" #'tmr-list-timers)
+(keymap-set my/leader-denote-timer-map "t" #'tmr)
- "p" '(:ignore t :wk "project")
- "pp" '(helm-projectile-switch-project :wk "switch project workspace")
- "pf" '(helm-projectile-find-file :wk "find file")
- "ps" '(helm-projectile-rg :wk "search")
- "pb" '(helm-projectile-switch-to-buffer :wk "buffers")
- "pk" '(projectile-kill-buffers :wk "kill buffers")
- "pd" '(projectile-remove-known-project :wk "delete project")
- "pr" '(my/project-replace :wk "project replace (wgrep)")
- "pa" '(projectile-add-known-project :wk "add project")
- "pi" '(projectile-invalidate-cache :wk "invalidate cache")
- "pt" '(projectile-run-task :wk "tasks")
- "pc" '(my/project-ctags-setup :wk "generate/update ctags")
+(keymap-set my/leader-map "f" my/leader-file-map)
+(keymap-set my/leader-file-map "d" #'dired-jump)
+(keymap-set my/leader-file-map "D" #'dired-jump)
+(keymap-set my/leader-file-map "r" #'helm-recentf)
+(keymap-set my/leader-file-map "f" #'helm-find-files)
+(keymap-set my/leader-file-map "s" #'save-buffer)
- "g" '(:ignore t :wk "git")
- "gc" '(magit-clone :wk "clone")
- "gg" '(magit-status :wk "status")
- "gf" '(fossil-ui-status :wk "fossil status")
- "gl" '(magit-log-current :wk "log")
- "gi" '(magit-init :wk "init")
- "gd" '(xref-find-definitions :wk "go to definition")
- "gD" '((lambda () (interactive)
- (let ((current-prefix-arg 4))
- (call-interactively #'xref-find-definitions)))
- :wk "definition other window")
- "gI" '(citre-query-jump :wk "find definition")
- "gt" '(citre-peek :wk "peek definition")
- "gr" '(xref-find-references :wk "find references")
- "gs" '(magit-file-stage :wk "stage file")
- "gu" '(my/magit-uncommit :wk "uncommit (keep & unstage)")
- "gb" '(vc-annotate :wk "blame")
- "gT" '(my/code-todos-harvest :wk "harvest code TODOs")
- "aa" '(agent-shell :wk "start")
- "at" '(agent-shell-toggle :wk "toggle")
- "am" '(agent-shell-help-menu :wk "open session")
- "ar" '(agent-shell-send-region :wk "send region")
- "ac" '(agent-shell-send-clipboard-image :wk "send screenshot")
- "o" '(:ignore t :wk "open")
- "oa" '(my/app-launcher :wk "app launcher")
- "os" '(my/snip-upload :wk "snip buffer/region")
- "oS" '(my/snip-upload-file :wk "snip file")
- "op" '(pass :wk "pass")
- "ot" '(ghostel :wk "ghostel")
- "oz" '(zfs :wk "zfs")
+(keymap-set my/leader-map "b" my/leader-buffer-map)
+(keymap-set my/leader-buffer-map "b" #'helm-filtered-bookmarks)
+(keymap-set my/leader-buffer-map "i" #'my/ibuffer-workspace)
+(keymap-set my/leader-buffer-map "p" #'projectile-ibuffer)
+(keymap-set my/leader-buffer-map "d" #'bookmark-delete)
+(keymap-set my/leader-buffer-map "k" #'kill-current-buffer)
+(keymap-set my/leader-buffer-map "s" #'bookmark-set)
+(keymap-set my/leader-buffer-map "r" #'rename-buffer)
- "h" '(:ignore t :wk "help")
- "hm" '(describe-mode :wk "mode")
- "hf" '(describe-function :wk "function")
- "hv" '(describe-variable :wk "variable")
- "hk" '(describe-key :wk "key")
- ;; "ht" '(load-theme :wk "load theme")
+(keymap-set my/leader-map "p" my/leader-project-map)
+(keymap-set my/leader-project-map "p" #'helm-projectile-switch-project)
+(keymap-set my/leader-project-map "f" #'helm-projectile-find-file)
+(keymap-set my/leader-project-map "s" #'helm-projectile-rg)
+(keymap-set my/leader-project-map "b" #'helm-projectile-switch-to-buffer)
+(keymap-set my/leader-project-map "k" #'projectile-kill-buffers)
+(keymap-set my/leader-project-map "d" #'projectile-remove-known-project)
+(keymap-set my/leader-project-map "r" #'my/project-replace)
+(keymap-set my/leader-project-map "a" #'projectile-add-known-project)
+(keymap-set my/leader-project-map "i" #'projectile-invalidate-cache)
+(keymap-set my/leader-project-map "t" #'projectile-run-task)
+(keymap-set my/leader-project-map "c" #'my/project-ctags-setup)
- "w w" '(evil-window-next :wk "Close window")
- "w c" '(evil-window-delete :wk "Close window")
- "w o" '(delete-other-windows :wk "Maximize window")
- "w n" '(evil-window-new :wk "New window")
- "w s" '(evil-window-split :wk "Horizontal split window")
- "w v" '(evil-window-vsplit :wk "Vertical split window")
- "w h" '(evil-window-left :wk "Window left")
- "w j" '(evil-window-down :wk "Window down")
- "w k" '(evil-window-up :wk "Window up")
- "w l" '(evil-window-right :wk "Window right")
- "w w" '(evil-window-next :wk "Goto next window")
- "w H" '(buf-move-left :wk "Buffer move left")
- "w J" '(buf-move-down :wk "Buffer move down")
- "w K" '(buf-move-up :wk "Buffer move up")
- "w L" '(buf-move-right :wk "Buffer move right")
+(keymap-set my/leader-map "g" my/leader-git-map)
+(keymap-set my/leader-git-map "c" #'magit-clone)
+(keymap-set my/leader-git-map "g" #'magit-status)
+(keymap-set my/leader-git-map "f" #'fossil-ui-status)
+(keymap-set my/leader-git-map "l" #'magit-log-current)
+(keymap-set my/leader-git-map "i" #'magit-init)
+(keymap-set my/leader-git-map "d" #'xref-find-definitions)
+(keymap-set my/leader-git-map "D" #'my/xref-find-definitions-other-window)
+(keymap-set my/leader-git-map "I" #'citre-query-jump)
+(keymap-set my/leader-git-map "t" #'citre-peek)
+(keymap-set my/leader-git-map "r" #'xref-find-references)
+(keymap-set my/leader-git-map "s" #'magit-file-stage)
+(keymap-set my/leader-git-map "u" #'my/magit-uncommit)
+(keymap-set my/leader-git-map "b" #'vc-annotate)
+(keymap-set my/leader-git-map "T" #'my/code-todos-harvest)
- "c" '(:ignore t :wk "code")
- "cc" '(my/compile-or-recompile :wk "compile")
- "cC" '(ghostel-compile :wk "recompile")
- "ca" '(citre-query-jump :wk "find definition")
- "cr" '(query-replace :wk "replace")
- "cf" '(my/format-buffer :wk "format buffer")
- "cs" '(yas-insert-snippet :wk "snippets")
- "cl" '(flycheck-list-errors :wk "list errors")
+(keymap-set my/leader-map "a" my/leader-agent-map)
+(keymap-set my/leader-agent-map "a" #'agent-shell)
+(keymap-set my/leader-agent-map "t" #'agent-shell-toggle)
+(keymap-set my/leader-agent-map "m" #'agent-shell-help-menu)
+(keymap-set my/leader-agent-map "r" #'agent-shell-send-region)
+(keymap-set my/leader-agent-map "c" #'agent-shell-send-clipboard-image)
- "q" '(:ignore t :wk "quit")
- "qq" '(save-buffers-kill-terminal :wk "quit emacs")
- "qr" '(restart-emacs :wk "restart")
- "hr" '(my/reload-config :wk "reload config")
+(keymap-set my/leader-map "o" my/leader-open-map)
+(keymap-set my/leader-open-map "a" #'my/app-launcher)
+(keymap-set my/leader-open-map "s" #'my/snip-upload)
+(keymap-set my/leader-open-map "S" #'my/snip-upload-file)
+(keymap-set my/leader-open-map "p" #'pass)
+(keymap-set my/leader-open-map "t" #'ghostel)
+(keymap-set my/leader-open-map "z" #'zfs)
+(keymap-set my/leader-open-map "c" #'croc-ui)
+(keymap-set my/leader-open-map "v" #'yggdrasil-ui)
+(keymap-set my/leader-open-map "y" #'sync-ui)
+(keymap-set my/leader-open-map "r" #'rsync-ui)
- "x" '(org-capture :wk "capture")
+(keymap-set my/leader-map "h" my/leader-help-map)
+(keymap-set my/leader-help-map "m" #'describe-mode)
+(keymap-set my/leader-help-map "f" #'describe-function)
+(keymap-set my/leader-help-map "v" #'describe-variable)
+(keymap-set my/leader-help-map "k" #'describe-key)
+(keymap-set my/leader-help-map "r" #'my/reload-config)
- ";" '(embark-act :wk "embark")
- "P" '(helm-show-kill-ring :wk "paste history")
+(keymap-set my/leader-map "w" my/leader-window-map)
+(keymap-set my/leader-window-map "w" #'evil-window-next)
+(keymap-set my/leader-window-map "c" #'evil-window-delete)
+(keymap-set my/leader-window-map "o" #'delete-other-windows)
+(keymap-set my/leader-window-map "n" #'evil-window-new)
+(keymap-set my/leader-window-map "s" #'evil-window-split)
+(keymap-set my/leader-window-map "v" #'evil-window-vsplit)
+(keymap-set my/leader-window-map "h" #'evil-window-left)
+(keymap-set my/leader-window-map "j" #'evil-window-down)
+(keymap-set my/leader-window-map "k" #'evil-window-up)
+(keymap-set my/leader-window-map "l" #'evil-window-right)
+(keymap-set my/leader-window-map "H" #'buf-move-left)
+(keymap-set my/leader-window-map "J" #'buf-move-down)
+(keymap-set my/leader-window-map "K" #'buf-move-up)
+(keymap-set my/leader-window-map "L" #'buf-move-right)
- ;; "t" '(:ignore t :wk "treesitter")
- ;; "ts" '(flash-treesitter :wk "flash treesitter")
+(keymap-set my/leader-map "c" my/leader-code-map)
+(keymap-set my/leader-code-map "c" #'my/compile-or-recompile)
+(keymap-set my/leader-code-map "C" #'ghostel-compile)
+(keymap-set my/leader-code-map "a" #'citre-query-jump)
+(keymap-set my/leader-code-map "r" #'query-replace)
+(keymap-set my/leader-code-map "f" #'my/format-buffer)
+(keymap-set my/leader-code-map "s" #'yas-insert-snippet)
+(keymap-set my/leader-code-map "l" #'flycheck-list-errors)
+
+(keymap-set my/leader-map "q" my/leader-quit-map)
+(keymap-set my/leader-quit-map "q" #'save-buffers-kill-terminal)
+(keymap-set my/leader-quit-map "r" #'restart-emacs)
+
+(keymap-set my/leader-map "e" my/leader-web-map)
+(keymap-set my/leader-web-map "e" #'eww)
+(keymap-set my/leader-web-map "n" #'my/eww-new-buffer)
+(keymap-set my/leader-web-map "b" #'eww-list-bookmarks)
+(keymap-set my/leader-web-map "h" #'eww-list-histories)
+(keymap-set my/leader-web-map "f" #'elfeed)
+(keymap-set my/leader-web-map "L" #'link-hint-open-link)
+(keymap-set my/leader-web-map "C" #'link-hint-copy-link)
+(keymap-set my/leader-web-map "x" #'xwidget-webkit-browse-url)
+
+(keymap-set my/leader-map "TAB" my/leader-workspace-map)
+(keymap-set my/leader-workspace-map "TAB" #'my/workspace-display)
+(keymap-set my/leader-workspace-map "." #'my/workspace-switch-to)
+(keymap-set my/leader-workspace-map "`" #'my/workspace-other)
+(keymap-set my/leader-workspace-map "n" #'my/workspace-new)
+(keymap-set my/leader-workspace-map "r" #'my/workspace-rename)
+(keymap-set my/leader-workspace-map "d" #'my/workspace-kill)
+(keymap-set my/leader-workspace-map "x" #'my/workspace-kill-session)
+(keymap-set my/leader-workspace-map "s" #'my/workspace-save-session)
+(keymap-set my/leader-workspace-map "l" #'my/workspace-load-session)
+(keymap-set my/leader-workspace-map "[" #'my/workspace-switch-left)
+(keymap-set my/leader-workspace-map "]" #'my/workspace-switch-right)
+(dotimes (index 9)
+ (keymap-set my/leader-workspace-map (number-to-string (1+ index))
+ #'my/workspace-switch-by-key))
+(keymap-set my/leader-workspace-map "0" #'my/workspace-switch-to-final)
+
+(keymap-set my/local-leader-map "a" #'org-agenda)
+(keymap-set my/local-leader-map "c" #'my/centered-cursor)
+(keymap-set my/local-leader-map "f" #'dirvish)
+(keymap-set my/local-leader-map "m" #'mu4e)
+(keymap-set my/local-leader-map "i" #'clatter)
+(keymap-set my/local-leader-map "r" #'async-shell-command)
+(keymap-set my/local-leader-map "t" #'ghostel-project)
+(keymap-set my/local-leader-map "T" #'ghostel-list-buffers)
+(keymap-set my/local-leader-map "z" #'golden-ratio-mode)
+(keymap-set my/local-leader-map "o" #'my/global-olivetti-mode)
+(keymap-set my/local-leader-map "s" #'my-org-sidecar-left)
+(dotimes (index 9)
+ (keymap-set my/local-leader-map (number-to-string (1+ index))
+ #'my/workspace-switch-by-key))
+(keymap-set my/local-leader-map "0" #'my/workspace-switch-to-final)
+
+(with-eval-after-load 'which-key
+ (which-key-add-keymap-based-replacements
+ my/leader-map
+ "s" "search" "d" "denote" "f" "files" "b" "buffers/bookmarks"
+ "p" "project" "g" "git" "a" "agent" "o" "open" "h" "help"
+ "w" "windows" "c" "code" "q" "quit" "e" "web" "TAB" "workspace")
+ (which-key-add-keymap-based-replacements
+ my/leader-denote-map "m" "merge" "t" "timer"))
+#+end_src
+** global native bindings
- "e" '(:ignore t :wk "eww/web")
- "e e" '(eww :wk "eww browse / search")
- "e n" '(my/eww-new-buffer :wk "new eww buffer")
- "e b" '(eww-list-bookmarks :wk "bookmarks")
- "e h" '(eww-list-histories :wk "history")
- "e f" '(elfeed :wk "elfeed (rss)")
- "e L" '(link-hint-open-link :wk "hint open link")
- "e C" '(link-hint-copy-link :wk "hint copy link")
- "e x" '(xwidget-webkit-browse-url :wk "webkit browser")
- )
+#+begin_src emacs-lisp
+(keymap-global-set "C-s" #'isearch-forward)
+(keymap-global-set "C-r" #'isearch-backward)
+(keymap-global-set "C-x v d" #'vc-dir)
+(keymap-global-set "C-x v =" #'vc-diff)
+(keymap-global-set "C-x v D" #'vc-root-diff)
+(keymap-global-set "C-x v v" #'vc-next-action)
+(keymap-global-set "C-x p" projectile-command-map)
+(keymap-global-set "C-c t" tmr-prefix-map)
+(keymap-global-set "<f11>" #'org-clock-goto)
+(keymap-global-set "M-x" #'helm-M-x)
+(keymap-global-set "C-=" #'text-scale-increase)
+(keymap-global-set "C--" #'text-scale-decrease)
+#+end_src
-(defun my/flash-enabled-p ()
- (and (not (derived-mode-p 'magit-mode 'dired-mode 'ibuffer-mode))
- (not (eq major-mode 'dirvish-mode))))
+** global evil bindings
+#+begin_src emacs-lisp
(defun my/s-key-dispatch ()
+ "Run Flash with stable viewport margins."
(interactive)
- (if (derived-mode-p 'magit-mode)
- (call-interactively 'magit-stage)
- (when (my/flash-enabled-p)
- (let ((scroll-margin 0)
- (maximum-scroll-margin 0))
- (call-interactively 'flash-evil-jump)))))
+ (let ((scroll-margin 0)
+ (maximum-scroll-margin 0))
+ (call-interactively #'flash-evil-jump)))
+
+;; The wrapper must retain Flash's inclusive operator semantics. Without
+;; these properties `d s' becomes exclusive and stops before the target.
+(evil-set-command-properties #'my/s-key-dispatch
+ :type 'inclusive
+ :keep-visual t
+ :repeat 'motion)
-(general-def '(normal visual operator) 'override
- :predicate '(not (derived-mode-p 'mu4e-main-mode 'mu4e-headers-mode
- 'mu4e-view-mode 'mu4e-compose-mode
- 'croc-ui-mode 'sync-ui-mode
- 'fossil-ui-mode))
- "s" 'my/s-key-dispatch)
+;; Flash's enhanced character motions claim `;' and `,'. Keep `;' free for
+;; mode-local prefixes and reserve `,' for the local leader below.
+(evil-define-key 'motion 'global
+ (kbd ";") nil
+ (kbd ",") nil)
+(evil-define-key '(normal visual) 'global (kbd "SPC") my/leader-map)
+(evil-define-key 'insert 'global (kbd "M-SPC") my/leader-map)
+(evil-define-key '(normal visual) 'global (kbd ",") my/local-leader-map)
+(evil-define-key '(normal visual operator) 'global (kbd "s") #'my/s-key-dispatch)
+(evil-define-key 'insert 'global (kbd "C-w") evil-window-map)
+(evil-define-key 'normal 'global
+ (kbd "K") #'my/eldoc-and-jump
+ (kbd "] d") #'flycheck-next-error
+ (kbd "[ d") #'flycheck-previous-error
+ (kbd "] c") #'diff-hl-next-hunk
+ (kbd "[ c") #'diff-hl-previous-hunk
+ (kbd "] b") #'switch-to-next-buffer
+ (kbd "[ b") #'switch-to-prev-buffer
+ (kbd "] t") #'tab-next
+ (kbd "[ t") #'tab-previous
+ (kbd "g c c") #'my/comment-line
+ (kbd "g d") #'citre-jump
+ (kbd "g D") #'citre-query-jump
+ (kbd "g p") #'citre-peek
+ (kbd "g r") #'citre-jump-to-reference
+ (kbd "g b") #'citre-jump-back)
-(general-def 'normal 'override
- "K" 'my/eldoc-and-jump
- "]d" 'flycheck-next-error
- "[d" 'flycheck-previous-error
- "]c" 'diff-hl-next-hunk
- "[c" 'diff-hl-previous-hunk
- "]b" 'switch-to-next-buffer
- "[b" 'switch-to-prev-buffer
- "]t" 'tab-next
- "[t" 'tab-previous
- ;; "P" 'helm-show-kill-ring
- ;; "?" 'casual-avy-tmenu
- "gcc" (lambda ()
- (interactive)
- (unless (use-region-p)
- (comment-or-uncomment-region
- (line-beginning-position)
- (line-end-position)))))
+(evil-define-key 'visual 'global (kbd "g c") #'my/comment-region)
+(evil-define-key 'visual 'global
+ (kbd "I") #'my/evil-mc-visual-block-insert
+ (kbd "A") #'my/evil-mc-visual-block-append)
-(general-def 'visual 'override
- "gc" (lambda ()
- (interactive)
- (when (use-region-p)
- (comment-or-uncomment-region
- (region-beginning)
- (region-end)))))
+(define-key evil-inner-text-objects-map "b" #'evil-textobj-anyblock-inner-block)
+(define-key evil-outer-text-objects-map "b" #'evil-textobj-anyblock-a-block)
+(define-key evil-inner-text-objects-map "q" #'my-evil-textobj-anyblock-inner-quote)
+(define-key evil-outer-text-objects-map "q" #'my-evil-textobj-anyblock-a-quote)
#+end_src
-** org mode keymaps
+** agent-shell-mode
#+begin_src emacs-lisp
-(my-leader
- :keymaps 'org-mode-map
- "m" '(:ignore :wk "org")
+(with-eval-after-load 'agent-shell
+ (keymap-set agent-shell-mode-map "RET" #'my/agent-shell-ret)
+ (keymap-set agent-shell-mode-map "<return>" #'my/agent-shell-ret)
+ (evil-define-key '(normal motion) agent-shell-mode-map
+ (kbd "RET") #'my/agent-shell-ret
+ (kbd "<return>") #'my/agent-shell-ret)
+ (evil-define-key 'insert agent-shell-mode-map
+ (kbd "RET") #'agent-shell-submit
+ (kbd "<return>") #'agent-shell-submit))
+#+end_src
+
+** agent-shell-viewport-view-mode
+
+#+begin_src emacs-lisp
+(with-eval-after-load 'agent-shell
+ (keymap-set agent-shell-viewport-view-mode-map "RET" #'my/agent-shell-ret)
+ (keymap-set agent-shell-viewport-view-mode-map "<return>" #'my/agent-shell-ret)
+ (evil-define-key '(normal motion) agent-shell-viewport-view-mode-map
+ (kbd "RET") #'my/agent-shell-ret
+ (kbd "<return>") #'my/agent-shell-ret))
+#+end_src
+
+** async-shell-command buffer
+
+#+begin_src emacs-lisp
+(defun my/async-shell-command-bindings ()
+ "Install bindings local to the displayed async shell buffer."
+ (evil-local-set-key 'normal (kbd "q") #'quit-window)
+ (evil-local-set-key 'motion (kbd "q") #'quit-window))
+#+end_src
- "mt" '(org-todo :wk "TODO")
- "ma" '(org-add-note :wk "add note")
- "mC" '(org-capture :wk "capture")
- "mf" '(org-fmt-buffer :wk "format buffer")
+** compilation-mode
- "mc" '(:wk "set" :ignore)
- "mk" '(kitty-graphics-org-heading-sizes :wk "kgfx headlines")
- "mcd" '(org-deadline :wk "deadline")
- "mcs" '(org-schedule :wk "schedule")
- "mce" '(org-set-effort :wk "effort")
- "mcr" '(org-clock-report :wk "clock report")
- "m," '(org-priority :wk "priority")
- "mI" '(org-clock-in :wk "clock in")
- "mO" '(org-clock-out :wk "clock out")
+#+begin_src emacs-lisp
+(with-eval-after-load 'compile
+ (keymap-set compilation-mode-map "C-c o" #'nix-compile-goto-option))
+#+end_src
- "l" '(:ignore :wk "link")
- "lc" '(org-cliplink :wk "cliplink")
- "li" '(org-download-clipboard :wk "image")
- "ll" '(org-insert-link :wk "link various things")
+** dired-mode
- "n" '(org-toggle-narrow-to-subtree :wk "narrow"))
+#+begin_src emacs-lisp
+(defun my/dired-view-file-externally ()
+ "Open the current Dired file through mailcap."
+ (interactive)
+ (mailcap-view-file (dired-get-filename)))
-(evil-define-key 'normal org-mode-map (kbd "RET") 'org-open-at-point)
+(with-eval-after-load 'dired
+ (evil-set-initial-state 'dired-mode 'normal)
+ (keymap-set dired-mode-map "E" #'my/dired-view-file-externally)
+ (keymap-set dired-mode-map "C-c r" #'rsync-ui-dired)
+ (evil-define-key 'normal dired-mode-map
+ (kbd "h") #'dired-up-directory
+ (kbd "l") #'dired-find-file))
#+end_src
-** dirvish keymaps
+** dirvish-mode
#+begin_src emacs-lisp
+(defvar my/dirvish-yank-map (make-sparse-keymap))
+(defvar my/dirvish-symlink-map (make-sparse-keymap))
+
(defun dirvish-next-file (arg)
- "Move down ARG lines, landing on the filename column.
-Uses raw line motion so hidden detail lines (permissions, owner,
-date) and the dired header are never skipped."
+ "Move down ARG raw lines and land on the filename column."
(interactive "^p")
(forward-line arg)
(dired-move-to-filename))
(defun dirvish-prev-file (arg)
- "Move up ARG lines, landing on the filename column."
+ "Move up ARG raw lines and land on the filename column."
(interactive "^p")
(forward-line (- arg))
(dired-move-to-filename))
-(general-def 'normal dired-mode-map
- "h" 'dired-up-directory
- "l" 'dired-find-file)
-
-(general-def 'normal dirvish-mode-map
- "?" 'dirvish-dispatch
- "q" 'dirvish-quit
- "b" 'dirvish-quick-access
- "f" 'dirvish-file-info-menu
- "p" 'dirvish-yank
- "S" 'dirvish-quicksort
- "F" 'dirvish-layout-toggle
- "z" 'zoxide-travel
- "j" 'dirvish-next-file
- "k" 'dirvish-prev-file
- "gh" 'dirvish-subtree-up
- "gl" 'dirvish-subtree-toggle
- "h" 'dired-up-directory
- "l" 'dired-find-file
- "TAB" 'dirvish-subtree-toggle
- "[h" 'dirvish-history-go-backward
- "]h" 'dirvish-history-go-forward)
-
-(general-def '(normal visual) dirvish-mode-map
- :prefix "y"
- "l" 'dirvish-copy-file-true-path
- "n" 'dirvish-copy-file-name
- "p" 'dirvish-copy-file-path
- "y" 'dired-do-copy)
-
-(general-def 'normal dirvish-mode-map
- :prefix "s"
- "s" 'dirvish-symlink
- "S" 'dirvish-relative-symlink
- "h" 'dirvish-hardlink)
-
(defun my/dirvish-copy-to-clipboard (&rest _)
(my/send-to-clipboard (car kill-ring)))
+(setq my/dirvish-yank-map (make-sparse-keymap)
+ my/dirvish-symlink-map (make-sparse-keymap))
+(keymap-set my/dirvish-yank-map "l" #'dirvish-copy-file-true-path)
+(keymap-set my/dirvish-yank-map "n" #'dirvish-copy-file-name)
+(keymap-set my/dirvish-yank-map "p" #'dirvish-copy-file-path)
+(keymap-set my/dirvish-yank-map "y" #'dired-do-copy)
+(keymap-set my/dirvish-symlink-map "s" #'dirvish-symlink)
+(keymap-set my/dirvish-symlink-map "S" #'dirvish-relative-symlink)
+(keymap-set my/dirvish-symlink-map "h" #'dirvish-hardlink)
+
+(with-eval-after-load 'dirvish
+ (evil-define-key 'normal dirvish-mode-map
+ (kbd "?") #'dirvish-dispatch
+ (kbd "q") #'dirvish-quit
+ (kbd "b") #'dirvish-quick-access
+ (kbd "f") #'dirvish-file-info-menu
+ (kbd "p") #'dirvish-yank
+ (kbd "S") #'dirvish-quicksort
+ (kbd "F") #'dirvish-layout-toggle
+ (kbd "z") #'zoxide-travel
+ (kbd "j") #'dirvish-next-file
+ (kbd "k") #'dirvish-prev-file
+ (kbd "g h") #'dirvish-subtree-up
+ (kbd "g l") #'dirvish-subtree-toggle
+ (kbd "h") #'dired-up-directory
+ (kbd "l") #'dired-find-file
+ (kbd "TAB") #'dirvish-subtree-toggle
+ (kbd "[ h") #'dirvish-history-go-backward
+ (kbd "] h") #'dirvish-history-go-forward
+ (kbd "s") my/dirvish-symlink-map)
+ (evil-define-key '(normal visual) dirvish-mode-map
+ (kbd "y") my/dirvish-yank-map))
+
(dolist (fn '(dirvish-copy-file-path
dirvish-copy-file-name
dirvish-copy-file-true-path))
(advice-add fn :after #'my/dirvish-copy-to-clipboard))
#+end_src
-** eshell keymaps
+** elfeed-search-mode
+
+#+begin_src emacs-lisp
+(with-eval-after-load 'elfeed
+ (evil-define-key '(normal visual) elfeed-search-mode-map
+ (kbd "f") #'link-hint-open-link
+ (kbd "F") #'link-hint-copy-link))
+#+end_src
+
+** elfeed-show-mode
+
+#+begin_src emacs-lisp
+(with-eval-after-load 'elfeed
+ (evil-define-key '(normal visual) elfeed-show-mode-map
+ (kbd "f") #'link-hint-open-link
+ (kbd "F") #'link-hint-copy-link))
+#+end_src
+
+** erc-mode
+
+#+begin_src emacs-lisp
+(with-eval-after-load 'erc
+ (evil-set-initial-state 'erc-mode 'normal)
+ (evil-define-key 'normal erc-mode-map
+ (kbd "q") #'quit-window
+ (kbd "Q") #'my/erc-quit-all
+ (kbd "g b") #'my/erc-switch-channel
+ (kbd "g n") #'erc-track-switch-buffer
+ (kbd "g H") #'my/erc-fetch-history
+ (kbd "g o") #'erc-channel-names
+ (kbd "g j") #'erc-join-channel
+ (kbd "g l") #'my/erc-list-channels
+ (kbd "g r") #'my/erc-reconnect
+ (kbd "RET") #'erc-send-current-line)
+ (evil-define-key '(normal insert) erc-mode-map
+ (kbd "M-n") #'erc-track-switch-buffer
+ (kbd "C-k") #'erc-previous-command
+ (kbd "C-j") #'erc-next-command))
+#+end_src
+
+** eshell-mode
#+begin_src emacs-lisp
(defun my/eshell-clear ()
@@ -4454,41 +4495,182 @@ date) and the dired header are never skipped."
(interactive)
(eshell-atuin-history))
+(defun my/setup-eshell-bindings ()
+ "Apply native and Evil bindings to `eshell-mode-map'."
+ (keymap-set eshell-mode-map "C-l" #'my/eshell-clear)
+ (keymap-set eshell-mode-map "C-r" #'my/atuin-history)
+ (evil-define-key '(normal insert) eshell-mode-map
+ (kbd "C-r") #'my/atuin-history
+ (kbd "C-l") #'my/eshell-clear))
+
+(defun my/eshell-bindings-after-evil-collection (mode _maps &rest _)
+ "Reapply Eshell bindings after Evil Collection configures MODE."
+ (when (eq mode 'eshell)
+ (my/setup-eshell-bindings)))
+
(with-eval-after-load 'eshell
- (add-hook 'eshell-mode-hook
- (lambda ()
- (local-set-key (kbd "C-l") 'my/eshell-clear)
- (local-set-key (kbd "C-r") 'my/atuin-history))))
+ (my/setup-eshell-bindings))
+(add-hook 'evil-collection-setup-hook
+ #'my/eshell-bindings-after-evil-collection)
+#+end_src
+
+** eww-mode
+
+#+begin_src emacs-lisp
+(defvar my/eww-local-leader-map (make-sparse-keymap))
+(setq my/eww-local-leader-map (make-sparse-keymap))
+(set-keymap-parent my/eww-local-leader-map my/local-leader-map)
+(keymap-set my/eww-local-leader-map "B" #'eww-add-bookmark)
+(keymap-set my/eww-local-leader-map "r" #'eww-readable)
+(keymap-set my/eww-local-leader-map "i" #'my/eww-toggle-images)
+(keymap-set my/eww-local-leader-map "y" #'my/eww-copy-as-org)
+(keymap-set my/eww-local-leader-map "d" #'eww-download)
+
+(with-eval-after-load 'eww
+ (evil-define-key '(normal visual) eww-mode-map
+ (kbd ",") my/eww-local-leader-map
+ (kbd "f") #'link-hint-open-link
+ (kbd "F") #'link-hint-copy-link))
+#+end_src
+
+** ghostel-mode
+
+#+begin_src emacs-lisp
+(with-eval-after-load 'ghostel
+ (evil-define-key 'normal ghostel-mode-map
+ (kbd "g t") #'ghostel-next
+ (kbd "g T") #'ghostel-previous
+ (kbd "g n") #'ghostel
+ (kbd "g b") #'ghostel-list-buffers))
+#+end_src
+
+** helm
+
+#+begin_src emacs-lisp
+(with-eval-after-load 'helm
+ (keymap-set helm-map "C-j" #'helm-next-line)
+ (keymap-set helm-map "C-k" #'helm-previous-line))
+#+end_src
+
+** help-mode
+
+#+begin_src emacs-lisp
+(evil-set-initial-state 'help-mode 'emacs)
+#+end_src
+
+** ibuffer-mode
+
+#+begin_src emacs-lisp
+(evil-set-initial-state 'ibuffer-mode 'normal)
+#+end_src
+
+** messages-buffer-mode
+
+#+begin_src emacs-lisp
+(evil-set-initial-state 'messages-buffer-mode 'normal)
+#+end_src
+
+** org-mode
+
+#+begin_src emacs-lisp
+(defvar my/org-leader-map (make-sparse-keymap))
+(defvar my/org-command-map (make-sparse-keymap))
+(defvar my/org-set-map (make-sparse-keymap))
+(defvar my/org-link-map (make-sparse-keymap))
-(with-eval-after-load 'evil
- (with-eval-after-load 'eshell
- (evil-define-key '(normal insert) eshell-mode-map
- (kbd "C-r") 'my/atuin-history
- (kbd "C-l") 'my/eshell-clear)))
+(setq my/org-leader-map (make-sparse-keymap)
+ my/org-command-map (make-sparse-keymap)
+ my/org-set-map (make-sparse-keymap)
+ my/org-link-map (make-sparse-keymap))
+(set-keymap-parent my/org-leader-map my/leader-map)
+(keymap-set my/org-leader-map "m" my/org-command-map)
+(keymap-set my/org-command-map "t" #'org-todo)
+(keymap-set my/org-command-map "a" #'org-add-note)
+(keymap-set my/org-command-map "C" #'org-capture)
+(keymap-set my/org-command-map "f" #'org-fmt-buffer)
+(keymap-set my/org-command-map "k" #'kitty-graphics-org-heading-sizes)
+(keymap-set my/org-command-map "c" my/org-set-map)
+(keymap-set my/org-set-map "d" #'org-deadline)
+(keymap-set my/org-set-map "s" #'org-schedule)
+(keymap-set my/org-set-map "e" #'org-set-effort)
+(keymap-set my/org-set-map "r" #'org-clock-report)
+(keymap-set my/org-command-map "," #'org-priority)
+(keymap-set my/org-command-map "I" #'org-clock-in)
+(keymap-set my/org-command-map "O" #'org-clock-out)
+(keymap-set my/org-leader-map "l" my/org-link-map)
+(keymap-set my/org-link-map "c" #'org-cliplink)
+(keymap-set my/org-link-map "i" #'org-download-clipboard)
+(keymap-set my/org-link-map "l" #'org-insert-link)
+(keymap-set my/org-leader-map "n" #'org-toggle-narrow-to-subtree)
+
+(with-eval-after-load 'org
+ (keymap-set org-mode-map "C-c e" #'my/org-set-effort-from-complexity)
+ (keymap-set org-mode-map "C-c C-x C-i" #'org-clock-in)
+ (keymap-set org-mode-map "C-c C-x C-o" #'org-clock-out)
+ (evil-define-key '(normal visual) org-mode-map
+ (kbd "SPC") my/org-leader-map)
+ (evil-define-key 'insert org-mode-map
+ (kbd "M-SPC") my/org-leader-map
+ (kbd "TAB") #'indent-for-tab-command)
+ (evil-define-key 'normal org-mode-map
+ (kbd "RET") #'org-open-at-point))
+
+(with-eval-after-load 'which-key
+ (which-key-add-keymap-based-replacements
+ my/org-leader-map "m" "org" "l" "link")
+ (which-key-add-keymap-based-replacements
+ my/org-command-map "c" "set"))
+#+end_src
+
+** pass-mode
+
+#+begin_src emacs-lisp
+(with-eval-after-load 'pass
+ (evil-define-key 'normal pass-mode-map
+ (kbd "q") #'quit-window
+ (kbd "j") #'pass-next-entry
+ (kbd "k") #'pass-prev-entry
+ (kbd "RET") #'pass-view
+ (kbd "d") #'pass-kill
+ (kbd "y") #'pass-copy
+ (kbd "Y") #'pass-copy-field
+ (kbd "e") #'pass-edit
+ (kbd "a") #'pass-insert
+ (kbd "G") #'pass-insert-generated
+ (kbd "o") #'pass-otp-options
+ (kbd "r") #'pass-rename
+ (kbd "/") #'isearch-forward))
#+end_src
-** local leader
+** pass-view-mode
#+begin_src emacs-lisp
-(my-local-leader
- "a" '(org-agenda :wk "org agenda")
- "c" '(my/centered-cursor :wk "center cursor")
- "f" '(dirvish :wk "file manager")
- "m" '(mu4e :wk "mu4e")
- "i" '(clatter :wk "irc")
- "r" '(async-shell-command :wk "run async")
- "t" '(ghostel-project :wk "terminal (project)")
- "T" '(ghostel-list-buffers :wk "terminal (switch)")
- "z" '(golden-ratio-mode :wk "zoom/golden ratio")
- "o" '(my/global-olivetti-mode :wk "center buffer")
- "s" '(my-org-sidecar-left :wk "org sidecar"))
+(with-eval-after-load 'pass
+ (evil-define-key 'normal pass-view-mode-map
+ (kbd "q") #'quit-window
+ (kbd "t") #'pass-view-toggle-password
+ (kbd "y") #'pass-copy
+ (kbd "Y") #'pass-copy-field))
+#+end_src
+
+** persp-mode
+
+#+begin_src emacs-lisp
+(with-eval-after-load 'persp-mode
+ (define-key persp-mode-map [remap delete-window]
+ #'my/workspace-close-window-or-workspace)
+ (define-key persp-mode-map [remap evil-window-delete]
+ #'my/workspace-close-window-or-workspace))
#+end_src
-** convenient keys
+** smerge-mode
#+begin_src emacs-lisp
-(global-set-key (kbd "C-=") 'text-scale-increase)
-(global-set-key (kbd "C--") 'text-scale-decrease)
+(with-eval-after-load 'smerge-mode
+ (keymap-set smerge-mode-map "C-c ^ u" #'smerge-keep-upper)
+ (keymap-set smerge-mode-map "C-c ^ l" #'smerge-keep-lower)
+ (keymap-set smerge-mode-map "C-c ^ n" #'smerge-next)
+ (keymap-set smerge-mode-map "C-c ^ p" #'smerge-previous))
#+end_src
* elfeed
@@ -4634,16 +4816,7 @@ reset is unnecessary, so do the handler resolution ourselves and skip
shr-max-image-proportion 0.6
shr-width nil
shr-cookie-policy nil)
- (make-directory eww-bookmarks-directory t)
-
- (with-eval-after-load 'general
- (my-local-leader
- :keymaps 'eww-mode-map
- "B" '(eww-add-bookmark :wk "add bookmark")
- "r" '(eww-readable :wk "reader mode")
- "i" '(my/eww-toggle-images :wk "toggle images")
- "y" '(my/eww-copy-as-org :wk "copy page as org")
- "d" '(eww-download :wk "download"))))
+ (make-directory eww-bookmarks-directory t))
#+end_src
** helper functions
@@ -4709,12 +4882,6 @@ reset is unnecessary, so do the handler resolution ourselves and skip
(use-package link-hint
:ensure t
:commands (link-hint-open-link link-hint-copy-link))
-
-(with-eval-after-load 'general
- (general-def :states '(normal visual)
- :keymaps '(eww-mode-map elfeed-show-mode-map elfeed-search-mode-map)
- "f" #'link-hint-open-link
- "F" #'link-hint-copy-link))
#+end_src
* mu4e
@@ -4945,11 +5112,6 @@ opening another file in same project does not re-notify."
(ghostel-compile-global-mode 1)
(setq-default window-adjust-process-window-size-function
#'window-adjust-process-window-size-largest)
- (evil-define-key 'normal ghostel-mode-map
- (kbd "g t") #'ghostel-next
- (kbd "g T") #'ghostel-previous
- (kbd "g n") #'ghostel
- (kbd "g b") #'ghostel-list-buffers)
(defun my/ghostel-transparent-buffer-face (fg _bg)
(unless (equal fg ghostel--face-cookie-fg-bg)
(when ghostel--face-cookie
@@ -4989,36 +5151,45 @@ opening another file in same project does not re-notify."
* garden
#+begin_src emacs-lisp
-(when-let* ((garden-dir (expand-file-name
- (or (alist-get 'garden my/local-packages) "~/garden")))
- ((file-directory-p garden-dir)))
- (use-package vui
- :ensure (:host github :repo "d12frosted/vui.el" :files ("*.el")))
-
- (add-to-list 'load-path (expand-file-name "lisp/" garden-dir))
- (setq garden-directory (file-name-as-directory garden-dir))
- (require 'garden-core)
+(use-package garden-core
+ :ensure nil
+ :demand t
+ :custom
+ (garden-directory (file-name-as-directory (expand-file-name "~/garden")))
+ :config
(garden-auto-index-mode 1)
- (autoload 'garden "garden-dashboard" nil t)
- (autoload 'garden-sidecar "garden-dashboard" nil t)
- (autoload 'garden-search "garden-dashboard" nil t)
- (autoload 'garden-fleet "garden-fleet" nil t)
- (autoload 'garden-connect "garden-connect" nil t)
- (autoload 'garden-connect-pick "garden-connect" nil t)
- (autoload 'garden-publish "garden-publish" nil t)
- (autoload 'garden-publish-all "garden-publish" nil t)
- (autoload 'garden-publish-wiki "garden-publish" nil t)
- (autoload 'garden-publish-blog "garden-publish" nil t)
- (autoload 'garden-publish-deploy "garden-publish" nil t)
- (autoload 'denote-capf-setup "denote-capf" nil t)
- (with-eval-after-load 'denote-capf
- (setq denote-capf-directories '("~/org/")))
- (add-hook 'org-mode-hook #'denote-capf-setup)
+)
+
+(use-package garden-dashboard
+ :ensure nil
+ :commands (garden garden-sidecar garden-search garden-search-text
+ garden-visit-random garden-reclassify-tag))
- (defun my/denote-capf-tab-in-insert ()
- (when (featurep 'evil)
- (evil-local-set-key 'insert (kbd "TAB") #'indent-for-tab-command)))
- (add-hook 'org-mode-hook #'my/denote-capf-tab-in-insert))
+(use-package garden-fleet
+ :ensure nil
+ :commands (garden-fleet garden-fleet-import-all))
+
+(use-package garden-connect
+ :ensure nil
+ :commands (garden-connect garden-connect-pick garden-connect-all))
+
+(use-package garden-publish
+ :ensure nil
+ :commands (garden-publish garden-publish-one garden-publish-all
+ garden-publish-wiki garden-publish-blog garden-publish-pages
+ garden-publish-deploy))
+
+(use-package garden-links
+ :ensure nil
+ :commands (garden-insert-denote-links garden-update-denote-links))
+
+(use-package denote-capf
+ :ensure nil
+ :commands (denote-capf-setup denote-capf-teardown denote-capf-refresh)
+ :custom
+ (denote-capf-directories '("~/org/"))
+ :hook
+ (org-mode . denote-capf-setup))
#+end_src
* sops
@@ -5158,12 +5329,7 @@ opening another file in same project does not re-notify."
#+begin_src emacs-lisp
(use-package rsync-ui
:ensure nil
- :commands (rsync-ui rsync-ui-dired)
- :bind (:map dired-mode-map
- ("C-c r" . rsync-ui-dired))
- :init
- (with-eval-after-load 'general
- (my-leader "or" '(rsync-ui :wk "rsync"))))
+ :commands (rsync-ui rsync-ui-dired))
#+end_src
* exp
@@ -5291,6 +5457,67 @@ opening another file in same project does not re-notify."
(clatter-org-setup)))
#+end_src
+* bluesky
+
+#+begin_src emacs-lisp
+(defvar my/bluesky-feeds
+ '(("❄ Nix and NixOS · @alesya.social"
+ . "at://did:plc:s44h5nbko454vhpa3oyza5lf/app.bsky.feed.generator/aaah6ge3eqpck")
+ ("λ Emacs · @spathi.bsky.social"
+ . "at://did:plc:f6p7kxtbvmsujg67gayjux7m/app.bsky.feed.generator/aaabl5c2jmqaw"))
+ "Named custom feeds offered by `my/bluesky-feed'.")
+
+(defun my/bluesky-feed ()
+ "Select and open one of `my/bluesky-feeds' with Helm."
+ (interactive)
+ (require 'helm)
+ (bluesky-feed
+ (helm-comp-read "Feed: " my/bluesky-feeds
+ :alistp t
+ :buffer "*helm bluesky feeds*"
+ :fuzzy t
+ :must-match t
+ :name "🦋 Bluesky feeds")))
+
+(use-package bluesky
+ :ensure (:host github :repo "ahyatt/emacs-bluesky")
+ :commands (bluesky
+ bluesky-author
+ bluesky-search
+ bluesky-tag
+ bluesky-feed
+ bluesky-notifications
+ bluesky-likes
+ bluesky-replies)
+ :config
+ (require 'password-store)
+
+ ;; Feed, thread, search, author, and notification buffers all use
+ ;; `bluesky-mode'; composers use `bluesky-post-mode'.
+ (with-eval-after-load 'evil
+ (evil-set-initial-state 'bluesky-mode 'emacs)
+ (evil-set-initial-state 'bluesky-post-mode 'emacs))
+
+ ;; Thread views are opened with `pop-to-buffer', so quitting the window
+ ;; naturally reveals the timeline that was underneath.
+ (keymap-set bluesky-mode-map "q" #'quit-window)
+
+ (defun my/bluesky-password-store-auth
+ (orig callback &optional username password host discard-result)
+ "Authenticate Bluesky with the password-store entry `bsky.app'."
+ (funcall orig
+ callback
+ (or username (password-store-get-field "bsky.app" "username"))
+ (or password (password-store-get "bsky.app"))
+ host
+ discard-result))
+
+ (unless (advice-member-p #'my/bluesky-password-store-auth
+ #'bluesky--authenticate)
+ (advice-add 'bluesky--authenticate
+ :around #'my/bluesky-password-store-auth)))
+#+end_src
+
* org-other-agenda
#+begin_src emacs-lisp
diff --git a/elpaca-lock.eld b/elpaca-lock.eld
new file mode 100644
index 0000000..1cc5420
--- /dev/null
+++ b/elpaca-lock.eld
@@ -0,0 +1,1602 @@
+((ace-link :source "elpaca-menu-lock-file" :recipe
+ (:package "ace-link" :repo "abo-abo/ace-link" :fetcher
+ github :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id ace-link :type git :protocol
+ https :inherit t :depth treeless :ref
+ "d9bd4a25a02bdfde4ea56247daf3a9ff15632ea4"))
+ (acp :source "elpaca-menu-lock-file" :recipe
+ (:package "acp" :fetcher github :repo "xenodium/acp.el" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi" "*.texinfo"
+ "doc/dir" "doc/*.info" "doc/*.texi" "doc/*.texinfo"
+ "lisp/*.el" "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "elpaca-menu-lock-file" :id acp :type git
+ :protocol https :inherit t :depth treeless :ref
+ "7d5c16ebcf2af86aa0f14ad9ae0ce45df4e8c8a5"))
+ (adaptive-wrap :source "elpaca-menu-lock-file" :recipe
+ (:package "adaptive-wrap" :repo
+ ("https://github.com/emacsmirror/gnu_elpa"
+ . "adaptive-wrap")
+ :tar "0.9" :host gnu :branch
+ "externals/adaptive-wrap" :files
+ ("*" (:exclude ".git")) :source "GNU ELPA"
+ :id adaptive-wrap :type git :protocol https
+ :inherit t :depth treeless :ref
+ "e929b38c12f17aa6f8d6270326301d61fbb09cab"))
+ (agent-shell :source "elpaca-menu-lock-file" :recipe
+ (:package "agent-shell" :fetcher github :repo
+ "xenodium/agent-shell" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el" "*-tests.el"
+ "LICENSE" "README*" "*-pkg.el"))
+ :source "elpaca-menu-lock-file" :id
+ agent-shell :type git :protocol https :inherit
+ t :depth treeless :ref
+ "0baa3537be2fa17f7b60fe67a994dc6486ae2ec9"))
+ (alert :source "elpaca-menu-lock-file" :recipe
+ (:package "alert" :fetcher github :repo "jwiegley/alert"
+ :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id alert :type git :protocol https
+ :inherit t :depth treeless :ref
+ "31fc56855289d0846e73d7ca9b84b628aeac16a0"))
+ (async :source "elpaca-menu-lock-file" :recipe
+ (:package "async" :repo "jwiegley/emacs-async" :fetcher github
+ :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "elpaca-menu-lock-file" :id async :type git
+ :protocol https :inherit t :depth treeless :ref
+ "4fdcb061a166e0d6ccc27d3829a28e04415ae825"))
+ (avy :source "elpaca-menu-lock-file" :recipe
+ (:package "avy" :repo "abo-abo/avy" :fetcher github :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi" "*.texinfo"
+ "doc/dir" "doc/*.info" "doc/*.texi" "doc/*.texinfo"
+ "lisp/*.el" "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id avy :type git :protocol https
+ :inherit t :depth treeless :ref
+ "933d1f36cca0f71e4acb5fac707e9ae26c536264"))
+ (benchmark-init :source "elpaca-menu-lock-file" :recipe
+ (:package "benchmark-init" :fetcher github :repo
+ "dholm/benchmark-init-el" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el"
+ "*-tests.el" "LICENSE" "README*"
+ "*-pkg.el"))
+ :source "elpaca-menu-lock-file" :id
+ benchmark-init :wait t :type git :protocol
+ https :inherit t :depth treeless :ref
+ "54b9703389f25012e4cc20fe4a0d4ea253ce4820"))
+ (bluesky :source "elpaca-menu-lock-file" :recipe
+ (:source nil :package "bluesky" :id bluesky :host github
+ :repo "ahyatt/emacs-bluesky" :type git :protocol
+ https :inherit t :depth treeless :ref
+ "b4b7957a3af0533918535486f44afcb808d38d24"))
+ (cape :source "elpaca-menu-lock-file" :recipe
+ (:package "cape" :repo "minad/cape" :fetcher github :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi" "*.texinfo"
+ "doc/dir" "doc/*.info" "doc/*.texi" "doc/*.texinfo"
+ "lisp/*.el" "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id cape :wait t :type git :protocol
+ https :inherit t :depth treeless :ref
+ "bd827d913f487d2bb3815731ac20b60419b9cb03"))
+ (citre :source "elpaca-menu-lock-file" :recipe
+ (:package "citre" :repo "universal-ctags/citre" :fetcher
+ github :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id citre :type git :protocol https
+ :inherit t :depth treeless :ref
+ "be0d9c6dc9b1ac67d76fc7ed315f2369d5c3bde8"))
+ (clatter :source "elpaca-menu-lock-file" :recipe
+ (:source nil :package "clatter" :id clatter :host github
+ :repo "parenworks/clatter.el" :type git :protocol
+ https :inherit t :depth treeless :ref
+ "8b1ae9949717da2491d4a0d869592a2c4d6ba2bf"))
+ (clipetty :source "elpaca-menu-lock-file" :recipe
+ (:package "clipetty" :repo "spudlyo/clipetty" :fetcher
+ github :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "elpaca-menu-lock-file" :id clipetty
+ :type git :protocol https :inherit t :depth
+ treeless :ref
+ "01b39044b9b65fa4ea7d3166f8b1ffab6f740362"))
+ (colorful-mode :source "elpaca-menu-lock-file" :recipe
+ (:package "colorful-mode" :repo
+ ("https://github.com/DevelopmentCool2449/colorful-mode"
+ . "colorful-mode")
+ :tar "1.2.5" :host gnu :files
+ ("*" (:exclude ".git")) :source "GNU ELPA"
+ :id colorful-mode :type git :protocol https
+ :inherit t :depth treeless :ref
+ "6712e9c969bfa8fc8538a0c1bd57a8ef9f974780"))
+ (company :source "elpaca-menu-lock-file" :recipe
+ (:package "company" :fetcher github :repo
+ "company-mode/company-mode" :files
+ (:defaults "icons"
+ ("images/small"
+ "doc/images/small/*.png"))
+ :source "MELPA" :id company :type git :protocol
+ https :inherit t :depth treeless :ref
+ "1cc907ac9e46ae4209eb5a341131787e0c678406"))
+ (compat :source "elpaca-menu-lock-file" :recipe
+ (:package "compat" :repo
+ ("https://github.com/emacs-compat/compat"
+ . "compat")
+ :tar "31.0.0.1" :host gnu :files
+ ("*" (:exclude ".git")) :source
+ "elpaca-menu-lock-file" :id compat :type git
+ :protocol https :inherit t :depth treeless :ref
+ "ad2720457067b72859ba1247f6fdb3361b204bc6"))
+ (cond-let
+ :source "elpaca-menu-lock-file" :recipe
+ (:package "cond-let" :fetcher github :repo "tarsius/cond-let"
+ :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi" "*.texinfo"
+ "doc/dir" "doc/*.info" "doc/*.texi" "doc/*.texinfo"
+ "lisp/*.el" "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE" "README*"
+ "*-pkg.el"))
+ :source "elpaca-menu-lock-file" :id cond-let :type git
+ :protocol https :inherit t :depth treeless :ref
+ "c48600dfab6372670225f046cace263700c78eab"))
+ (dash :source "elpaca-menu-lock-file" :recipe
+ (:package "dash" :fetcher github :repo "magnars/dash.el" :files
+ ("dash.el" "dash.texi") :source
+ "elpaca-menu-lock-file" :id dash :type git :protocol
+ https :inherit t :depth treeless :ref
+ "d746dd9edcb67a108818beb0cdc78dc1cb466832"))
+ (denote :source "elpaca-menu-lock-file" :recipe
+ (:package "denote" :repo
+ ("https://github.com/protesilaos/denote" . "denote")
+ :tar "4.2.3" :host gnu :files
+ ("*" (:exclude ".git" "COPYING" "doclicense.texi"))
+ :source "GNU ELPA" :id denote :wait t :type git
+ :protocol https :inherit t :depth treeless :ref
+ "9ae9d0e1ea30b4fcc1700caafbd55a1ff08ad7b6"))
+ (denote-agenda :source "elpaca-menu-lock-file" :recipe
+ (:package "denote-agenda" :fetcher sourcehut :repo
+ "swflint/denote-agenda" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el"
+ "*-tests.el" "LICENSE" "README*"
+ "*-pkg.el"))
+ :source "MELPA" :id denote-agenda :type git
+ :protocol https :inherit t :depth treeless
+ :ref
+ "51935f6c9372f320be1b873ae4205b77490572d0"))
+ (denote-journal :source "elpaca-menu-lock-file" :recipe
+ (:package "denote-journal" :repo
+ ("https://github.com/protesilaos/denote-journal"
+ . "denote-journal")
+ :tar "0.3.0" :host gnu :files
+ ("*" (:exclude ".git")) :source "GNU ELPA"
+ :id denote-journal :type git :protocol
+ https :inherit t :depth treeless :ref
+ "374224ad2b3162c1fa92bb9ecdb61f157a781c4b"))
+ (denote-merge :source "elpaca-menu-lock-file" :recipe
+ (:source nil :package "denote-merge" :id denote-merge
+ :host github :repo "protesilaos/denote-merge"
+ :type git :protocol https :inherit t :depth
+ treeless :ref
+ "2f8d168d37e66b5c8a3b983e211031b9abbf402e"))
+ (denote-org :source "elpaca-menu-lock-file" :recipe
+ (:package "denote-org" :repo
+ ("https://github.com/protesilaos/denote-org"
+ . "denote-org")
+ :tar "0.3.0" :host gnu :files
+ ("*" (:exclude ".git")) :source "GNU ELPA" :id
+ denote-org :type git :protocol https :inherit t
+ :depth treeless :ref
+ "b6b788db84fbf0c918bce6b3ce65508dd651bb4c"))
+ (denote-publish :source "elpaca-menu-lock-file" :recipe
+ (:source nil :package "denote-publish" :id
+ denote-publish :repo
+ "https://github.com/vedang/denote-publish"
+ :type git :protocol https :inherit t :depth
+ treeless :ref
+ "42fa69618f650cd7e1b60a6cf020ed21ab7d90ee"))
+ (denote-solo :source "elpaca-menu-lock-file" :recipe
+ (:source nil :package "denote-solo" :id denote-solo
+ :host github :repo "pavlo/denote-solo" :type
+ git :protocol https :inherit t :depth treeless
+ :ref "69b25cbdecf8477e5e0a920a82f8f079209fd45e"))
+ (diff-hl :source "elpaca-menu-lock-file" :recipe
+ (:package "diff-hl" :fetcher github :repo "dgutov/diff-hl"
+ :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id diff-hl :type git :protocol
+ https :inherit t :depth treeless :ref
+ "6f1df2b83d1140a2938409b35de7f4c9c7b1defd"))
+ (diredfl :source "elpaca-menu-lock-file" :recipe
+ (:package "diredfl" :fetcher github :repo "purcell/diredfl"
+ :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id diredfl :type git :protocol
+ https :inherit t :depth treeless :ref
+ "fe72d2e42ee18bf6228bba9d7086de4098f18a70"))
+ (dirvish :source "elpaca-menu-lock-file" :recipe
+ (:package "dirvish" :fetcher github :repo
+ "latiagertrutis/dirvish" :files
+ (:defaults "extensions/*.el") :source "MELPA" :id
+ dirvish :host github :branch "main" :type git
+ :protocol https :inherit t :depth treeless :ref
+ "bf164ee21e128837ede59be03836a9900c4a41be"))
+ (dotenv-mode :source "elpaca-menu-lock-file" :recipe
+ (:package "dotenv-mode" :repo
+ "preetpalS/emacs-dotenv-mode" :fetcher github
+ :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el" "*-tests.el"
+ "LICENSE" "README*" "*-pkg.el"))
+ :source "MELPA" :id dotenv-mode :type git
+ :protocol https :inherit t :depth treeless
+ :ref
+ "e3701bf739bde44f6484eb7753deadaf691b73fb"))
+ (elfeed :source "elpaca-menu-lock-file" :recipe
+ (:package "elfeed" :fetcher github :repo
+ "emacs-elfeed/elfeed" :files
+ (:defaults "README.md") :source "MELPA" :id elfeed
+ :host github :branch "main" :type git :protocol
+ https :inherit t :depth treeless :ref
+ "ccfdbf753819fe1ce2f97a1317749003c001443f"))
+ (elfeed-goodies :source "elpaca-menu-lock-file" :recipe
+ (:package "elfeed-goodies" :repo
+ "jeetelongname/elfeed-goodies" :fetcher
+ github :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el"
+ "*-tests.el" "LICENSE" "README*"
+ "*-pkg.el"))
+ :source "MELPA" :id elfeed-goodies :type
+ git :protocol https :inherit t :depth
+ treeless :ref
+ "544ef42ead011d960a0ad1c1d34df5d222461a6b"))
+ (elfeed-protocol :source "elpaca-menu-lock-file" :recipe
+ (:package "elfeed-protocol" :repo
+ "fasheng/elfeed-protocol" :fetcher github
+ :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el"
+ "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id elfeed-protocol :host
+ github :type git :protocol https :inherit
+ t :depth treeless :ref
+ "58936590459ccc2dfd6132f69983011d15d9404a"))
+ (elpaca :source
+ "elpaca-menu-lock-file" :recipe
+ (:source nil :package "elpaca" :id elpaca :repo
+ "https://github.com/progfolio/elpaca.git" :ref
+ "6530ffa73b18ccee858e7c471415ab7e0c0d8ce1" :depth 1
+ :inherit ignore :files
+ (:defaults "elpaca-test.el" (:exclude "extensions"))
+ :build (:not elpaca-activate) :type git :protocol https))
+ (elpaca-use-package :source "elpaca-menu-lock-file" :recipe
+ (:package "elpaca-use-package" :wait t :repo
+ "https://github.com/progfolio/elpaca.git"
+ :files
+ ("extensions/elpaca-use-package.el")
+ :main
+ "extensions/elpaca-use-package.el"
+ :build
+ (:not elpaca-source elpaca-build-docs)
+ :source "Elpaca extensions" :id
+ elpaca-use-package :type git :protocol
+ https :inherit t :depth treeless :ref
+ "6530ffa73b18ccee858e7c471415ab7e0c0d8ce1"))
+ (envrc :source "elpaca-menu-lock-file" :recipe
+ (:package "envrc" :fetcher github :repo "purcell/envrc" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id envrc :type git :protocol https
+ :inherit t :depth treeless :ref
+ "e4f9fd79a612c8f81b4ba5b12ee7261fc33568f0"))
+ (esxml :source "elpaca-menu-lock-file" :recipe
+ (:package "esxml" :fetcher github :repo "tali713/esxml" :files
+ ("esxml.el" "esxml-query.el") :source "MELPA" :id
+ esxml :type git :protocol https :inherit t :depth
+ treeless :ref
+ "6a375888a74b7563eb53176aad81faee8b858189"))
+ (evil :source "elpaca-menu-lock-file" :recipe
+ (:package "evil" :repo "emacs-evil/evil" :fetcher github :files
+ (:defaults "doc/build/texinfo/evil.texi"
+ (:exclude "evil-test-helpers.el"))
+ :source "MELPA" :id evil :wait t :type git :protocol
+ https :inherit t :depth treeless :ref
+ "6a3e1ddd04ac504a016590940d0af2a3361b9efd"))
+ (evil-collection :source "elpaca-menu-lock-file" :recipe
+ (:package "evil-collection" :fetcher github :repo
+ "emacs-evil/evil-collection" :files
+ (:defaults "modes") :source "MELPA" :id
+ evil-collection :wait t :type git
+ :protocol https :inherit t :depth treeless
+ :ref
+ "fa8da0ebba4bbf2a84a78183420d8303179ef427"))
+ (evil-ghostel :source "elpaca-menu-lock-file" :recipe
+ (:package "evil-ghostel" :fetcher github :repo
+ "dakra/ghostel" :files
+ ("extensions/evil-ghostel/*.el") :source
+ "MELPA" :id evil-ghostel :host github :type
+ git :protocol https :inherit t :depth
+ treeless :ref
+ "dd72e1f4ae891345a1f76ed98c5cbd71c18e808e"))
+ (evil-matchit :source "elpaca-menu-lock-file" :recipe
+ (:package "evil-matchit" :fetcher github :repo
+ "redguardtoo/evil-matchit" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el"
+ "*-tests.el" "LICENSE" "README*"
+ "*-pkg.el"))
+ :source "MELPA" :id evil-matchit :type git
+ :protocol https :inherit t :depth treeless
+ :ref
+ "dd03aacd8602ffd2cd9b67d0072092f8d57d5e01"))
+ (evil-mc :source "elpaca-menu-lock-file" :recipe
+ (:package "evil-mc" :fetcher github :repo "gabesoft/evil-mc"
+ :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id evil-mc :type git :protocol
+ https :inherit t :depth treeless :ref
+ "7e363dd6b0a39751e13eb76f2e9b7b13c7054a43"))
+ (evil-org :source "elpaca-menu-lock-file" :recipe
+ (:package "evil-org" :fetcher github :repo
+ "Somelauw/evil-org-mode" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id evil-org :type git :protocol
+ https :inherit t :depth treeless :ref
+ "b1f309726b1326e1a103742524ec331789f2bf94"))
+ (evil-smartparens :source "elpaca-menu-lock-file" :recipe
+ (:package "evil-smartparens" :fetcher github :repo
+ "expez/evil-smartparens" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el"
+ "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id evil-smartparens
+ :type git :protocol https :inherit t
+ :depth treeless :ref
+ "026d4a3cfce415a4dfae1457f871b385386e61d3"))
+ (evil-surround :source "elpaca-menu-lock-file" :recipe
+ (:package "evil-surround" :repo
+ "emacs-evil/evil-surround" :fetcher github
+ :old-names (surround) :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el"
+ "*-tests.el" "LICENSE" "README*"
+ "*-pkg.el"))
+ :source "MELPA" :id evil-surround :type git
+ :protocol https :inherit t :depth treeless
+ :ref
+ "e6548372e8359ee55e67d73ca418314086011f1a"))
+ (evil-textobj-anyblock :source "elpaca-menu-lock-file" :recipe
+ (:package "evil-textobj-anyblock" :fetcher
+ github :repo
+ "noctuid/evil-textobj-anyblock"
+ :files
+ ("*.el" "*.el.in" "dir" "*.info"
+ "*.texi" "*.texinfo" "doc/dir"
+ "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info"
+ "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el"
+ "test.el" "tests.el"
+ "*-test.el" "*-tests.el"
+ "LICENSE" "README*"
+ "*-pkg.el"))
+ :source "MELPA" :id
+ evil-textobj-anyblock :type git
+ :protocol https :inherit t :depth
+ treeless :ref
+ "ff00980f0634f95bf2ad9956b615a155ea8743be"))
+ (f :source "elpaca-menu-lock-file" :recipe
+ (:package "f" :fetcher github :repo "rejeep/f.el" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi" "*.texinfo"
+ "doc/dir" "doc/*.info" "doc/*.texi" "doc/*.texinfo"
+ "lisp/*.el" "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE" "README*"
+ "*-pkg.el"))
+ :source "elpaca-menu-lock-file" :id f :type git
+ :protocol https :inherit t :depth treeless :ref
+ "931b6d0667fe03e7bf1c6c282d6d8d7006143c52"))
+ (flash :source "elpaca-menu-lock-file" :recipe
+ (:package "flash" :fetcher github :repo "Prgebish/flash"
+ :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id flash :host github :type git
+ :protocol https :inherit t :depth treeless :ref
+ "db3bfa84866f143a0665d1b5a48c3b30dc7b528f"))
+ (fontaine :source "elpaca-menu-lock-file" :recipe
+ (:package "fontaine" :repo
+ ("https://github.com/protesilaos/fontaine"
+ . "fontaine")
+ :tar "3.0.1" :host gnu :files
+ ("*"
+ (:exclude ".git" "COPYING" "doclicense.texi"))
+ :source "GNU ELPA" :id fontaine :type git
+ :protocol https :inherit t :depth treeless :ref
+ "8cf9f6693e2a780b530ff5fd9920863b751d5bd5"))
+ (futur :source "elpaca-menu-lock-file" :recipe
+ (:package "futur" :repo
+ ("https://github.com/emacsmirror/gnu_elpa" . "futur")
+ :tar "1.7" :host gnu :branch "externals/futur"
+ :files ("*" (:exclude ".git")) :source "GNU ELPA"
+ :id futur :type git :protocol https :inherit t
+ :depth treeless :ref
+ "bcfc5c34b6a616d80bfbc41772a8f3a65e39b01a"))
+ (ghostel :source "elpaca-menu-lock-file" :recipe
+ (:package "ghostel" :fetcher github :repo "dakra/ghostel"
+ :files
+ (:defaults "etc" "src" "vendor" "build.zig"
+ "build.zig.zon" "symbols.map")
+ :source "MELPA" :id ghostel :type git :protocol
+ https :inherit t :depth treeless :ref
+ "dd72e1f4ae891345a1f76ed98c5cbd71c18e808e"))
+ (gntp :source "elpaca-menu-lock-file" :recipe
+ (:package "gntp" :repo "tekai/gntp.el" :fetcher github :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi" "*.texinfo"
+ "doc/dir" "doc/*.info" "doc/*.texi" "doc/*.texinfo"
+ "lisp/*.el" "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id gntp :type git :protocol https
+ :inherit t :depth treeless :ref
+ "767571135e2c0985944017dc59b0be79af222ef5"))
+ (golden-ratio :source "elpaca-menu-lock-file" :recipe
+ (:package "golden-ratio" :repo "roman/golden-ratio.el"
+ :fetcher github :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el"
+ "*-tests.el" "LICENSE" "README*"
+ "*-pkg.el"))
+ :source "MELPA" :id golden-ratio :type git
+ :protocol https :inherit t :depth treeless
+ :ref
+ "375c9f287dfad68829582c1e0a67d0c18119dab9"))
+ (goto-chg :source "elpaca-menu-lock-file" :recipe
+ (:package "goto-chg" :repo "emacs-evil/goto-chg" :fetcher
+ github :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id goto-chg :type git :protocol
+ https :inherit t :depth treeless :ref
+ "72f556524b88e9d30dc7fc5b0dc32078c166fda7"))
+ (helm :source "elpaca-menu-lock-file" :recipe
+ (:package "helm" :fetcher github :repo "emacs-helm/helm" :files
+ (:defaults "emacs-helm.sh"
+ (:exclude "helm-lib.el" "helm-source.el"
+ "helm-multi-match.el"
+ "helm-core.el"))
+ :source "MELPA" :id helm :wait t :type git :protocol
+ https :inherit t :depth treeless :ref
+ "acff7751af0623f1bbebb1f3bf1ee4831f7f5035"))
+ (helm-core :source "elpaca-menu-lock-file" :recipe
+ (:package "helm-core" :repo "emacs-helm/helm" :fetcher
+ github :files
+ ("helm-core.el" "helm-lib.el" "helm-source.el"
+ "helm-multi-match.el")
+ :source "MELPA" :id helm-core :type git
+ :protocol https :inherit t :depth treeless :ref
+ "acff7751af0623f1bbebb1f3bf1ee4831f7f5035"))
+ (helm-projectile :source "elpaca-menu-lock-file" :recipe
+ (:package "helm-projectile" :repo
+ "bbatsov/helm-projectile" :fetcher github
+ :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el"
+ "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id helm-projectile :type
+ git :protocol https :inherit t :depth
+ treeless :ref
+ "4dae1d072cc2650749846cfcab1f60686471cc45"))
+ (helm-xref :source "elpaca-menu-lock-file" :recipe
+ (:package "helm-xref" :repo "brotzeit/helm-xref" :fetcher
+ github :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id helm-xref :type git
+ :protocol https :inherit t :depth treeless :ref
+ "ea0e4ed8a9baf236e4085cbc7178241f109a53fa"))
+ (hl-todo :source "elpaca-menu-lock-file" :recipe
+ (:package "hl-todo" :fetcher github :repo "tarsius/hl-todo"
+ :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id hl-todo :host github :type git
+ :protocol https :inherit t :depth treeless :ref
+ "527d545b8c2f36243194cbe4a8d0e6ac9d50e6a7"))
+ (ht :source "elpaca-menu-lock-file" :recipe
+ (:package "ht" :fetcher github :repo "Wilfred/ht.el" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi" "*.texinfo"
+ "doc/dir" "doc/*.info" "doc/*.texi" "doc/*.texinfo"
+ "lisp/*.el" "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE" "README*"
+ "*-pkg.el"))
+ :source "elpaca-menu-lock-file" :id ht :type git
+ :protocol https :inherit t :depth treeless :ref
+ "1c49aad1c820c86f7ee35bf9fff8429502f60fef"))
+ (indent-guide :source "elpaca-menu-lock-file" :recipe
+ (:package "indent-guide" :fetcher github :repo
+ "zk-phi/indent-guide" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el"
+ "*-tests.el" "LICENSE" "README*"
+ "*-pkg.el"))
+ :source "MELPA" :id indent-guide :type git
+ :protocol https :inherit t :depth treeless
+ :ref
+ "1332f95d6f08afee35f62621793e2622b9f86f27"))
+ (inheritenv :source "elpaca-menu-lock-file" :recipe
+ (:package "inheritenv" :fetcher github :repo
+ "purcell/inheritenv" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el" "*-tests.el"
+ "LICENSE" "README*" "*-pkg.el"))
+ :source "MELPA" :id inheritenv :type git
+ :protocol https :inherit t :depth treeless :ref
+ "b9e67cc20c069539698a9ac54d0e6cc11e616c6f"))
+ (javelin :source "elpaca-menu-lock-file" :recipe
+ (:package "javelin" :fetcher github :repo
+ "DamianB-BitFlipper/javelin.el" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id javelin :type git :protocol
+ https :inherit t :depth treeless :ref
+ "e2f1ccad5a7ad0dd38f62773723353081d57bae8"))
+ (just-mode :source "elpaca-menu-lock-file" :recipe
+ (:package "just-mode" :repo "leon-barrett/just-mode.el"
+ :fetcher github :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id just-mode :type git
+ :protocol https :inherit t :depth treeless :ref
+ "5513ab0350d2701a94deb8cafef57d3c369a5e9b"))
+ (language-detection :source "elpaca-menu-lock-file" :recipe
+ (:package "language-detection" :fetcher github
+ :repo
+ "andreasjansson/language-detection.el"
+ :files
+ ("*.el" "*.el.in" "dir" "*.info"
+ "*.texi" "*.texinfo" "doc/dir"
+ "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el"
+ "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id language-detection
+ :type git :protocol https :inherit t
+ :depth treeless :ref
+ "54a6ecf55304fba7d215ef38a4ec96daff2f35a4"))
+ (link-hint :source "elpaca-menu-lock-file" :recipe
+ (:package "link-hint" :fetcher github :repo
+ "noctuid/link-hint.el" :version-regexp
+ "none-since-rename" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id link-hint :type git
+ :protocol https :inherit t :depth treeless :ref
+ "8fda5dcb9caff5a3c49d22b82e570ac9e29af7dd"))
+ (llama :source "elpaca-menu-lock-file" :recipe
+ (:package "llama" :fetcher github :repo "tarsius/llama" :files
+ ("llama.el" ".dir-locals.el") :source
+ "elpaca-menu-lock-file" :id llama :type git
+ :protocol https :inherit t :depth treeless :ref
+ "4d4024048053b898a01521046e0f063ee47615b0"))
+ (log4e :source "elpaca-menu-lock-file" :recipe
+ (:package "log4e" :repo "aki2o/log4e" :fetcher github :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id log4e :type git :protocol https
+ :inherit t :depth treeless :ref
+ "6d71462df9bf595d3861bfb328377346aceed422"))
+ (magit :source "elpaca-menu-lock-file" :recipe
+ (:package "magit" :fetcher github :repo "magit/magit" :files
+ ("lisp/magit*.el" "lisp/git-*.el" "docs/magit.texi"
+ "docs/AUTHORS.md" "LICENSE" ".dir-locals.el"
+ ("githooks" "githooks/*")
+ ("git-hooks" "git-hooks/*")
+ (:exclude "lisp/magit-section.el"))
+ :source "MELPA" :id magit :wait t :type git
+ :protocol https :inherit t :depth treeless :ref
+ "bb426845dc2c182da316d64ea19efc837b303e85"))
+ (magit-delta :source "elpaca-menu-lock-file" :recipe
+ (:package "magit-delta" :fetcher github :repo
+ "dandavison/magit-delta" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el" "*-tests.el"
+ "LICENSE" "README*" "*-pkg.el"))
+ :source "MELPA" :id magit-delta :type git
+ :protocol https :inherit t :depth treeless
+ :ref
+ "5fc7dbddcfacfe46d3fd876172ad02a9ab6ac616"))
+ (magit-section :source "elpaca-menu-lock-file" :recipe
+ (:package "magit-section" :fetcher github :repo
+ "magit/magit" :files
+ ("lisp/magit-section.el"
+ "docs/magit-section.texi"
+ "magit-section-pkg.el")
+ :source "MELPA" :id magit-section :type git
+ :protocol https :inherit t :depth treeless
+ :ref
+ "bb426845dc2c182da316d64ea19efc837b303e85"))
+ (markdown-mode :source "elpaca-menu-lock-file" :recipe
+ (:package "markdown-mode" :fetcher github :repo
+ "jrblevin/markdown-mode" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el"
+ "*-tests.el" "LICENSE" "README*"
+ "*-pkg.el"))
+ :source "elpaca-menu-lock-file" :id
+ markdown-mode :type git :protocol https
+ :inherit t :depth treeless :ref
+ "f441e8bc9951e73b12c61e9198658488dd8e86e1"))
+ (minimal-dashboard :source "elpaca-menu-lock-file" :recipe
+ (:package "minimal-dashboard" :fetcher github
+ :repo
+ "dheerajshenoy/minimal-dashboard.el"
+ :files
+ ("*.el" "*.el.in" "dir" "*.info"
+ "*.texi" "*.texinfo" "doc/dir"
+ "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el"
+ "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id minimal-dashboard
+ :type git :protocol https :inherit t
+ :depth treeless :ref
+ "260a074d44eaa9e49576dca24c09cca54f1e193d"))
+ (mode-line-maker :source "elpaca-menu-lock-file" :recipe
+ (:package "mode-line-maker" :repo
+ "rougier/mode-line-maker" :tar nil :host
+ github :files ("*" (:exclude ".git"))
+ :source "GNU ELPA" :id mode-line-maker
+ :type git :protocol https :inherit t
+ :depth treeless :ref
+ "dc0bd20750f34340448ba1c7be4ed2b62af0f78c"))
+ (mu4e-alert :source "elpaca-menu-lock-file" :recipe
+ (:package "mu4e-alert" :fetcher github :repo
+ "xzz53/mu4e-alert" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el" "*-tests.el"
+ "LICENSE" "README*" "*-pkg.el"))
+ :source "MELPA" :id mu4e-alert :type git
+ :protocol https :inherit t :depth treeless :ref
+ "9f20f30b15a5f5cc43fe448684fe1d4b981639aa"))
+ (nerd-icons :source "elpaca-menu-lock-file" :recipe
+ (:package "nerd-icons" :repo
+ "rainstormstudio/nerd-icons.el" :fetcher github
+ :files (:defaults "data") :source
+ "elpaca-menu-lock-file" :id nerd-icons :type
+ git :protocol https :inherit t :depth treeless
+ :ref "1e75075e323dedaf9f2fd5837082c60a2d0dfae3"))
+ (nerd-icons-dired :source "elpaca-menu-lock-file" :recipe
+ (:package "nerd-icons-dired" :repo
+ "rainstormstudio/nerd-icons-dired"
+ :fetcher github :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el"
+ "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id nerd-icons-dired
+ :type git :protocol https :inherit t
+ :depth treeless :ref
+ "104acd8879528b8115589f35f1bbcbe231ad732f"))
+ (nerd-icons-ibuffer :source "elpaca-menu-lock-file" :recipe
+ (:package "nerd-icons-ibuffer" :repo
+ "seagle0128/nerd-icons-ibuffer"
+ :fetcher github :files
+ ("*.el" "*.el.in" "dir" "*.info"
+ "*.texi" "*.texinfo" "doc/dir"
+ "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el"
+ "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "elpaca-menu-lock-file" :id
+ nerd-icons-ibuffer :type git :protocol
+ https :inherit t :depth treeless :ref
+ "22dcbeea8c7677c83c8464c34247cf968250ee18"))
+ (nix-ts-mode :source "elpaca-menu-lock-file" :recipe
+ (:package "nix-ts-mode" :fetcher github :repo
+ "nix-community/nix-ts-mode" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el" "*-tests.el"
+ "LICENSE" "README*" "*-pkg.el"))
+ :source "MELPA" :id nix-ts-mode :type git
+ :protocol https :inherit t :depth treeless
+ :ref
+ "09c89886a22b8e37ac0de9210ff5b2b79c520fd7"))
+ (nov :source "elpaca-menu-lock-file" :recipe
+ (:package "nov" :fetcher git :url
+ "https://depp.brause.cc/nov.el.git" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi" "*.texinfo"
+ "doc/dir" "doc/*.info" "doc/*.texi" "doc/*.texinfo"
+ "lisp/*.el" "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id nov :type git :protocol https
+ :inherit t :depth treeless :ref
+ "874daf5e4791a6d4f47741422c80e2736e907351"))
+ (ob-async :source "elpaca-menu-lock-file" :recipe
+ (:package "ob-async" :repo "astahlman/ob-async" :fetcher
+ github :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "elpaca-menu-lock-file" :id ob-async
+ :type git :protocol https :inherit t :depth
+ treeless :ref
+ "9aac486073f5c356ada20e716571be33a350a982"))
+ (olivetti :source "elpaca-menu-lock-file" :recipe
+ (:package "olivetti" :fetcher github :repo "rnkn/olivetti"
+ :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id olivetti :type git :protocol
+ https :inherit t :depth treeless :ref
+ "d2ccae56b442d9c5b06dd2481057abbd7eb82551"))
+ (org :source "elpaca-menu-lock-file" :recipe
+ (:package "org" :host github :repo "emacsmirror/org" :autoloads
+ "org-loaddefs.el" :depth nil :build
+ ((:not elpaca-build-autoloads)
+ (:before elpaca-build-link elpaca-menu-org--build))
+ :files
+ (:defaults ("etc/styles/" "etc/styles/*" "doc/*.texi"))
+ :source "elpaca-menu-lock-file" :id org :ref
+ "0d02d72627428dddf21ec66b1eb87e46c2849ded" :wait t
+ :type git :protocol https :inherit t))
+ (org-appear :source "elpaca-menu-lock-file" :recipe
+ (:package "org-appear" :fetcher github :repo
+ "awth13/org-appear" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el" "*-tests.el"
+ "LICENSE" "README*" "*-pkg.el"))
+ :source "elpaca-menu-lock-file" :id org-appear
+ :type git :protocol https :inherit t :depth
+ treeless :ref
+ "77d23efec5f5c25fc0798364d2b51a3ce3d8d518"))
+ (org-auto-tangle :source "elpaca-menu-lock-file" :recipe
+ (:package "org-auto-tangle" :repo
+ "yilkalargaw/org-auto-tangle" :fetcher
+ github :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el"
+ "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id org-auto-tangle :type
+ git :protocol https :inherit t :depth
+ treeless :ref
+ "b4e7abc019179df473ecddf0af80561ddad8fc58"))
+ (org-cliplink :source "elpaca-menu-lock-file" :recipe
+ (:package "org-cliplink" :repo "rexim/org-cliplink"
+ :fetcher github :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el"
+ "*-tests.el" "LICENSE" "README*"
+ "*-pkg.el"))
+ :source "MELPA" :id org-cliplink :type git
+ :protocol https :inherit t :depth treeless
+ :ref
+ "13e0940b65d22bec34e2de4bc8cba1412a7abfbc"))
+ (org-contrib :source "elpaca-menu-lock-file" :recipe
+ (:package "org-contrib" :host github :repo
+ "emacsmirror/org-contrib" :files (:defaults)
+ :source "elpaca-menu-lock-file" :id
+ org-contrib :wait t :type git :protocol https
+ :inherit t :depth treeless :ref
+ "82f94c5612c20286d234613f0ce0d92eac2c0845"))
+ (org-download :source "elpaca-menu-lock-file" :recipe
+ (:package "org-download" :repo "abo-abo/org-download"
+ :fetcher github :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el"
+ "*-tests.el" "LICENSE" "README*"
+ "*-pkg.el"))
+ :source "MELPA" :id org-download :type git
+ :protocol https :inherit t :depth treeless
+ :ref
+ "c8be2611786d1d8d666b7b4f73582de1093f25ac"))
+ (org-modern :source "elpaca-menu-lock-file" :recipe
+ (:package "org-modern" :repo "minad/org-modern" :fetcher
+ github :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el" "*-tests.el"
+ "LICENSE" "README*" "*-pkg.el"))
+ :source "elpaca-menu-lock-file" :id org-modern
+ :type git :protocol https :inherit t :depth
+ treeless :ref
+ "d106d02cf045cc968c09989e9c48e0f2b44c574c"))
+ (org-other-agenda :source "elpaca-menu-lock-file" :recipe
+ (:source nil :package "org-other-agenda" :id
+ org-other-agenda :host github :repo
+ "yibie/org-other-agenda" :files ("*.el")
+ :type git :protocol https :inherit t
+ :depth treeless :ref
+ "ae23b51661d8f3b90d05c19d68000524419b7a63"))
+ (org-ql :source "elpaca-menu-lock-file" :recipe
+ (:package "org-ql" :fetcher github :repo "alphapapa/org-ql"
+ :files (:defaults (:exclude "helm-org-ql.el"))
+ :source "elpaca-menu-lock-file" :id org-ql :type
+ git :protocol https :inherit t :depth treeless :ref
+ "4b8330a683c43bb4a2c64ccce8cd5a90c8b174ca"))
+ (org-super-agenda :source "elpaca-menu-lock-file" :recipe
+ (:package "org-super-agenda" :fetcher github :repo
+ "alphapapa/org-super-agenda" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el"
+ "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "elpaca-menu-lock-file" :id
+ org-super-agenda :type git :protocol
+ https :inherit t :depth treeless :ref
+ "fb20ad9c8a9705aa05d40751682beae2d094e0fe"))
+ (ov :source "elpaca-menu-lock-file" :recipe
+ (:package "ov" :fetcher github :repo "emacsorphanage/ov" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi" "*.texinfo"
+ "doc/dir" "doc/*.info" "doc/*.texi" "doc/*.texinfo"
+ "lisp/*.el" "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE" "README*"
+ "*-pkg.el"))
+ :source "elpaca-menu-lock-file" :id ov :type git
+ :protocol https :inherit t :depth treeless :ref
+ "e2971ad986b6ac441e9849031d34c56c980cf40b"))
+ (ox-gfm :source "elpaca-menu-lock-file" :recipe
+ (:package "ox-gfm" :fetcher github :repo "larstvei/ox-gfm"
+ :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id ox-gfm :type git :protocol
+ https :inherit t :depth treeless :ref
+ "4f774f13d34b3db9ea4ddb0b1edc070b1526ccbb"))
+ (ox-json :source "elpaca-menu-lock-file" :recipe
+ (:package "ox-json" :fetcher github :repo "jlumpe/ox-json"
+ :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id ox-json :type git :protocol
+ https :inherit t :depth treeless :ref
+ "0f7c63b9bbbf6c8b2547e46adc7f34289869105f"))
+ (ox-pandoc :source "elpaca-menu-lock-file" :recipe
+ (:package "ox-pandoc" :repo "emacsorphanage/ox-pandoc"
+ :fetcher github :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id ox-pandoc :wait t :type git
+ :protocol https :inherit t :depth treeless :ref
+ "1caeb56a4be26597319e7288edbc2cabada151b4"))
+ (ox-typst :source "elpaca-menu-lock-file" :recipe
+ (:package "ox-typst" :fetcher github :repo
+ "jmpunkt/ox-typst" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id ox-typst :host github :type
+ git :protocol https :inherit t :depth treeless
+ :ref "3e499609a201405a6064144792dab14e3cc19b93"))
+ (pass :source "elpaca-menu-lock-file" :recipe
+ (:package "pass" :fetcher github :repo "NicolasPetton/pass"
+ :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi" "*.texinfo"
+ "doc/dir" "doc/*.info" "doc/*.texi" "doc/*.texinfo"
+ "lisp/*.el" "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id pass :type git :protocol https
+ :inherit t :depth treeless :ref
+ "143456809fd2dbece9f241f4361085e1de0b0e75"))
+ (password-store :source "elpaca-menu-lock-file" :recipe
+ (:package "password-store" :fetcher github :repo
+ "zx2c4/password-store" :files
+ ("contrib/emacs/*.el") :source "MELPA" :id
+ password-store :type git :protocol https
+ :inherit t :depth treeless :ref
+ "3ca13cd8882cae4083c1c478858adbf2e82dd037"))
+ (password-store-otp :source "elpaca-menu-lock-file" :recipe
+ (:package "password-store-otp" :repo
+ "volrath/password-store-otp.el"
+ :fetcher github :files
+ ("*.el" "*.el.in" "dir" "*.info"
+ "*.texi" "*.texinfo" "doc/dir"
+ "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el"
+ "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id password-store-otp
+ :type git :protocol https :inherit t
+ :depth treeless :ref
+ "be3a00a981921ed1b2f78012944dc25eb5a0beca"))
+ (persp-mode :source "elpaca-menu-lock-file" :recipe
+ (:package "persp-mode" :repo "Bad-ptr/persp-mode.el"
+ :fetcher github :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el" "*-tests.el"
+ "LICENSE" "README*" "*-pkg.el"))
+ :source "MELPA" :id persp-mode :type git
+ :protocol https :inherit t :depth treeless :ref
+ "fab4bf76927445d2e431f06e74572acba81f47d5"))
+ (plz :source "elpaca-menu-lock-file" :recipe
+ (:package "plz" :repo
+ ("https://github.com/alphapapa/plz.el.git" . "plz")
+ :tar "0.9.1" :host gnu :files
+ ("*" (:exclude ".git" "LICENSE")) :source "GNU ELPA"
+ :id plz :type git :protocol https :inherit t :depth
+ treeless :ref
+ "e2d07838e3b64ee5ebe59d4c3c9011adefb7b58e"))
+ (popwin :source "elpaca-menu-lock-file" :recipe
+ (:package "popwin" :fetcher github :repo
+ "emacsorphanage/popwin" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id popwin :type git :protocol
+ https :inherit t :depth treeless :ref
+ "b67254bef763ffa5ab781460bc47d6adf6f87127"))
+ (posframe :source "elpaca-menu-lock-file" :recipe
+ (:package "posframe" :fetcher github :repo
+ "tumashu/posframe" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id posframe :type git :protocol
+ https :inherit t :depth treeless :ref
+ "74c8c56131ed866db47ae4191364b72dd4852456"))
+ (powerline :source "elpaca-menu-lock-file" :recipe
+ (:package "powerline" :fetcher github :repo
+ "milkypostman/powerline" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id powerline :type git
+ :protocol https :inherit t :depth treeless :ref
+ "c35c35bdf5ce2d992882c1f06f0f078058870d4a"))
+ (projectile :source "elpaca-menu-lock-file" :recipe
+ (:package "projectile" :fetcher github :repo
+ "bbatsov/projectile" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el" "*-tests.el"
+ "LICENSE" "README*" "*-pkg.el"))
+ :source "MELPA" :id projectile :wait t :type
+ git :protocol https :inherit t :depth treeless
+ :ref "10fa39917aa3a5c7f64aae31da49b4dfd9f6eb55"))
+ (pulsar :source "elpaca-menu-lock-file" :recipe
+ (:package "pulsar" :repo
+ ("https://github.com/protesilaos/pulsar" . "pulsar")
+ :tar "1.3.4" :host gnu :files
+ ("*" (:exclude ".git" "COPYING" "doclicense.texi"))
+ :source "GNU ELPA" :id pulsar :type git :protocol
+ https :inherit t :depth treeless :ref
+ "1849c0720c3fe5eb92f800b20351b58bce8804b6"))
+ (pyvenv :source "elpaca-menu-lock-file" :recipe
+ (:package "pyvenv" :fetcher github :repo
+ "jorgenschaefer/pyvenv" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id pyvenv :type git :protocol
+ https :inherit t :depth treeless :ref
+ "31ea715f2164dd611e7fc77b26390ef3ca93509b"))
+ (rainbow-delimiters :source "elpaca-menu-lock-file" :recipe
+ (:package "rainbow-delimiters" :fetcher github
+ :repo "Fanael/rainbow-delimiters"
+ :files
+ ("*.el" "*.el.in" "dir" "*.info"
+ "*.texi" "*.texinfo" "doc/dir"
+ "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el"
+ "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id rainbow-delimiters
+ :type git :protocol https :inherit t
+ :depth treeless :ref
+ "f40ece58df8b2f0fb6c8576b527755a552a5e763"))
+ (s :source "elpaca-menu-lock-file" :recipe
+ (:package "s" :fetcher github :repo "magnars/s.el" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi" "*.texinfo"
+ "doc/dir" "doc/*.info" "doc/*.texi" "doc/*.texinfo"
+ "lisp/*.el" "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE" "README*"
+ "*-pkg.el"))
+ :source "elpaca-menu-lock-file" :id s :type git
+ :protocol https :inherit t :depth treeless :ref
+ "d7c04b84d03481a1ed62ee13dbe595224ccbe57c"))
+ (shell-maker :source "elpaca-menu-lock-file" :recipe
+ (:package "shell-maker" :fetcher github :repo
+ "xenodium/shell-maker" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el" "*-tests.el"
+ "LICENSE" "README*" "*-pkg.el"))
+ :source "elpaca-menu-lock-file" :id
+ shell-maker :type git :protocol https :inherit
+ t :depth treeless :ref
+ "ab4f8ebaf4ef7a7db4762c5d5075baea580044ba"))
+ (shr-tag-pre-highlight :source "elpaca-menu-lock-file" :recipe
+ (:package "shr-tag-pre-highlight" :fetcher
+ github :repo
+ "xuchunyang/shr-tag-pre-highlight.el"
+ :files
+ ("*.el" "*.el.in" "dir" "*.info"
+ "*.texi" "*.texinfo" "doc/dir"
+ "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info"
+ "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el"
+ "test.el" "tests.el"
+ "*-test.el" "*-tests.el"
+ "LICENSE" "README*"
+ "*-pkg.el"))
+ :source "MELPA" :id
+ shr-tag-pre-highlight :host github
+ :type git :protocol https :inherit t
+ :depth treeless :ref
+ "02a93d48f030d71eba460bd09d091baedcad6626"))
+ (shrface :source "elpaca-menu-lock-file" :recipe
+ (:package "shrface" :fetcher github :repo
+ "chenyanming/shrface" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id shrface :type git :protocol
+ https :inherit t :depth treeless :ref
+ "6dad83387dcdd7b018c34c9ec6799926e54e66ec"))
+ (smartparens :source "elpaca-menu-lock-file" :recipe
+ (:package "smartparens" :fetcher github :repo
+ "Fuco1/smartparens" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el" "*-tests.el"
+ "LICENSE" "README*" "*-pkg.el"))
+ :source "MELPA" :id smartparens :type git
+ :protocol https :inherit t :depth treeless
+ :ref
+ "82d2cf084a19b0c2c3812e0550721f8a61996056"))
+ (sops :source "elpaca-menu-lock-file" :recipe
+ (:package "sops" :fetcher github :repo "djgoku/sops" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi" "*.texinfo"
+ "doc/dir" "doc/*.info" "doc/*.texi" "doc/*.texinfo"
+ "lisp/*.el" "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id sops :type git :host github
+ :protocol https :inherit t :depth treeless :ref
+ "95b2178a71dcbf3e69729c52cba2bc23171e059d"))
+ (textui :source "elpaca-menu-lock-file" :recipe
+ (:source nil :package "textui" :id textui :host github :repo
+ "yibie/textui" :files ("*.el") :type git :protocol
+ https :inherit t :depth treeless :ref
+ "f6b3a06b95d80e2a2122d38c13d0df94bdf4df85"))
+ (tldr :source "elpaca-menu-lock-file" :recipe
+ (:package "tldr" :fetcher github :repo "kuanyui/tldr.el" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi" "*.texinfo"
+ "doc/dir" "doc/*.info" "doc/*.texi" "doc/*.texinfo"
+ "lisp/*.el" "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id tldr :type git :protocol https
+ :inherit t :depth treeless :ref
+ "1b09d2032491d3904bd7ee9bf5ba7c7503db6593"))
+ (tmr :source "elpaca-menu-lock-file" :recipe
+ (:package "tmr" :repo "protesilaos/tmr" :tar "1.3.0" :host
+ github :files
+ ("*"
+ (:exclude ".git" "COPYING" "doclicense.texi"
+ "Makefile"))
+ :source "GNU ELPA" :id tmr :type git :protocol https
+ :inherit t :depth treeless :ref
+ "de456e7ea5bb2ace79349f17a81f9557b610f6e0"))
+ (transient :source "elpaca-menu-lock-file" :recipe
+ (:package "transient" :fetcher github :repo
+ "magit/transient" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "elpaca-menu-lock-file" :id transient
+ :wait t :type git :protocol https :inherit t
+ :depth treeless :ref
+ "416bddb37eda2f838f1e08dddde208dae271cc39"))
+ (ts :source "elpaca-menu-lock-file" :recipe
+ (:package "ts" :fetcher github :repo "alphapapa/ts.el" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi" "*.texinfo"
+ "doc/dir" "doc/*.info" "doc/*.texi" "doc/*.texinfo"
+ "lisp/*.el" "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE" "README*"
+ "*-pkg.el"))
+ :source "elpaca-menu-lock-file" :id ts :type git
+ :protocol https :inherit t :depth treeless :ref
+ "552936017cfdec89f7fc20c254ae6b37c3f22c5b"))
+ (typst-ts-mode :source "elpaca-menu-lock-file" :recipe
+ (:package "typst-ts-mode" :repo
+ ("https://codeberg.org/meow_king/typst-ts-mode"
+ . "typst-ts-mode")
+ :tar "0.12.2" :host nongnu :files
+ ("*" (:exclude ".git")) :source
+ "NonGNU ELPA" :id typst-ts-mode :type git
+ :protocol https :inherit t :depth treeless
+ :ref
+ "155bb36cff3afe701a0f6b57bd3fe5c9effaa314"))
+ (vc-fossil :source "elpaca-menu-lock-file" :recipe
+ (:package "vc-fossil" :fetcher github :repo
+ "venks1/emacs-fossil" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "elpaca-menu-lock-file" :id vc-fossil
+ :type git :protocol https :inherit t :depth
+ treeless :ref
+ "4a4a3e4df83ba2f1ea8bfd8aa7e9f9b2c1c32ca9"))
+ (vui :source "elpaca-menu-lock-file" :recipe
+ (:package "vui" :fetcher github :repo "d12frosted/vui.el" :files
+ ("*.el") :source "MELPA" :id vui :host github :ref
+ "8cecdcafeb306f654c5eed1847a0c36cb6ff370b" :type git
+ :protocol https :inherit t :depth treeless))
+ (wfnames :source "elpaca-menu-lock-file" :recipe
+ (:package "wfnames" :fetcher github :repo
+ "thierryvolpiatto/wfnames" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id wfnames :type git :protocol
+ https :inherit t :depth treeless :ref
+ "d8839fa42a24f7c781cd2d8c3f40eda31faa19be"))
+ (wgrep :source "elpaca-menu-lock-file" :recipe
+ (:package "wgrep" :fetcher github :repo
+ "mhayashi1120/Emacs-wgrep" :files ("wgrep.el")
+ :source "MELPA" :id wgrep :type git :protocol https
+ :inherit t :depth treeless :ref
+ "49f09ab9b706d2312cab1199e1eeb1bcd3f27f6f"))
+ (wgrep-helm :source "elpaca-menu-lock-file" :recipe
+ (:package "wgrep-helm" :fetcher github :repo
+ "mhayashi1120/Emacs-wgrep" :files
+ ("wgrep-helm.el") :version-regexp "helm-%v"
+ :source "MELPA" :id wgrep-helm :type git
+ :protocol https :inherit t :depth treeless :ref
+ "49f09ab9b706d2312cab1199e1eeb1bcd3f27f6f"))
+ (which-key :source "elpaca-menu-lock-file" :recipe
+ (:package "which-key" :repo "justbur/emacs-which-key"
+ :fetcher github :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id which-key :type git
+ :protocol https :inherit t :depth treeless :ref
+ "38d4308d1143b61e4004b6e7a940686784e51500"))
+ (with-editor :source "elpaca-menu-lock-file"
+ :recipe
+ (:package "with-editor" :fetcher github :repo "magit/with-editor"
+ :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi" "*.texinfo"
+ "doc/dir" "doc/*.info" "doc/*.texi" "doc/*.texinfo"
+ "lisp/*.el" "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE" "README*"
+ "*-pkg.el"))
+ :source "MELPA" :id with-editor :type git :protocol https
+ :inherit t :depth treeless :ref
+ "a1f92a26e53033ec58e1d2ce9b132da7ebae816e"))
+ (xonsh-mode :source "elpaca-menu-lock-file" :recipe
+ (:package "xonsh-mode" :repo "seanfarley/xonsh-mode"
+ :fetcher github :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el" "*-tests.el"
+ "LICENSE" "README*" "*-pkg.el"))
+ :source "MELPA" :id xonsh-mode :host github
+ :type git :protocol https :inherit t :depth
+ treeless :ref
+ "7fa581524533a9b6b770426e4445e571a69e469d"))
+ (xterm-color :source "elpaca-menu-lock-file" :recipe
+ (:package "xterm-color" :repo "atomontage/xterm-color"
+ :fetcher github :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info"
+ "doc/*.texi" "doc/*.texinfo" "lisp/*.el"
+ "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el"
+ "tests.el" "*-test.el" "*-tests.el"
+ "LICENSE" "README*" "*-pkg.el"))
+ :source "MELPA" :id xterm-color :type git
+ :protocol https :inherit t :depth treeless
+ :ref
+ "ffdad85e584dfc0857f2a1fb970f5ef0f5d31ba3"))
+ (yasnippet :source "elpaca-menu-lock-file" :recipe
+ (:package "yasnippet" :fetcher github :repo
+ "joaotavora/yasnippet" :files
+ (:defaults ("doc" "doc/*.org")) :source "MELPA"
+ :id yasnippet :wait t :type git :protocol https
+ :inherit t :depth treeless :ref
+ "c1e6ff23e9af16b856c88dfaab9d3ad7b746ad37"))
+ (yasnippet-snippets :source "elpaca-menu-lock-file" :recipe
+ (:package "yasnippet-snippets" :repo
+ "AndreaCrotti/yasnippet-snippets"
+ :fetcher github :files
+ ("*.el" "snippets" ".nosearch") :source
+ "MELPA" :id yasnippet-snippets :wait t
+ :type git :protocol https :inherit t
+ :depth treeless :ref
+ "606ee926df6839243098de6d71332a697518cb86"))
+ (zoom :source "elpaca-menu-lock-file" :recipe
+ (:package "zoom" :repo "cyrus-and/zoom" :fetcher github :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi" "*.texinfo"
+ "doc/dir" "doc/*.info" "doc/*.texi" "doc/*.texinfo"
+ "lisp/*.el" "docs/dir" "docs/*.info" "docs/*.texi"
+ "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id zoom :type git :protocol https
+ :inherit t :depth treeless :ref
+ "5e524d98c7f2b4dd6ed41d95573b29b263632eb2"))
+ (zoxide :source "elpaca-menu-lock-file" :recipe
+ (:package "zoxide" :fetcher sourcehut :repo
+ "vonfry/zoxide.el" :files
+ ("*.el" "*.el.in" "dir" "*.info" "*.texi"
+ "*.texinfo" "doc/dir" "doc/*.info" "doc/*.texi"
+ "doc/*.texinfo" "lisp/*.el" "docs/dir"
+ "docs/*.info" "docs/*.texi" "docs/*.texinfo"
+ (:exclude ".dir-locals.el" "test.el" "tests.el"
+ "*-test.el" "*-tests.el" "LICENSE"
+ "README*" "*-pkg.el"))
+ :source "MELPA" :id zoxide :type git :protocol
+ https :inherit t :depth treeless :ref
+ "0ec359c83b041c8ffe2ec0e70b16c5d218d9bfd9")))
diff --git a/init.el b/init.el
index 1703681..0870261 100644
--- a/init.el
+++ b/init.el
@@ -44,6 +44,12 @@
(require 'elpaca)
(elpaca-generate-autoloads "elpaca" repo)
(let ((load-source-file-function nil)) (load "./elpaca-autoloads"))))
+
+;; Consult exact package revisions before declaring any Elpaca orders below.
+;; `my/elpaca-write-lock-file' in config.org refreshes this file explicitly.
+(setq elpaca-lock-file
+ (expand-file-name "elpaca-lock.eld" user-emacs-directory))
+
(add-hook 'after-init-hook #'elpaca-process-queues)
(elpaca `(,@elpaca-order))
diff --git a/lisp/denote-menu.el b/lisp/denote-menu.el
index eb1c76c..7e1c8e0 100644
--- a/lisp/denote-menu.el
+++ b/lisp/denote-menu.el
@@ -8,8 +8,7 @@
;; A keyboard-first browser for Denote notes built on vui.el.
;; Lists notes from `denote-directory' (silo-aware) in a borderless table with
;; live filtering, column sorting, and one-key actions. Ships its own
-;; `denote-menu-mode' derived from `vui-mode' and registers normal-state
-;; keys with helix when it is available.
+;; `denote-menu-mode' derived from `vui-mode' with Evil normal-state keys.
;;; Code:
@@ -17,6 +16,9 @@
(require 'vui-components)
(require 'denote)
+(declare-function evil-define-key* "evil-core" (state keymap key def &rest bindings))
+(declare-function evil-set-initial-state "evil-core" (mode state))
+
(defgroup denote-menu nil
"A vui-based browser for Denote notes."
:group 'denote
@@ -34,10 +36,20 @@
"Width of the file extension column."
:type 'integer)
+(defcustom denote-menu-wide-breakpoint 78
+ "Minimum window width for the sticky table layout."
+ :type 'integer)
+
+(defcustom denote-menu-preview-width 0.38
+ "Fractional width of the optional note preview side window."
+ :type 'number)
+
(defvar-local denote-menu--dispatch nil
"Alist of actions and visible paths for the current buffer.
Set during render; consumed by the mode's interactive commands.")
+(defconst denote-menu--preview-buffer-name "*Denote Preview*")
+
(defun denote-menu--format-date (identifier)
"Format a Denote IDENTIFIER (20260315T120000) as \"2026-03-15 12:00\"."
(if (and identifier (string-match "\\`\\([0-9]\\{4\\}\\)\\([0-9]\\{2\\}\\)\\([0-9]\\{2\\}\\)T\\([0-9]\\{2\\}\\)\\([0-9]\\{2\\}\\)" identifier))
@@ -94,32 +106,11 @@ Set during render; consumed by the mode's interactive commands.")
(string< (funcall key-fn a) (funcall key-fn b))))))
(if sort-desc (nreverse sorted) sorted)))
-(defun denote-menu--format-keywords (keywords width)
- "Format KEYWORDS as [a,b] fitting WIDTH, collapsing overflow to ,+N."
- (let ((parts '())
- (used 0)
- (idx 0)
- (total (length keywords))
- (done nil))
- (while (and (< idx total) (not done))
- (let* ((kw (nth idx keywords))
- (remaining (- total idx 1))
- (suffix (if (> remaining 0)
- (+ 2 (length (number-to-string remaining)))
- 0))
- (cost (+ (if parts 1 0) (length kw))))
- (if (<= (+ used cost suffix 2) width)
- (setq parts (cons kw parts)
- used (+ used cost)
- idx (1+ idx))
- (setq done t))))
- (let ((remaining (- total idx)))
- (cond
- ((> remaining 0)
- (if parts
- (format "[%s,+%d]" (string-join (nreverse parts) ",") remaining)
- (format "[%s...]" (substring (car keywords) 0 (max 1 (- width 5))))))
- (t (format "[%s]" (string-join (nreverse parts) ",")))))))
+(defun denote-menu--keywords-label (keywords)
+ "Return a compact display label for KEYWORDS."
+ (if keywords
+ (format "#%s" (string-join keywords " #"))
+ "untagged"))
(defun denote-menu--sort-indicator (sort-key sort-desc)
"Human-readable sort indicator for SORT-KEY and SORT-DESC."
@@ -131,6 +122,74 @@ Set during render; consumed by the mode's interactive commands.")
(format "%s %s" label (if sort-desc "↓" "↑"))
label))
+(defun denote-menu--window-width ()
+ "Return the live body width of the window displaying this menu."
+ (if-let* ((window (get-buffer-window (current-buffer) t)))
+ (window-body-width window)
+ (window-width)))
+
+(defun denote-menu--wide-table (visible sort-key sort-desc width)
+ "Render VISIBLE as a sticky table using responsive WIDTH columns."
+ (let* ((date-width (if (>= width 100) denote-menu-date-width 10))
+ (keyword-width (max 14 (min denote-menu-keywords-width (/ width 4))))
+ (title-width (max 18 (- width date-width keyword-width
+ denote-menu-ext-width 5))))
+ (vui-table
+ :sticky-header t
+ :columns (list
+ (list :header (denote-menu--column-header
+ "Date" 'date sort-key sort-desc)
+ :width date-width :truncate t)
+ (list :header "Type" :width denote-menu-ext-width
+ :truncate t)
+ (list :header (denote-menu--column-header
+ "Keywords" 'keywords sort-key sort-desc)
+ :width keyword-width :truncate t)
+ (list :header (denote-menu--column-header
+ "Title" 'title sort-key sort-desc)
+ :width title-width :grow t :truncate t))
+ :rows
+ (mapcar
+ (lambda (note)
+ (list
+ (vui-text (plist-get note :date) :face 'shadow)
+ (vui-text (if-let* ((ext (plist-get note :ext)))
+ (concat "." ext)
+ "—")
+ :face 'shadow)
+ (vui-text (denote-menu--keywords-label (plist-get note :keywords))
+ :face 'font-lock-comment-face)
+ (vui-button (plist-get note :title)
+ :key (plist-get note :path)
+ :no-decoration t :help-echo nil
+ :on-click (lambda ()
+ (find-file (plist-get note :path))))))
+ visible))))
+
+(defun denote-menu--compact-card (note width)
+ "Render NOTE as a two-line card fitting WIDTH."
+ (let ((path (plist-get note :path)))
+ (vui-vstack
+ :spacing 0
+ (vui-flex
+ :width 'window :justify :space-between
+ (vui-flex-item
+ :grow 1
+ (lambda (available)
+ (vui-button (plist-get note :title)
+ :key path :no-decoration t :help-echo nil
+ :max-width (max 8 available)
+ :face '(:inherit bold)
+ :on-click (lambda () (find-file path)))))
+ (vui-text (format "%s .%s"
+ (substring (plist-get note :date) 0 10)
+ (or (plist-get note :ext) ""))
+ :face 'shadow))
+ (vui-box
+ (vui-text (denote-menu--keywords-label (plist-get note :keywords))
+ :face 'font-lock-comment-face)
+ :width (max 12 (- width 2)) :padding-left 2))))
+
(vui-defcomponent denote-menu-app ()
:state ((query "")
(sort-key 'date)
@@ -158,96 +217,117 @@ Set during render; consumed by the mode's interactive commands.")
(_ 'date))))
:toggle-direction (vui-with-async-context
(vui-set-state :sort-desc (not sort-desc)))))
- (vui-vstack
+ (let ((width (denote-menu--window-width)))
+ (vui-vstack
:spacing 1
- (vui-hstack
- :spacing 2
- (vui-heading-2 "Denotes")
- (vui-text (format "▸ %s" silo) :face '(:inherit success :weight bold))
+ (vui-flex
+ :width 'window :justify :space-between
+ (vui-hstack
+ :spacing 2
+ (vui-heading-2 "Denotes")
+ (vui-text (format "▸ %s" silo)
+ :face '(:inherit success :weight bold)))
(vui-text (format "%d%s notes"
(length visible)
- (if filtering
- (format " of %d" (length notes))
- ""))
- :face 'shadow)
- (vui-button "new" :on-click (lambda ()
- (let ((file (call-interactively #'denote)))
- (when file (funcall (plist-get denote-menu--dispatch :refresh)))))
- :help-echo "Create a new note")
- (vui-button "export" :on-click #'denote-menu-export-to-dired
- :help-echo "Open the visible notes in Dired"))
- (vui-text "RET open · / filter · s/S sort · e export · n new · q quit"
- :face 'shadow)
- (vui-hstack
- :spacing 2
+ (if filtering (format " / %d" (length notes)) ""))
+ :face 'shadow))
+ (vui-flex
+ :width 'window :justify :space-between
(vui-hstack
:spacing 1
- (vui-text "filter:" :face 'shadow)
- (if filtering
- (vui-text query :face '(:inherit warning :weight bold))
- (vui-text "none (press / to filter)" :face 'shadow)))
- (vui-text (format "sort: %s" (denote-menu--sort-indicator sort-key sort-desc))
- :face 'shadow))
+ (vui-text "filter" :face 'shadow)
+ (vui-text (if filtering query "all notes")
+ :face (if filtering 'warning 'shadow)))
+ (vui-hstack
+ :spacing 1
+ (vui-text (format "sort %s"
+ (denote-menu--sort-indicator sort-key sort-desc))
+ :face 'shadow)
+ (vui-button "new" :on-click #'denote-menu-new-note)
+ (vui-button "dired" :on-click #'denote-menu-export-to-dired)))
(if (null visible)
(vui-vstack
:spacing 1
(vui-box (vui-text "∅" :face '(:inherit shadow :height 2.0))
- :width fill-column :align :center)
+ :width width :align :center)
(vui-box (vui-text (if filtering
(format "No notes matching \"%s\"" query)
"No notes here yet")
:face 'shadow)
- :width fill-column :align :center)
+ :width width :align :center)
(vui-box (vui-button "Create the first one"
:on-click (lambda () (call-interactively #'denote)))
- :width fill-column :align :center))
- (let* ((win (or (get-buffer-window (current-buffer)) (selected-window)))
- (title-width (max 10 (- (window-width win)
- denote-menu-date-width
- denote-menu-ext-width
- denote-menu-keywords-width
- 4))))
- (vui-table
- :columns (list (list :header (denote-menu--column-header "Date" 'date sort-key sort-desc)
- :width denote-menu-date-width)
- (list :header ""
- :width denote-menu-ext-width)
- (list :header (denote-menu--column-header "Keywords" 'keywords sort-key sort-desc)
- :width denote-menu-keywords-width :truncate t)
- (list :header (denote-menu--column-header "Title" 'title sort-key sort-desc)
- :width title-width :truncate t))
- :rows (mapcar
- (lambda (note)
- (list (vui-text (plist-get note :date) :face 'shadow)
- (vui-text (if-let* ((ext (plist-get note :ext)))
- (concat "." ext)
- "")
- :face 'shadow)
- (if (plist-get note :keywords)
- (vui-text (denote-menu--format-keywords
- (plist-get note :keywords)
- denote-menu-keywords-width)
- :face 'font-lock-comment-face)
- (vui-text "·" :face 'shadow))
- (vui-button (plist-get note :title)
- :key (plist-get note :path)
- :no-decoration t
- :help-echo nil
- :on-click (lambda () (find-file (plist-get note :path))))))
- visible)))))))
+ :width width :align :center))
+ (if (>= width denote-menu-wide-breakpoint)
+ (denote-menu--wide-table visible sort-key sort-desc width)
+ (vui-list visible
+ (lambda (note) (denote-menu--compact-card note width))
+ (lambda (note) (plist-get note :path))
+ :spacing 1)))
+ (vui-flex
+ :width 'window :justify :space-between
+ (vui-text "j/k move · RET/l open · / filter · p preview"
+ :face 'shadow)
+ (vui-text "s/S sort · n new · g refresh · ? help · q close"
+ :face 'shadow))))))
(defun denote-menu--action (name)
"Return the buffer's dispatch action NAME, or signal an error."
(or (plist-get denote-menu--dispatch name)
(user-error "Denote-explorer is not ready in this buffer")))
+(defun denote-menu--path-on-line ()
+ "Return the note path keyed on the current visual row."
+ (save-excursion
+ (beginning-of-line)
+ (let ((end (line-end-position)) path)
+ (while (and (not path) (< (point) end))
+ (setq path (vui-key-at))
+ (forward-char 1))
+ path)))
+
(defun denote-menu-open-at-point ()
"Open the note at point."
(interactive)
- (if-let* ((path (vui-key-at)))
+ (if-let* ((path (or (vui-key-at) (denote-menu--path-on-line))))
(find-file path)
(user-error "No note at point")))
+(defun denote-menu-preview-at-point ()
+ "Show the note at point in an opt-in right side window."
+ (interactive)
+ (if-let* ((path (or (vui-key-at) (denote-menu--path-on-line))))
+ (let ((buffer (get-buffer-create denote-menu--preview-buffer-name)))
+ (with-current-buffer buffer
+ (let ((inhibit-read-only t))
+ (erase-buffer)
+ (insert-file-contents path)
+ (org-mode)
+ (setq-local buffer-file-name path)
+ (setq-local mode-line-format nil)
+ (goto-char (point-min))
+ (view-mode 1)))
+ (display-buffer-in-side-window
+ buffer `((side . right)
+ (slot . 1)
+ (window-width . ,denote-menu-preview-width))))
+ (user-error "No note at point")))
+
+(defun denote-menu-help ()
+ "Show the Denote Menu key summary."
+ (interactive)
+ (message "Denote Menu: j/k move, TAB/S-TAB widgets, RET/l open, / filter, p preview, n new, s/S sort, e Dired, g refresh, h/q close"))
+
+(defun denote-menu-quit ()
+ "Close the menu and its optional preview."
+ (interactive)
+ (when-let* ((preview (get-buffer denote-menu--preview-buffer-name))
+ (window (get-buffer-window preview t)))
+ (delete-window window))
+ (when-let* ((preview (get-buffer denote-menu--preview-buffer-name)))
+ (kill-buffer preview))
+ (vui-quit))
+
(defun denote-menu-filter ()
"Prompt for a filter query."
(interactive)
@@ -299,14 +379,25 @@ for `denote-directory' and silently drop the file-list filter."
(defvar denote-menu-mode-map
(let ((map (make-sparse-keymap)))
+ (set-keymap-parent map vui-mode-map)
(define-key map (kbd "RET") #'denote-menu-open-at-point)
+ (define-key map (kbd "l") #'denote-menu-open-at-point)
+ (define-key map (kbd "j") #'next-line)
+ (define-key map (kbd "k") #'previous-line)
+ (define-key map (kbd "TAB") #'vui-forward)
+ (define-key map (kbd "<backtab>") #'vui-backward)
+ (define-key map (kbd "p") #'denote-menu-preview-at-point)
(define-key map (kbd "n") #'denote-menu-new-note)
(define-key map (kbd "e") #'denote-menu-export-to-dired)
(define-key map (kbd "r") #'denote-menu-refresh)
+ (define-key map (kbd "g") #'denote-menu-refresh)
(define-key map (kbd "/") #'denote-menu-filter)
(define-key map (kbd "c") #'denote-menu-clear-filter)
(define-key map (kbd "s") #'denote-menu-cycle-sort)
(define-key map (kbd "S") #'denote-menu-toggle-sort-direction)
+ (define-key map (kbd "?") #'denote-menu-help)
+ (define-key map (kbd "h") #'denote-menu-quit)
+ (define-key map (kbd "q") #'denote-menu-quit)
map)
"Keymap for `denote-menu-mode'.")
@@ -319,18 +410,20 @@ across re-renders.
:group 'denote-menu
(hl-line-mode 1))
+(with-eval-after-load 'evil
+ (evil-set-initial-state 'denote-menu-mode 'normal)
+ (evil-define-key* '(normal motion) denote-menu-mode-map
+ (kbd "j") #'next-line
+ (kbd "k") #'previous-line
+ (kbd "RET") #'denote-menu-open-at-point
+ (kbd "l") #'denote-menu-open-at-point
+ (kbd "h") #'denote-menu-quit
+ (kbd "q") #'denote-menu-quit
+ (kbd "g") #'denote-menu-refresh
+ (kbd "/") #'denote-menu-filter
+ (kbd "p") #'denote-menu-preview-at-point
+ (kbd "?") #'denote-menu-help))
-(with-eval-after-load 'helix
- (helix-define-key 'normal "RET" #'denote-menu-open-at-point 'denote-menu-mode)
- (helix-define-key 'normal "o" #'denote-menu-open-at-point 'denote-menu-mode)
- (helix-define-key 'normal "/" #'denote-menu-filter 'denote-menu-mode)
- (helix-define-key 'normal "c" #'denote-menu-clear-filter 'denote-menu-mode)
- (helix-define-key 'normal "s" #'denote-menu-cycle-sort 'denote-menu-mode)
- (helix-define-key 'normal "S" #'denote-menu-toggle-sort-direction 'denote-menu-mode)
- (helix-define-key 'normal "e" #'denote-menu-export-to-dired 'denote-menu-mode)
- (helix-define-key 'normal "n" #'denote-menu-new-note 'denote-menu-mode)
- (helix-define-key 'normal "r" #'denote-menu-refresh 'denote-menu-mode)
- (helix-define-key 'normal "q" #'vui-quit 'denote-menu-mode))
;;;###autoload
(defun denote-menu ()
diff --git a/lisp/garden-connect.el b/lisp/garden-connect.el
new file mode 100644
index 0000000..6c6ffee
--- /dev/null
+++ b/lisp/garden-connect.el
@@ -0,0 +1,365 @@
+;;; garden-connect.el --- unlinked-reference connector -*- lexical-binding: t; -*-
+
+(require 'garden-core)
+(require 'garden-icons)
+(require 'garden-fleet)
+(require 'vui)
+(require 'vui-components)
+
+(declare-function evil-define-key* "evil-core" (state keymap key def &rest bindings))
+(declare-function evil-set-initial-state "evil-core" (mode state))
+
+(defcustom garden-connect-min-title-length 3 "Minimum title length for a note to be scanned for mentions." :type 'integer :group 'garden)
+(defcustom garden-connect-max-results 80 "Maximum number of unlinked references shown at once." :type 'integer :group 'garden)
+(defcustom garden-connect-max-line 1500 "Maximum line length in characters before a line is skipped." :type 'integer :group 'garden)
+
+(defvar-local garden--ref-target nil
+ "Cons of (ID . TITLE) of the note shown in the connect buffer.")
+
+(defvar-local garden-connect--dispatch nil
+ "Async component actions exposed to `garden-connect-mode' commands.")
+
+(defun garden--long-line-p ()
+ "Whether the current line exceeds `garden-connect-max-line'."
+ (> (- (line-end-position) (line-beginning-position)) garden-connect-max-line))
+
+(defun garden--in-link-p (pos)
+ "Whether POS is inside an Org bracket link."
+ (save-excursion
+ (goto-char pos)
+ (let ((bol (line-beginning-position)) (depth 0))
+ (goto-char bol)
+ (while (< (point) pos)
+ (cond ((looking-at "\\[\\[") (setq depth (1+ depth)) (forward-char 2))
+ ((looking-at "\\]\\]") (when (> depth 0) (setq depth (1- depth))) (forward-char 2))
+ (t (forward-char 1))))
+ (> depth 0))))
+
+(defun garden--in-block-p (pos)
+ "Whether POS is inside an Org src or example block."
+ (save-excursion
+ (save-match-data
+ (goto-char pos)
+ (let ((case-fold-search t))
+ (and (re-search-backward "^[ \t]*#\\+\\(begin\\|end\\)_\\(src\\|example\\)\\>" nil t)
+ (string-equal (downcase (match-string 1)) "begin"))))))
+
+(defun garden--denote-link-text (id matched)
+ "Return a denote link to ID with MATCHED as the description."
+ (format "[[denote:%s][%s]]" id matched))
+
+(defun garden--note-list ()
+ "Return all garden notes as a list."
+ (let (acc) (maphash (lambda (_ n) (push n acc)) (garden-notes)) acc))
+
+(defun garden-unlinked-references-for (id title)
+ "Find notes that mention TITLE without linking to the note ID.
+Return a list of plists with the first mention found per note."
+ (let ((re (concat "\\_<" (regexp-quote title) "\\_>"))
+ (case-fold-search t)
+ (results '()))
+ (dolist (note (garden--note-list))
+ (unless (or (equal (garden-note-id note) id)
+ (member id (garden-note-links note)))
+ (with-temp-buffer
+ (insert-file-contents (garden-note-file note))
+ (goto-char (point-min))
+ (catch 'found
+ (while (re-search-forward re nil t)
+ (let ((mb (match-beginning 0)))
+ (if (garden--long-line-p)
+ (goto-char (line-end-position))
+ (save-excursion
+ (goto-char mb)
+ (beginning-of-line)
+ (when (and (not (looking-at-p "[ \t]*\\(#\\+\\|:[A-Za-z]\\)"))
+ (not (garden--in-link-p mb))
+ (not (garden--in-block-p mb)))
+ (push (list :id (garden-note-id note)
+ :title (garden-note-title note)
+ :file (garden-note-file note)
+ :beg mb
+ :line (line-number-at-pos mb)
+ :context (string-trim (buffer-substring
+ (line-beginning-position)
+ (line-end-position))))
+ results)
+ (throw 'found t))))))))))
+ (nreverse results)))
+
+(defun garden-linkify (file beg id title)
+ "Replace the mention of TITLE at BEG in FILE with a link to ID.
+Save FILE afterwards."
+ (with-current-buffer (find-file-noselect file)
+ (save-excursion
+ (goto-char beg)
+ (let ((case-fold-search t)
+ (re (concat "\\_<" (regexp-quote title) "\\_>")))
+ (when (looking-at re)
+ (let ((matched (match-string 0)))
+ (unless (save-match-data (or (garden--in-link-p beg) (garden--in-block-p beg)))
+ (replace-match (garden--denote-link-text id matched) t t))))))
+ (save-buffer)))
+
+(defun garden--read-note ()
+ "Prompt for a garden note and return its id."
+ (let* ((table (let (acc)
+ (maphash (lambda (_id n) (push (cons (garden-note-title n) n) acc)) (garden-notes))
+ acc))
+ (annotate (lambda (title)
+ (when-let* ((note (cdr (assoc title table))))
+ (garden-note-annotation note))))
+ (choice (completing-read "Find unlinked references to: "
+ (garden-completion-table (mapcar #'car table) annotate) nil t)))
+ (garden-note-id (cdr (assoc choice table)))))
+
+(defun garden--current-note-id ()
+ "Return the note id of the current buffer's file, or nil."
+ (and (buffer-file-name)
+ (let ((id (garden--file-id (buffer-file-name))))
+ (and id (garden-note id) id))))
+
+(defun garden-connect--window-width ()
+ "Return the body width of the window displaying the connect buffer."
+ (if-let* ((window (get-buffer-window (current-buffer) t)))
+ (window-body-width window)
+ (window-width)))
+
+(defun garden-connect--scan (id title)
+ "Return the capped unlinked-reference scan for ID and TITLE."
+ (seq-take (garden-unlinked-references-for id title)
+ garden-connect-max-results))
+
+(defun garden--ref-card (ref id title on-linked)
+ "Render REF as a narrow card for target ID and TITLE.
+Call ON-LINKED after successfully adding its link."
+ (let ((file (plist-get ref :file))
+ (line (plist-get ref :line))
+ (beg (plist-get ref :beg)))
+ (vui-vstack :spacing 0
+ (vui-flex :width 'window :justify :space-between
+ (vui-flex-item :grow 1
+ (lambda (width)
+ (vui-button (plist-get ref :title)
+ :key file :no-decoration t :max-width width
+ :face '(:inherit bold)
+ :on-click (lambda ()
+ (find-file file)
+ (goto-char (point-min))
+ (forward-line (1- line))))))
+ (vui-muted (format "line %d" line))
+ (vui-button (concat (garden-icon 'link) " link")
+ :on-click (lambda ()
+ (garden-linkify file beg id title)
+ (garden-refresh)
+ (garden-fleet--refresh)
+ (funcall on-linked))))
+ (vui-box (vui-text (plist-get ref :context) :face 'shadow)
+ :width (max 12 (- (garden-connect--window-width) 2))
+ :padding-left 2))))
+
+(defun garden-connect--table (refs id title on-linked width)
+ "Render REFS as a sticky review table for ID and TITLE at WIDTH."
+ (let ((context-width (max 22 (- width 34))))
+ (vui-table
+ :sticky-header t
+ :columns `((:header "Note" :width 20 :grow t :truncate t)
+ (:header "Line" :width 6 :align :right)
+ (:header "Mention" :width ,context-width :grow t :truncate t)
+ (:header "" :width 7))
+ :rows
+ (mapcar
+ (lambda (ref)
+ (let ((file (plist-get ref :file))
+ (line (plist-get ref :line))
+ (beg (plist-get ref :beg)))
+ (list
+ (vui-button (plist-get ref :title)
+ :key file :no-decoration t :help-echo file
+ :on-click (lambda ()
+ (find-file file)
+ (goto-char (point-min))
+ (forward-line (1- line))))
+ (vui-text (number-to-string line) :face 'shadow)
+ (vui-text (plist-get ref :context) :face 'shadow)
+ (vui-button "link" :face 'success
+ :on-click (lambda ()
+ (garden-linkify file beg id title)
+ (garden-refresh)
+ (garden-fleet--refresh)
+ (funcall on-linked))))))
+ refs))))
+
+(defun garden--linkify-note (file id title)
+ "Link the first valid mention of TITLE in FILE to ID.
+Return the number of replacements made."
+ (let ((re (concat "\\_<" (regexp-quote title) "\\_>"))
+ (case-fold-search t)
+ (n 0))
+ (with-current-buffer (find-file-noselect file)
+ (save-excursion
+ (goto-char (point-min))
+ (catch 'done
+ (while (re-search-forward re nil t)
+ (let ((mb (match-beginning 0))
+ (matched (match-string 0)))
+ (cond
+ ((garden--long-line-p) (goto-char (line-end-position)))
+ ((save-match-data
+ (save-excursion
+ (goto-char mb)
+ (or (progn (beginning-of-line)
+ (looking-at-p "[ \t]*\\(#\\+\\|:[A-Za-z]\\)"))
+ (garden--in-link-p mb)
+ (garden--in-block-p mb)))))
+ (t (replace-match (garden--denote-link-text id matched) t t)
+ (setq n 1)
+ (throw 'done t)))))))
+ (when (> n 0) (save-buffer)))
+ n))
+
+(defun garden-connect-all (&optional id title)
+ "Link every note that mentions the target note, after confirming.
+When called interactively, ID and TITLE default to the note shown in
+the current connect buffer."
+ (interactive)
+ (let* ((id (or id (car garden--ref-target)))
+ (title (or title (cdr garden--ref-target)))
+ (refs (garden-unlinked-references-for id title))
+ (files (delete-dups (mapcar (lambda (r) (plist-get r :file)) refs)))
+ (n 0))
+ (when (and refs
+ (yes-or-no-p (format "Connect all %d note(s) that mention “%s”? " (length refs) title)))
+ (dolist (f files) (setq n (+ n (garden--linkify-note f id title))))
+ (garden-refresh)
+ (message "garden: connected %d note(s) to %s" n title))))
+
+(vui-defcomponent garden-references (id title)
+ :state ((refs (garden-connect--scan id title))
+ (busy nil)
+ (status nil))
+ :render
+ (let* ((width (garden-connect--window-width))
+ (rescan (vui-async-callback ()
+ (vui-batch
+ (vui-set-state :busy t)
+ (vui-set-state :status nil))
+ (garden-refresh)
+ (vui-batch
+ (vui-set-state :refs (garden-connect--scan id title))
+ (vui-set-state :busy nil))))
+ (linked (vui-async-callback ()
+ (vui-set-state :refs (garden-connect--scan id title)))))
+ (setq garden-connect--dispatch (list :refresh rescan))
+ (vui-vstack
+ :spacing 1 :indent 2
+ (vui-flex
+ :width 'window :justify :space-between
+ (vui-heading-2 (format "%s connect" (garden-icon 'link)))
+ (vui-text (format "%d / %d max" (length refs) garden-connect-max-results)
+ :face 'shadow))
+ (vui-text title :face '(:inherit outline-2 :weight bold))
+ (vui-muted "Review first mentions one by one; bulk linking always asks for confirmation.")
+ (vui-hstack
+ :spacing 2
+ (when refs
+ (vui-button (concat (garden-icon 'link) " link all")
+ :face 'warning
+ :on-click (lambda ()
+ (garden-connect-all id title)
+ (funcall rescan))))
+ (vui-button (concat (garden-icon 'refresh) " rescan")
+ :on-click rescan)
+ (vui-button (concat (garden-icon 'star) " another note")
+ :on-click #'garden-connect-pick))
+ (cond
+ (busy (vui-warning "Scanning the garden…"))
+ ((null refs)
+ (vui-vstack
+ :spacing 1
+ (vui-box (vui-text (garden-icon 'sparkle)
+ :face '(:inherit success :height 2.0))
+ :width width :align :center)
+ (vui-box (vui-success "All linked up") :width width :align :center)
+ (vui-box (vui-muted "Every mention already points back to this note")
+ :width width :align :center)))
+ ((>= width 88)
+ (garden-connect--table refs id title linked width))
+ (t
+ (vui-list refs
+ (lambda (ref) (garden--ref-card ref id title linked))
+ (lambda (ref) (plist-get ref :file))
+ :spacing 1)))
+ (when status (vui-text (car status) :face (cdr status)))
+ (vui-flex :width 'window :justify :space-between
+ (vui-muted "j/k move · TAB/S-TAB elements · RET/l activate")
+ (vui-muted "g rescan · ? help · h/q close")))))
+
+(defun garden-connect-refresh ()
+ "Rescan the current connect target."
+ (interactive)
+ (if-let* ((fn (plist-get garden-connect--dispatch :refresh)))
+ (funcall fn)
+ (user-error "Connect view is not ready")))
+
+(defun garden-connect-help ()
+ "Show keyboard help for the connect review screen."
+ (interactive)
+ (message "Garden Connect: j/k move, TAB/S-TAB elements, RET/l activate, g rescan, h/q close"))
+
+(defvar garden-connect-mode-map
+ (let ((map (make-sparse-keymap)))
+ (set-keymap-parent map vui-mode-map)
+ (keymap-set map "j" #'next-line)
+ (keymap-set map "k" #'previous-line)
+ (keymap-set map "TAB" #'vui-forward)
+ (keymap-set map "<backtab>" #'vui-backward)
+ (keymap-set map "RET" #'vui-activate)
+ (keymap-set map "l" #'vui-activate)
+ (keymap-set map "g" #'garden-connect-refresh)
+ (keymap-set map "?" #'garden-connect-help)
+ (keymap-set map "h" #'vui-quit)
+ (keymap-set map "q" #'vui-quit)
+ map))
+
+(define-derived-mode garden-connect-mode vui-mode "Garden-Connect"
+ "Review unlinked Garden references."
+ (hl-line-mode 1))
+
+(with-eval-after-load 'evil
+ (evil-set-initial-state 'garden-connect-mode 'normal)
+ (evil-define-key* '(normal motion) garden-connect-mode-map
+ (kbd "j") #'next-line (kbd "k") #'previous-line
+ (kbd "RET") #'vui-activate (kbd "l") #'vui-activate
+ (kbd "g") #'garden-connect-refresh (kbd "?") #'garden-connect-help
+ (kbd "h") #'vui-quit (kbd "q") #'vui-quit))
+
+(defun garden--connect-show (id)
+ "Open the connect buffer for the note with ID."
+ (let ((note (garden-note id)))
+ (unless note (user-error "Not a garden note"))
+ (let ((title (garden-note-title note)))
+ (let ((buffer (get-buffer-create "*garden connect*")))
+ (with-current-buffer buffer
+ (garden-connect-mode)
+ (setq-local garden--ref-target (cons id title))
+ (vui-mount (vui-component 'garden-references :id id :title title) buffer)
+ (vui-rerender-on-resize))
+ (switch-to-buffer buffer)))))
+
+;;;###autoload
+(defun garden-connect ()
+ "Show unlinked references to the current note, or prompt for one."
+ (interactive)
+ (garden-build)
+ (garden--connect-show (or (garden--current-note-id) (garden--read-note))))
+
+;;;###autoload
+(defun garden-connect-pick ()
+ "Prompt for a note and show its unlinked references."
+ (interactive)
+ (garden-build)
+ (garden--connect-show (garden--read-note)))
+
+(provide 'garden-connect)
+;;; garden-connect.el ends here
diff --git a/lisp/garden-core.el b/lisp/garden-core.el
new file mode 100644
index 0000000..79fc3a8
--- /dev/null
+++ b/lisp/garden-core.el
@@ -0,0 +1,372 @@
+;;; garden-core.el --- garden model -*- lexical-binding: t; -*-
+
+(require 'cl-lib)
+(require 'seq)
+(require 'subr-x)
+
+(defvar denote-directory)
+(declare-function org-update-all-dblocks "org")
+
+(defgroup garden nil "A cute Denote-based digital garden." :group 'denote)
+
+(defcustom garden-directory (expand-file-name "~/garden/")
+ "Directory holding the garden's Denote notes."
+ :type 'directory :group 'garden)
+
+(defcustom garden-data-file (expand-file-name ".garden/tags.eld" garden-directory)
+ "File persisting the topic/meta overrides for keywords."
+ :type 'file :group 'garden)
+
+(defcustom garden-meta-frequency 0.30
+ "Fraction of notes a keyword must reach to count as a meta tag."
+ :type 'number :group 'garden)
+
+(defcustom garden-suggest-excluded-tags '("fleet" "capture")
+ "Status tags never offered by `garden-suggest-tags'."
+ :type '(repeat string) :group 'garden)
+
+(cl-defstruct garden-note
+ "A garden note parsed from a Denote file." id title keywords file links)
+
+(defvar garden--notes nil
+ "Hash table of note id to `garden-note', or nil before the first build.")
+(defvar garden--backlinks nil
+ "Hash table of note id to the ids linking to it.")
+(defvar garden--keyword-counts nil
+ "Cached hash table of keyword to number of notes carrying it.")
+(defvar garden--meta-overrides nil
+ "Alist of keyword to explicit meta flag, loaded from `garden-data-file'.")
+(defvar garden--suggest-cache (make-hash-table :test 'equal)
+ "Cache of file to (MTIME . SUGGESTIONS) for `garden-suggest-tags'.")
+
+(defconst garden--id-regexp "\\`\\([0-9]\\{8\\}T[0-9]\\{6\\}\\)"
+ "Regexp matching the Denote identifier at the start of a file name.")
+(defconst garden--link-regexp "\\[\\[denote:\\([0-9]\\{8\\}T[0-9]\\{6\\}\\)"
+ "Regexp matching the target id of a denote link.")
+
+(defun garden--note-files ()
+ "Return all Denote-named org files in `garden-directory'."
+ (directory-files garden-directory t "\\`[0-9]\\{8\\}T[0-9]\\{6\\}.*\\.org\\'"))
+
+(defun garden--file-id (file)
+ "Return the Denote identifier of FILE, or nil."
+ (let ((base (file-name-nondirectory file)))
+ (when (string-match garden--id-regexp base) (match-string 1 base))))
+
+(defun garden--file-keywords (file)
+ "Return the keywords encoded in FILE's name, minus provenance tags."
+ (let ((base (file-name-nondirectory file)))
+ (when (string-match "__\\([a-zA-Z0-9_-]+\\)\\.org\\'" base)
+ (seq-remove (lambda (k) (member k '("wiki" "")))
+ (split-string (match-string 1 base) "_" t)))))
+
+(defun garden--file-title (file)
+ "Return the #+title of FILE, falling back to its base name."
+ (with-temp-buffer
+ (insert-file-contents file nil 0 2000)
+ (goto-char (point-min))
+ (if (re-search-forward "^#\\+title:[ \t]*\\(.*\\)$" nil t)
+ (string-trim (match-string 1))
+ (file-name-base file))))
+
+(defun garden--file-links (file id)
+ "Return the ids FILE links to, excluding ID itself."
+ (let ((links '()))
+ (with-temp-buffer
+ (insert-file-contents file)
+ (goto-char (point-min))
+ (while (re-search-forward garden--link-regexp nil t)
+ (let ((target (match-string 1)))
+ (unless (equal target id) (push target links)))))
+ (delete-dups (nreverse links))))
+
+(defun garden-build ()
+ "Rebuild the in-memory note index from `garden-directory'."
+ (let ((notes (make-hash-table :test 'equal)))
+ (dolist (file (garden--note-files))
+ (let ((id (garden--file-id file)))
+ (when id
+ (puthash id (make-garden-note :id id
+ :title (garden--file-title file)
+ :keywords (garden--file-keywords file)
+ :file file
+ :links (garden--file-links file id))
+ notes))))
+ (let ((backlinks (make-hash-table :test 'equal)))
+ (maphash (lambda (id note)
+ (dolist (target (garden-note-links note))
+ (when (gethash target notes)
+ (push id (gethash target backlinks)))))
+ notes)
+ (setq garden--notes notes
+ garden--backlinks backlinks
+ garden--keyword-counts nil))
+ (clrhash garden--suggest-cache)
+ (garden--load-overrides)
+ garden--notes))
+
+(defun garden-notes ()
+ "Return the note index, building it on first use."
+ (or garden--notes (garden-build)))
+
+(defun garden-note (id)
+ "Return the `garden-note' with ID, or nil."
+ (gethash id (garden-notes)))
+
+(defun garden-backlinks (id)
+ "Return the ids of notes linking to ID."
+ (garden-notes)
+ (gethash id garden--backlinks))
+
+(defun garden-internal-links (note)
+ "Return NOTE's outgoing links that resolve to garden notes."
+ (seq-filter #'garden-note (garden-note-links note)))
+
+(defun garden-degree (id)
+ "Return the total number of links in and out of note ID."
+ (let ((note (garden-note id)))
+ (+ (length (garden-internal-links note))
+ (length (garden-backlinks id)))))
+
+(defun garden-all-ids ()
+ "Return the ids of all notes in the garden."
+ (let (ids) (maphash (lambda (id _n) (push id ids)) (garden-notes)) ids))
+
+(defun garden-note-count ()
+ "Return the number of notes in the garden."
+ (hash-table-count (garden-notes)))
+
+(defun garden-link-count ()
+ "Return the number of internal links across all notes."
+ (let ((s 0))
+ (maphash (lambda (_id note) (setq s (+ s (length (garden-internal-links note))))) (garden-notes))
+ s))
+
+(defun garden-orphans ()
+ "Return the ids of notes with no links in or out."
+ (seq-filter (lambda (id) (zerop (garden-degree id))) (garden-all-ids)))
+
+(defun garden-hubs (&optional n)
+ "Return the N (default 6) best-connected note ids, descending."
+ (seq-take (seq-sort-by #'garden-degree #'>
+ (seq-filter (lambda (id) (> (garden-degree id) 0)) (garden-all-ids)))
+ (or n 6)))
+
+(defun garden-recent (&optional n)
+ "Return the N (default 6) most recent note ids."
+ (seq-take (seq-sort #'string> (garden-all-ids)) (or n 6)))
+
+(defun garden-keyword-counts ()
+ "Return a hash table of keyword to note count, cached per build."
+ (or garden--keyword-counts
+ (let ((counts (make-hash-table :test 'equal)))
+ (maphash (lambda (_id note)
+ (dolist (k (garden-note-keywords note))
+ (puthash k (1+ (gethash k counts 0)) counts)))
+ (garden-notes))
+ (setq garden--keyword-counts counts))))
+
+(defun garden-keywords-sorted ()
+ "Return an alist of (KEYWORD . COUNT) sorted by descending count."
+ (let (alist)
+ (maphash (lambda (k c) (push (cons k c) alist)) (garden-keyword-counts))
+ (seq-sort-by #'cdr #'> alist)))
+
+(defun garden-meta-tag-p (keyword)
+ "Return non-nil when KEYWORD is a meta tag rather than a topic.
+Overrides in `garden--meta-overrides' win; otherwise a keyword is
+meta when it appears in at least `garden-meta-frequency' of notes."
+ (let ((ov (assoc keyword garden--meta-overrides)))
+ (if ov (cdr ov)
+ (>= (/ (float (gethash keyword (garden-keyword-counts) 0))
+ (max 1 (garden-note-count)))
+ garden-meta-frequency))))
+
+(defun garden-topic-keywords ()
+ "Return the (KEYWORD . COUNT) alist of topic keywords."
+ (seq-remove (lambda (kc) (garden-meta-tag-p (car kc))) (garden-keywords-sorted)))
+
+(defun garden-meta-keywords ()
+ "Return the (KEYWORD . COUNT) alist of meta keywords."
+ (seq-filter (lambda (kc) (garden-meta-tag-p (car kc))) (garden-keywords-sorted)))
+
+(defun garden-notes-with-keyword (keyword)
+ "Return the notes tagged with KEYWORD, sorted by title."
+ (let (acc)
+ (maphash (lambda (_id note)
+ (when (member keyword (garden-note-keywords note)) (push note acc)))
+ (garden-notes))
+ (seq-sort-by #'garden-note-title #'string< acc)))
+
+(defun garden--suggest-candidates (current)
+ "Return suggestible keywords: all topics not in CURRENT."
+ (seq-remove (lambda (k)
+ (or (member k current)
+ (member k garden-suggest-excluded-tags)
+ (garden-meta-tag-p k)))
+ (mapcar #'car (garden-keywords-sorted))))
+
+(defun garden--count-matches (regexp text cap)
+ "Count occurrences of REGEXP in TEXT, stopping at CAP."
+ (let ((start 0) (hits 0))
+ (while (and (< hits cap) (string-match regexp text start))
+ (setq hits (1+ hits))
+ (setq start (match-end 0)))
+ hits))
+
+(defun garden--cooccurrence-counts (topics id)
+ "Tally keywords of notes other than ID that share a tag in TOPICS."
+ (let ((counts (make-hash-table :test 'equal)))
+ (when topics
+ (maphash (lambda (other-id note)
+ (unless (equal other-id id)
+ (let ((kws (garden-note-keywords note)))
+ (when (seq-intersection topics kws)
+ (dolist (k kws)
+ (puthash k (1+ (gethash k counts 0)) counts))))))
+ (garden-notes)))
+ counts))
+
+(defun garden--suggest-score (keyword title content linked cooc)
+ "Score KEYWORD against TITLE, CONTENT, LINKED keywords and COOC tallies."
+ (let ((regexp (concat "\\b" (regexp-quote keyword) "\\b")))
+ (+ (if (string-match-p regexp title) 3 0)
+ (garden--count-matches regexp content 5)
+ (* 2 (seq-count (lambda (k) (equal k keyword)) linked))
+ (min 3 (gethash keyword cooc 0)))))
+
+(defun garden--compute-suggestions (file limit)
+ "Compute up to LIMIT suggested tags for FILE, best first."
+ (let* ((id (garden--file-id file))
+ (note (and id (garden-note id)))
+ (current (garden--file-keywords file))
+ (topics (seq-difference current garden-suggest-excluded-tags))
+ (title (downcase (garden--file-title file)))
+ (content (downcase (with-temp-buffer (insert-file-contents file) (buffer-string))))
+ (linked (and note
+ (seq-mapcat (lambda (target)
+ (let ((n (garden-note target)))
+ (and n (garden-note-keywords n))))
+ (garden-note-links note))))
+ (cooc (garden--cooccurrence-counts topics id))
+ (scored (seq-keep (lambda (k)
+ (let ((score (garden--suggest-score k title content linked cooc)))
+ (when (> score 0) (cons k score))))
+ (garden--suggest-candidates current))))
+ (mapcar #'car (seq-take (seq-sort-by #'cdr #'> scored) limit))))
+
+(defun garden-suggest-tags (file &optional limit)
+ "Return up to LIMIT (default 3) suggested topic tags for FILE.
+Results are cached per file modification time."
+ (let ((mtime (file-attribute-modification-time (file-attributes file)))
+ (cached (gethash file garden--suggest-cache)))
+ (if (and cached (equal (car cached) mtime))
+ (cdr cached)
+ (let ((tags (garden--compute-suggestions file (or limit 3))))
+ (puthash file (cons mtime tags) garden--suggest-cache)
+ tags))))
+
+(defun garden--load-overrides ()
+ "Load keyword meta overrides from `garden-data-file'."
+ (setq garden--meta-overrides
+ (when (file-exists-p garden-data-file)
+ (with-temp-buffer
+ (insert-file-contents garden-data-file)
+ (ignore-errors (read (current-buffer)))))))
+
+(defun garden--save-overrides ()
+ "Persist keyword meta overrides to `garden-data-file'."
+ (make-directory (file-name-directory garden-data-file) t)
+ (with-temp-file garden-data-file (prin1 garden--meta-overrides (current-buffer))))
+
+(defun garden-toggle-meta (keyword)
+ "Flip KEYWORD between topic and meta, persisting the override."
+ (setf (alist-get keyword garden--meta-overrides nil nil #'equal)
+ (not (garden-meta-tag-p keyword)))
+ (garden--save-overrides))
+
+(defun garden-refresh ()
+ "Rebuild the garden index."
+ (interactive)
+ (garden-build))
+
+(defun garden-delete-note (id)
+ "Move the note with ID to the system trash after confirmation.
+Kills any buffer visiting it and rebuilds the index. Returns
+non-nil when the note was actually deleted."
+ (let* ((note (garden-note id))
+ (file (garden-note-file note)))
+ (when (yes-or-no-p (format "Move “%s” to the trash? " (garden-note-title note)))
+ (when-let* ((buf (find-buffer-visiting file)))
+ (kill-buffer buf))
+ (let ((delete-by-moving-to-trash t))
+ (delete-file file t))
+ (garden-build)
+ (message "garden: trashed %s" (garden-note-title note))
+ t)))
+
+(defun garden-completion-table (candidates annotate)
+ "Build a completion table over CANDIDATES with annotations.
+ANNOTATE is called with a candidate string and returns the
+annotation suffix. Candidate order is preserved in completion
+UIs that honor `display-sort-function'."
+ (lambda (string pred action)
+ (if (eq action 'metadata)
+ `(metadata (annotation-function . ,annotate)
+ (display-sort-function . identity)
+ (cycle-sort-function . identity))
+ (complete-with-action action candidates string pred))))
+
+(defun garden-note-annotation (note)
+ "Return a completion annotation for NOTE: its date and keywords."
+ (let ((id (garden-note-id note)))
+ (propertize (format " %s-%s-%s · %s"
+ (substring id 0 4) (substring id 4 6) (substring id 6 8)
+ (string-join (garden-note-keywords note) " "))
+ 'face 'completions-annotations)))
+
+(defun garden--dblock-files ()
+ "Return the note files containing denote dynamic blocks."
+ (seq-filter (lambda (f)
+ (with-temp-buffer
+ (insert-file-contents f)
+ (goto-char (point-min))
+ (re-search-forward "^#\\+BEGIN: denote" nil t)))
+ (garden--note-files)))
+
+(defun garden-update-indexes ()
+ "Refresh the denote dynamic blocks in every note that has one."
+ (interactive)
+ (require 'denote)
+ (require 'denote-org)
+ (let ((denote-directory (file-name-as-directory (expand-file-name garden-directory)))
+ (updated 0))
+ (dolist (file (garden--dblock-files))
+ (with-current-buffer (find-file-noselect file)
+ (org-update-all-dblocks)
+ (when (buffer-modified-p) (save-buffer) (setq updated (1+ updated)))))
+ (when (called-interactively-p 'interactive)
+ (message "garden: refreshed link indexes in %d note(s)" updated))
+ updated))
+
+(defun garden--before-save-update-dblocks ()
+ "Update denote dynamic blocks before saving a garden note."
+ (when (and (derived-mode-p 'org-mode)
+ buffer-file-name
+ (string-prefix-p (file-name-as-directory (expand-file-name garden-directory))
+ (expand-file-name buffer-file-name))
+ (save-excursion (goto-char (point-min))
+ (re-search-forward "^#\\+BEGIN: denote" nil t)))
+ (require 'denote-org)
+ (let ((denote-directory (file-name-as-directory (expand-file-name garden-directory))))
+ (org-update-all-dblocks))))
+
+(define-minor-mode garden-auto-index-mode
+ "Keep denote dynamic blocks in garden notes fresh on save."
+ :global t
+ (if garden-auto-index-mode
+ (add-hook 'before-save-hook #'garden--before-save-update-dblocks)
+ (remove-hook 'before-save-hook #'garden--before-save-update-dblocks)))
+
+(provide 'garden-core)
+;;; garden-core.el ends here
diff --git a/lisp/garden-dashboard.el b/lisp/garden-dashboard.el
new file mode 100644
index 0000000..4fb833b
--- /dev/null
+++ b/lisp/garden-dashboard.el
@@ -0,0 +1,461 @@
+;;; garden-dashboard.el --- cute garden explorer -*- lexical-binding: t; -*-
+
+(require 'garden-core)
+(require 'garden-icons)
+(require 'garden-preview)
+(require 'garden-connect)
+(require 'garden-fleet)
+(require 'garden-publish)
+(require 'vui)
+(require 'vui-components)
+
+(declare-function evil-define-key* "evil-core" (state keymap key def &rest bindings))
+(declare-function evil-set-initial-state "evil-core" (mode state))
+
+(defface garden-star '((t :inherit shadow)) "Face for the decorative star sprinkles." :group 'garden)
+(defface garden-meta '((t :inherit shadow :height 0.9)) "Face for muted helper text." :group 'garden)
+(defface garden-rule '((t :inherit shadow)) "Face for decorative rule lines." :group 'garden)
+(defface garden-tag-xl '((t :weight bold :height 1.7)) "Face for the most frequent topic tags." :group 'garden)
+(defface garden-tag-l '((t :weight bold :height 1.4)) "Face for very frequent topic tags." :group 'garden)
+(defface garden-tag-m '((t :height 1.2)) "Face for moderately frequent topic tags." :group 'garden)
+(defface garden-tag-s '((t :height 1.05)) "Face for less frequent topic tags." :group 'garden)
+(defface garden-tag-xs '((t :inherit shadow :height 0.95)) "Face for rare topic tags." :group 'garden)
+(defface garden-card-num '((t :weight bold :inherit success)) "Face for the stat card numbers." :group 'garden)
+(defface garden-card-frame '((t :inherit shadow)) "Face for the stat card rules." :group 'garden)
+
+(defcustom garden-content-width 66
+ "Maximum width in columns of decorative rule lines in the dashboard.
+This variable no longer forces centering; it only caps the length
+of banners and separators."
+ :type 'integer :group 'garden)
+(defcustom garden-sidecar-width 34
+ "Width in columns of the compact sidecar window."
+ :type 'integer :group 'garden)
+
+(defvar-local garden--compact nil
+ "Whether this dashboard buffer renders the compact view.")
+
+(defun garden--window-width ()
+ "Return the live body width of the Garden window."
+ (if-let* ((window (get-buffer-window (current-buffer) t)))
+ (window-body-width window)
+ (window-width)))
+
+(defun garden--tag-face (count maxc)
+ "Return the meadow face for a tag with COUNT uses out of MAXC."
+ (let ((r (/ (float count) (max 1 maxc))))
+ (cond ((>= r 0.75) 'garden-tag-xl)
+ ((>= r 0.50) 'garden-tag-l)
+ ((>= r 0.28) 'garden-tag-m)
+ ((>= r 0.12) 'garden-tag-s)
+ (t 'garden-tag-xs))))
+
+(defun garden--chunk (list n)
+ "Partition LIST into consecutive chunks of N elements."
+ (let (rows)
+ (while list (push (seq-take list n) rows) (setq list (nthcdr n list)))
+ (nreverse rows)))
+
+(defun garden-browse-keyword (keyword)
+ "Pick one of the notes tagged with KEYWORD and visit it."
+ (let* ((notes (garden-notes-with-keyword keyword))
+ (table (mapcar (lambda (n) (cons (garden-note-title n) n)) notes))
+ (annotate (lambda (title)
+ (when-let* ((note (cdr (assoc title table))))
+ (garden-note-annotation note))))
+ (choice (completing-read (format "%s %s (%d): " (garden-icon 'leaf) keyword (length notes))
+ (garden-completion-table (mapcar #'car table) annotate) nil t)))
+ (find-file (garden-note-file (cdr (assoc choice table))))))
+
+(defun garden-search ()
+ "Pick any garden note by title and visit it."
+ (interactive)
+ (let* ((ids (garden-all-ids))
+ (table (mapcar (lambda (id) (cons (garden-note-title (garden-note id)) id)) ids))
+ (annotate (lambda (title)
+ (when-let* ((id (cdr (assoc title table))))
+ (garden-note-annotation (garden-note id)))))
+ (choice (completing-read (format "%s garden (%d): " (garden-icon 'search) (length ids))
+ (garden-completion-table (mapcar #'car table) annotate) nil t)))
+ (find-file (garden-note-file (garden-note (cdr (assoc choice table)))))))
+
+(defun garden-search-text ()
+ "Full-text search across all garden notes."
+ (interactive)
+ (if (fboundp 'consult-ripgrep)
+ (consult-ripgrep garden-directory)
+ (rgrep (read-string "Search garden: ") "*.org" garden-directory)))
+
+(defun garden-visit-random ()
+ "Visit a random garden note."
+ (interactive)
+ (let ((ids (garden-all-ids)))
+ (when ids (find-file (garden-note-file (garden-note (nth (random (length ids)) ids)))))))
+
+(defun garden-reclassify-tag ()
+ "Toggle a keyword between topic and meta."
+ (interactive)
+ (let ((k (completing-read "Toggle topic/meta: " (mapcar #'car (garden-keywords-sorted)) nil t)))
+ (garden-toggle-meta k)
+ (when (derived-mode-p 'vui-mode) (vui-refresh))))
+
+(defun garden--id-time (id)
+ "Return the creation time encoded in the Denote ID."
+ (encode-time (list (string-to-number (substring id 13 15))
+ (string-to-number (substring id 11 13))
+ (string-to-number (substring id 9 11))
+ (string-to-number (substring id 6 8))
+ (string-to-number (substring id 4 6))
+ (string-to-number (substring id 0 4))
+ nil -1 nil)))
+
+(defun garden--age-suffix (id)
+ "Return a growth-stage age label for note ID, nil when older than a month."
+ (let ((days (/ (float-time (time-subtract (current-time) (garden--id-time id))) 86400)))
+ (cond ((< days 1) (format "%s just now" (garden-icon 'sprout)))
+ ((<= days 7) (format "%s this week" (garden-icon 'leaf)))
+ ((<= days 31) (format "%s last month" (garden-icon 'grass))))))
+
+(defun garden--hub-suffix (id maxd)
+ "Return height dots and the degree of note ID scaled against MAXD."
+ (let* ((degree (garden-degree id))
+ (dots (max 1 (ceiling (* 4 (/ (float degree) (max 1 maxd)))))))
+ (format "·%s %d" (make-string dots ?•) degree)))
+
+(defun garden--note-button (id &optional suffix width suffix-width)
+ "Render a row for note ID with optional SUFFIX.
+When WIDTH is given, the title and suffix sit in fixed-width
+columns so the action icons line up across rows; otherwise the
+row flows. SUFFIX-WIDTH defaults to 12 columns."
+ (let* ((note (garden-note id))
+ (file (garden-note-file note))
+ (suffix-node (vui-muted (or suffix "")))
+ (actions (vui-hstack :spacing 1
+ (vui-button (garden-icon 'preview)
+ :no-decoration t
+ :help-echo "peek in the side window"
+ :on-click (lambda () (garden-preview-show file)))
+ (vui-button (garden-icon 'delete)
+ :no-decoration t
+ :help-echo "move to the trash"
+ :on-click (lambda () (unwind-protect (garden-delete-note id) (garden-fleet--refresh))))))
+ (row-width (or width 'fill-column)))
+ (vui-flex
+ :width row-width :justify :space-between
+ (vui-flex-item
+ :grow 1
+ (lambda (available)
+ (vui-button (garden-note-title note)
+ :key id :no-decoration t :help-echo file
+ :max-width available
+ :on-click (lambda () (find-file file)))))
+ (when suffix
+ (if suffix-width
+ (vui-box suffix-node :width suffix-width :align :right)
+ suffix-node))
+ actions)))
+
+(defun garden--tag-button (k count maxc)
+ "Render the meadow button for keyword K used COUNT times out of MAXC."
+ (vui-button (format "%s" k)
+ :no-decoration t
+ :face (garden--tag-face count maxc)
+ :help-echo (format "%d notes — click to wander" count)
+ :on-click (lambda () (garden-browse-keyword k))))
+
+(defun garden--rule (&optional width)
+ "Return a decorative horizontal rule WIDTH columns wide."
+ (vui-box (vui-text (format "%s · %s · %s"
+ (garden-icon 'leaf)
+ (garden-icon 'star)
+ (garden-icon 'leaf))
+ :face 'garden-rule)
+ :width (max 1 (or width garden-content-width))
+ :align :center :face 'fringe))
+
+(defun garden--header ()
+ "Render the left-aligned dashboard banner."
+ (let ((s (garden-icon 'star))
+ (g (garden-icon 'garden)))
+ (let ((width (min garden-content-width (garden--window-width))))
+ (vui-vstack
+ (vui-flex :width 'window :justify :space-between
+ (vui-text (format "%s · %s" s (garden-icon 'leaf))
+ :face 'garden-star)
+ (vui-text (format "%s · %s" (garden-icon 'leaf) s)
+ :face 'garden-star))
+ (vui-box (vui-heading-1 (format "%s cashmere's garden" g))
+ :width width :align :center)
+ (vui-box (vui-muted "a little constellation of notes — wander slowly")
+ :width width :align :center)
+ (garden--rule width)))))
+
+(defun garden--toolbar ()
+ "Render the dashboard action buttons as responsive toolbars."
+ (let ((primary
+ (list
+ (vui-button (concat (garden-icon 'refresh) " refresh")
+ :on-click (lambda () (garden-refresh) (garden-fleet--refresh)))
+ (vui-button (concat (garden-icon 'search) " search") :on-click #'garden-search)
+ (vui-button (concat (garden-icon 'grep) " grep") :on-click #'garden-search-text)
+ (vui-button (concat (garden-icon 'dice) " surprise me") :on-click #'garden-visit-random)
+ (vui-button (concat (garden-icon 'publish) " publish…") :on-click #'garden-publish-one)))
+ (secondary
+ (list
+ (vui-button (concat (garden-icon 'cog) " classify tag") :on-click #'garden-reclassify-tag)
+ (vui-button (concat (garden-icon 'link) " connect…") :on-click #'garden-connect-pick)
+ (vui-button (concat (garden-icon 'fleet) " fleet") :on-click #'garden-fleet)
+ (vui-button (concat (garden-icon 'preview) " preview")
+ :on-click (lambda () (garden-preview-mode 'toggle) (garden-fleet--refresh)))
+ (vui-button (concat (garden-icon 'leaf) " rebuild indexes")
+ :on-click (lambda () (garden-update-indexes) (garden-refresh) (garden-fleet--refresh))))))
+ (vui-vstack
+ (vui-flex :width 'window :justify :start :spacing 1 :indent 2
+ (apply #'vui-hstack :spacing 1 primary))
+ (vui-flex :width 'window :justify :space-between :spacing 1 :indent 2
+ (apply #'vui-hstack :spacing 1 secondary)
+ (vui-muted "q quit")))))
+
+(defun garden--health-line ()
+ "Return a one-line mood describing the garden's link density."
+ (let ((density (/ (float (garden-link-count)) (max 1 (garden-note-count)))))
+ (cond ((< density 0.5)
+ (format "%s quietly rooting — every link helps it grow" (garden-icon 'web)))
+ ((< density 1.2)
+ (format "%s a cozy web is forming" (garden-icon 'link)))
+ (t
+ (format "%s %.1f links per note — a thriving web!" (garden-icon 'sparkle) density)))))
+
+(defun garden--cards ()
+ "Render the stat cards and the health line using `vui-table'."
+ (let* ((cells (list (cons (concat (garden-icon 'note) " notes") (garden-note-count))
+ (cons (concat (garden-icon 'leaf) " topics") (length (garden-topic-keywords)))
+ (cons (concat (garden-icon 'link) " links") (garden-link-count))
+ (cons (concat (garden-icon 'sprout) " seeds") (length (garden-orphans)))))
+ (cols (mapcar (lambda (_) '(:width 14 :align :center)) cells)))
+ (vui-vstack
+ (vui-table
+ :columns cols
+ :rows (list
+ (mapcar (lambda (c) (vui-text (number-to-string (cdr c)) :face 'garden-card-num)) cells)
+ (mapcar (lambda (c) (vui-text (car c) :face 'garden-meta)) cells))
+ :border :ascii
+ :border-face 'garden-card-frame)
+ (vui-text (garden--health-line) :face 'vui-success))))
+
+(defun garden--tag-rows (cols &optional max)
+ "Render the topic keywords in COLS columns, MAX tags at most."
+ (let* ((all (garden-topic-keywords))
+ (topics (if max (seq-take all max) all))
+ (maxc (if all (cdar all) 1)))
+ (apply #'vui-vstack :indent 1 :spacing 0
+ (mapcar (lambda (row)
+ (apply #'vui-hstack :spacing 2
+ (mapcar (lambda (kc) (garden--tag-button (car kc) (cdr kc) maxc)) row)))
+ (garden--chunk topics cols)))))
+
+(defun garden--meadow ()
+ "Render the topic meadow tag cloud."
+ (vui-vstack
+ (vui-heading-2 (concat (garden-icon 'grass) " topic meadow"))
+ (garden--tag-rows 6)))
+
+(defun garden--hubs (&optional width)
+ "Render the best-connected notes."
+ (let* ((hubs (garden-hubs 6))
+ (maxd (if hubs (garden-degree (car hubs)) 1)))
+ (vui-vstack
+ (vui-heading-3 (concat (garden-icon 'tree) " tallest trees"))
+ (apply #'vui-vstack :indent 2
+ (mapcar (lambda (id) (garden--note-button id (garden--hub-suffix id maxd) width)) hubs)))))
+
+(defun garden--recent (&optional width)
+ "Render the most recent notes with their age labels."
+ (let ((recent (garden-recent 6)))
+ (vui-vstack
+ (vui-heading-3 (concat (garden-icon 'sprout) " just sprouted"))
+ (apply #'vui-vstack :indent 2
+ (mapcar (lambda (id) (garden--note-button id (garden--age-suffix id) width)) recent)))))
+
+(defun garden--orphans ()
+ "Render the collapsible list of unlinked notes."
+ (let* ((orphans (seq-sort #'string> (garden-orphans)))
+ (shown (seq-take orphans 60)))
+ (vui-collapsible
+ :title (format "%s %d unlinked notes — no links in or out yet"
+ (garden-icon 'fallen) (length orphans))
+ :initially-expanded nil
+ :title-face 'vui-heading-3
+ :expanded-indicator (format "%s " (garden-icon 'down))
+ :collapsed-indicator (format "%s " (garden-icon 'up))
+ (apply #'vui-vstack :indent 2
+ (mapcar (lambda (id) (garden--note-button id)) shown)))))
+
+(defun garden--meta-strip ()
+ "Render the strip of meta keywords with toggle buttons."
+ (let ((metas (garden-meta-keywords)))
+ (when metas
+ (vui-vstack
+ (vui-heading-3 (concat (garden-icon 'cog) " provenance & status"))
+ (apply #'vui-hstack :spacing 2 :indent 2
+ (mapcar (lambda (kc)
+ (let ((k (car kc)))
+ (vui-button (format "%s·%d" k (cdr kc))
+ :no-decoration t :face 'garden-meta
+ :help-echo "click → make it a topic"
+ :on-click (lambda () (garden-toggle-meta k) (vui-refresh)))))
+ metas))))))
+
+(defun garden--view-full ()
+ "Render the full dashboard view."
+ (let* ((width (garden--window-width))
+ (wide (>= width 110))
+ (panel-width (if wide (max 44 (/ (- width 8) 2)) width)))
+ (vui-vstack :spacing 1 :indent 2
+ (garden--header)
+ (garden--toolbar)
+ (garden--cards)
+ (if wide
+ (vui-hstack
+ :spacing 4
+ (vui-vstack :spacing 1
+ (garden--meadow)
+ (garden--meta-strip))
+ (vui-vstack :spacing 1
+ (garden--hubs panel-width)
+ (garden--recent panel-width)))
+ (vui-vstack :spacing 1
+ (garden--meadow)
+ (garden--hubs panel-width)
+ (garden--recent panel-width)
+ (garden--meta-strip)))
+ (garden--orphans)
+ (vui-flex :width 'window :justify :space-between
+ (vui-muted "j/k move · TAB/S-TAB elements · RET/l activate")
+ (vui-muted "g refresh · / search · ? help · h/q close")))))
+
+(defun garden--view-compact ()
+ "Render the compact sidecar view."
+ (let* ((indent 1)
+ (width (max 24 (min garden-sidecar-width (garden--window-width))))
+ (suffix-w 9)
+ (col-w (floor (- width 5) 2))
+ (hubs (garden-hubs 6))
+ (maxd (if hubs (garden-degree (car hubs)) 1)))
+ (vui-vstack :spacing 0 :indent indent
+ (vui-flex :width width :justify :space-between :indent indent
+ (vui-heading-2 (format "%s garden" (garden-icon 'garden)))
+ (vui-text (garden-icon 'star) :face 'garden-star))
+ (garden--rule width)
+ (vui-table
+ :columns `((:width ,col-w :align :center)
+ (:width ,col-w :align :center))
+ :rows (list
+ (list (vui-text (number-to-string (garden-note-count)) :face 'garden-card-num)
+ (vui-text (number-to-string (length (garden-topic-keywords))) :face 'garden-card-num))
+ (list (vui-text (concat (garden-icon 'note) " notes") :face 'garden-meta)
+ (vui-text (concat (garden-icon 'leaf) " topics") :face 'garden-meta))
+ (list (vui-text (number-to-string (garden-link-count)) :face 'garden-card-num)
+ (vui-text (number-to-string (length (garden-orphans))) :face 'garden-card-num))
+ (list (vui-text (concat (garden-icon 'link) " links") :face 'garden-meta)
+ (vui-text (concat (garden-icon 'sprout) " seeds") :face 'garden-meta)))
+ :border :ascii
+ :border-face 'garden-card-frame)
+ (vui-flex :width width :justify :start :spacing 1 :indent indent
+ (vui-button (garden-icon 'refresh)
+ :on-click (lambda () (garden-refresh) (garden-fleet--refresh)))
+ (vui-button (garden-icon 'search) :on-click #'garden-search)
+ (vui-button (garden-icon 'dice) :on-click #'garden-visit-random)
+ (vui-button (garden-icon 'publish) :on-click #'garden-publish-one)
+ (vui-muted "q"))
+ (vui-heading-3 (concat (garden-icon 'grass) " top topics"))
+ (garden--tag-rows 2 16)
+ (vui-heading-3 (concat (garden-icon 'tree) " hubs"))
+ (apply #'vui-vstack
+ (mapcar (lambda (id) (garden--note-button id (garden--hub-suffix id maxd) width suffix-w)) hubs))
+ (vui-heading-3 (concat (garden-icon 'sprout) " recent"))
+ (apply #'vui-vstack
+ (mapcar (lambda (id) (garden--note-button id (garden--age-suffix id) width suffix-w)) (garden-recent 6))))))
+
+(defun garden--view ()
+ "Render the view matching this buffer's compact flag."
+ (if garden--compact (garden--view-compact) (garden--view-full)))
+
+(vui-defcomponent garden-dashboard ()
+ :render (garden--view))
+
+(defun garden-dashboard-refresh ()
+ "Rebuild the Garden index and redraw the dashboard."
+ (interactive)
+ (garden-refresh)
+ (garden-fleet--refresh))
+
+(defun garden-dashboard-help ()
+ "Show Garden dashboard keyboard help."
+ (interactive)
+ (message "Garden: j/k move, TAB/S-TAB elements, RET/l activate, g refresh, / note search, s text search, h/q close"))
+
+(defvar garden-dashboard-map
+ (let ((map (make-sparse-keymap)))
+ (set-keymap-parent map vui-mode-map)
+ (define-key map (kbd "/") #'garden-search)
+ (define-key map (kbd "s") #'garden-search-text)
+ (define-key map (kbd "P") #'garden-publish-one)
+ (define-key map (kbd "j") #'next-line)
+ (define-key map (kbd "k") #'previous-line)
+ (define-key map (kbd "TAB") #'vui-forward)
+ (define-key map (kbd "<backtab>") #'vui-backward)
+ (define-key map (kbd "RET") #'vui-activate)
+ (define-key map (kbd "l") #'vui-activate)
+ (define-key map (kbd "g") #'garden-dashboard-refresh)
+ (define-key map (kbd "?") #'garden-dashboard-help)
+ (define-key map (kbd "h") #'vui-quit)
+ (define-key map (kbd "q") #'vui-quit)
+ map)
+ "Keymap for `garden-dashboard-mode'.")
+
+(define-derived-mode garden-dashboard-mode vui-mode "Garden"
+ "Explore the Denote garden dashboard."
+ (hl-line-mode 1))
+
+(with-eval-after-load 'evil
+ (evil-set-initial-state 'garden-dashboard-mode 'normal)
+ (evil-define-key* '(normal motion) garden-dashboard-mode-map
+ (kbd "j") #'next-line (kbd "k") #'previous-line
+ (kbd "RET") #'vui-activate (kbd "l") #'vui-activate
+ (kbd "g") #'garden-dashboard-refresh
+ (kbd "/") #'garden-search (kbd "s") #'garden-search-text
+ (kbd "?") #'garden-dashboard-help
+ (kbd "h") #'vui-quit (kbd "q") #'vui-quit))
+
+(defun garden--prepare (compact)
+ "Build the index and mount the dashboard, COMPACT when non-nil."
+ (garden-build)
+ (let ((buffer (get-buffer-create "*garden*")))
+ (with-current-buffer buffer
+ (garden-dashboard-mode)
+ (setq-local garden--compact compact)
+ (vui-mount (vui-component 'garden-dashboard) buffer)
+ (vui-rerender-on-resize))))
+
+;;;###autoload
+(defun garden ()
+ "Open the garden dashboard."
+ (interactive)
+ (garden--prepare nil)
+ (switch-to-buffer "*garden*")
+ (vui-refresh))
+
+;;;###autoload
+(defun garden-sidecar ()
+ "Open the compact garden dashboard in a left side window."
+ (interactive)
+ (garden--prepare t)
+ (let ((win (display-buffer-in-side-window
+ (get-buffer "*garden*")
+ `((side . left) (slot . 0) (window-width . ,garden-sidecar-width)
+ (window-parameters . ((no-delete-other-windows . t)))))))
+ (when win (select-window win) (vui-refresh))))
+
+(provide 'garden-dashboard)
+;;; garden-dashboard.el ends here
diff --git a/lisp/garden-fleet.el b/lisp/garden-fleet.el
new file mode 100644
index 0000000..70c5d72
--- /dev/null
+++ b/lisp/garden-fleet.el
@@ -0,0 +1,464 @@
+;;; garden-fleet.el --- import & process fleeting notes -*- lexical-binding: t; -*-
+
+(require 'garden-core)
+(require 'garden-icons)
+(require 'garden-preview)
+(require 'vui)
+(require 'vui-components)
+
+(declare-function evil-define-key* "evil-core" (state keymap key def &rest bindings))
+(declare-function evil-set-initial-state "evil-core" (mode state))
+
+(defvar denote-directory)
+(defvar denote-rename-confirmations)
+(defvar denote-save-buffers)
+(defvar denote-kill-buffers)
+(declare-function denote-rename-file "denote")
+(declare-function garden "garden-dashboard")
+
+(defcustom garden-fleet-source-directory (expand-file-name "~/org/")
+ "Directory scanned for fleeting notes to import."
+ :type 'directory :group 'garden)
+
+(defcustom garden-fleet-tags '("fleet" "capture")
+ "Tags marking a note as fleeting."
+ :type '(repeat string) :group 'garden)
+
+(defcustom garden-fleet-data-file (expand-file-name ".garden/fleet.eld" garden-directory)
+ "File persisting the manual ordering of staged fleet notes."
+ :type 'file :group 'garden)
+
+(defvar garden-fleet--order nil
+ "List of staged note ids in their manual order.")
+
+(defvar-local garden-fleet--dispatch nil
+ "Async actions exposed by the mounted Fleet component.")
+
+(defun garden-fleet--refresh ()
+ "Re-render every live garden VUI buffer."
+ (dolist (name '("*garden-fleet*" "*garden*" "*garden connect*"))
+ (when-let* ((buf (get-buffer name))
+ (root (buffer-local-value 'vui--root-instance buf)))
+ (vui-rerender root))))
+
+(defun garden-fleet--load-order ()
+ "Load the staged-note order from `garden-fleet-data-file'."
+ (setq garden-fleet--order
+ (when (file-exists-p garden-fleet-data-file)
+ (with-temp-buffer
+ (insert-file-contents garden-fleet-data-file)
+ (ignore-errors (read (current-buffer)))))))
+
+(defun garden-fleet--save-order ()
+ "Persist the staged-note order to `garden-fleet-data-file'."
+ (make-directory (file-name-directory garden-fleet-data-file) t)
+ (with-temp-file garden-fleet-data-file (prin1 garden-fleet--order (current-buffer))))
+
+(defun garden-fleet--file-tags (file)
+ "Return all tags encoded in FILE's name, provenance included."
+ (let ((base (file-name-nondirectory file)))
+ (when (string-match "__\\([a-zA-Z0-9_-]+\\)\\.org\\'" base)
+ (split-string (match-string 1 base) "_" t))))
+
+(defun garden-fleet--fleet-p (tags)
+ "Return non-nil when TAGS marks a fleeting note."
+ (seq-intersection tags garden-fleet-tags))
+
+(defun garden-fleet--source-files ()
+ "Return the Denote-named org files in `garden-fleet-source-directory'."
+ (directory-files garden-fleet-source-directory t "\\`[0-9]\\{8\\}T[0-9]\\{6\\}.*\\.org\\'"))
+
+(defun garden-fleet-candidates ()
+ "Return fleeting files in the inbox not yet imported."
+ (seq-filter (lambda (f)
+ (and (garden-fleet--fleet-p (garden-fleet--file-tags f))
+ (not (garden-note (garden--file-id f)))))
+ (garden-fleet--source-files)))
+
+(defun garden-fleet-staged ()
+ "Return imported fleet notes, manual order first, newest after."
+ (let (acc)
+ (maphash (lambda (_id note)
+ (when (garden-fleet--fleet-p (garden-fleet--file-tags (garden-note-file note)))
+ (push note acc)))
+ (garden-notes))
+ (let ((by-id (make-hash-table :test 'equal)))
+ (dolist (note acc) (puthash (garden-note-id note) note by-id))
+ (let ((ordered (seq-keep (lambda (id) (gethash id by-id)) garden-fleet--order))
+ (rest (seq-remove (lambda (note) (member (garden-note-id note) garden-fleet--order)) acc)))
+ (append ordered (seq-sort-by #'garden-note-id #'string> rest))))))
+
+(defun garden-fleet-import (file)
+ "Move FILE from the inbox into the garden and stage it."
+ (garden-fleet--load-order)
+ (let* ((id (garden--file-id file))
+ (dest (expand-file-name (file-name-nondirectory file) garden-directory)))
+ (cond
+ ((file-exists-p dest)
+ (message "garden: %s already in the garden — skipped" (file-name-nondirectory file)))
+ (t
+ (rename-file file dest)
+ (setq garden-fleet--order (append garden-fleet--order (list id)))
+ (garden-fleet--save-order)
+ (garden-build)
+ (message "garden: imported %s" (file-name-nondirectory dest))))))
+
+(defun garden-fleet-import-all ()
+ "Import every fleeting note from the inbox after confirmation."
+ (interactive)
+ (let ((files (garden-fleet-candidates)))
+ (when (and files
+ (yes-or-no-p (format "Import all %d fleet/capture note(s) into the garden? " (length files))))
+ (dolist (f files) (garden-fleet-import f))
+ (garden-build))))
+
+(defun garden-fleet--rename (file keywords)
+ "Rename FILE via denote so its name carries exactly KEYWORDS."
+ (require 'denote)
+ (let ((denote-directory (file-name-as-directory (expand-file-name garden-directory)))
+ (denote-rename-confirmations nil)
+ (denote-save-buffers t)
+ (denote-kill-buffers t))
+ (denote-rename-file file 'keep-current keywords 'keep-current 'keep-current 'keep-current)))
+
+(defun garden-fleet-classify (id)
+ "Prompt for topic tags to add to the note with ID.
+Suggested tags are listed first and marked in the completion UI."
+ (let* ((note (garden-note id))
+ (file (garden-note-file note))
+ (current (garden-fleet--file-tags file))
+ (suggested (garden-suggest-tags file))
+ (choices (seq-difference (mapcar #'car (garden-keywords-sorted)) current))
+ (choices (append suggested (seq-difference choices suggested)))
+ (annotate (lambda (k)
+ (concat
+ (propertize (format " %d notes" (gethash k (garden-keyword-counts) 0))
+ 'face 'completions-annotations)
+ (or (when (member k suggested)
+ (propertize (format " %s suggested" (garden-icon 'sparkle))
+ 'face 'completions-annotations))
+ ""))))
+ (prompt (if suggested
+ (format "Topic tag(s) for “%s” (%s maybe: %s): "
+ (garden-note-title note) (garden-icon 'sparkle)
+ (string-join suggested ", "))
+ (format "Topic tag(s) for “%s”: " (garden-note-title note))))
+ (added (completing-read-multiple prompt (garden-completion-table choices annotate))))
+ (when added
+ (garden-fleet--rename file (delete-dups (append current added)))
+ (garden-build))))
+
+(defun garden-fleet-tag (id tag)
+ "Add the single TAG to the note with ID without prompting."
+ (let* ((note (garden-note id))
+ (file (garden-note-file note))
+ (current (garden-fleet--file-tags file)))
+ (garden-fleet--rename file (delete-dups (append current (list tag))))
+ (garden-build)
+ (message "garden: tagged “%s” with %s" (garden-note-title note) tag)))
+
+(defun garden-fleet-delete (id)
+ "Trash the staged note with ID and drop it from the order."
+ (garden-fleet--load-order)
+ (when (garden-delete-note id)
+ (setq garden-fleet--order (delete id garden-fleet--order))
+ (garden-fleet--save-order)))
+
+(defun garden-fleet-return (id)
+ "Send the staged note with ID back to the ~/org inbox.
+Moves its file to `garden-fleet-source-directory', drops it from
+the manual order, and rebuilds the index."
+ (garden-fleet--load-order)
+ (let* ((note (garden-note id))
+ (file (garden-note-file note))
+ (dest (expand-file-name (file-name-nondirectory file) garden-fleet-source-directory)))
+ (make-directory garden-fleet-source-directory t)
+ (if (file-exists-p dest)
+ (message "garden: %s already in ~/org — left in place" (file-name-nondirectory file))
+ (when-let* ((buf (find-buffer-visiting file)))
+ (kill-buffer buf))
+ (rename-file file dest)
+ (setq garden-fleet--order (delete id garden-fleet--order))
+ (garden-fleet--save-order)
+ (garden-build)
+ (message "garden: returned “%s” to ~/org" (garden-note-title note)))))
+
+(defun garden-fleet-discard (file)
+ "Trash the inbox candidate FILE after confirmation."
+ (when (yes-or-no-p (format "Move “%s” to the trash? " (garden--file-title file)))
+ (let ((delete-by-moving-to-trash t))
+ (delete-file file t))
+ (message "garden: discarded %s" (file-name-nondirectory file))))
+
+(defun garden-fleet-graduate (id)
+ "Remove the fleeting tags from the note with ID after confirmation."
+ (garden-fleet--load-order)
+ (let* ((note (garden-note id))
+ (file (garden-note-file note))
+ (keywords (seq-remove (lambda (k) (member k garden-fleet-tags))
+ (garden-fleet--file-tags file))))
+ (when (yes-or-no-p (format "Graduate “%s” out of fleet? " (garden-note-title note)))
+ (garden-fleet--rename file keywords)
+ (setq garden-fleet--order (delete id garden-fleet--order))
+ (garden-fleet--save-order)
+ (garden-build)
+ (message "garden: graduated %s" (garden-note-title note)))))
+
+(defun garden-fleet-move (id delta)
+ "Move the staged note with ID by DELTA positions in the order."
+ (garden-fleet--load-order)
+ (let* ((ids (mapcar #'garden-note-id (garden-fleet-staged)))
+ (pos (seq-position ids id))
+ (new (and pos (+ pos delta))))
+ (when (and pos new (>= new 0) (< new (length ids)))
+ (let ((other (nth new ids)))
+ (setq garden-fleet--order
+ (mapcar (lambda (x) (cond ((equal x id) other) ((equal x other) id) (t x))) ids))
+ (garden-fleet--save-order)))))
+
+(defun garden-fleet--topic-tags (file)
+ "Return FILE's topic keywords, with provenance and status dropped."
+ (seq-difference (garden--file-keywords file) garden-fleet-tags))
+
+(defun garden-fleet--icon-action (icon help on-click &optional face label)
+ "Render a quiet ICON button with HELP tooltip running ON-CLICK."
+ (vui-button (if label (format "%s %s" (garden-icon icon) label)
+ (garden-icon icon))
+ :no-decoration (null label) :help-echo help :face face :on-click on-click))
+
+(defun garden-fleet--candidate-row (file width)
+ "Render an inbox card for FILE using WIDTH."
+ (let ((tags-str (string-join (garden-fleet--topic-tags file) " "))
+ (actions (vui-hstack :spacing 1
+ (garden-fleet--icon-action 'import "import into the garden"
+ (lambda () (unwind-protect (garden-fleet-import file) (garden-fleet--refresh)))
+ 'success "import")
+ (garden-fleet--icon-action 'preview "peek in the side window"
+ (lambda () (garden-preview-show file)) 'link)
+ (garden-fleet--icon-action 'delete "move to the trash"
+ (lambda () (unwind-protect (garden-fleet-discard file) (garden-fleet--refresh)))
+ 'error))))
+ (vui-vstack
+ :spacing 0 :face 'fringe
+ (vui-flex
+ :width width :justify :space-between
+ (vui-flex-item
+ :grow 1
+ (lambda (available)
+ (vui-button (garden--file-title file)
+ :key file :no-decoration t :help-echo file
+ :max-width available :face '(:inherit bold)
+ :on-click (lambda () (find-file file)))))
+ (vui-muted "inbox"))
+ (vui-box (vui-text (if (string-empty-p tags-str)
+ "no topic tags"
+ (format "#%s" (string-replace " " " #" tags-str)))
+ :face 'shadow)
+ :width width :padding-left 2)
+ actions)))
+
+(defun garden-fleet--chips (id tags)
+ "Return a dot-separated list of one-click chips tagging note ID."
+ (let (out)
+ (dolist (tag tags)
+ (when out (push (vui-muted "·") out))
+ (push (vui-button tag
+ :no-decoration t
+ :help-echo (format "click to tag with “%s” right away" tag)
+ :on-click (lambda () (unwind-protect (garden-fleet-tag id tag) (garden-fleet--refresh))))
+ out))
+ (nreverse out)))
+
+(defun garden-fleet--suggestion-chips (id file)
+ "Render one-click suggested-tag chips for the note ID in FILE.
+The chips line up beneath the title column of the staged row above."
+ (let ((suggested (garden-suggest-tags file)))
+ (when suggested
+ (apply #'vui-hstack :spacing 1
+ (vui-muted "maybe →")
+ (garden-fleet--chips id suggested)))))
+
+(defun garden-fleet--staged-row (note width)
+ "Render a staged card for NOTE using WIDTH."
+ (let* ((id (garden-note-id note))
+ (file (garden-note-file note))
+ (tags-str (string-join (garden-fleet--topic-tags file) " "))
+ (queue (vui-hstack :spacing 1
+ (garden-fleet--icon-action 'up "move up the queue"
+ (lambda () (unwind-protect (garden-fleet-move id -1) (garden-fleet--refresh))))
+ (garden-fleet--icon-action 'down "move down the queue"
+ (lambda () (unwind-protect (garden-fleet-move id 1) (garden-fleet--refresh))))))
+ (actions (vui-hstack :spacing 1
+ (garden-fleet--icon-action 'classify "add topic tags"
+ (lambda () (unwind-protect (garden-fleet-classify id) (garden-fleet--refresh)))
+ 'link "classify")
+ (garden-fleet--icon-action 'graduate "graduate out of fleet"
+ (lambda () (unwind-protect (garden-fleet-graduate id) (garden-fleet--refresh)))
+ 'success "graduate")
+ (garden-fleet--icon-action 'preview "peek in the side window"
+ (lambda () (garden-preview-show file)) 'link)
+ (garden-fleet--icon-action 'return "send back to the ~/org inbox"
+ (lambda () (unwind-protect (garden-fleet-return id) (garden-fleet--refresh)))
+ 'warning))))
+ (vui-vstack :spacing 0 :face 'fringe
+ (vui-flex :width width :justify :space-between
+ queue
+ (vui-flex-item :grow 1
+ (lambda (w)
+ (vui-button (garden-note-title note)
+ :key id :no-decoration t :help-echo file
+ :max-width w :face '(:inherit bold)
+ :on-click (lambda () (find-file file)))))
+ (vui-muted "staged"))
+ (vui-box (vui-text (if (string-empty-p tags-str)
+ "no topic tags"
+ (format "#%s" (string-replace " " " #" tags-str)))
+ :face 'shadow)
+ :width width :padding-left 2)
+ (garden-fleet--suggestion-chips id file)
+ actions)))
+
+(defun garden-fleet--header ()
+ "Render the fleet dashboard banner."
+ (let ((icon (garden-icon 'fleet))
+ (wave (garden-icon 'leaf)))
+ (vui-vstack
+ (vui-text (format "~ %s ~ %s ~ %s ~" wave icon wave) :face 'garden-meta)
+ (vui-heading-1 (format "%s fleeting notes" icon))
+ (vui-muted "import from ~/org, then order · classify · redo them"))))
+
+(defun garden-fleet--window-width ()
+ "Return the live Fleet window body width."
+ (if-let* ((window (get-buffer-window (current-buffer) t)))
+ (window-body-width window)
+ (window-width)))
+
+(defun garden-fleet--panel (title empty items renderer width)
+ "Render a Fleet panel with TITLE, EMPTY message and ITEMS.
+RENDERER receives each item and WIDTH."
+ (vui-vstack
+ :spacing 1
+ (vui-flex :width width :justify :space-between
+ (vui-heading-2 title)
+ (vui-text (number-to-string (length items)) :face 'shadow))
+ (if (null items)
+ (vui-box (vui-text empty :face 'shadow) :width width :align :center)
+ (vui-list items
+ (lambda (item) (funcall renderer item width))
+ (lambda (item)
+ (if (stringp item) item (garden-note-id item)))
+ :spacing 1))))
+
+(defun garden-fleet--view ()
+ "Render the fleet dashboard: inbox candidates and staged notes."
+ (let* ((candidates (garden-fleet-candidates))
+ (staged (garden-fleet-staged))
+ (width (garden-fleet--window-width))
+ (wide (>= width 100))
+ (panel-width (if wide (max 42 (/ (- width 6) 2)) (max 28 (- width 4))))
+ (refresh (vui-with-async-context
+ (garden-refresh)
+ (garden-fleet--refresh))))
+ (setq garden-fleet--dispatch (list :refresh refresh))
+ (vui-vstack :spacing 1 :indent 2
+ (garden-fleet--header)
+ (vui-flex :width 'window :justify :space-between
+ (vui-hstack :spacing 2
+ (vui-button (concat (garden-icon 'refresh) " rescan")
+ :on-click refresh)
+ (vui-button (concat (garden-icon 'import) " import all")
+ :on-click (lambda () (unwind-protect (garden-fleet-import-all) (garden-fleet--refresh))))
+ (vui-button (concat (garden-icon 'preview) " preview")
+ :on-click (lambda () (garden-preview-mode 'toggle) (garden-fleet--refresh)))
+ (vui-button (concat (garden-icon 'star) " garden")
+ :on-click (lambda () (garden))))
+ (vui-muted (format "%d inbox · %d staged" (length candidates) (length staged))))
+ (if wide
+ (vui-hstack
+ :spacing 4
+ (garden-fleet--panel
+ (concat (garden-icon 'inbox) " inbox")
+ "Inbox empty — nothing to import"
+ candidates #'garden-fleet--candidate-row panel-width)
+ (garden-fleet--panel
+ (concat (garden-icon 'tools) " staged")
+ "Nothing staged — import a note first"
+ staged #'garden-fleet--staged-row panel-width))
+ (vui-vstack
+ :spacing 1
+ (garden-fleet--panel
+ (concat (garden-icon 'inbox) " inbox")
+ "Inbox empty — nothing to import"
+ candidates #'garden-fleet--candidate-row panel-width)
+ (garden-fleet--panel
+ (concat (garden-icon 'tools) " staged")
+ "Nothing staged — import a note first"
+ staged #'garden-fleet--staged-row panel-width)))
+ (vui-flex :width 'window :justify :space-between
+ (vui-muted "j/k move · TAB/S-TAB elements · RET/l activate")
+ (vui-muted "g rescan · ? help · h garden · q close")))))
+
+(vui-defcomponent garden-fleet-dashboard ()
+ :render (garden-fleet--view))
+
+(defun garden-fleet-refresh ()
+ "Rescan and refresh the Fleet dashboard."
+ (interactive)
+ (if-let* ((fn (plist-get garden-fleet--dispatch :refresh)))
+ (funcall fn)
+ (user-error "Fleet dashboard is not ready")))
+
+(defun garden-fleet-back ()
+ "Return to a live Garden dashboard, or close Fleet."
+ (interactive)
+ (if-let* ((buffer (get-buffer "*garden*")))
+ (switch-to-buffer buffer)
+ (vui-quit)))
+
+(defun garden-fleet-help ()
+ "Show Fleet keyboard help."
+ (interactive)
+ (message "Garden Fleet: j/k move, TAB/S-TAB elements, RET/l activate, g rescan, h garden, q close"))
+
+(defvar garden-fleet-mode-map
+ (let ((map (make-sparse-keymap)))
+ (set-keymap-parent map vui-mode-map)
+ (keymap-set map "j" #'next-line)
+ (keymap-set map "k" #'previous-line)
+ (keymap-set map "TAB" #'vui-forward)
+ (keymap-set map "<backtab>" #'vui-backward)
+ (keymap-set map "RET" #'vui-activate)
+ (keymap-set map "l" #'vui-activate)
+ (keymap-set map "g" #'garden-fleet-refresh)
+ (keymap-set map "?" #'garden-fleet-help)
+ (keymap-set map "h" #'garden-fleet-back)
+ (keymap-set map "q" #'vui-quit)
+ map))
+
+(define-derived-mode garden-fleet-mode vui-mode "Garden-Fleet"
+ "Manage the Garden fleeting-note queues."
+ (hl-line-mode 1))
+
+(with-eval-after-load 'evil
+ (evil-set-initial-state 'garden-fleet-mode 'normal)
+ (evil-define-key* '(normal motion) garden-fleet-mode-map
+ (kbd "j") #'next-line (kbd "k") #'previous-line
+ (kbd "RET") #'vui-activate (kbd "l") #'vui-activate
+ (kbd "g") #'garden-fleet-refresh (kbd "?") #'garden-fleet-help
+ (kbd "h") #'garden-fleet-back (kbd "q") #'vui-quit))
+
+;;;###autoload
+(defun garden-fleet ()
+ "Open the fleet dashboard for importing and classifying fleeting notes."
+ (interactive)
+ (garden-build)
+ (garden-fleet--load-order)
+ (let ((buffer (get-buffer-create "*garden-fleet*")))
+ (with-current-buffer buffer
+ (garden-fleet-mode)
+ (vui-mount (vui-component 'garden-fleet-dashboard) buffer)
+ (vui-rerender-on-resize))
+ (switch-to-buffer buffer)))
+
+(provide 'garden-fleet)
+;;; garden-fleet.el ends here
diff --git a/lisp/garden-icons.el b/lisp/garden-icons.el
new file mode 100644
index 0000000..c083409
--- /dev/null
+++ b/lisp/garden-icons.el
@@ -0,0 +1,64 @@
+;;; garden-icons.el --- nerd-font icon glyphs for the garden UIs -*- lexical-binding: t; -*-
+
+;;; Commentary:
+
+;; One place for every pictogram the garden dashboards use. When
+;; nerd-icons is available (and `garden-use-nerd-icons' is non-nil)
+;; the UIs render single-width nerd-font glyphs, which align cleanly
+;; in a monospace buffer; otherwise they fall back to the original
+;; emoji.
+
+;;; Code:
+
+(require 'nerd-icons nil t)
+
+(declare-function nerd-icons-mdicon "nerd-icons")
+
+(defcustom garden-use-nerd-icons t
+ "Whether garden UIs render nerd-font glyphs instead of emoji."
+ :type 'boolean :group 'garden)
+
+(defconst garden-icon-alist
+ '((garden "nf-md-flower_outline" "✦")
+ (fleet "nf-md-ferry" "🚢")
+ (inbox "nf-md-inbox_arrow_down" "📥")
+ (import "nf-md-import" "⇨")
+ (classify "nf-md-tag_outline" "🏷")
+ (graduate "nf-md-school" "✓")
+ (delete "nf-md-trash_can_outline" "🗑")
+ (return "nf-md-undo_variant" "↩")
+ (preview "nf-md-eye_outline" "👁")
+ (refresh "nf-md-refresh" "↻")
+ (cog "nf-md-cog_outline" "⚙")
+ (tools "nf-md-hammer_wrench" "🛠")
+ (link "nf-md-link_variant" "🔗")
+ (web "nf-md-spider_web" "🕸")
+ (dice "nf-md-dice_multiple" "✦")
+ (sprout "nf-md-sprout" "🌱")
+ (leaf "nf-md-leaf" "🌿")
+ (grass "nf-md-grass" "🌾")
+ (tree "nf-md-tree" "🌳")
+ (fallen "nf-md-leaf_maple" "🍂")
+ (note "nf-md-note_text_outline" "📝")
+ (search "nf-md-magnify" "🔍")
+ (grep "nf-md-text_search" "🔎")
+ (publish "nf-md-publish" "🚀")
+ (sparkle "nf-md-shimmer" "✨")
+ (star "nf-md-star_four_points" "✦")
+ (up "nf-md-arrow_up_bold" "▲")
+ (down "nf-md-arrow_down_bold" "▼"))
+ "Mapping of semantic icon names to (NERD-GLYPH EMOJI-FALLBACK).")
+
+(defun garden-icon (name)
+ "Return the glyph string for the semantic icon NAME.
+NAME is a key of `garden-icon-alist'. Returns a propertized
+nerd-font glyph when available, the emoji fallback otherwise."
+ (let ((entry (alist-get name garden-icon-alist)))
+ (unless entry (error "garden: unknown icon %s" name))
+ (if (and garden-use-nerd-icons (fboundp 'nerd-icons-mdicon))
+ (or (nerd-icons-mdicon (car entry))
+ (cadr entry))
+ (cadr entry))))
+
+(provide 'garden-icons)
+;;; garden-icons.el ends here
diff --git a/lisp/garden-links.el b/lisp/garden-links.el
new file mode 100644
index 0000000..8fc20c8
--- /dev/null
+++ b/lisp/garden-links.el
@@ -0,0 +1,29 @@
+;;; garden-links.el --- Denote links dynamic block helpers -*- lexical-binding: t; -*-
+
+;; Thin wrappers around `denote-org' dynamic blocks.
+
+;;; Code:
+
+(require 'garden-core)
+
+(declare-function denote-org-dblock-insert-links "denote-org")
+
+;;;###autoload
+(defun garden-insert-denote-links ()
+ "Insert a `denote-links' dynamic block at point.
+Prompts for a regexp and inserts a block that lists all Denote
+notes whose file name matches it. Use `org-update-dblock' or
+`C-c C-c' on the block to refresh it later."
+ (interactive)
+ (require 'denote-org)
+ (call-interactively #'denote-org-dblock-insert-links))
+
+;;;###autoload
+(defun garden-update-denote-links ()
+ "Refresh every `denote-links' dynamic block in the current buffer."
+ (interactive)
+ (require 'denote-org)
+ (org-update-all-dblocks))
+
+(provide 'garden-links)
+;;; garden-links.el ends here
diff --git a/lisp/garden-preview.el b/lisp/garden-preview.el
new file mode 100644
index 0000000..f7f89a0
--- /dev/null
+++ b/lisp/garden-preview.el
@@ -0,0 +1,111 @@
+;;; garden-preview.el --- side-window note preview -*- lexical-binding: t; -*-
+
+;;; Commentary:
+
+;; Shows garden notes in a read-only side window on the right. Two
+;; entry points: `garden-preview-show' for one-off peeks (wired to
+;; per-row eye buttons), and `garden-preview-mode', a buffer-local
+;; minor mode for the VUI dashboards that previews the note under
+;; point as you move around. Buffers opened purely for previewing
+;; are killed again by `garden-preview-quit'.
+
+;;; Code:
+
+(require 'garden-core)
+(require 'cl-lib)
+
+(declare-function vui-element-at "vui")
+(declare-function vui-element-get "vui")
+
+(defcustom garden-preview-width 45
+ "Width of the preview side window in columns."
+ :type 'integer :group 'garden)
+
+(defvar garden-preview--opened nil
+ "Buffers the preview opened itself and may kill on quit.")
+
+(defvar-local garden-preview--owned nil
+ "Non-nil when this buffer was created by `garden-preview-show'.")
+
+(defvar garden-preview--last nil
+ "File shown in the preview window most recently.")
+
+(defun garden-preview-show (file)
+ "Display FILE read-only in a side window on the right.
+Focus returns to the window that was selected before the call."
+ (interactive "fPreview note: ")
+ (let* ((existing (find-buffer-visiting file))
+ (buf (or existing (find-file-noselect file)))
+ (origin (selected-window)))
+ (if existing
+ (progn
+ (with-current-buffer buf (setq garden-preview--owned nil))
+ (setq garden-preview--opened (delq buf garden-preview--opened)))
+ (with-current-buffer buf
+ (setq garden-preview--owned t)
+ (cl-pushnew buf garden-preview--opened)))
+ (setq garden-preview--last file)
+ (with-current-buffer buf (setq buffer-read-only t))
+ (or (with-selected-window (window-main-window)
+ (display-buffer-in-side-window
+ buf
+ `((side . right) (slot . 0) (window-width . ,garden-preview-width)
+ (window-parameters . ((no-delete-other-windows . t))))))
+ (display-buffer buf '((display-buffer-reuse-window display-buffer-use-some-window)
+ (inhibit-same-window . t))))
+ (select-window origin)))
+
+(defun garden-preview-quit ()
+ "Close the preview window and kill buffers opened for previewing.
+Only buffers that the preview itself created are killed."
+ (interactive)
+ (when-let* ((buf (and garden-preview--last (find-buffer-visiting garden-preview--last)))
+ (win (get-buffer-window buf)))
+ (when (window-parameter win 'window-side)
+ (delete-window win)))
+ (dolist (buf garden-preview--opened)
+ (when (and (buffer-live-p buf)
+ (buffer-local-value 'garden-preview--owned buf)
+ (not (buffer-modified-p buf))
+ (not (get-buffer-window buf 'visible)))
+ (kill-buffer buf)))
+ (setq garden-preview--opened nil
+ garden-preview--last nil))
+
+(defun garden-preview--file-at-line ()
+ "Return the note file referenced by a VUI element on the current line, if any.
+Works with both `button.el' text buttons (vui 1.3+) and widget fields."
+ (save-excursion
+ (let ((end (line-end-position))
+ (file nil))
+ (beginning-of-line)
+ (while (and (not file) (< (point) end))
+ (let* ((elt (vui-element-at (point)))
+ (echo (and elt (vui-element-get elt 'help-echo))))
+ (when (and (stringp echo)
+ (string-suffix-p ".org" echo)
+ (file-exists-p echo))
+ (setq file echo)))
+ (forward-char 1))
+ file)))
+
+(defun garden-preview--post-command ()
+ "Preview the note under point when it changed."
+ (when-let* ((file (garden-preview--file-at-line)))
+ (unless (equal file garden-preview--last)
+ (garden-preview-show file))))
+
+;;;###autoload
+(define-minor-mode garden-preview-mode
+ "Preview the note under point in a right side window.
+Buffer-local; meant for the garden VUI dashboards."
+ :lighter " peek"
+ (if garden-preview-mode
+ (progn
+ (add-hook 'post-command-hook #'garden-preview--post-command nil t)
+ (garden-preview--post-command))
+ (remove-hook 'post-command-hook #'garden-preview--post-command t)
+ (garden-preview-quit)))
+
+(provide 'garden-preview)
+;;; garden-preview.el ends here
diff --git a/lisp/garden-publish.el b/lisp/garden-publish.el
new file mode 100644
index 0000000..cafbcda
--- /dev/null
+++ b/lisp/garden-publish.el
@@ -0,0 +1,450 @@
+;;; garden-publish.el --- publish the garden to the zola site -*- lexical-binding: t; -*-
+
+(require 'garden-core)
+(require 'subr-x)
+
+(declare-function denote-publish-to-md "denote-publish")
+(declare-function org-publish "ox-publish")
+
+(defvar denote-directory)
+(defvar org-export-with-broken-links)
+(defvar org-export-use-babel)
+(defvar org-publish-project-alist)
+
+(defcustom garden-publish-site-directory (expand-file-name "~/cashmere.rs/") "Root directory of the Zola site checkout." :type 'directory :group 'garden)
+(defcustom garden-publish-wiki-subdir "content/wiki" "Wiki output directory relative to the site root." :type 'string :group 'garden)
+
+(defun garden-publish--clean-generated (dir)
+ "Delete all generated Markdown files in DIR except _index.md."
+ (dolist (old (directory-files dir t "\\.md\\'"))
+ (unless (equal (file-name-nondirectory old) "_index.md") (delete-file old))))
+
+(defun garden-publish--ensure-index (dir title)
+ "Create an _index.md with TITLE in DIR unless one already exists."
+ (let ((idx (expand-file-name "_index.md" dir)))
+ (unless (file-exists-p idx)
+ (with-temp-file idx (insert (format "+++\ntitle = %S\nsort_by = \"date\"\n+++\n" title))))))
+
+(defun garden-publish--filename-slug (file)
+ "Return the slug part of the denote FILE name, or nil."
+ (let ((base (file-name-nondirectory file)))
+ (when (string-match "\\`[0-9]\\{8\\}T[0-9]\\{6\\}--\\([a-z0-9-]+\\)\\(?:__[a-z0-9_-]+\\)?\\.org\\'" base)
+ (match-string 1 base))))
+
+(defun garden-publish--slug-map ()
+ "Return a hash table mapping note ids to unique URL slugs."
+ (let ((map (make-hash-table :test 'equal))
+ (seen (make-hash-table :test 'equal)))
+ (dolist (file (sort (garden--note-files) #'string<))
+ (let* ((id (garden--file-id file))
+ (base (garden-publish--filename-slug file))
+ (kw (car (garden--file-keywords file))))
+ (when (and id base)
+ (let ((slug base))
+ (when (gethash slug seen)
+ (setq slug (if (and kw (not (gethash (concat base "-" kw) seen)))
+ (concat base "-" kw)
+ (let ((n 2))
+ (while (gethash (format "%s-%d" base n) seen) (setq n (1+ n)))
+ (format "%s-%d" base n)))))
+ (puthash slug t seen)
+ (puthash id slug map)))))
+ map))
+
+(defcustom garden-publish-exclude-keywords '("draft" "private" "noexport") "File keywords that exclude a note from publishing." :type '(repeat string) :group 'garden)
+
+(defun garden-publish--exclude-ids ()
+ "Return a hash table of note ids that must not be published."
+ (let ((ex (make-hash-table :test 'equal)))
+ (dolist (file (garden--note-files))
+ (when (seq-intersection garden-publish-exclude-keywords (garden--file-keywords file))
+ (puthash (garden--file-id file) t ex)))
+ ex))
+
+(defun garden-publish--rewrite-denote-links (map)
+ "Rewrite exported denote links in the current buffer using MAP.
+MAP maps note ids to wiki slugs; links to unknown ids are reduced
+to their description."
+ (goto-char (point-min))
+ (while (re-search-forward
+ "<a href=\"denote:\\([0-9]\\{8\\}T[0-9]\\{6\\}\\)\\(?:::[^\"]*\\)?\\(?:\\.html\\)?\"[^>]*>\\(\\(?:.\\|\n\\)*?\\)</a>"
+ nil t)
+ (let* ((tid (match-string 1))
+ (desc (match-string 2))
+ (tslug (gethash tid map)))
+ (replace-match (if tslug (format "<a href=\"/wiki/%s/\">%s</a>" tslug desc) desc) t t)))
+ (goto-char (point-min))
+ (while (re-search-forward
+ "\\[\\([^]]*\\)\\](denote:\\([0-9]\\{8\\}T[0-9]\\{6\\}\\)[^)]*)"
+ nil t)
+ (let* ((desc (match-string 1))
+ (tslug (gethash (match-string 2) map)))
+ (replace-match (if tslug (format "[%s](/wiki/%s/)" desc tslug) desc) t t))))
+
+(defun garden-publish--fix-wiki-frontmatter ()
+ "Move top-level tags/category in YAML front matter into a Zola taxonomies block.
+This is done in the current buffer."
+ (save-excursion
+ (goto-char (point-min))
+ (when (looking-at "^---$")
+ (let ((start (point)))
+ (forward-line)
+ (when (re-search-forward "^---$" nil t)
+ (let ((end (point)))
+ (save-restriction
+ (narrow-to-region start end)
+ (goto-char (point-min))
+ (when (re-search-forward "^\\(tags:\\|category:\\)" nil t)
+ (beginning-of-line)
+ (insert "taxonomies:\n")
+ (goto-char (point-min))
+ (while (re-search-forward "^tags:" nil t)
+ (replace-match " tags:"))
+ (goto-char (point-min))
+ (while (re-search-forward "^category:" nil t)
+ (replace-match " categories:"))))))))))
+
+(defun garden-publish--fix-wiki (dir map exclude)
+ "Post-process exported wiki files in DIR.
+Rewrite denote links using the id-to-slug table MAP, delete files
+whose id is in the EXCLUDE table, strip PGP blocks and TODO
+keywords, fix front matter taxonomies, and rename each file to its slug."
+ (dolist (md (directory-files dir t "\\.md\\'"))
+ (let* ((base (file-name-nondirectory md))
+ (id (and (string-match "\\`\\([0-9]\\{8\\}T[0-9]\\{6\\}\\)" base) (match-string 1 base)))
+ (slug (and id (gethash id map))))
+ (cond
+ ((and id (gethash id exclude)) (delete-file md))
+ ((null slug) nil)
+ (t
+ (with-temp-buffer
+ (insert-file-contents md)
+ (garden-publish--fix-wiki-frontmatter)
+ (goto-char (point-min))
+ (while (re-search-forward "\\(?:-----\\|&ndash;&mdash;\\)?BEGIN PGP\\(?:.\\|\n\\)*?END PGP[^&\n-]*\\(?:-----\\|&ndash;&mdash;\\)?" nil t)
+ (replace-match ""))
+ (goto-char (point-min))
+ (while (re-search-forward "^\\(#+\\)[ \t]+\\(?:TODO\\|DONE\\|NEXT\\|WAIT[A-Z]*\\|CANCELL?ED\\|HOLD\\|STRT\\)[ \t]+" nil t)
+ (replace-match "\\1 "))
+ (garden-publish--rewrite-denote-links map)
+ (write-region (point-min) (point-max) md nil 'silent))
+ (unless (equal base (concat slug ".md"))
+ (rename-file md (expand-file-name (concat slug ".md") dir) t)))))))
+
+;;;###autoload
+(defun garden-publish-wiki ()
+ "Export all garden notes to Markdown in the site wiki directory."
+ (interactive)
+ (require 'denote)
+ (require 'ox-gfm)
+ (require 'denote-publish)
+ (let* ((out (expand-file-name garden-publish-wiki-subdir garden-publish-site-directory))
+ (denote-directory (expand-file-name garden-directory))
+ (org-export-with-broken-links t)
+ (org-export-use-babel nil)
+ (make-backup-files nil)
+ (org-publish-project-alist
+ `(("garden-wiki"
+ :base-directory ,(expand-file-name garden-directory)
+ :publishing-directory ,out
+ :publishing-function denote-publish-to-md
+ :recursive nil
+ :exclude-tags ("noexport" "draft")
+ :with-toc nil))))
+ (make-directory out t)
+ (garden-publish--clean-generated out)
+ (garden-publish--ensure-index out "Wiki")
+ (garden-build)
+ (org-publish "garden-wiki" t)
+ (garden-publish--fix-wiki out (garden-publish--slug-map) (garden-publish--exclude-ids))
+ (message "garden: published wiki to %s" out)))
+
+(defcustom garden-publish-blog-source (expand-file-name "~/garden/posts/")
+ "Directory holding the Org source files for blog posts."
+ :type 'directory :group 'garden)
+
+(defcustom garden-publish-pages-source (expand-file-name "~/garden/pages/")
+ "Directory holding the Org source files for standalone pages."
+ :type 'directory :group 'garden)
+
+(defcustom garden-publish-images-source (expand-file-name "~/garden/posts/images/")
+ "Directory holding images referenced by blog posts."
+ :type 'directory :group 'garden)
+
+(defcustom garden-publish-blog-subdir "content/blog" "Blog output directory relative to the site root." :type 'string :group 'garden)
+
+(declare-function org-export-as "ox")
+
+(defun garden-publish--parse-post (file)
+ "Parse the Org post FILE into a (PROPERTIES . BODY) pair."
+ (let ((props nil) (body-start 0) (i 0)
+ (lines (with-temp-buffer (insert-file-contents file) (split-string (buffer-string) "\n"))))
+ (catch 'done
+ (dolist (line lines)
+ (cond
+ ((string-match "\\`#\\+\\([A-Za-z_]+\\):[ \t]*\\(.*\\)\\'" line)
+ (push (cons (downcase (match-string 1 line)) (string-trim (match-string 2 line))) props))
+ ((string-empty-p (string-trim line)) nil)
+ (t (setq body-start i) (throw 'done nil)))
+ (setq i (1+ i))))
+ (cons props (string-join (nthcdr body-start lines) "\n"))))
+
+(defun garden-publish--strip-toc (body)
+ "Return BODY with any Table of Contents section removed."
+ (let ((out '()) (skip nil))
+ (dolist (line (split-string body "\n"))
+ (cond
+ ((string-match-p "\\`\\*+ +Table of Contents[ \t]*\\'" line) (setq skip t))
+ ((and skip (string-match-p "\\`\\*+ " line)) (setq skip nil) (push line out))
+ (skip nil)
+ (t (push line out))))
+ (string-join (nreverse out) "\n")))
+
+(defun garden-publish--clean-slug (s)
+ "Normalize string S into a lowercase URL slug."
+ (let ((x (replace-regexp-in-string "[^a-z0-9]+" "-" (downcase (string-trim s)))))
+ (replace-regexp-in-string "\\(?:\\`-+\\|-+\\'\\)" "" x)))
+
+(defun garden-publish--org-to-gfm (body)
+ "Export the Org string BODY to GitHub-flavored Markdown."
+ (require 'ox-gfm)
+ (with-temp-buffer
+ (insert body)
+ (org-mode)
+ (let ((org-export-with-broken-links t)
+ (org-export-use-babel nil))
+ (org-export-as 'gfm nil nil t '(:with-toc nil :with-section-numbers nil)))))
+
+(defun garden-publish--blog-one (file out-dir)
+ "Convert the blog post FILE to Markdown in OUT-DIR unless drafted."
+ (let* ((parsed (garden-publish--parse-post file))
+ (props (car parsed))
+ (body (cdr parsed)))
+ (unless (equal (cdr (assoc "status" props)) "draft")
+ (let* ((title (or (cdr (assoc "title" props)) (file-name-base file)))
+ (slug (garden-publish--clean-slug (or (cdr (assoc "slug" props)) (file-name-base file))))
+ (date (let ((d (cdr (assoc "date" props)))) (and d (substring d 0 (min 10 (length d))))))
+ (tags (let ((tg (cdr (assoc "tags" props)))) (and tg (split-string tg "[ ,]+" t))))
+ (desc (cdr (assoc "description" props)))
+ (md (garden-publish--org-to-gfm
+ (garden-publish--strip-toc
+ (replace-regexp-in-string "/content/images/" "/images/" body))))
+ (fm (concat "+++\n"
+ (format "title = %S\n" title)
+ (or (when date (format "date = \"%s\"\n" date)) "")
+ (or (when (and desc (not (string-empty-p desc)))
+ (format "description = %S\n" desc))
+ "")
+ (or (when tags
+ (concat "[taxonomies]\n"
+ (format "tags = [%s]\n"
+ (mapconcat (lambda (x) (format "%S" x)) tags ", "))))
+ "")
+ "+++\n\n")))
+ (with-temp-file (expand-file-name (concat slug ".md") out-dir)
+ (insert fm md))))))
+
+;;;###autoload
+(defun garden-publish-blog ()
+ "Export all blog posts and images to the site blog directory."
+ (interactive)
+ (let ((out (expand-file-name garden-publish-blog-subdir garden-publish-site-directory))
+ (postsrc garden-publish-blog-source)
+ (imgsrc garden-publish-images-source)
+ (imgdst (expand-file-name "static/images" garden-publish-site-directory)))
+ (make-directory out t)
+ (garden-publish--clean-generated out)
+ (garden-publish--ensure-index out "Blog")
+ (if (file-directory-p postsrc)
+ (dolist (f (directory-files postsrc t "\\.org\\'"))
+ (garden-publish--blog-one f out))
+ (message "garden: blog source missing, skipping posts"))
+ (when (file-directory-p imgsrc)
+ (make-directory imgdst t)
+ (copy-directory imgsrc imgdst t t t))
+ (message "garden: published blog to %s" out)))
+
+(defcustom garden-publish-pages-skip '("wiki-archive") "Page slugs that are never published." :type '(repeat string) :group 'garden)
+
+(defcustom garden-publish-pages-manifest (expand-file-name ".garden/pages.eld" garden-directory) "File recording the page files written by the last publish run." :type 'file :group 'garden)
+
+(defun garden-publish--load-pages-manifest ()
+ "Read the pages manifest file and return its entries, or nil."
+ (when (file-exists-p garden-publish-pages-manifest)
+ (with-temp-buffer
+ (insert-file-contents garden-publish-pages-manifest)
+ (ignore-errors (read (current-buffer))))))
+
+(defun garden-publish--save-pages-manifest (entries)
+ "Write ENTRIES to the pages manifest file."
+ (make-directory (file-name-directory garden-publish-pages-manifest) t)
+ (with-temp-file garden-publish-pages-manifest (prin1 entries (current-buffer))))
+
+(defun garden-publish--page-one (file out-dir)
+ "Convert the page FILE to Markdown in OUT-DIR and return its name."
+ (let* ((parsed (garden-publish--parse-post file))
+ (props (car parsed))
+ (body (cdr parsed))
+ (slug (garden-publish--clean-slug (or (cdr (assoc "slug" props)) (file-name-base file)))))
+ (unless (or (equal (cdr (assoc "status" props)) "draft")
+ (member slug garden-publish-pages-skip))
+ (let* ((title (or (cdr (assoc "title" props)) (file-name-base file)))
+ (desc (cdr (assoc "description" props)))
+ (md (garden-publish--org-to-gfm
+ (garden-publish--strip-toc
+ (replace-regexp-in-string "/content/images/" "/images/" body))))
+ (fm (concat "+++\n"
+ (format "title = %S\n" title)
+ (or (when (and desc (not (string-empty-p desc)))
+ (format "description = %S\n" desc))
+ "")
+ "+++\n\n")))
+ (with-temp-file (expand-file-name (concat slug ".md") out-dir)
+ (insert fm md))
+ (concat slug ".md")))))
+
+;;;###autoload
+(defun garden-publish-pages ()
+ "Export all standalone pages and remove stale ones from the site."
+ (interactive)
+ (let ((out (expand-file-name "content" garden-publish-site-directory))
+ (src garden-publish-pages-source)
+ (written '()))
+ (make-directory out t)
+ (if (file-directory-p src)
+ (dolist (f (directory-files src t "\\.org\\'"))
+ (let ((name (garden-publish--page-one f out)))
+ (when name (push name written))))
+ (message "garden: pages source missing, skipping pages"))
+ (let ((stale (seq-difference (garden-publish--load-pages-manifest) written)))
+ (dolist (name stale)
+ (let ((md (expand-file-name name out)))
+ (when (file-exists-p md) (delete-file md))))
+ (garden-publish--save-pages-manifest written)
+ (if stale
+ (message "garden: published pages to %s (removed %d stale)" out (length stale))
+ (message "garden: published pages to %s" out)))))
+
+(defcustom garden-publish-deploy-command "just publish"
+ "Shell command that deploys the generated site."
+ :type 'string :group 'garden)
+
+(defun garden-publish--post-files ()
+ "Return the Org files of all blog posts, or nil if the source is missing."
+ (when (file-directory-p garden-publish-blog-source)
+ (directory-files garden-publish-blog-source t "\\.org\\'")))
+
+(defun garden-publish--page-files ()
+ "Return the Org files of all standalone pages, or nil if the source is missing."
+ (when (file-directory-p garden-publish-pages-source)
+ (directory-files garden-publish-pages-source t "\\.org\\'")))
+
+(defun garden-publish--pick-file (prompt files)
+ "Pick one of FILES by base name with PROMPT."
+ (if (null files)
+ (user-error "No source files available")
+ (let ((table (mapcar (lambda (f) (cons (file-name-nondirectory f) f)) files)))
+ (cdr (assoc (completing-read prompt table nil t) table)))))
+
+;;;###autoload
+(defun garden-publish-post (file)
+ "Export the single blog post FILE to the site blog directory."
+ (interactive (list (garden-publish--pick-file "Publish post: " (garden-publish--post-files))))
+ (let ((out (expand-file-name garden-publish-blog-subdir garden-publish-site-directory))
+ (props (car (garden-publish--parse-post file))))
+ (if (equal (cdr (assoc "status" props)) "draft")
+ (message "garden: %s is a draft, not published" (file-name-base file))
+ (make-directory out t)
+ (garden-publish--ensure-index out "Blog")
+ (garden-publish--blog-one file out)
+ (message "garden: published post %s → %s" (file-name-base file) out))))
+
+;;;###autoload
+(defun garden-publish-page (file)
+ "Export the single standalone page FILE to the site content directory."
+ (interactive (list (garden-publish--pick-file "Publish page: " (garden-publish--page-files))))
+ (let* ((out (expand-file-name "content" garden-publish-site-directory))
+ (name (progn (make-directory out t) (garden-publish--page-one file out))))
+ (if (null name)
+ (message "garden: %s is a draft or skipped, not published" (file-name-base file))
+ (let ((manifest (garden-publish--load-pages-manifest)))
+ (unless (member name manifest)
+ (garden-publish--save-pages-manifest (cons name manifest))))
+ (message "garden: published page %s" name))))
+
+;;;###autoload
+(defun garden-publish-note-as-page (&optional file slug)
+ "Export the garden note FILE as the standalone page SLUG on the site.
+Interactively, offers the current buffer's note when visiting one,
+and prompts for the slug. The note file itself stays untouched;
+tag it noexport if it should not also appear under /wiki/."
+ (interactive)
+ (let* ((current (buffer-file-name))
+ (file (or file
+ (if (and current
+ (string-prefix-p (expand-file-name garden-directory)
+ (expand-file-name current))
+ (y-or-n-p (format "Publish %s as a page? "
+ (file-name-nondirectory current))))
+ current
+ (garden-publish--pick-file "Publish note as page: " (garden--note-files)))))
+ (parsed (garden-publish--parse-post file))
+ (props (car parsed))
+ (title (or (cdr (assoc "title" props)) (file-name-base file)))
+ (slug (garden-publish--clean-slug
+ (or slug
+ (read-string "Page slug: " (or (garden-publish--filename-slug file)
+ (garden-publish--clean-slug title))))))
+ (md (garden-publish--org-to-gfm
+ (garden-publish--strip-toc
+ (replace-regexp-in-string "/content/images/" "/images/" (cdr parsed)))))
+ (out (expand-file-name "content" garden-publish-site-directory))
+ (map (garden-publish--slug-map)))
+ (make-directory out t)
+ (with-temp-file (expand-file-name (concat slug ".md") out)
+ (insert (format "+++\ntitle = %S\n+++\n\n" title) md)
+ (garden-publish--rewrite-denote-links map))
+ (message "garden: published note as page /%s" slug)))
+
+;;;###autoload
+(defun garden-publish-one ()
+ "Publish a single post, page or note, then offer to deploy."
+ (interactive)
+ (let* ((candidates
+ (append
+ (mapcar (lambda (f) (cons (concat "post: " (file-name-base f)) (cons #'garden-publish-post f)))
+ (garden-publish--post-files))
+ (mapcar (lambda (f) (cons (concat "page: " (file-name-base f)) (cons #'garden-publish-page f)))
+ (garden-publish--page-files))
+ (mapcar (lambda (f) (cons (concat "note: " (file-name-nondirectory f)) (cons #'garden-publish-note-as-page f)))
+ (garden--note-files))))
+ (choice (cdr (assoc (completing-read "Publish one: " candidates nil t) candidates))))
+ (funcall (car choice) (cdr choice))
+ (when (y-or-n-p (format "Deploy now (%s)? " garden-publish-deploy-command))
+ (garden-publish-deploy))))
+
+;;;###autoload
+(defun garden-publish ()
+ "Publish the wiki, the blog and the pages to the site directory."
+ (interactive)
+ (garden-publish-wiki)
+ (garden-publish-blog)
+ (garden-publish-pages)
+ (message "garden: published wiki + blog + pages → %s" garden-publish-site-directory))
+
+;;;###autoload
+(defun garden-publish-deploy ()
+ "Run the deploy command asynchronously in the site directory."
+ (interactive)
+ (let ((default-directory (expand-file-name garden-publish-site-directory)))
+ (async-shell-command garden-publish-deploy-command "*garden-deploy*")))
+
+;;;###autoload
+(defun garden-publish-all ()
+ "Publish the whole site and then deploy it."
+ (interactive)
+ (garden-publish)
+ (garden-publish-deploy))
+
+(provide 'garden-publish)
+;;; garden-publish.el ends here
diff --git a/lisp/my-mode-line.el b/lisp/my-mode-line.el
index f7cc66a..ac14cb1 100644
--- a/lisp/my-mode-line.el
+++ b/lisp/my-mode-line.el
@@ -63,6 +63,12 @@
(defconst my/mode-line--missing (make-symbol "missing")
"Sentinel used when checking membership in package hash tables.")
+(defvar my/mode-line--format nil
+ "The installed global header-line mode-line format.")
+
+(defvar-local my/mode-line--saved-mode-line-format nil
+ "Clatter's mode-line format saved while the global mode line is hidden.")
+
(defun my/mode-line--update-title-cache ()
"Update the cached #+title for the current buffer."
(setq-local my/mode-line--buffer-title
@@ -228,6 +234,58 @@ Optional THEME is ignored so this function also fits
:inverse-video nil
:strike-through nil))
+(defun my/mode-line--hide-clatter-buffers (&rest _)
+ "Hide Clatter's local mode lines while the global mode line is disabled."
+ (unless my/global-mode-line-mode
+ (dolist (buffer (buffer-list))
+ (with-current-buffer buffer
+ (when (derived-mode-p 'clatter-mode)
+ (unless (local-variable-p 'my/mode-line--saved-mode-line-format)
+ (setq-local my/mode-line--saved-mode-line-format mode-line-format))
+ (setq-local mode-line-format nil))))))
+
+(defun my/mode-line--restore-package-mode-lines ()
+ "Restore package-local mode lines hidden by the global toggle."
+ (dolist (buffer (buffer-list))
+ (with-current-buffer buffer
+ (when (local-variable-p 'my/mode-line--saved-mode-line-format)
+ (setq-local mode-line-format my/mode-line--saved-mode-line-format)
+ (kill-local-variable 'my/mode-line--saved-mode-line-format)))))
+
+(define-minor-mode my/global-mode-line-mode
+ "Show the custom global mode line.
+
+When disabled, the header-based global mode line and Clatter's package-local
+bottom mode lines are hidden. Local informational headers, such as Elfeed's
+column headings, remain visible."
+ :global t
+ :init-value t
+ :group 'mode-line
+ (if my/global-mode-line-mode
+ (progn
+ (setq-default header-line-format my/mode-line--format)
+ (my/mode-line--restore-package-mode-lines))
+ (setq-default header-line-format nil)
+ (my/mode-line--hide-clatter-buffers))
+ (force-mode-line-update t))
+
+(defun my/toggle-mode-line ()
+ "Toggle the custom mode line globally."
+ (interactive)
+ (my/global-mode-line-mode (if my/global-mode-line-mode -1 1)))
+
+(with-eval-after-load 'clatter-ui
+ (unless (advice-member-p #'my/mode-line--hide-clatter-buffers
+ 'clatter-ui-setup-buffer)
+ (advice-add 'clatter-ui-setup-buffer :after
+ #'my/mode-line--hide-clatter-buffers)))
+
+(with-eval-after-load 'clatter-track
+ (unless (advice-member-p #'my/mode-line--hide-clatter-buffers
+ 'clatter-track--refresh-mode-lines)
+ (advice-add 'clatter-track--refresh-mode-lines :after
+ #'my/mode-line--hide-clatter-buffers)))
+
(defun my/mode-line-install ()
"Install the custom mode-line built with mode-line-maker.
The mode-line lives at the top of the window via `header-line-format';
@@ -236,25 +294,28 @@ the bottom mode-line is hidden."
(my/mode-line-sync-theme)
(add-hook 'enable-theme-functions #'my/mode-line-sync-theme)
(setq-default mode-line-format nil)
- (setq-default header-line-format
- (mode-line-maker
- '((:eval (my/mode-line-evil-state)) " "
- (:eval (my/mode-line-buffer-state))
- (:eval (my/mode-line-remote)) " "
- (:eval (my/mode-line-buffer-icon)) " "
- (:eval (propertize (my/mode-line-buffer-name)
- 'face 'mode-line-buffer-id
- 'mouse-face 'mode-line-highlight
- 'help-echo (concat (or buffer-file-truename (buffer-name))
- "\nmouse-1: Previous buffer\nmouse-3: Next buffer")
- 'local-map mode-line-buffer-identification-keymap)) " "
- mode-line-position)
- '("" (:eval (my/mode-line-irc)) " "
- (:eval (my/mode-line-clatter)) " "
- (:eval (my/mode-line-mu4e)) " "
- mode-line-process " "
- (:eval (my/mode-line-time)) " "
- mode-name " ")))
+ (setq my/mode-line--format
+ (mode-line-maker
+ '((:eval (my/mode-line-evil-state)) " "
+ (:eval (my/mode-line-buffer-state))
+ (:eval (my/mode-line-remote)) " "
+ (:eval (my/mode-line-buffer-icon)) " "
+ (:eval (propertize (my/mode-line-buffer-name)
+ 'face 'mode-line-buffer-id
+ 'mouse-face 'mode-line-highlight
+ 'help-echo (concat (or buffer-file-truename (buffer-name))
+ "\nmouse-1: Previous buffer\nmouse-3: Next buffer")
+ 'local-map mode-line-buffer-identification-keymap)) " "
+ mode-line-position)
+ '("" (:eval (my/mode-line-irc)) " "
+ (:eval (my/mode-line-clatter)) " "
+ (:eval (my/mode-line-mu4e)) " "
+ mode-line-process " "
+ (:eval (my/mode-line-time)) " "
+ mode-name " ")))
+ ;; The mode line is visible on every startup, regardless of its previous
+ ;; interactive state in a long-running session.
+ (my/global-mode-line-mode 1)
(add-hook 'before-save-hook #'my/mode-line--invalidate-title-cache))
(provide 'my-mode-line)
diff --git a/lisp/rsync-ui.el b/lisp/rsync-ui.el
index 3282178..19ce985 100644
--- a/lisp/rsync-ui.el
+++ b/lisp/rsync-ui.el
@@ -260,14 +260,10 @@
(destination-label (if remote "SSH DESTINATION" "LOCAL DESTINATION"))
(destination-glyph (if remote "⌁" "◎"))
(destination-face (if remote 'warning 'success)))
- (if (>= width 72)
+ (if (>= width 80)
(let* ((card-width (min 30 (/ (- width 8) 2)))
- (text-width (- card-width 2))
- (source (truncate-string-to-width
- (rsync-ui--source-title sources) text-width nil nil "…"))
- (destination (truncate-string-to-width
- (rsync-ui--destination-title kind local-target ssh-host ssh-path)
- text-width nil nil "…")))
+ (source (rsync-ui--source-title sources))
+ (destination (rsync-ui--destination-title kind local-target ssh-host ssh-path)))
(vui-vstack
:spacing 0
(vui-hstack
@@ -293,12 +289,8 @@
(vui-box (vui-text destination :face 'rsync-ui-strong)
:width card-width :align :center))))
(let* ((card-width (min 56 width))
- (text-width (- card-width 2))
- (source (truncate-string-to-width
- (rsync-ui--source-title sources) text-width nil nil "…"))
- (destination (truncate-string-to-width
- (rsync-ui--destination-title kind local-target ssh-host ssh-path)
- text-width nil nil "…")))
+ (source (rsync-ui--source-title sources))
+ (destination (rsync-ui--destination-title kind local-target ssh-host ssh-path)))
(vui-vstack
:spacing 0
(vui-hstack
@@ -314,12 +306,12 @@
:width card-width :align :left :padding-left 2))))))
(defun rsync-ui--field-row (label field)
- (vui-hstack
+ (vui-flex :width 'window
(vui-box (vui-text label :face 'rsync-ui-faded) :width 16 :align :right)
- field))
+ (vui-flex-item :grow 1 field)))
-(defun rsync-ui--gauge (progress)
- (let* ((width 44)
+(defun rsync-ui--gauge (progress &optional available)
+ (let* ((width (max 12 (min 44 (- (or available 52) 8))))
(done (max 0 (min width (round (* width (/ progress 100.0)))))))
(vui-vstack
:spacing 0
@@ -328,12 +320,12 @@
:spacing 0
(vui-text (make-string done ?█) :face 'success)
(vui-text (make-string (- width done) ?░) :face 'shadow))
- :width 52 :align :center)
+ :width (or available 52) :align :center)
(vui-box (vui-text (format "%3d%%" progress)
:face '(:inherit bold :height 1.6))
- :width 52 :align :center))))
+ :width (or available 52) :align :center))))
-(defun rsync-ui--preview-panel (preview delete)
+(defun rsync-ui--preview-panel (preview delete width)
(let ((count (plist-get preview :count))
(size (plist-get preview :size)))
(vui-vstack
@@ -343,17 +335,18 @@
"Everything is already in sync"
(format "%d changes · %s to transfer" count size))
:face (if (zerop count) 'success 'rsync-ui-strong))
- :width 66 :align :center)
+ :width width :align :center)
(when delete
(vui-box (vui-text "Delete is enabled: extra files at the destination will be removed."
:face '(:inherit warning :weight bold))
- :width 66 :align :center))
+ :width width :align :center))
(when-let* ((sample (plist-get preview :sample)))
- (vui-vstack
- :spacing 0 :indent 4
- (mapcar (lambda (line)
- (vui-text (truncate-string-to-width line 62 nil nil "…") :face 'shadow))
- sample))))))
+ (vui-table
+ :sticky-header t
+ :columns `((:header "Preview — first changes"
+ :width ,(max 20 (- width 2)) :grow t :truncate t))
+ :rows (mapcar (lambda (line) (list (vui-text line :face 'shadow)))
+ sample))))))
(vui-defcomponent rsync-ui-app (initial-sources initial-target)
@@ -514,7 +507,7 @@
(vui-set-state :status nil)
(vui-set-state :progress 0)))))
(busy (process-live-p process))
- (source-width (max 12 (min 42 (- (rsync-ui--window-width) 31)))))
+ (view-width (max 28 (- (rsync-ui--window-width) 2))))
(setq rsync-ui--actions
(list :choose-source choose-source
:choose-target choose-target
@@ -530,27 +523,27 @@
((memq stage '(running done failed cancelled))
(vui-vstack
:spacing 1
- (rsync-ui--gauge progress)
+ (rsync-ui--gauge progress (min 64 view-width))
(vui-flex
:width 52 :justify :space-between
(vui-text transferred :face 'rsync-ui-strong)
(vui-text (if (string-empty-p speed) "Waiting for data…" speed) :face 'shadow)
(vui-text (if (string-empty-p eta) "" (format "ETA %s" eta)) :face 'shadow))))
((eq stage 'ready)
- (rsync-ui--preview-panel preview delete))
+ (rsync-ui--preview-panel preview delete view-width))
(t
(vui-vstack
:spacing 1
(rsync-ui--field-row
"Source"
- (vui-hstack
- :spacing 1
- (vui-box
- (vui-text
- (truncate-string-to-width
- (rsync-ui--source-title sources) (max 10 (- source-width 2)) nil nil "…"))
- :width source-width)
- (vui-button "Choose…" :on-click choose-source :disabled busy)))
+ (lambda (width)
+ (vui-flex
+ :width width
+ (vui-flex-item
+ :grow 1
+ (vui-box (vui-text (rsync-ui--source-title sources))
+ :width (max 8 (- width 10))))
+ (vui-button "Choose…" :on-click choose-source :disabled busy))))
(rsync-ui--field-row
"Destination"
(vui-select :value kind
@@ -560,15 +553,17 @@
(vui-vstack
:spacing 1
(rsync-ui--field-row
- "SSH host"
- (vui-field :value ssh-host :size 42 :key 'ssh-host
- :placeholder "user@example.org"
- :on-change (lambda (value) (funcall invalidate :ssh-host value))))
+ "SSH host"
+ (lambda (width)
+ (vui-field :value ssh-host :size (max 8 width) :key 'ssh-host
+ :placeholder "user@example.org"
+ :on-change (lambda (value) (funcall invalidate :ssh-host value)))))
(rsync-ui--field-row
"Remote folder"
- (vui-field :value ssh-path :size 42 :key 'ssh-path
- :placeholder "~/Backups/"
- :on-change (lambda (value) (funcall invalidate :ssh-path value))))
+ (lambda (width)
+ (vui-field :value ssh-path :size (max 8 width) :key 'ssh-path
+ :placeholder "~/Backups/"
+ :on-change (lambda (value) (funcall invalidate :ssh-path value)))))
(rsync-ui--field-row
"SSH port"
(vui-field :value ssh-port :size 8 :key 'ssh-port
@@ -578,12 +573,17 @@
(vui-muted "Uses your SSH key or agent; no password is stored.")))
(rsync-ui--field-row
"Local folder"
- (vui-hstack
- :spacing 1
- (vui-field :value local-target :size 36 :key 'local-target
- :placeholder "~/Backups/"
- :on-change (lambda (value) (funcall invalidate :local-target value)))
- (vui-button "Choose…" :on-click choose-target :disabled busy))))
+ (lambda (width)
+ (vui-flex
+ :width width
+ (vui-flex-item
+ :grow 1
+ (lambda (field-width)
+ (vui-field :value local-target :size (max 8 field-width)
+ :key 'local-target :placeholder "~/Backups/"
+ :on-change (lambda (value)
+ (funcall invalidate :local-target value)))))
+ (vui-button "Choose…" :on-click choose-target :disabled busy)))))
(rsync-ui--field-row
"Folder behavior"
(vui-select :value mode
@@ -604,7 +604,8 @@
(when delete
(vui-warning "Preview deletions carefully. This option changes the destination.")))))))
(when status
- (vui-box (vui-text (car status) :face (cdr status)) :width 66 :align :center))
+ (vui-box (vui-text (car status) :face (cdr status))
+ :width view-width :align :center))
(vui-flex
:width 'fill-column
:justify :space-between
@@ -616,7 +617,7 @@
:face (when (eq stage 'ready) 'success))
(vui-button "Cancel" :on-click cancel-transfer :disabled (not busy) :face 'warning)
(vui-button "Reset" :on-click reset :disabled busy :face 'shadow))
- (vui-muted "p preview · s start · x cancel · r reset · q close")))))
+ (vui-muted "p preview · t transfer · x cancel · r reset · ? help · q close")))))
(defun rsync-ui--invoke (action)
(if-let* ((callback (plist-get rsync-ui--actions action)))
@@ -650,34 +651,44 @@
(defun rsync-ui-next-widget ()
(interactive)
(condition-case nil
- (widget-forward 1)
+ (vui-forward 1)
(error
(goto-char (point-min))
- (widget-forward 1))))
+ (vui-forward 1))))
(defun rsync-ui-previous-widget ()
(interactive)
(condition-case nil
- (widget-backward 1)
+ (vui-backward 1)
(error
(goto-char (point-max))
- (widget-backward 1))))
+ (vui-backward 1))))
+
+(defun rsync-ui-help ()
+ "Show keyboard help for the Rsync wizard."
+ (interactive)
+ (message "Rsync: j/k move, TAB/S-TAB elements, RET/l activate, p/g preview, t transfer, x cancel, r reset, h/q close"))
(defvar rsync-ui-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map vui-mode-map)
- (define-key map (kbd "j") #'rsync-ui-next-widget)
- (define-key map (kbd "k") #'rsync-ui-previous-widget)
+ (define-key map (kbd "j") #'next-line)
+ (define-key map (kbd "k") #'previous-line)
(define-key map (kbd "TAB") #'rsync-ui-next-widget)
(define-key map (kbd "<backtab>") #'rsync-ui-previous-widget)
(define-key map (kbd "RET") #'vui-activate)
+ (define-key map (kbd "l") #'vui-activate)
(define-key map (kbd "p") #'rsync-ui-preview)
+ (define-key map (kbd "g") #'rsync-ui-preview)
+ (define-key map (kbd "t") #'rsync-ui-start)
(define-key map (kbd "s") #'rsync-ui-start)
(define-key map (kbd "x") #'rsync-ui-cancel)
(define-key map (kbd "r") #'rsync-ui-reset)
(define-key map (kbd "o") #'rsync-ui-choose-source)
(define-key map (kbd "d") #'rsync-ui-choose-target)
- (define-key map (kbd "q") #'quit-window)
+ (define-key map (kbd "?") #'rsync-ui-help)
+ (define-key map (kbd "h") #'vui-quit)
+ (define-key map (kbd "q") #'vui-quit)
map))
(define-derived-mode rsync-ui-mode vui-mode "Rsync"
@@ -689,16 +700,21 @@
(with-eval-after-load 'evil
(evil-set-initial-state 'rsync-ui-mode 'normal)
(evil-define-key* '(normal motion) rsync-ui-mode-map
- (kbd "j") #'rsync-ui-next-widget
- (kbd "k") #'rsync-ui-previous-widget
+ (kbd "j") #'next-line
+ (kbd "k") #'previous-line
(kbd "RET") #'vui-activate
+ (kbd "l") #'vui-activate
(kbd "p") #'rsync-ui-preview
+ (kbd "g") #'rsync-ui-preview
+ (kbd "t") #'rsync-ui-start
(kbd "s") #'rsync-ui-start
(kbd "x") #'rsync-ui-cancel
(kbd "r") #'rsync-ui-reset
(kbd "o") #'rsync-ui-choose-source
(kbd "d") #'rsync-ui-choose-target
- (kbd "q") #'quit-window))
+ (kbd "?") #'rsync-ui-help
+ (kbd "h") #'vui-quit
+ (kbd "q") #'vui-quit))
(defun rsync-ui--open (sources target)
(let ((buffer (get-buffer-create rsync-ui-buffer-name)))
diff --git a/lisp/zfs.el b/lisp/zfs.el
index 07bb020..ff88154 100644
--- a/lisp/zfs.el
+++ b/lisp/zfs.el
@@ -82,6 +82,20 @@ Same plist shape as documented there."
((>= (or percent 0) 75) 'warning)
(t 'success)))
+(defun zfs-ui--window-width ()
+ "Return the live ZFS window body width."
+ (if-let* ((window (get-buffer-window (current-buffer) t)))
+ (window-body-width window)
+ (window-width)))
+
+(defun zfs-ui--stat-card (label value face width)
+ "Render a ZFS stat card with LABEL, VALUE, FACE, and WIDTH."
+ (vui-vstack
+ :face 'fringe
+ (vui-box (vui-text value :face `(:inherit ,face :weight bold :height 1.25))
+ :width width :align :center)
+ (vui-box (vui-text label :face 'shadow) :width width :align :center)))
+
(defun zfs--direct-children (datasets parent)
(let ((prefix (concat parent "/")))
(seq-filter (lambda (entry)
@@ -344,7 +358,8 @@ Same plist shape as documented there."
(cadr (assoc mode zfs-ui--copy-modes)))
(defun zfs-ui--copy-preview-line (line)
- (truncate-string-to-width line (max 24 (min 76 (- (window-width) 4))) nil nil "…"))
+ "Return LINE unchanged; the VUI container owns visual truncation."
+ line)
(defun zfs-ui--copy-preview (output)
(let* ((lines (mapcar #'string-trim (split-string (or output "") "\n")))
@@ -467,25 +482,23 @@ Same plist shape as documented there."
(defun zfs-ui--dataset-row (entry depth snap-count)
(let* ((name (car entry))
(short (file-name-nondirectory name))
- (label (concat (make-string (* 2 depth) ?\s)
- (if (> snap-count 0) "▸ " " ")
+ (label (concat (if (> snap-count 0) "▸ " "")
short
(when (> snap-count 0) (format " (%d)" snap-count))))
(encryption (zfs-cli-prop entry 'encryption))
(keystatus (zfs-cli-prop entry 'keystatus)))
- (vui-hstack
- (vui-box (vui-button (truncate-string-to-width label 32) :no-decoration t
- :key (concat "ds:" name)
- :on-click (lambda () (zfs-ui--go 'dataset name)))
- :width 34)
- (vui-box (vui-text (zfs--format-bytes (zfs-cli-prop entry 'used))) :width 9)
- (vui-box (vui-text (zfs--format-bytes (zfs-cli-prop entry 'available)) :face 'shadow) :width 9)
- (vui-box (vui-text (format "%s" (or (zfs-cli-prop entry 'compressratio) "-")) :face 'shadow) :width 7)
- (vui-box (cond
- ((or (null encryption) (equal encryption "off")) (vui-text "·" :face 'shadow))
- ((equal keystatus "available") (vui-text "🔓" :face 'success))
- (t (vui-text "🔒" :face 'warning)))
- :width 4))))
+ (list
+ (vui-vstack
+ :indent (* 2 depth)
+ (vui-button label :no-decoration t :key (concat "ds:" name)
+ :on-click (lambda () (zfs-ui--go 'dataset name))))
+ (vui-text (zfs--format-bytes (zfs-cli-prop entry 'used)))
+ (vui-text (zfs--format-bytes (zfs-cli-prop entry 'available)) :face 'shadow)
+ (vui-text (format "%s" (or (zfs-cli-prop entry 'compressratio) "-")) :face 'shadow)
+ (cond
+ ((or (null encryption) (equal encryption "off")) (vui-text "·" :face 'shadow))
+ ((equal keystatus "available") (vui-text "🔓" :face 'success))
+ (t (vui-text "🔒" :face 'warning))))))
(defun zfs-ui--dataset-rows (entry depth datasets snaps)
(let* ((name (car entry))
@@ -510,19 +523,13 @@ Same plist shape as documented there."
(list :open (vui-with-async-context (zfs-ui--go 'pool name))
:create-child (vui-with-async-context (zfs-ui--go 'create name))))
zfs-ui--row-actions)
- (vui-hstack
- :spacing 1
- (vui-box (vui-button name :no-decoration t
- :key (concat "pool:" name)
- :on-click (lambda () (zfs-ui--go 'pool name)))
- :width 10)
- (vui-box (vui-text (or health "?") :face (zfs--health-face health)) :width 9)
- (vui-box (vui-text (zfs--gauge capacity)
- :face (zfs--capacity-face capacity))
- :width 22)
- (vui-text (format "%d%% · %s free of %s"
- (or capacity 0)
- (zfs--format-bytes free)
+ (list
+ (vui-button name :no-decoration t :key (concat "pool:" name)
+ :on-click (lambda () (zfs-ui--go 'pool name)))
+ (vui-text (or health "?") :face (zfs--health-face health))
+ (vui-text (zfs--gauge capacity) :face (zfs--capacity-face capacity))
+ (vui-text (format "%d%% · %s / %s free"
+ (or capacity 0) (zfs--format-bytes free)
(zfs--format-bytes size))
:face 'shadow))))
@@ -538,18 +545,15 @@ Same plist shape as documented there."
(list :import (vui-with-async-context (zfs-ui--do-import device))))
(list :unlock (vui-with-async-context (zfs-ui--do-unlock device)))))
zfs-ui--row-actions)
- (vui-hstack
- :spacing 1
- (vui-box (vui-text (if unlocked "🔓" "🔒") :face (if unlocked 'warning 'shadow)) :width 3)
- (vui-box (vui-button name :no-decoration t
- :key (concat "lock:" name)
- :on-click (lambda () nil))
- :width 10)
- (vui-box (vui-text (or (plist-get device :model) "?") :face 'shadow) :width 24)
- (vui-box (vui-text (zfs--format-bytes (plist-get device :size)) :face 'shadow) :width 8)
+ (list
+ (vui-text (if unlocked "🔓" "🔒") :face (if unlocked 'warning 'shadow))
+ (vui-button name :no-decoration t :key (concat "lock:" name)
+ :on-click (lambda () nil))
+ (vui-text (or (plist-get device :model) "?") :face 'shadow)
+ (vui-text (zfs--format-bytes (plist-get device :size)) :face 'shadow)
(vui-text (cond
(imported (format "pool %s imported" pool))
- (unlocked (format "unlocked as %s · i imports pool %s" mapper (or pool "?")))
+ (unlocked (format "unlocked as %s · i imports %s" mapper (or pool "?")))
(t "locked · u unlock"))
:face 'shadow))))
@@ -559,53 +563,73 @@ Same plist shape as documented there."
:admin (vui-with-async-context (zfs-ui--go 'admin))
:targets (vui-with-async-context (zfs-ui--go 'targets))))
zfs-ui--row-actions)
- (let ((roots (seq-filter (lambda (entry) (not (string-match-p "/" (car entry)))) datasets)))
- (apply
- #'vui-vstack
+ (let* ((roots (seq-filter (lambda (entry)
+ (not (string-match-p "/" (car entry))))
+ datasets))
+ (width (zfs-ui--window-width))
+ (card-width (max 10 (min 16 (/ (max 44 (- width 6)) 4))))
+ (name-width (max 18 (min 36 (- width 37)))))
+ (vui-vstack
:spacing 1
- (append
- (when locked
- (list (vui-vstack
- :spacing 0
- (vui-heading-3 "Disks")
- (vui-muted "LUKS disks ZFS can use once unlocked")
- (apply #'vui-vstack :spacing 0
- (mapcar (lambda (device) (zfs-ui--locked-disk-row device pools)) locked)))))
- (list
- (vui-vstack
- :spacing 0
- (vui-heading-3 "Utilities")
- (vui-hstack
- :spacing 2
- (vui-box (vui-text "C" :face 'success) :width 3)
- (vui-button "Copy files" :no-decoration t :face 'success
- :on-click (lambda () (zfs-ui--go 'copy)))
- (vui-text "dry-run, copy, or move paths with rsync" :face 'shadow))
- (vui-hstack
- :spacing 2
- (vui-box (vui-text "A" :face 'warning) :width 3)
- (vui-button "Admin actions" :no-decoration t :face 'warning
- :on-click (lambda () (zfs-ui--go 'admin)))
- (vui-text "ack-gated export and format tools" :face 'shadow)))
- (vui-vstack
- :spacing 0
- (vui-heading-3 "Pools")
- (vui-muted "teams of disks; datasets live inside them · RET details")
- (if pools
- (apply #'vui-vstack :spacing 0 (mapcar #'zfs-ui--pool-row pools))
- (vui-muted "No pools found. Kernel module loaded, pools imported?")))
+ (vui-hstack
+ :spacing 2
+ (zfs-ui--stat-card "pools" (number-to-string (length pools)) 'success card-width)
+ (zfs-ui--stat-card "datasets" (number-to-string (length datasets)) 'link card-width)
+ (zfs-ui--stat-card "snapshots" (number-to-string (length snaps)) 'warning card-width)
+ (zfs-ui--stat-card "locked disks" (number-to-string (length locked))
+ (if locked 'warning 'shadow) card-width))
+ (vui-vstack
+ :spacing 0
+ (vui-heading-3 "Utilities")
+ (vui-hstack
+ :spacing 2
+ (vui-button "Copy files" :face 'success
+ :on-click (lambda () (zfs-ui--go 'copy)))
+ (vui-button "Admin actions" :face 'warning
+ :on-click (lambda () (zfs-ui--go 'admin)))
+ (vui-button "Backup targets" :on-click (lambda () (zfs-ui--go 'targets)))))
+ (when locked
(vui-vstack
:spacing 0
- (vui-heading-3 "Datasets")
- (vui-muted "named drawers · RET details · b backup · c new dataset inside")
- (vui-hstack
- (vui-box (vui-text "NAME" :face 'shadow) :width 34)
- (vui-box (vui-text "USED" :face 'shadow) :width 9)
- (vui-box (vui-text "AVAIL" :face 'shadow) :width 9)
- (vui-box (vui-text "RATIO" :face 'shadow) :width 7)
- (vui-box (vui-text "ENC" :face 'shadow) :width 4))
- (apply #'vui-vstack :spacing 0
- (mapcan (lambda (root) (zfs-ui--dataset-rows root 0 datasets snaps)) roots))))))))
+ (vui-heading-3 "Disks")
+ (vui-muted "LUKS disks ZFS can use once unlocked")
+ (vui-table
+ :sticky-header t
+ :columns '((:header "" :width 3)
+ (:header "Device" :width 12 :truncate t)
+ (:header "Model" :width 24 :grow t :truncate t)
+ (:header "Size" :width 9 :align :right)
+ (:header "State" :width 26 :grow t :truncate t))
+ :rows (mapcar (lambda (device)
+ (zfs-ui--locked-disk-row device pools))
+ locked))))
+ (vui-vstack
+ :spacing 0
+ (vui-heading-3 "Pools")
+ (vui-muted "Teams of disks; RET opens health and VDEV details")
+ (if pools
+ (vui-table
+ :sticky-header t
+ :columns '((:header "Pool" :width 12 :grow t :truncate t)
+ (:header "Health" :width 9)
+ (:header "Capacity" :width 20)
+ (:header "Space" :width 26 :grow t :truncate t))
+ :rows (mapcar #'zfs-ui--pool-row pools))
+ (vui-muted "No pools found. Kernel module loaded, pools imported?")))
+ (vui-vstack
+ :spacing 0
+ (vui-heading-3 "Datasets")
+ (vui-muted "RET details · b backup · c create child")
+ (vui-table
+ :sticky-header t
+ :columns `((:header "Dataset" :width ,name-width :grow t :truncate t)
+ (:header "Used" :width 9 :align :right)
+ (:header "Available" :width 9 :align :right)
+ (:header "Ratio" :width 7 :align :right)
+ (:header "Enc" :width 4 :align :center))
+ :rows (mapcan (lambda (root)
+ (zfs-ui--dataset-rows root 0 datasets snaps))
+ roots))))))
(defun zfs-ui--property-row (entry property)
(let* ((raw (zfs-cli-prop entry (intern property)))
@@ -617,10 +641,9 @@ Same plist shape as documented there."
((numberp raw) (zfs--format-bytes raw))
(t (format "%s" (or raw "-")))))
(gloss (cdr (assoc property zfs-ui--property-glossary))))
- (vui-hstack
- (vui-box (vui-text property :face 'shadow) :width 18)
- (vui-box (vui-text value) :width 22)
- (vui-text (or gloss "") :face 'shadow))))
+ (list (vui-text property :face 'shadow)
+ (vui-text value)
+ (vui-text (or gloss "") :face 'shadow))))
(defun zfs-ui--snapshot-row (snap dataset host)
(let* ((full-name (car snap))
@@ -630,12 +653,10 @@ Same plist shape as documented there."
:rollback (vui-with-async-context (zfs-ui--go 'rollback full-name))
:backup (vui-with-async-context (zfs-ui--go 'backup (cons dataset full-name)))))
zfs-ui--row-actions)
- (vui-hstack
- (vui-box (vui-button (concat "@" leaf) :no-decoration t :face 'shadow
- :key (concat "snap:" full-name)
- :on-click (lambda () nil))
- :width 38)
- (vui-box (vui-text (zfs--format-bytes (zfs-cli-prop snap 'used)) :face 'shadow) :width 9)
+ (list
+ (vui-button (concat "@" leaf) :no-decoration t :face 'shadow
+ :key (concat "snap:" full-name) :on-click (lambda () nil))
+ (vui-text (zfs--format-bytes (zfs-cli-prop snap 'used)) :face 'shadow)
(vui-text (zfs--format-ago (zfs-cli-prop snap 'creation)) :face 'shadow))))
(defun zfs-ui--page-dataset (name host datasets snaps)
@@ -661,8 +682,12 @@ Same plist shape as documented there."
(vui-muted (format "Everything in this dataset and its snapshots uses %s."
(zfs--format-bytes (zfs-cli-prop entry 'used))))
(vui-text "s snapshot · b backup · c new dataset inside · m mount" :face 'shadow)
- (apply #'vui-vstack :spacing 0
- (mapcar (lambda (property) (zfs-ui--property-row entry property))
+ (vui-table
+ :sticky-header t
+ :columns '((:header "Property" :width 18 :truncate t)
+ (:header "Value" :width 22 :grow t :truncate t)
+ (:header "Meaning" :width 48 :grow t :truncate t))
+ :rows (mapcar (lambda (property) (zfs-ui--property-row entry property))
'("used" "referenced" "usedbysnapshots" "usedbychildren" "available"
"compressratio" "encryption" "keystatus" "mountpoint" "recordsize"
"quota" "creation" "mounted"))))
@@ -674,16 +699,21 @@ Same plist shape as documented there."
(list (vui-heading-3 (format "Snapshots (%d)" (length own-snaps)))
(vui-muted "frozen moments · d diff · b backup · R rollback opens an ack page"))
(if own-snaps
- (mapcar (lambda (snap) (zfs-ui--snapshot-row snap name host)) own-snaps)
+ (list
+ (vui-table
+ :sticky-header t
+ :columns '((:header "Snapshot" :width 38 :grow t :truncate t)
+ (:header "Used" :width 9 :align :right)
+ (:header "Age" :width 12))
+ :rows (mapcar (lambda (snap) (zfs-ui--snapshot-row snap name host)) own-snaps)))
(list (vui-muted "none yet — press s to freeze this moment")))))))))))
(defun zfs-ui--vdev-row (vdev depth)
- (vui-hstack
- (vui-box (vui-text (concat (make-string (* 2 depth) ?\s) (plist-get vdev :name))) :width 40)
- (vui-box (vui-text (or (plist-get vdev :state) "?")
- :face (zfs--health-face (plist-get vdev :state)))
- :width 9)
- (vui-text (format "%s/%s/%s errors"
+ (list
+ (vui-vstack :indent (* 2 depth) (vui-text (plist-get vdev :name)))
+ (vui-text (or (plist-get vdev :state) "?")
+ :face (zfs--health-face (plist-get vdev :state)))
+ (vui-text (format "%s/%s/%s"
(or (plist-get vdev :read-errors) 0)
(or (plist-get vdev :write-errors) 0)
(or (plist-get vdev :checksum-errors) 0))
@@ -719,8 +749,12 @@ Same plist shape as documented there."
(vui-vstack
:spacing 0
(vui-heading-3 "Devices")
- (apply #'vui-vstack :spacing 0
- (mapcar (lambda (vdev) (zfs-ui--vdev-row vdev 0)) vdevs))))))))
+ (vui-table
+ :sticky-header t
+ :columns '((:header "VDEV" :width 40 :grow t :truncate t)
+ (:header "State" :width 9)
+ (:header "Read/Write/Checksum" :width 20 :align :right))
+ :rows (mapcar (lambda (vdev) (zfs-ui--vdev-row vdev 0)) vdevs))))))))
(defun zfs-ui--form-row (label content &optional gloss)
(vui-hstack
@@ -1078,16 +1112,19 @@ Same plist shape as documented there."
(vui-heading-3 "Backup targets")
(vui-muted (format "Stored as plain elisp data in %s — edit by hand or here." zfs-targets-file)))
(if targets
- (list (apply
- #'vui-vstack
- :spacing 0
- (mapcar (lambda (target)
- (vui-hstack
- :spacing 2
- (vui-box (vui-text (plist-get target :name)) :width 16)
- (vui-box (vui-text (or (plist-get target :ssh) "this machine") :face 'shadow) :width 24)
- (vui-text (plist-get target :dataset) :face 'shadow)))
- targets)))
+ (list
+ (vui-table
+ :sticky-header t
+ :columns '((:header "Name" :width 16 :grow t :truncate t)
+ (:header "Host" :width 24 :grow t :truncate t)
+ (:header "Dataset prefix" :width 32 :grow t :truncate t))
+ :rows (mapcar (lambda (target)
+ (list
+ (vui-text (plist-get target :name))
+ (vui-text (or (plist-get target :ssh) "this machine")
+ :face 'shadow)
+ (vui-text (plist-get target :dataset) :face 'shadow)))
+ targets)))
(list (vui-muted "No targets yet — add one below.")))
(list
(vui-vstack
@@ -1139,7 +1176,7 @@ Same plist shape as documented there."
(setq zfs-ui--back-action
(vui-with-async-context
(if (eq page 'overview)
- (bury-buffer)
+ (vui-quit)
(zfs-ui--go 'overview))))
(let ((content
(cond
@@ -1175,11 +1212,11 @@ Same plist shape as documented there."
(vui-text " "))
content
(vui-text (pcase page
- ('overview "RET open · C copy · A admin · u unlock · T targets · r refresh · q quit")
- ('dataset "q back · s snapshot · b backup · c new dataset · d diff · R rollback · r refresh")
- ('pool "q back · 1 scrub start · 2 scrub stop · x export · r refresh")
- ('backup "q back · click a target · r refresh")
- (_ "q back · r refresh"))
+ ('overview "j/k move · RET/l open · C copy · A admin · u unlock · T targets · g refresh · ? help · q quit")
+ ('dataset "h/q back · s snapshot · b backup · c new dataset · d diff · R rollback · g refresh")
+ ('pool "h/q back · 1 scrub start · 2 scrub stop · x export · g refresh")
+ ('backup "h/q back · choose a target · g refresh")
+ (_ "h/q back · g refresh · ? help"))
:face 'shadow)))))
(defun zfs-ui--key-on-line ()
@@ -1281,9 +1318,18 @@ Same plist shape as documented there."
(interactive)
(zfs-ui--invoke :scrub-stop))
+(defun zfs-ui-help ()
+ "Show keyboard help for the ZFS interface."
+ (interactive)
+ (message "ZFS: j/k move, TAB/S-TAB elements, RET/l activate, h/q back or close, g refresh, s snapshot, b backup, C copy, A admin, ? help"))
+
(define-key zfs-mode-map (kbd "RET") #'zfs-ui-open)
(define-key zfs-mode-map (kbd "<return>") #'zfs-ui-open)
(define-key zfs-mode-map (kbd "l") #'zfs-ui-open)
+(define-key zfs-mode-map (kbd "j") #'next-line)
+(define-key zfs-mode-map (kbd "k") #'previous-line)
+(define-key zfs-mode-map (kbd "TAB") #'vui-forward)
+(define-key zfs-mode-map (kbd "<backtab>") #'vui-backward)
(define-key zfs-mode-map (kbd "q") #'zfs-ui-back)
(define-key zfs-mode-map (kbd "h") #'zfs-ui-back)
(define-key zfs-mode-map (kbd "<left>") #'zfs-ui-back)
@@ -1306,6 +1352,7 @@ Same plist shape as documented there."
(define-key zfs-mode-map (kbd "x") #'zfs-ui-export)
(define-key zfs-mode-map (kbd "1") #'zfs-ui-scrub-start)
(define-key zfs-mode-map (kbd "2") #'zfs-ui-scrub-stop)
+(define-key zfs-mode-map (kbd "?") #'zfs-ui-help)
(with-eval-after-load 'evil
(evil-set-initial-state 'zfs-mode 'normal)
@@ -1313,6 +1360,8 @@ Same plist shape as documented there."
(kbd "RET") #'zfs-ui-open
(kbd "<return>") #'zfs-ui-open
(kbd "l") #'zfs-ui-open
+ (kbd "j") #'next-line
+ (kbd "k") #'previous-line
(kbd "q") #'zfs-ui-back
(kbd "h") #'zfs-ui-back
(kbd "<left>") #'zfs-ui-back
@@ -1334,7 +1383,8 @@ Same plist shape as documented there."
(kbd "F") #'zfs-ui-format
(kbd "x") #'zfs-ui-export
(kbd "1") #'zfs-ui-scrub-start
- (kbd "2") #'zfs-ui-scrub-stop))
+ (kbd "2") #'zfs-ui-scrub-stop
+ (kbd "?") #'zfs-ui-help))
;;;###autoload
(defun zfs ()
diff --git a/local.el.example b/local.el.example
index 1fc40bc..0d881b6 100644
--- a/local.el.example
+++ b/local.el.example
@@ -10,5 +10,4 @@
;; so uncommitted edits are live after a reload/restart.
(setq my/local-packages
- '((kitty-graphics . "~/code/kitty-graphics.el")
- (garden . "~/garden")))
+ '((kitty-graphics . "~/code/kitty-graphics.el")))
diff --git a/themes/tinty-theme.el b/themes/tinty-theme.el
index cb04fa2..2aee854 100644
--- a/themes/tinty-theme.el
+++ b/themes/tinty-theme.el
@@ -65,67 +65,67 @@
(deftheme tinty "Noctalia theme with proper color contrast.")
(let* (;; Tinted8 UI surface colors
- (ui-bg "#f5f5f5")
- (ui-fg "#1f1f1f")
- (ui-fg-dim "#635d97")
+ (ui-bg "#282a36")
+ (ui-fg "#f8f8f2")
+ (ui-fg-dim "#6272a4")
;; Use dim foreground for borders/muted chrome so they stay visible in
;; light variants (the raw border color is nearly white on latte).
- (ui-border "#635d97")
- (ui-line-highlight "#dcdeef")
- (ui-bg-dark "#ece9df")
- (ui-bg-light "#ffffff")
+ (ui-border "#6272a4")
+ (ui-line-highlight "#353747")
+ (ui-bg-dark "#191a21")
+ (ui-bg-light "#343746")
;; Tinted8 UI accent/status colors
- (ui-accent "#644ac9")
- (ui-link "#036a96")
- (ui-info "#036a96")
- (ui-error "#cb3a2a")
- (ui-success "#14720a")
- (ui-warning "#a34d14")
+ (ui-accent "#bd93f9")
+ (ui-link "#8be9fd")
+ (ui-info "#8be9fd")
+ (ui-error "#ff5555")
+ (ui-success "#50fa7b")
+ (ui-warning "#ffb86c")
;; Tinted8 UI container colors (for BACKGROUNDS only)
- (ui-selection-bg "#cfcfde")
- (ui-selection-fg "#1f1f1f")
- (ui-button-bg "#ece9df")
- (ui-button-fg "#1f1f1f")
- (ui-tooltip-bg "#efeddc")
- (ui-tooltip-fg "#1f1f1f")
- (ui-active-text-bg "#cfcfde")
- (ui-active-text-fg "#000000")
- (ui-search-bg "#a34d14")
- (ui-search-fg "#f5f5f5")
+ (ui-selection-bg "#44475a")
+ (ui-selection-fg "#f8f8f2")
+ (ui-button-bg "#343746")
+ (ui-button-fg "#f8f8f2")
+ (ui-tooltip-bg "#343746")
+ (ui-tooltip-fg "#f8f8f2")
+ (ui-active-text-bg "#44475a")
+ (ui-active-text-fg "#ffffff")
+ (ui-search-bg "#ffb86c")
+ (ui-search-fg "#282a36")
;; Tinted8 syntax colors
;; Comments use dim foreground instead of the raw syntax-comment gray,
;; which becomes almost invisible in light variants.
- (syntax-comment "#635d97")
- (syntax-constant "#a34d14")
- (syntax-function "#14720a")
- (syntax-keyword "#a3134d")
- (syntax-string "#836e15")
- (syntax-type "#036a96")
+ (syntax-comment "#6272a4")
+ (syntax-constant "#ffb86c")
+ (syntax-function "#50fa7b")
+ (syntax-keyword "#ff79c6")
+ (syntax-string "#f1fa8c")
+ (syntax-type "#8be9fd")
;; Give variables/imported names a distinct hue instead of blending into
;; the default foreground.
- (syntax-variable "#1f1f1f")
- (syntax-warning "#d74c3d")
- (syntax-preprocessor "#462da9")
- (syntax-annotation "#836e15")
- (syntax-attribute "#036a96")
- (syntax-number "#a34d14")
- (syntax-operator "#036a96")
+ (syntax-variable "#f8f8f2")
+ (syntax-warning "#ff6e6e")
+ (syntax-preprocessor "#9954fb")
+ (syntax-annotation "#f1fa8c")
+ (syntax-attribute "#8be9fd")
+ (syntax-number "#ffb86c")
+ (syntax-operator "#8be9fd")
;; Raw palette colors for rainbow headings, delimiters, terminal faces,
;; and anywhere else we need a predictable hue cycle.
- (pal-red "#cb3a2a")
- (pal-orange "#a34d14")
- (pal-yellow "#836e15")
- (pal-green "#14720a")
- (pal-cyan "#036a96")
- (pal-blue "#644ac9")
- (pal-magenta "#a3134d")
- (pal-gray "#635d97")
- (pal-white "#f5f5f5")
- (pal-black "#1f1f1f")
+ (pal-red "#ff5555")
+ (pal-orange "#ffb86c")
+ (pal-yellow "#f1fa8c")
+ (pal-green "#50fa7b")
+ (pal-cyan "#8be9fd")
+ (pal-blue "#bd93f9")
+ (pal-magenta "#ff79c6")
+ (pal-gray "#6272a4")
+ (pal-white "#f8f8f2")
+ (pal-black "#21222c")
(h1 (if tinty-scale-headings tinty-height-1 1.0))
(h2 (if tinty-scale-headings tinty-height-2 1.0))
(h3 (if tinty-scale-headings tinty-height-3 1.0))
@@ -344,13 +344,13 @@
;; === Terminal colors - mapped to the palette so they stay correct in both variants ===
`(term-color-black ((t (:foreground ,ui-bg-dark :background ,ui-bg-dark))))
- `(term-color-red ((t (:foreground "#cb3a2a" :background "#cb3a2a"))))
- `(term-color-green ((t (:foreground "#14720a" :background "#14720a"))))
- `(term-color-yellow ((t (:foreground "#836e15" :background "#836e15"))))
- `(term-color-blue ((t (:foreground "#644ac9" :background "#644ac9"))))
- `(term-color-magenta ((t (:foreground "#a3134d" :background "#a3134d"))))
- `(term-color-cyan ((t (:foreground "#036a96" :background "#036a96"))))
- `(term-color-white ((t (:foreground "#f5f5f5" :background "#f5f5f5"))))
+ `(term-color-red ((t (:foreground "#ff5555" :background "#ff5555"))))
+ `(term-color-green ((t (:foreground "#50fa7b" :background "#50fa7b"))))
+ `(term-color-yellow ((t (:foreground "#f1fa8c" :background "#f1fa8c"))))
+ `(term-color-blue ((t (:foreground "#bd93f9" :background "#bd93f9"))))
+ `(term-color-magenta ((t (:foreground "#ff79c6" :background "#ff79c6"))))
+ `(term-color-cyan ((t (:foreground "#8be9fd" :background "#8be9fd"))))
+ `(term-color-white ((t (:foreground "#f8f8f2" :background "#f8f8f2"))))
;; === EShell ===
`(eshell-prompt ((t (:foreground ,ui-accent :weight bold))))
@@ -556,13 +556,13 @@
`(ansi-color-magenta ((t (:foreground ,pal-magenta :background ,pal-magenta))))
`(ansi-color-cyan ((t (:foreground ,pal-cyan :background ,pal-cyan))))
`(ansi-color-white ((t (:foreground ,pal-white :background ,pal-white))))
- `(ansi-color-bright-black ((t (:foreground "#2c2b31" :background "#2c2b31"))))
- `(ansi-color-bright-red ((t (:foreground "#d74c3d" :background "#d74c3d"))))
- `(ansi-color-bright-green ((t (:foreground "#198d0c" :background "#198d0c"))))
- `(ansi-color-bright-yellow ((t (:foreground "#9e841a" :background "#9e841a"))))
- `(ansi-color-bright-blue ((t (:foreground "#7862d0" :background "#7862d0"))))
- `(ansi-color-bright-magenta ((t (:foreground "#bf185a" :background "#bf185a"))))
- `(ansi-color-bright-cyan ((t (:foreground "#047fb4" :background "#047fb4"))))
+ `(ansi-color-bright-black ((t (:foreground "#6272a4" :background "#6272a4"))))
+ `(ansi-color-bright-red ((t (:foreground "#ff6e6e" :background "#ff6e6e"))))
+ `(ansi-color-bright-green ((t (:foreground "#69ff94" :background "#69ff94"))))
+ `(ansi-color-bright-yellow ((t (:foreground "#ffffa5" :background "#ffffa5"))))
+ `(ansi-color-bright-blue ((t (:foreground "#d6acff" :background "#d6acff"))))
+ `(ansi-color-bright-magenta ((t (:foreground "#ff92df" :background "#ff92df"))))
+ `(ansi-color-bright-cyan ((t (:foreground "#a4ffff" :background "#a4ffff"))))
`(ansi-color-bright-white ((t (:foreground "#ffffff" :background "#ffffff"))))
;; === Anzu ===
@@ -1353,13 +1353,13 @@
;; === Term (extended) ===
`(term ((t (:foreground ,ui-fg :background ,ui-bg))))
`(term-bold ((t (:weight bold))))
- `(term-color-bright-black ((t (:foreground "#2c2b31" :background "#2c2b31"))))
- `(term-color-bright-red ((t (:foreground "#d74c3d" :background "#d74c3d"))))
- `(term-color-bright-green ((t (:foreground "#198d0c" :background "#198d0c"))))
- `(term-color-bright-yellow ((t (:foreground "#9e841a" :background "#9e841a"))))
- `(term-color-bright-blue ((t (:foreground "#7862d0" :background "#7862d0"))))
- `(term-color-bright-magenta ((t (:foreground "#bf185a" :background "#bf185a"))))
- `(term-color-bright-cyan ((t (:foreground "#047fb4" :background "#047fb4"))))
+ `(term-color-bright-black ((t (:foreground "#6272a4" :background "#6272a4"))))
+ `(term-color-bright-red ((t (:foreground "#ff6e6e" :background "#ff6e6e"))))
+ `(term-color-bright-green ((t (:foreground "#69ff94" :background "#69ff94"))))
+ `(term-color-bright-yellow ((t (:foreground "#ffffa5" :background "#ffffa5"))))
+ `(term-color-bright-blue ((t (:foreground "#d6acff" :background "#d6acff"))))
+ `(term-color-bright-magenta ((t (:foreground "#ff92df" :background "#ff92df"))))
+ `(term-color-bright-cyan ((t (:foreground "#a4ffff" :background "#a4ffff"))))
`(term-color-bright-white ((t (:foreground "#ffffff" :background "#ffffff"))))
;; === Transient ===