diff options
| author | cashmere <cashmere@autistici.org> | 2026-08-23 19:19:17 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-08-23 19:19:17 +0200 |
| commit | 15def4cc1edbd5a80973fa9c5bba35d5eb32c340 (patch) | |
| tree | a9172e36e74a6691738b7d5012289cec1767241d | |
| parent | e8b0abed0711cefcd0c06b8e12e3a7a9224fb329 (diff) | |
| download | emacs-15def4cc1edbd5a80973fa9c5bba35d5eb32c340.tar.gz | |
feat: refresh Emacs UI and lock Elpaca packages
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | config.el | 1190 | ||||
| -rw-r--r-- | config.org | 1305 | ||||
| -rw-r--r-- | elpaca-lock.eld | 1602 | ||||
| -rw-r--r-- | init.el | 6 | ||||
| -rw-r--r-- | lisp/denote-menu.el | 297 | ||||
| -rw-r--r-- | lisp/garden-connect.el | 365 | ||||
| -rw-r--r-- | lisp/garden-core.el | 372 | ||||
| -rw-r--r-- | lisp/garden-dashboard.el | 461 | ||||
| -rw-r--r-- | lisp/garden-fleet.el | 464 | ||||
| -rw-r--r-- | lisp/garden-icons.el | 64 | ||||
| -rw-r--r-- | lisp/garden-links.el | 29 | ||||
| -rw-r--r-- | lisp/garden-preview.el | 111 | ||||
| -rw-r--r-- | lisp/garden-publish.el | 450 | ||||
| -rw-r--r-- | lisp/my-mode-line.el | 99 | ||||
| -rw-r--r-- | lisp/rsync-ui.el | 142 | ||||
| -rw-r--r-- | lisp/zfs.el | 292 | ||||
| -rw-r--r-- | local.el.example | 3 | ||||
| -rw-r--r-- | themes/tinty-theme.el | 134 |
19 files changed, 5943 insertions, 1444 deletions
@@ -100,5 +100,6 @@ dirvish/thumbnails /elfeed/index /org-persist/ *.eld +!/elpaca-lock.eld /elfeed/ /cache/ @@ -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" @@ -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"))) @@ -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 "\\(?:-----\\|–—\\)?BEGIN PGP\\(?:.\\|\n\\)*?END PGP[^&\n-]*\\(?:-----\\|–—\\)?" 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 === |
