;;; config.el --- Emacs-Kick --- A feature rich Emacs config for (neo)vi(m)mers -*- lexical-binding: t; -*- ;; (pixel-scroll-precision-mode 1) (setq pgtk-wait-for-event-timeout 0.001) (setq package-enable-at-startup nil ;; Local modules are edited independently of their optional byte-code files, so never let an older .elc silently shadow newer source. load-prefer-newer t) ;; (setq-default mode-line-format t) ;; disabled: boolean t is not valid for mode-line-format (add-to-list 'default-frame-alist '(undecorated . t)) ;; GC + file-name-handler-alist tuning lives in early-init.el now. (setq create-lockfiles nil) (setq make-backup-files nil) (setq backup-directory-alist `(("." . ,(expand-file-name "backup" user-emacs-directory)))) (setq tramp-backup-directory-alist backup-directory-alist) (setq backup-by-copying-when-linked t) (setq backup-by-copying t) ; Backup by copying rather renaming (setq delete-old-versions t) ; Delete excess backup versions silently (setq version-control t) ; Use version numbers for backup files (setq kept-new-versions 5) (setq kept-old-versions 5) (eval-and-compile (defvar my/local-packages nil "Alist of (PACKAGE . DIRECTORY) loaded from a local checkout. Set per-host in the gitignored `local.el'.") (defvar cashmere/frame-alpha nil "Per-host opacity for the entire frame, or nil to keep foreground elements opaque.") (defvar cashmere/frame-alpha-background nil "Per-host background-only opacity, or nil for the Emacs default.") (defvar cashmere/font-family "Maple Mono NF" "Per-host default monospace font family.") (defvar cashmere/font-features '("cv01" "cv32" "cv34" "cv36" "cv63" "cv64" "cv66") "OpenType features enabled for the default monospace font.") (defvar cashmere/font-height 180 "Per-host default font height in tenths of a point.") (defvar cashmere/theme nil "Per-host theme to load, or nil for no theme.") (load (locate-user-emacs-file "local.el") 'noerror 'nomessage)) ;; Apply appearance settings at runtime only. Keeping these outside `eval-and-compile' avoids loading themes or mutating the initial daemon frame while native compilation is inspecting the configuration. Whole-frame opacity takes precedence because combining both alpha modes would dim the frame twice. (cond (cashmere/frame-alpha (add-to-list 'default-frame-alist '(alpha-background . 100)) (add-to-list 'default-frame-alist `(alpha . ,cashmere/frame-alpha)) (when (display-graphic-p) (set-frame-parameter nil 'alpha-background 100) (set-frame-parameter nil 'alpha cashmere/frame-alpha))) (cashmere/frame-alpha-background (add-to-list 'default-frame-alist '(alpha . 100)) (add-to-list 'default-frame-alist `(alpha-background . ,cashmere/frame-alpha-background)) (when (display-graphic-p) (set-frame-parameter nil 'alpha 100) (set-frame-parameter nil 'alpha-background cashmere/frame-alpha-background)))) (when cashmere/theme (add-to-list 'custom-theme-load-path (expand-file-name "themes" user-emacs-directory)) (unless (memq cashmere/theme custom-enabled-themes) (load-theme cashmere/theme t))) (dolist (entry my/local-packages) (add-to-list 'load-path (expand-file-name (cdr entry)))) (add-to-list 'load-path (expand-file-name "lisp" user-emacs-directory)) (defun my/elpaca-skip-local (orig name _keyword args) (if (assq name my/local-packages) nil (funcall orig name _keyword args))) (advice-add 'use-package-normalize/:ensure :around #'my/elpaca-skip-local) (require 'my-tinty) (setq my/tinty-theme-file (expand-file-name "themes/tinty-theme.el" user-emacs-directory) my/tinty-reload-delay 0.15) (my/tinty-auto-reload-mode 1) (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 (column-number-mode t) (auto-save-default nil) ;; Disable automatic saving of buffers. (create-lockfiles nil) ;; Prevent the creation of lock files when editing. (delete-by-moving-to-trash t) ;; Move deleted files to the trash instead of permanently deleting them. (delete-selection-mode 1) ;; Enable replacing selected text with typed text. (display-line-numbers-type 'relative) ;; Use relative line numbering in programming modes. (global-auto-revert-non-file-buffers t) ;; Automatically refresh non-file buffers. (history-length 25) ;; Set the length of the command history. (inhibit-startup-message t) ;; Disable the startup message when Emacs launches. (initial-scratch-message "") ;; Clear the initial message in the *scratch* buffer. (ispell-dictionary "en_US") ;; Set the default dictionary for spell checking. (text-mode-ispell-word-completion nil) ;; Stop ispell capf in text/org buffers (no wordlist on NixOS → corfu errors). (make-backup-files nil) ;; Disable creation of backup files. ;; (pixel-scroll-precision-mode t) ;; Enable precise pixel scrolling. (pixel-scroll-precision-use-momentum nil) ;; Disable momentum scrolling for pixel precision. (ring-bell-function 'ignore) ;; Disable the audible bell. (split-width-threshold 300) ;; Prevent automatic window splitting if the window width exceeds 300 pixels. (switch-to-buffer-obey-display-actions t) ;; Make buffer switching respect display actions. (tab-always-indent 'complete) ;; Make the TAB key complete text instead of just indenting. (tab-width 4) ;; Set the tab width to 4 spaces. (treesit-font-lock-level 4) ;; Use advanced font locking for Treesit mode. (truncate-lines t) ;; Enable line truncation to avoid wrapping long lines. (use-dialog-box nil) ;; Disable dialog boxes in favor of minibuffer prompts. (use-short-answers t) ;; Use short answers in prompts for quicker responses (y instead of yes) (warning-minimum-level :emergency) ;; Set the minimum level of warnings to display. :hook ;; Add hooks to enable specific features in certain modes. (prog-mode . display-line-numbers-mode) (conf-mode . display-line-numbers-mode) (markdown-mode . display-line-numbers-mode) (text-mode . visual-line-mode) ;; Soft-wrap prose/text files at window edge. (conf-mode . visual-line-mode) ;; Soft-wrap .conf and similar config files. :config (add-to-list 'custom-theme-load-path user-emacs-directory) (setq custom-safe-themes t) ;; By default emacs gives you access to a lot of *special* buffers, while navigating with [b and ]b, ;; this might be confusing for newcomers. This settings make sure ]b and [b will always load a ;; file buffer. To see all buffers use SPC, b l, or b i. (defun skip-these-buffers (_window buffer _bury-or-kill) "Function for `switch-to-prev-buffer-skip'." (string-match "\\*[^*]+\\*" (buffer-name buffer))) (setq switch-to-prev-buffer-skip 'skip-these-buffers) (setq switch-to-prev-buffer-skip-regexp '("\\*[^*]+\\*" "^magit" "^\\*magit")) ;; Configure font settings based on the operating system. ;; Ok, this kickstart is meant to be used on the terminal, not on GUI. ;; But without this, I fear you could start Graphical Emacs and be sad :( ;; (set-face-attribute 'default nil :family "Maple Mono NF" :height 150) ;; (when (eq system-type 'darwin) ;; Check if the system is macOS. ;; (setq mac-command-modifier 'meta) ;; Set the Command key to act as the Meta key. ;; (set-face-attribute 'default nil :family "Fragment Mono" :height 130)) ;; Save manual customizations to a separate file instead of cluttering `init.el'. ;; You can M-x customize, M-x customize-group, or M-x customize-themes, etc. ;; The saves you do manually using the Emacs interface would overwrite this file. ;; The following makes sure those customizations are in a separate file. (setq custom-file (locate-user-emacs-file "custom-vars.el")) ;; Specify the custom file path. (load custom-file 'noerror 'nomessage) ;; Load the custom file quietly, ignoring errors. ;; Makes Emacs vertical divisor the symbol │ instead of |. (unless standard-display-table (setq standard-display-table (make-display-table))) (set-display-table-slot standard-display-table 'vertical-border (make-glyph-code ?│)) :init ;; Initialization settings that apply before the package is loaded. (tool-bar-mode -1) (menu-bar-mode -1) (scroll-bar-mode -1) (add-to-list 'default-frame-alist '(vertical-scroll-bars . nil)) (add-to-list 'default-frame-alist '(horizontal-scroll-bars . nil)) ;; (global-hl-line-mode 1) ;; Highlight the current line (add-hook 'org-mode-hook (lambda () (setq-local global-hl-line-mode nil))) ;; hl-line repaints wipe kitty-graphics scaled headings (add-hook 'markdown-mode-hook (lambda () (setq-local global-hl-line-mode nil))) ;; same conflict for kitty-graphics markdown headings (global-auto-revert-mode 1) ;; Enable global auto-revert mode to keep buffers up to date with their corresponding files. (setq-default indent-tabs-mode nil) (when (daemonp) (recentf-mode 1) ;; Enable tracking of recently opened files. (savehist-mode 1) ;; Enable saving of command history. (save-place-mode 1)) ;; Enable saving the place in files for easier return. (winner-mode 1) ;; Enable winner mode to easily undo window configuration changes. (xterm-mouse-mode 1) ;; Enable mouse support in terminal mode. (file-name-shadow-mode 1) ;; Enable shadowing of filenames for clarity. (repeat-mode 1) ;; Enable repeat key after key chord ;;oding system for files to UTF-8. (modify-coding-system-alist 'file "" 'utf-8) ;; Add a hook to run code after Emacs has fully initialized. (add-hook 'after-init-hook (lambda () (message "Emacs has fully loaded. This code runs after startup.") ;; Insert a welcome message in the *scratch* buffer displaying loading time and activated packages. (with-current-buffer (get-buffer-create "*scratch*") (insert (format ";; Welcome to Emacs! ;; ;; Loading time : %s ;; Packages : %s " (emacs-init-time) (length (elpaca--queued)))))))) (defcustom ek-use-nerd-fonts t "Configuration for using Nerd Fonts Symbols." :type 'boolean :group 'appearance) (use-package bookmark :ensure nil :custom (bookmark-default-file (locate-user-emacs-file "bookmarks")) (bookmark-save-flag 1) (bookmark-watch-bookmark-file 'silent)) (when (eq system-type 'berkeley-unix) ;; Start the Emacs server so emacsclient (and pinentry-emacs) can connect. (require 'server) (unless (server-running-p) (server-start)) ;; Handler for pinentry-emacs: GPG passphrase prompts in the minibuffer. (defun pinentry-emacs (desc prompt _ok _error) (read-passwd (concat (replace-regexp-in-string "%22" "\"" (replace-regexp-in-string "%0A" "\n" desc)) prompt ": "))) ;; Open a file as root via doas. (defun my/find-file-doas (filename) "Open FILENAME as root using doas." (interactive "FFile (with doas): ") (find-file (concat "/doas::" (expand-file-name filename))))) (defun my/compilation-buffer-p (buffer-name _action) "Match any compilation-derived buffer (compile, grep, …) for display." (with-current-buffer buffer-name (derived-mode-p 'compilation-mode))) (defun my/compile-or-recompile () "Recompile with the last command if a ghostel compilation ran before, else prompt. Selects the compilation window so the cursor lands in it." (interactive) (if (get-buffer ghostel-compile-buffer-name) (ghostel-recompile) (call-interactively #'ghostel-compile)) (let ((buffer (get-buffer ghostel-compile-buffer-name))) (when buffer (pop-to-buffer buffer)))) (defun my/focus-async-shell-window (window) "Focus the async-shell window and make q bury it without killing the process." (select-window window) (with-current-buffer (window-buffer window) (my/async-shell-command-bindings) (evil-normal-state))) (use-package window :ensure nil :custom (display-buffer-alist '( (my/compilation-buffer-p (display-buffer-reuse-window display-buffer-below-selected) (window-height . 0.3) (body-function . select-window)) ("\\*Async Shell Command\\*" (display-buffer-reuse-window display-buffer-below-selected) (window-height . 0.3) (body-function . my/focus-async-shell-window)) ("\\*\\(Backtrace\\|Warnings\\|Compile-Log\\|[Hh]elp\\|Messages\\|Bookmark List\\|Occur\\)\\*" (display-buffer-in-side-window) (window-height . 0.25) (side . bottom) (slot . 0)) ("\\*\\(eldoc\\)\\*" (display-buffer-in-side-window) (window-height . 0.25) (side . bottom) (slot . 1) (window-parameters . ((no-delete-other-windows . t))) (body-function . (lambda (window) (select-window window)))) ("\\*\\(Flymake diagnostics\\|xref\\|ivy\\|Swiper\\|Completions\\)" (display-buffer-in-side-window) (window-height . 0.25) (side . bottom) (slot . 1))))) (use-package dired :ensure nil :custom (dired-listing-switches "-lah --group-directories-first") (dired-dwim-target t) (dired-guess-shell-alist-user '(("\\.\\(mp[34]\\|m4a\\|ogg\\|flac\\|webm\\|mkv\\)" "mpv" "xdg-open" "open") (".*" "open" "xdg-open"))) (dired-kill-when-opening-new-dired-buffer t) (dired-create-destination-dirs 'always) :config (with-eval-after-load 'async (dired-async-mode 1)) (require 'mailcap) (setq mailcap-prefer-mailcap-viewers nil) (mailcap-add "image/png" "kitty --hold kitty +kitten icat %s") (mailcap-add "image/jpeg" "kitty --hold kitty +kitten icat %s") (mailcap-add "image/jpg" "kitty --hold kitty +kitten icat %s") (mailcap-add "image/gif" "kitty --hold kitty +kitten icat %s") (advice-add 'dired-create-directory :around (lambda (orig-fun dirname) (let ((current-buffer (current-buffer))) (funcall orig-fun dirname) (with-current-buffer current-buffer (revert-buffer))))) (when (eq system-type 'darwin) (let ((gls (executable-find "gls"))) (when gls (setq insert-directory-program gls))))) (use-package async :ensure t) (defvar-keymap my/leader-map :doc "Global leader map used from Evil states.") (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. ;; Soju routes to the upstream network via the USER string, so we still need ;; one connection per network. We connect to Libera and Ergo only. (defun my/soju-password () "Read the Soju bouncer password from the sops-nix secret." (string-trim (with-temp-buffer (insert-file-contents "/home/cashmere/.config/sops-nix/secrets/senpai_password") (buffer-string)))) (defun run-irc () "Connect to Libera and Ergo via the Soju bouncer." (interactive) (let ((pw (my/soju-password))) (erc-tls :server "bouncer.cashmere.rs" :port 6699 :id 'libera :nick "cashmere1337" :user "cashmere/irc.libera.chat@emacs" :password pw) (erc-tls :server "bouncer.cashmere.rs" :port 6699 :id 'ergo :nick "cashmere" :user "cashmere/ergo@emacs" :password pw))) (use-package erc :ensure nil :defer t :custom ;; connection (erc-nick "cashmere1337") (erc-user-full-name "cashmere") ;; behaviour (erc-join-buffer 'buffer) (erc-kill-buffer-on-part nil) (erc-kill-queries-on-quit t) (erc-kill-server-buffer-on-quit t) ;; visuals (erc-fill-function 'erc-fill-wrap) (erc-timestamp-format "[%H:%M]") (erc-timestamp-format-left "[%H:%M]") (erc-timestamp-format-right "[%H:%M]") (erc-hide-list '("JOIN" "PART" "QUIT" "NICK" "MODE")) (erc-track-exclude-types '("JOIN" "PART" "QUIT" "NICK" "MODE" "324" "329" "332" "333" "353" "477")) (erc-track-shorten-start 4) (erc-track-visibility 'visible) ;; prompt (erc-prompt (lambda () (concat (buffer-name) ">"))) ;; modules (erc-modules '(autojoin button completion fill irccontrols list match move-to-prompt netsplit networks nicks noncommands notifications readonly ring scrolltobottom stamp track)) :config ;; highlight own nick (setq erc-current-nick-highlight-type 'all) (setq erc-keywords '("cashmere")) (setq erc-pals '("cashmere")) ;; nicks module (built-in nick coloring in Emacs 30) (setq erc-nicks-contrast-range '(40 . 90)) ;; scrolltobottom -- keep input always at bottom (setq erc-scrolltobottom-all t) (erc-scrolltobottom-mode 1) ;; fill-wrap -- modern chat-like wrapping (setq erc-fill-wrap-align-prompt nil) (setq erc-fill-static-center 14) ;; track -- activity NOT in mode-line (channel list would clutter it) (setq erc-track-position-in-mode-line nil) ;; keep large buffer for soju history replay (setq erc-max-buffer-size 100000) (erc-update-modules)) (defun my/erc-switch-channel () "Switch to an ERC channel buffer via helm." (interactive) (require 'helm) (helm :sources (helm-build-sync-source "IRC" :candidates (lambda () (mapcar #'buffer-name (cl-remove-if-not (lambda (buf) (with-current-buffer buf (derived-mode-p 'erc-mode))) (buffer-list)))) :action '(("Switch to buffer" . switch-to-buffer))) :buffer "*helm erc*")) (defun my/erc-fetch-history (&optional count) "Fetch COUNT previous messages from Soju via CHATHISTORY. Defaults to 100 (soju max). With prefix arg, prompts for count. Temporarily disables notifications during the fetch." (interactive "P") (let ((target (erc-default-target))) (if (not target) (message "Not in a channel buffer.") (let ((n (min (if count (read-number "Messages to fetch: " 100) 100) 100))) (erc-notifications-disable) (erc-server-send (format "CHATHISTORY LATEST %s * %d" target n)) (run-at-time 5 nil #'erc-notifications-enable))))) (defun my/erc-quit-all () "Disconnect from IRC and kill all ERC buffers." (interactive) (erc-cmd-QUIT "bye") (run-at-time 1 nil (lambda () (dolist (buf (buffer-list)) (when (with-current-buffer buf (derived-mode-p 'erc-mode)) (kill-buffer buf)))))) (defun my/erc-reconnect () "Reconnect to the current ERC server." (interactive) (erc-cmd-RECONNECT)) (defun my/erc-list-channels () "Request channel list from the current ERC server." (interactive) (erc-cmd-LIST)) (defun persp-irc () "Switch to (or create) the IRC frame and show ERC buffers." (interactive) (let* ((erc-bufs (cl-remove-if-not (lambda (buf) (with-current-buffer buf (derived-mode-p 'erc-mode))) (buffer-list))) (irc-frame (cl-find-if (lambda (f) (string= (frame-parameter f 'name) "irc")) (frame-list)))) (if irc-frame (select-frame-set-input-focus irc-frame) (select-frame-set-input-focus (make-frame '((name . "irc"))))) (if erc-bufs (switch-to-buffer (car erc-bufs)) (letrec ((hook-fn (lambda () (switch-to-buffer (current-buffer)) (remove-hook 'erc-join-hook hook-fn)))) (add-hook 'erc-join-hook hook-fn)) (run-irc)))) (with-eval-after-load 'erc (add-hook 'erc-mode-hook (lambda () (setq-local scroll-margin 0) (setq-local maximum-scroll-margin 0.0) (setq-local corfu-auto nil))) ;; Prevent accidental ERC buffer kills from parting channels (add-hook 'kill-buffer-query-functions (lambda () (if (and (derived-mode-p 'erc-mode) (erc-server-process-alive) (not (eq this-command 'my/erc-quit-all))) (yes-or-no-p (format "ERC buffer %s is connected. Really kill (will part channel)? " (buffer-name))) t)))) (defvar my/centered-cursor-enabled nil) (defun my/centered-cursor () (interactive) (if my/centered-cursor-enabled (progn (setq scroll-preserve-screen-position nil scroll-conservatively 0 maximum-scroll-margin 0.0 scroll-margin 0) (setq my/centered-cursor-enabled nil) (message "centered-cursor off")) (progn (setq scroll-preserve-screen-position t scroll-conservatively 101 maximum-scroll-margin 0.5 scroll-margin 99999) (setq my/centered-cursor-enabled t) (message "centered-cursor on")))) (use-package clipetty :ensure t :after evil :config (setq interprogram-cut-function nil) (defun my/send-to-clipboard (text) (when (stringp text) (if (display-graphic-p) (gui-select-text text) (clipetty-cut #'ignore text)))) (defun my/evil-yank-to-clipboard (_beg _end &optional _type register _yank-handler) (when (and (not register) (memq this-command '(evil-yank evil-yank-line))) (my/send-to-clipboard (car kill-ring)))) (advice-add 'evil-yank :after #'my/evil-yank-to-clipboard) (defun my/elfeed-yank-to-clipboard (&rest _) (my/send-to-clipboard (car kill-ring))) (advice-add 'elfeed-search-yank :after #'my/elfeed-yank-to-clipboard) (advice-add 'link-hint-copy-link :after #'my/elfeed-yank-to-clipboard) (with-eval-after-load 'pass (dolist (cmd '(pass-copy pass-copy-field pass-copy-username pass-copy-url pass-otp-token-copy pass-otp-uri-copy pass-view-copy-password pass-view-copy-token)) (advice-add cmd :after #'my/elfeed-yank-to-clipboard))) (with-eval-after-load 'passage (dolist (cmd '(passage-copy passage-copy-field passage-copy-username passage-copy-url passage-otp-token-copy passage-otp-uri-copy passage-view-copy-password)) (advice-add cmd :after #'my/elfeed-yank-to-clipboard)))) (use-package isearch :ensure nil ;; This is built-in, no need to fetch it. :config (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. (use-package vc :ensure nil ;; This is built-in, no need to fetch it. :defer t :config ;; Better colors for g b (blame file) (setq vc-annotate-color-map '((20 . "#f5e0dc") (40 . "#f2cdcd") (60 . "#f5c2e7") (80 . "#cba6f7") (100 . "#f38ba8") (120 . "#eba0ac") (140 . "#fab387") (160 . "#f9e2af") (180 . "#a6e3a1") (200 . "#94e2d5") (220 . "#89dceb") (240 . "#74c7ec") (260 . "#89b4fa") (280 . "#b4befe")))) (use-package vc-fossil ;; Keep from loading unnecessarily at startup. :ensure t ;; This allows VC to load vc-fossil when needed. :init (add-to-list 'vc-handled-backends 'Fossil t)) (use-package ibuffer :ensure nil :config (setq ibuffer-expert t) (setq ibuffer-display-summary nil) (setq ibuffer-use-other-window nil) (setq ibuffer-show-empty-filter-groups nil) (setq ibuffer-default-sorting-mode 'filename/process) (setq ibuffer-title-face 'font-lock-doc-face) (setq ibuffer-use-header-line t) (setq ibuffer-default-shrink-to-minimum-size nil) (setq ibuffer-formats '((mark modified read-only locked " " (name 30 30 :left :elide) " " (size 9 -1 :right) " " (mode 16 16 :left :elide) " " filename-and-process) (mark " " (name 16 -1) " " filename))) (setq ibuffer-saved-filter-groups '(("Main" ("Directories" (mode . dired-mode)) ("Rust" (or (mode . rust-mode) (mode . rust-ts-mode))) ("Go" (or (mode . go-mode) (mode . go-ts-mode))) ("Python" (or (mode . python-ts-mode) (mode . c-mode) (mode . python-mode))) ("Nix" (or (mode . nix-mode) (mode . nix-ts-mode))) ("Scripts" (or (mode . shell-script-mode) (mode . sh-mode) (mode . bash-ts-mode) (mode . lua-mode) (mode . bat-mode))) ("Config" (or (mode . conf-mode) (mode . conf-toml-mode) (mode . toml-ts-mode) (mode . conf-windows-mode) (name . "^\\.clangd$") (name . "^\\.gitignore$") (name . "^Doxyfile$") (name . "^config\\.toml$") (mode . yaml-ts-mode))) ("Web" (or (mode . mhtml-mode) (mode . html-mode) (mode . web-mode) (mode . nxml-mode))) ("CSS" (or (mode . css-mode) (mode . sass-mode))) ("JS" (or (mode . js-mode) (mode . rjsx-mode))) ("Markup" (or (mode . markdown-mode) (mode . adoc-mode))) ("Org" (mode . org-mode)) ("Terminal" (or (mode . ghostel-mode) (mode . eat-mode) (mode . eshell-mode) (mode . shell-mode) (mode . term-mode))) ("LaTeX" (name . "\.tex$")) ("Magit" (or (mode . magit-blame-mode) (mode . magit-cherry-mode) (mode . magit-diff-mode) (mode . magit-log-mode) (mode . magit-process-mode) (mode . magit-status-mode))) ("Elfeed" (or (mode . elfeed-search-mode) (mode . elfeed-show-mode))) ("Fundamental" (or (mode . fundamental-mode) (mode . text-mode))) ("ECA - Editor Code Assistant" (or (mode . eca-chat-mode))) ("IRC" (or (mode . erc-mode))) ("Emacs" (or (mode . emacs-lisp-mode) (name . "^\\*Help\\*$") (name . "^\\*Custom.*") (name . "^\\*Org Agenda\\*$") (name . "^\\*info\\*$") (name . "^\\*scratch\\*$") (name . "^\\*Backtrace\\*$") (name . "^\\*Messages\\*$")))))) :hook (ibuffer-mode . (lambda () (ibuffer-switch-to-saved-filter-groups "Main")))) (use-package nerd-icons :if ek-use-nerd-fonts :ensure t :custom (nerd-icons-font-family "SFProText Nerd Font")) (use-package nerd-icons-ibuffer :if ek-use-nerd-fonts :ensure t :hook (ibuffer-mode . nerd-icons-ibuffer-mode)) (use-package smerge-mode :ensure nil ;; This is built-in, no need to fetch it. :defer t) (use-package markdown-mode :ensure t :mode (("README\\.md\\'" . gfm-mode) ("\\.md\\'" . markdown-mode) ("\\.markdown\\'" . markdown-mode)) :hook (markdown-mode . abbrev-mode) :custom ;; Use markdown-mode's display properties only: the source text is never ;; replaced, and markup can always be restored with SPC m v. (markdown-hide-markup t) ;; Let the full markup toggle reveal link destinations as well. (markdown-hide-urls nil) (markdown-header-scaling t) (markdown-header-scaling-values '(1.65 1.40 1.22 1.10 1.00 0.92)) (markdown-fontify-code-blocks-natively t) (markdown-asymmetric-header t) (markdown-indent-on-enter 'indent-and-new-item) (markdown-make-gfm-checkboxes-buttons t) (markdown-gfm-uppercase-checkbox nil) (markdown-enable-math t) (markdown-list-item-bullets '("●" "○" "◆" "◇")) (markdown-blockquote-display-char '("▌" ">")) (markdown-hr-display-char '(?─ ?-)) :config (defun my/markdown-apply-minimal-faces (&optional _theme) "Apply the quiet, theme-adaptive typography used by Org buffers." (let ((foreground (face-attribute 'default :foreground nil t)) (muted (face-attribute 'shadow :foreground nil t))) (dolist (spec '((markdown-header-face-1 1.65 bold) (markdown-header-face-2 1.40 bold) (markdown-header-face-3 1.22 semibold) (markdown-header-face-4 1.10 medium) (markdown-header-face-5 1.00 medium))) (set-face-attribute (car spec) nil :family "SF Pro Display" :height (cadr spec) :weight (caddr spec) :foreground foreground)) (set-face-attribute 'markdown-header-face-6 nil :family "SF Pro Display" :height 0.92 :weight 'regular :foreground muted) (set-face-attribute 'markdown-header-delimiter-face nil :height 0.9 :weight 'normal :foreground muted) (set-face-attribute 'markdown-markup-face nil :foreground muted))) (add-hook 'enable-theme-functions #'my/markdown-apply-minimal-faces) (my/markdown-apply-minimal-faces)) (defun my/agent-shell-autoscroll (&rest _) (dolist (win (get-buffer-window-list (current-buffer) 'visible)) (when (>= (window-point win) (save-excursion (goto-char (point-max)) (point))) (set-window-point win (point-max))))) (defun my/agent-shell-ret-binding () (let ((keymap (get-text-property (point) 'keymap))) (when keymap (let ((binding (lookup-key keymap (kbd "RET")))) (when (commandp binding) binding))))) (defun my/agent-shell-output-at-point-p () (or (get-text-property (point) 'agent-shell-ui-state) (and (> (point) (point-min)) (get-text-property (1- (point)) 'agent-shell-ui-state)))) (defun my/agent-shell-ret () (interactive) (if-let* ((binding (my/agent-shell-ret-binding))) (call-interactively binding) (cond ((my/agent-shell-output-at-point-p) (agent-shell-ui-toggle-fragment)) ((and (derived-mode-p 'agent-shell-mode) (shell-maker-point-at-last-prompt-p)) (agent-shell-submit)) ((derived-mode-p 'agent-shell-viewport-view-mode) (agent-shell-viewport-next-item)) ((derived-mode-p 'agent-shell-mode) (agent-shell-next-item))))) (defun my/agent-shell-display-buffer-rules () '(("\\*Kimi\\(?:\\*\\|:[^*]*\\*\\)" (display-buffer-in-side-window) (side . bottom) (slot . 0) (window-height . 0.33) (dedicated . t) (window-parameters . ((no-other-window . nil) (no-delete-other-windows . nil)))) ("\\*agent-shell-diff\\*" (display-buffer-in-side-window) (side . right) (slot . 1) (window-width . 0.5) (dedicated . t) (window-parameters . ((no-other-window . nil) (no-delete-other-windows . nil)))))) (use-package agent-shell :ensure t :config (advice-add 'shell-maker-submit :after #'my/agent-shell-autoscroll) (dolist (rule (my/agent-shell-display-buffer-rules)) (add-to-list 'display-buffer-alist rule)) (setq agent-shell-tool-use-expand-by-default nil) (setq agent-shell-thought-process-expand-by-default t) (setq agent-shell-user-message-expand-by-default t) (setq agent-shell-activity-group-expand-by-default t) (setq agent-shell-diff-delta-command (when (executable-find "delta") '("delta" "--no-gitconfig" "--color-only"))) (setq agent-shell-activity-group-header-label-function #'agent-shell-activity-group-descriptive-label) ;; 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 ;; which require a GUI frame. (defun my/agent-shell-send-clipboard-image-in-terminal (orig-fn &optional pick-shell) "Call ORIG-FN, but allow `agent-shell-send-clipboard-image' in terminal Emacs." (if (window-system) (funcall orig-fn pick-shell) (let* ((screenshots-dir (agent-shell--dot-subdir "screenshots")) (image-path (agent-shell--save-clipboard-image :destination-dir screenshots-dir)) (shell-buffer (when pick-shell (agent-shell--read-shell-buffer :prompt "Send image to shell: ")))) (agent-shell-insert :text (agent-shell--get-files-context :files (list image-path)) :shell-buffer shell-buffer)))) (advice-add 'agent-shell-send-clipboard-image :around #'my/agent-shell-send-clipboard-image-in-terminal) ;; Also let C-y / `agent-shell-yank-dwim' try the clipboard image first ;; in terminal Emacs before falling back to plain text yank. (defun my/agent-shell-yank-dwim-in-terminal (orig-fn &optional arg) "Call ORIG-FN, but try clipboard image first in terminal Emacs." (if (window-system) (funcall orig-fn arg) (if-let* ((screenshots-dir (agent-shell--dot-subdir "screenshots")) (image-path (agent-shell--save-clipboard-image :destination-dir screenshots-dir :no-error t))) (agent-shell-insert :text (agent-shell--get-files-context :files (list image-path)) :shell-buffer (agent-shell--shell-buffer)) (funcall orig-fn arg)))) (advice-add 'agent-shell-yank-dwim :around #'my/agent-shell-yank-dwim-in-terminal)) (use-package eldoc :ensure nil :config (setq eldoc-idle-delay 0.25) (setq eldoc-echo-area-use-multiline-p nil) (setq eldoc-echo-area-display-truncation-message nil) :init (global-eldoc-mode)) (use-package flymake :ensure nil ;; This is built-in, no need to fetch it. :defer t ;; :hook (prog-mode . flymake-mode) :custom (flymake-margin-indicators-string '((error "!»" compilation-error) (warning "»" compilation-warning) (note "»" compilation-info)))) (setq trusted-content '("~/.emacs.d/config.el" "~/.emacs.d/init.el")) (use-package xref :ensure nil) (use-package project :ensure nil) (setq org-directory "~/org/") (use-package org :ensure nil :custom (org-list-demote-modify-bullet '(("+" . "-") ("-" . "+"))) :config (require 'org-tempo) (defun my/org-apply-minimal-faces (&optional _theme) "Apply quiet, theme-adaptive typography to Org headings." (let ((foreground (face-attribute 'default :foreground nil t)) (muted (face-attribute 'shadow :foreground nil t))) (set-face-attribute 'org-document-title nil :family "SF Pro Display" :height 1.80 :weight 'bold :foreground foreground) (dolist (spec '((org-level-1 1.55 bold) (org-level-2 1.35 semibold) (org-level-3 1.20 semibold) (org-level-4 1.10 medium) (org-level-5 1.00 medium))) (set-face-attribute (car spec) nil :family "SF Pro Display" :height (cadr spec) :weight (caddr spec) :foreground foreground)) (dolist (spec '((org-level-6 0.95) (org-level-7 0.90) (org-level-8 0.85))) (set-face-attribute (car spec) nil :family "SF Pro Display" :height (cadr spec) :weight 'regular :foreground muted)))) ;; Reapply the neutral heading palette after Tinty's or Fontaine's theme ;; changes, deriving colors from whichever theme is active at that point. (add-hook 'enable-theme-functions #'my/org-apply-minimal-faces) (my/org-apply-minimal-faces) (custom-set-faces '(org-block ((t (:inherit fixed-pitch)))) '(org-block-begin-line ((t (:inherit (shadow fixed-pitch))))) '(org-block-end-line ((t (:inherit (shadow fixed-pitch))))) '(org-code ((t (:inherit (shadow fixed-pitch))))) '(org-verbatim ((t (:inherit (shadow fixed-pitch))))) '(org-table ((t (:inherit fixed-pitch)))) '(org-checkbox ((t (:inherit fixed-pitch)))) '(org-tag ((t (:inherit fixed-pitch)))) '(org-meta-line ((t (:inherit (font-lock-comment-face fixed-pitch))))) '(org-special-keyword ((t (:inherit (font-lock-comment-face fixed-pitch))))) '(org-property-value ((t (:inherit fixed-pitch)))) '(org-drawer ((t (:inherit fixed-pitch)))) '(org-indent ((t (:inherit (org-hide fixed-pitch)))))) (setq org-startup-folded 'overview) (setq ;; org-adapt-indentation t ;; org-time-stamp-rounding-minutes '(5 5) org-return-follows-link t org-hide-leading-stars t org-pretty-entities t org-startup-truncated nil org-ellipsis " ") (setq org-src-fontify-natively t org-src-tab-acts-natively t org-edit-src-content-indentation 0) (add-to-list 'org-src-lang-modes '("nix" . nix-ts)) (setq org-log-done t org-auto-align-tags nil org-tags-column 0 org-fold-catch-invisible-edits 'show-and-error org-special-ctrl-a/e t org-insert-heading-respect-content t) ;; (add-hook 'org-mode-hook 'org-indent-mode) (add-hook 'org-mode-hook 'visual-line-mode) (add-hook 'org-mode-hook (lambda () (electric-indent-local-mode -1))) (add-hook 'org-mode-hook (lambda () (setq-local font-lock-extra-managed-props (cons 'display font-lock-extra-managed-props)))) (font-lock-add-keywords 'org-mode `(("^.*?\( \)\(:[[:alnum:]_@#%:]+:\)$" (1 `(face nil display (space :align-to (- right ,(org-string-width (match-string 2)) 3))) prepend))) t)) (use-package org-fmt :ensure nil :after org :commands (org-fmt-buffer)) (defun my-org-read-date-always-with-time (orig-fun &optional org-with-time to-time from-string prompt default-time default-input inactive) (cl-letf (((symbol-function 'org-read-date--get-current-time) (lambda () (or default-time (current-time))))) (let* ((default-input (or default-input (format-time-string "%H:%M" (or default-time (current-time)))))) (funcall orig-fun t to-time from-string prompt default-time default-input inactive)))) (advice-add 'org-read-date :around #'my-org-read-date-always-with-time) (use-package org-crypt :ensure nil :after org :config (org-crypt-use-before-save-magic) (setq org-crypt-key "1ED9D600A040D0AF193AAF30B877C0AC3B080FBB") (setq org-tags-exclude-from-inheritance '("crypt")) (add-hook 'org-mode-hook (lambda () (setq-local auto-save-default nil)))) (use-package org-appear :ensure t :commands (org-appear-mode) :hook (org-mode . org-appear-mode) :config (setq org-hide-emphasis-markers t) ;; Must be activated for org-appear to work (setq org-appear-autoemphasis t ;; Show bold, italics, verbatim, etc. org-appear-autolinks t ;; Show links org-appear-autosubmarkers t)) ;; Show sub- and superscripts (use-package org-modern :ensure t :hook (org-mode . org-modern-mode) :custom (org-modern-star 'fold) (org-modern-hide-stars 'leading) (org-modern-list '((?+ . "◦") (?- . "–") (?* . "•"))) (org-modern-checkbox '((?X . "☑") (?- . "⊟") (?\s . "☐"))) (org-modern-table t) (org-modern-horizontal-rule t) (org-modern-block-name t) (org-modern-block-fringe nil) (org-modern-todo nil) (org-modern-priority nil) (org-modern-tag nil) (org-modern-timestamp nil) (org-modern-progress nil) (org-modern-keyword nil) (org-modern-footnote nil) (org-modern-internal-target nil) (org-modern-radio-target nil) :custom-face (org-modern-block-name ((t (:inherit (shadow fixed-pitch) :height 0.85 :weight regular)))) :config (set-face-attribute 'org-modern-symbol nil :family cashmere/font-family :inherit 'shadow :weight 'regular)) (use-package kitty-graphics :ensure (:host github :repo "cashmeredev/kitty-graphics.el") :demand t :custom (kitty-graphics-enable-video t) (kitty-graphics-shr-scale 'fit) (kitty-graphics-shr-fit-width 0.4) (kitty-graphics-shr-fit-height 20) (kitty-graphics-doc-view-resolution-scale 2.0) :hook (dired-mode . kitty-graphics-dired-auto-preview-mode) :config (setq kitty-graphics-enable-browser t kitty-graphics-casty-program "~/projects/casty/bin/casty.js" kitty-graphics-casty-chrome "helium-browser") (kitty-graphics-setup)) (with-eval-after-load 'org (setq org-confirm-babel-evaluate nil) (org-babel-do-load-languages 'org-babel-load-languages '((emacs-lisp . t) (shell . t) (python . t) (ruby . t) (perl . t) (C . t) (js . t) (css . t) (sass . t) (sql . t) (sqlite . t) (lua . t) (lisp . t) (scheme . t) (clojure . t) (haskell . t) (ocaml . t) (java . t) (dot . t) (gnuplot . t) (plantuml . t) (ditaa . t) (calc . t) (latex . t) (org . t) (makefile . t) (awk . t) (sed . t) (screen . t) (eshell . t)))) (with-eval-after-load 'org (defun my/org-babel-tangle-targets-missing-p (&optional buffer) "Return non-nil if any :tangle target of BUFFER is missing. BUFFER defaults to the current buffer." (with-current-buffer (or buffer (current-buffer)) (when (and (buffer-file-name) (derived-mode-p 'org-mode)) (require 'ob-tangle) (let* ((default-directory (file-name-directory (buffer-file-name))) (targets (mapcar #'car (org-babel-tangle-collect-blocks))) missing) (dolist (file targets) (unless (or missing (file-exists-p file)) (setq missing t))) missing)))) (defun my/org-babel-tangle-if-missing (&optional org-file target-file lang) "Tangle only if a target file is missing. When called interactively with no arguments, check all :tangle targets in the current buffer and run `org-babel-tangle' only if at least one target is missing. When called non-interactively, tangle ORG-FILE to TARGET-FILE only if TARGET-FILE does not exist. Optional LANG restricts tangling to source blocks of that language." (interactive) (if (or org-file target-file lang) (unless (file-exists-p target-file) (org-babel-tangle-file org-file target-file lang)) (if (my/org-babel-tangle-targets-missing-p) (org-babel-tangle) (message "All tangle targets already exist; skipping"))))) (use-package ob-async :ensure t :after org) (with-eval-after-load 'org (defun org-babel-execute:just (body params) "Execute a justfile source block. BODY is the justfile content. PARAMS may include: :recipe — recipe name to run (default: the default recipe) :dir — working directory (default: `default-directory')" (let* ((recipe (cdr (assq :recipe params))) (dir (or (cdr (assq :dir params)) default-directory)) (tmp (make-temp-file "ob-just-" nil "justfile"))) (unwind-protect (progn (with-temp-file tmp (insert body)) (shell-command-to-string (format "just --justfile %s --working-directory %s %s" (shell-quote-argument tmp) (shell-quote-argument (expand-file-name dir)) (if recipe (shell-quote-argument recipe) "")))) (delete-file tmp)))) (add-to-list 'org-src-lang-modes '("just" . just))) (defun my/snip-upload (&optional file) "Upload FILE, region, or current buffer to bouncer.cashmere.rs. The xonsh `upload' command copies the URL to the clipboard; it is also added to the kill ring." (interactive) (let* ((ext (or (and file (file-name-extension file)) (and (buffer-file-name) (file-name-extension (buffer-file-name))) "txt")) (cmd (format "xonsh -c 'upload %s'" ext))) (if (and file (file-exists-p file)) (call-process-shell-command (format "cat %s | %s" (shell-quote-argument file) cmd)) (shell-command-on-region (if (use-region-p) (region-beginning) (point-min)) (if (use-region-p) (region-end) (point-max)) cmd nil nil)) (let ((url (string-trim (shell-command-to-string "wl-paste")))) (if (string-prefix-p "https://" url) (progn (kill-new url) (message "Uploaded: %s" url)) (message "Upload failed: %s" url))))) (defun my/snip-upload-file () "Prompt for a file and upload it to bouncer.cashmere.rs." (interactive) (my/snip-upload (read-file-name "Upload to bouncer: "))) (defun my/setup-git-remote (dev-name srht-name) "Set up origin (dual-push) and backup remotes for the current git repo. DEV-NAME is the repo name on dev.cashmere.rs, SRHT-NAME on sr.ht." (interactive (let* ((dev (read-string "Repo name (dev.cashmere.rs): ")) (srht (read-string (format "Repo name (sr.ht) [%s]: " dev) nil nil dev))) (list dev srht))) (let* ((root (or (locate-dominating-file default-directory ".git") (error "Not in a git repository"))) (default-directory root) (dev-url (format "ssh://git@dev.cashmere.rs/cashmere/%s" dev-name)) (srht-url (format "git@git.sr.ht:~cashmere/%s" srht-name))) (shell-command (format "git remote remove origin 2>/dev/null; git remote add origin %s" dev-url)) (shell-command (format "git remote set-url --add --push origin %s" dev-url)) (shell-command (format "git remote set-url --add --push origin %s" srht-url)) (shell-command (format "git remote remove backup 2>/dev/null; git remote add backup %s" srht-url)) (message "Remotes configured:\n origin -> %s (push to both)\n backup -> %s" dev-url srht-url))) (defun my/paste-md-to-org () "Yank Markdown text as Org. This command will convert Markdown text in the top of the `kill-ring' and convert it to Org using the pandoc utility." (interactive) (save-excursion (with-temp-buffer (yank) (shell-command-on-region (point-min) (point-max) "pandoc -f markdown -t org --wrap=preserve" t t) (kill-region (point-min) (point-max))) (yank))) (with-eval-after-load 'org ;; Log state changes with timestamps (setq org-log-done 'time org-log-redeadline 'time org-log-reschedule 'time org-log-into-drawer t org-enforce-todo-dependencies t) ;; Update TODO keywords to require notes for WAIT and CANCELLED ;; @ means: prompt for note when entering state ;; ! means: timestamp when leaving state (setq org-todo-keywords '((sequence "TODO(t)" "NEXT(n)" "ACTIVE(a!)" "WAIT(w@/!)" "|" "DONE(d!)" "CANCELLED(c@)"))) ;; Customize note prompts (setq org-log-note-headings '((done . "CLOSING NOTE %t") (state . "State %-12s from %-12S %t") (note . "Note taken on %t") (reschedule . "Rescheduled from %S on %t") (delschedule . "Not scheduled, was %S on %t") (redeadline . "New deadline from %S on %t") (deldeadline . "Removed deadline, was %S on %t") (refile . "Refiled on %t") (clock-out . "")))) (use-package org-clock :after org :ensure nil :commands (org-clock-in org-clock-out org-clock-goto) :config ;; Setup hooks for clock persistence (org-clock-persistence-insinuate) (setq org-clock-clocked-in-display 'mode-line org-clock-history-length 23 org-clock-in-switch-to-state 'my/clock-in-to-started org-clock-out-when-done t org-clock-persist t org-clock-persist-query-resume nil) ;; Helper function: detect if heading is a project (defun my/is-project-p () "Any task with a todo keyword subtask." (save-restriction (widen) (let ((has-subtask) (subtree-end (save-excursion (org-end-of-subtree t))) (is-a-task (member (nth 2 (org-heading-components)) org-todo-keywords-1))) (save-excursion (forward-line 1) (while (and (not has-subtask) (< (point) subtree-end) (re-search-forward "^\\*+ " subtree-end t)) (when (member (org-get-todo-state) org-todo-keywords-1) (setq has-subtask t)))) (and is-a-task has-subtask)))) ;; Helper function: detect if heading is a task (defun my/is-task-p () "Any task with a todo keyword and no subtask." (save-restriction (widen) (let ((has-subtask) (subtree-end (save-excursion (org-end-of-subtree t))) (is-a-task (member (nth 2 (org-heading-components)) org-todo-keywords-1))) (save-excursion (forward-line 1) (while (and (not has-subtask) (< (point) subtree-end) (re-search-forward "^\\*+ " subtree-end t)) (when (member (org-get-todo-state) org-todo-keywords-1) (setq has-subtask t)))) (and is-a-task (not has-subtask))))) ;; Auto-change state when clocking in (defun my/clock-in-to-started (kw) "Switch task from TODO/NEXT to ACTIVE when clocking in. Skips capture tasks and projects." (when (not (and (boundp 'org-capture-mode) org-capture-mode)) (cond ;; Change TODO/NEXT tasks to ACTIVE when clocking in ((and (member (org-get-todo-state) (list "TODO" "NEXT")) (my/is-task-p)) "ACTIVE") ;; 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"))))) (use-package org-clock-reminder :ensure (:host github :repo "inickey/org-clock-reminder") :after org :demand t :custom (org-clock-reminder-interval 15) (org-clock-reminder-inactive-notifications-p nil) (org-clock-reminder-active-title "Org: current task") (org-clock-reminder-active-text "%h\nClocked time: %c") (org-clock-reminder-inactive-title "Org: no running clock") (org-clock-reminder-inactive-text "No task is currently clocked in.") (org-clock-reminder-icons nil) (org-clock-reminder-notifiers '(org-clock-reminder-notify)) :config (org-clock-reminder-mode 1) ;; Synchronize when enabling reminders while a clock is already running. (when (org-clocking-p) (org-clock-reminder-on-clock-in))) (with-eval-after-load 'org (setq org-global-properties '(("EFFORT_ALL" . "0:15 0:30 1:00 2:00 3:00 5:00 8:00") ("COMPLEXITY_ALL" . "low medium high") ("PRIORITY_ALL" . "A B C"))) (setq org-columns-default-format "%40ITEM(Task) %TODO %10EFFORT{+} %10CLOCKSUM %10PROGRESS %COMPLEXITY %DEADLINE") ;; Auto-set effort from complexity (already exists, keeping it here for reference) ;; Low complexity = 30 min, Medium = 2h, High = 5h (defun my/org-set-effort-from-complexity () "Auto-set EFFORT property based on COMPLEXITY property." (interactive) (let* ((complexity (org-entry-get (point) "COMPLEXITY")) (effort (org-entry-get (point) "EFFORT"))) (unless effort (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"))))))) (defun my/create-project () "Create a new project file with denote and comprehensive project structure." (interactive) (let* ((project-name (read-string "Project name: ")) (tags (completing-read-multiple "Additional tags (comma-separated): " '("work" "personal" "university" "learning"))) (all-tags (append '("project" "agenda") tags)) (deadline-str (org-read-date nil nil nil "Project deadline (optional, RET to skip): ")) (effort-str (completing-read "Estimated total effort: " '("10h" "20h" "40h" "80h" "160h") nil nil "40h")) (file-name (denote project-name all-tags))) (find-file file-name) (goto-char (point-max)) (insert "\n") (insert "* " project-name "\n") (insert ":PROPERTIES:\n") (when (and deadline-str (not (string-empty-p deadline-str))) (insert ":DEADLINE: <" deadline-str ">\n")) (insert ":EFFORT: " effort-str "\n") (insert ":PROGRESS: 0%\n") (insert ":END:\n\n") (insert "** Overview\n\n") (insert "- Goal :: \n") (insert "- Why :: \n") (insert "- Success Criteria :: \n") (insert "- Status :: Planning\n\n") (insert "** Phases\n\n") (insert "*** TODO Phase 1: Planning & Research\n") (when (and deadline-str (not (string-empty-p deadline-str))) (insert "DEADLINE: <" deadline-str ">\n")) (insert ":PROPERTIES:\n") (insert ":CREATED: " (format-time-string "[%Y-%m-%d %a %H:%M]") "\n") (insert ":END:\n\n") (insert "- [ ] Define scope\n") (insert "- [ ] Research requirements\n") (insert "- [ ] Break down into tasks\n\n") (insert "*** TODO Phase 2: Implementation\n") (insert ":PROPERTIES:\n") (insert ":CREATED: " (format-time-string "[%Y-%m-%d %a %H:%M]") "\n") (insert ":END:\n\n") (insert "*** TODO Phase 3: Review & Polish\n") (insert ":PROPERTIES:\n") (insert ":CREATED: " (format-time-string "[%Y-%m-%d %a %H:%M]") "\n") (insert ":END:\n\n") (insert "** Resources\n\n") (insert "- Links :: \n") (insert "- Files :: \n") (insert "- References :: \n\n") (insert "** Time Tracking\n\n") (insert "#+BEGIN: clocktable :scope file :maxlevel 3 :emphasize nil :link t\n") (insert "#+END:\n\n") (insert "Update with C-c C-c on the BEGIN line\n\n") (insert "** Status Updates\n\n") (insert "Use =C-c C-x C-c= for column view to update PROGRESS and see overview.\n\n") (insert "** Retrospective\n\n") (insert "- What worked :: \n") (insert "- What didn't :: \n") (insert "- Lessons learned :: \n") (insert "- Would do differently :: \n") (org-mode) (goto-char (point-min)) (search-forward "- Goal :: " nil t))) (defun my/org-capture-to-journal (heading) (require 'denote-journal) (denote-journal-new-or-existing-entry) (goto-char (point-max)) (insert heading) (current-buffer)) (defun my/org-capture-timeblock-to-journal () (my/org-capture-to-journal "\n* Timeblock\n")) (defun my/org-capture-recurring-to-journal () (my/org-capture-to-journal "\n* Recurring\n")) (defun my/org-capture-meeting-to-journal () (my/org-capture-to-journal "\n* Meeting: ")) (defcustom my/org-capture-project-notes-file "notes.org" "File name for source notes captured at the current project root." :type 'string :group 'org-capture) (defcustom my/org-capture-source-language-alist '((c-mode . "C") (c-ts-mode . "C") (c++-mode . "C++") (c++-ts-mode . "C++") (emacs-lisp-mode . "emacs-lisp") (js-mode . "js") (js-ts-mode . "js") (shell-mode . "shell") (sh-mode . "shell")) "Exceptional major-mode to Org source language mappings used by project captures. Modes not listed here are inferred from `org-src-lang-modes' or their mode name." :type '(alist :key-type symbol :value-type string) :group 'org-capture) (defun my/org-capture-original-buffer () "Return the live buffer from which the current capture was invoked." (let ((buffer (org-capture-get :original-buffer))) (unless (buffer-live-p buffer) (user-error "The source buffer for this capture is no longer available")) buffer)) (defun my/org-capture-project-notes-path () "Return the project-root notes file for the current capture." (with-current-buffer (my/org-capture-original-buffer) (let ((root (or (and (fboundp 'projectile-project-root) (ignore-errors (projectile-project-root))) (when-let* ((project (project-current nil default-directory))) (project-root project))))) (unless root (user-error "Cannot capture source note outside a recognized project")) (expand-file-name my/org-capture-project-notes-file root)))) (defun my/org-capture-source-language () "Return the Org source language for the current capture's source buffer." (with-current-buffer (my/org-capture-original-buffer) (or (alist-get major-mode my/org-capture-source-language-alist) (cl-loop for (language . mode) in org-src-lang-modes for mode-function = (intern (format "%s-mode" mode)) when (derived-mode-p mode-function) return (format "%s" language)) (string-remove-suffix "-mode" (string-remove-suffix "-ts-mode" (symbol-name major-mode)))))) (defun my/org-capture-source-link () "Return a file link to the first source line of the current capture." (with-current-buffer (my/org-capture-original-buffer) (unless buffer-file-name (user-error "Cannot link a capture from a buffer without a file")) (let* ((position (if (and (org-capture-get :initial) (mark t)) (min (point) (mark t)) (point))) (line (line-number-at-pos position t)) (file (abbreviate-file-name buffer-file-name))) (org-link-make-string (format "file:%s::%d" file line) (format "%s:%d" (file-name-nondirectory file) line))))) (defun my/org-capture-task-to-journal () (let* ((context (read-string "Task: ")) (todo-state (completing-read "TODO state: " '("ACTIVE" "NEXT" "TODO" "WAIT")))) (my/org-capture-to-journal (format "\n* %s %s\n\n" todo-state context)))) (defun my/org-capture-deadline-to-journal () (let* ((context (read-string "Task with deadline: ")) (deadline (org-read-date t nil nil "Deadline: "))) (my/org-capture-to-journal (format "\n* %s\nDEADLINE: <%s>\n\n" context deadline)))) (defun my/org-capture-scheduled-to-journal () "Capture a scheduled task into today's denote-journal file." (let* ((context (read-string "Scheduled task: ")) (schedule (org-read-date t nil nil "Schedule: "))) (my/org-capture-to-journal (format "\n* %s\nSCHEDULED: <%s>\n\n" context schedule)))) (defun my/org-capture-float () (interactive) (select-frame (make-frame '((title . "org-capture") (width . 160) (height . 14)))) (org-capture)) (with-eval-after-load 'org (setq org-capture-templates '(("t" "Task" plain (function my/org-capture-task-to-journal) "" :immediate-finish t :jump-to-captured t) ("d" "Deadline" plain (function my/org-capture-deadline-to-journal) "" :immediate-finish t :jump-to-captured t) ("s" "Scheduled" plain (function my/org-capture-scheduled-to-journal) "" :immediate-finish t :jump-to-captured t) ("b" "Timeblock" plain (function my/org-capture-timeblock-to-journal) "" :immediate-finish nil :jump-to-captured t) ("r" "Recurring" plain (function my/org-capture-recurring-to-journal) "" :immediate-finish t :jump-to-captured t) ("m" "Meeting" plain (function my/org-capture-meeting-to-journal) "" :immediate-finish nil :jump-to-captured t) ("p" "Project source TODO" entry (file+headline my/org-capture-project-notes-path "Inbox") "* TODO %?\n\n#+begin_src %(my/org-capture-source-language)\n%i\n#+end_src\n\n%(my/org-capture-source-link)" :prepend t :empty-lines 1)))) (use-package org-super-agenda :ensure t :after org-agenda :init :config (org-super-agenda-mode)) (use-package org-ql :ensure t :after org-super-agenda) ;;; --- Agenda display settings --- (setq org-agenda-window-setup 'current-window org-agenda-inhibit-startup t org-agenda-remove-tags t org-agenda-dim-blocked-tasks nil org-agenda-skip-scheduled-if-done t org-agenda-skip-deadline-if-done t) (setq org-agenda-scheduled-leaders '("" "-%2d")) (setq org-agenda-time-grid '((daily today require-timed) (800 1000 1200 1400 1600 1800 2000) "......" "________________")) (setq org-agenda-current-time-string "--- NOW ---------------") (setq org-agenda-prefix-format '((agenda . " %i %?-12t% s") (todo . " %i ") (tags . " %i ") (search . " %i "))) ;;; --- Agenda views --- ;; ;; d - Today: daily driver, what to focus on now ;; w - Week: calendar + upcoming + backlog ;; p - Projects: all project tasks by state ;; o - Overview: schedule + topics (project/dev/blog/self) + backlog (setq org-agenda-custom-commands '( ;; ── Today ────────────────────────────────────────────── ("d" "Today" ( ;; Block 1: standard agenda — time-grid, NOW line, scheduled + deadlines (agenda "" ((org-agenda-span 1) (org-agenda-start-day ".") (org-deadline-warning-days 3) (org-scheduled-past-days 0) (org-agenda-day-face-function (lambda (date) 'org-agenda-date)) (org-agenda-overriding-header "") (org-super-agenda-groups '((:name "Overdue" :scheduled past :deadline past :order 0) (:name "Schedule" :time-grid t :order 1) (:name "Scheduled today" :scheduled today :order 2) (:name "Deadlines" :deadline today :order 3) (:discard (:anything t)))))) ;; Block 2: active + next tasks (what you can work on right now) (org-ql-block '(and (todo "ACTIVE" "NEXT") (not (done))) ((org-ql-block-header "In Progress") (org-super-agenda-groups '((:auto-tags t))))) ;; Block 3: waiting items (org-ql-block '(and (todo "WAIT") (not (done))) ((org-ql-block-header "Waiting On") (org-super-agenda-groups '((:auto-tags t))))))) ;; ── Week ─────────────────────────────────────────────── ("w" "Week" ( ;; Block 1: today in detail (agenda "" ((org-agenda-span 1) (org-deadline-warning-days 0) (org-scheduled-past-days 0) (org-agenda-day-face-function (lambda (date) 'org-agenda-date)) (org-agenda-overriding-header "Today") (org-super-agenda-groups '((:name "Overdue" :scheduled past :deadline past :order 0) (:name "Schedule" :time-grid t :order 1) (:name "Scheduled today" :scheduled today :order 2) (:discard (:anything t)))))) ;; Block 2: next 6 days (agenda "" ((org-agenda-start-on-weekday nil) (org-agenda-start-day "+1d") (org-agenda-span 6) (org-deadline-warning-days 0) (org-agenda-overriding-header "Next 6 Days") (org-super-agenda-groups '((:anything t))))) ;; Block 3: upcoming deadlines beyond this week (agenda "" ((org-agenda-time-grid nil) (org-agenda-start-on-weekday nil) (org-agenda-start-day "+7d") (org-agenda-span 14) (org-agenda-show-all-dates nil) (org-agenda-entry-types '(:deadline)) (org-agenda-overriding-header "Upcoming Deadlines (+14d)") (org-super-agenda-groups '((:anything t))))) ;; Block 4: unscheduled backlog (org-ql-block '(and (todo) (not (done)) (not (scheduled)) (not (deadline))) ((org-ql-block-header "Backlog (unscheduled)") (org-super-agenda-groups '((:name "ACTIVE" :todo "ACTIVE" :order 1) (:name "NEXT" :todo "NEXT" :order 2) (:name "TODO" :todo "TODO" :order 3) (:name "WAIT" :todo "WAIT" :order 4) (:discard (:anything t)))))))) ;; ── Projects ─────────────────────────────────────────── ("p" "Projects" ((org-ql-block '(and (tags "project") (not (done))) ((org-ql-block-header "All Project Tasks") (org-super-agenda-groups '((:name "ACTIVE" :todo "ACTIVE" :order 1) (:name "NEXT" :todo "NEXT" :order 2) (:name "TODO" :todo "TODO" :order 3) (:name "WAIT" :todo "WAIT" :order 4) (:discard (:anything t)))))))) ;; ── Overview ─────────────────────────────────────────── ;; Full dashboard: schedule + topic sections + backlog ("o" "Overview" ( ;; Block 1: today's agenda — schedule, deadlines, overdue (agenda "" ((org-agenda-span 1) (org-agenda-start-day ".") (org-deadline-warning-days 3) (org-agenda-show-future-repeats nil) (org-agenda-overriding-header "") (org-super-agenda-groups '((:name "Overdue" :scheduled past :deadline past :order 0) (:name "Schedule" :time-grid t :order 1) (:name "Scheduled today" :scheduled today :order 2) (:name "Deadlines" :deadline today :order 3) (:discard (:anything t)))))) ;; Block 2: next 6 days (agenda "" ((org-agenda-start-on-weekday nil) (org-agenda-start-day "+1d") (org-agenda-span 6) (org-deadline-warning-days 0) (org-agenda-overriding-header "Next 6 Days") (org-super-agenda-groups '((:anything t))))) ;; Block 3: upcoming deadlines beyond this week (agenda "" ((org-agenda-time-grid nil) (org-agenda-start-on-weekday nil) (org-agenda-start-day "+7d") (org-agenda-span 14) (org-agenda-show-all-dates nil) (org-agenda-entry-types '(:deadline)) (org-agenda-overriding-header "Upcoming Deadlines (+14d)") (org-super-agenda-groups '((:anything t))))) ;; Block 4: project (org-ql-block '(and (tags "project") (not (done))) ((org-ql-block-header "Project") (org-super-agenda-groups '((:name "ACTIVE" :todo "ACTIVE" :order 1) (:name "NEXT" :todo "NEXT" :order 2) (:name "TODO" :todo "TODO" :order 3) (:name "WAIT" :todo "WAIT" :order 4) (:discard (:anything t)))))) ;; Block 5: development (org-ql-block '(and (tags "development") (not (done))) ((org-ql-block-header "Development") (org-super-agenda-groups '((:name "ACTIVE" :todo "ACTIVE" :order 1) (:name "NEXT" :todo "NEXT" :order 2) (:name "TODO" :todo "TODO" :order 3) (:name "WAIT" :todo "WAIT" :order 4) (:discard (:anything t)))))) ;; Block 6: blog (org-ql-block '(and (tags "blog") (not (done))) ((org-ql-block-header "Blog") (org-super-agenda-groups '((:name "ACTIVE" :todo "ACTIVE" :order 1) (:name "NEXT" :todo "NEXT" :order 2) (:name "TODO" :todo "TODO" :order 3) (:name "WAIT" :todo "WAIT" :order 4) (:discard (:anything t)))))) ;; Block 7: self (org-ql-block '(and (tags "self") (not (done))) ((org-ql-block-header "Self") (org-super-agenda-groups '((:name "ACTIVE" :todo "ACTIVE" :order 1) (:name "NEXT" :todo "NEXT" :order 2) (:name "TODO" :todo "TODO" :order 3) (:name "WAIT" :todo "WAIT" :order 4) (:discard (:anything t)))))) ;; Block 8: backlog — everything not covered above (org-ql-block '(and (todo) (not (done)) (not (tags "project" "development" "blog" "self"))) ((org-ql-block-header "Backlog") (org-super-agenda-groups '((:name "ACTIVE" :todo "ACTIVE" :order 1) (:name "NEXT" :todo "NEXT" :order 2) (:name "TODO" :todo "TODO" :order 3) (:name "WAIT" :todo "WAIT" :order 4) (:discard (:anything t)))))))))) ;;; --- Theme-adaptive TODO keyword faces --- (setq org-todo-keyword-faces '(("TODO" . (:inherit (warning org-todo))) ("NEXT" . (:inherit (font-lock-type-face org-todo))) ("ACTIVE" . (:inherit (bold org-todo))) ("WAIT" . (:inherit (shadow org-todo))) ("DONE" . (:inherit (success org-todo))) ("CANCELLED" . (:inherit (shadow org-done))))) ;; (defun my/update-org-modern-faces () ;; (setq org-modern-todo-faces ;; `(("TODO" :background ,(face-attribute 'warning :foreground) ;; :foreground ,(face-attribute 'default :background)) ;; ("ACTIVE" :background ,(face-attribute 'error :foreground) ;; :foreground ,(face-attribute 'default :background)) ;; ("DONE" :background ,(face-attribute 'success :foreground) ;; :foreground ,(face-attribute 'default :background)) ;; ("CANCELLED" :background ,(face-attribute 'shadow :foreground) ;; :foreground ,(face-attribute 'default :background))))) ;; ;; (add-hook 'after-load-theme-hook #'my/update-org-modern-faces) ;; (add-hook 'after-init-hook #'my/update-org-modern-faces) ;;; --- Tag cleanup in agenda buffer --- (defun my/org-agenda-remove-tags () (save-excursion (goto-char (point-min)) (while (re-search-forward " +:[[:alnum:]_@#%:]+: *$" nil t) (replace-match "")))) (add-hook 'org-agenda-finalize-hook #'my/org-agenda-remove-tags) ;;; --- Late-indicator symbols for overdue items --- ;; ;; Replaces the numeric "days late" extra field with visual indicators: ;; 1-2 days: • 3-6 days: ! 7-13 days: ‼ 14+ days: ✖ (defun my/org-agenda-late-extra (orig-fn extra txt &rest args) "Advice around `org-agenda-format-item' to show overdue indicators." (let* ((agenda-extra (let ((d (when (stringp extra) (abs (string-to-number extra))))) (cond ((not d) "") ((= d 0) "") ((< d 3) " •") ((< d 7) " !") ((< d 14) " ‼") (t " ✖"))))) (apply orig-fn agenda-extra txt args))) (advice-add 'org-agenda-format-item :around #'my/org-agenda-late-extra) ;;; --- Clean up excessive blank lines between agenda blocks --- (defun my/org-agenda-fix-block-spacing () "Collapse triple+ newlines to double in the agenda buffer." (goto-char (point-min)) (while (re-search-forward "\n\n\n+" nil t) (replace-match "\n\n"))) (add-hook 'org-agenda-finalize-hook #'my/org-agenda-fix-block-spacing) (use-package org-contrib :ensure (:wait t)) (require 'ox-extra) (ox-extras-activate '(ignore-headlines)) (use-package ox-typst :ensure (:host github :repo "jmpunkt/ox-typst") :after org) (use-package org-cliplink :ensure t :defer t) (use-package org-download :ensure t :hook (org-mode . org-download-enable) :config (setq-default org-download-image-dir "~/org/attachments/") (setq org-download-method 'directory) (setq org-download-timestamp "%Y%m%d-%H%M%S_") (setq org-download-heading-lvl nil)) ;; (setq org-publish-project-alist ;; '(("wiki" ;; :author "cashmere" ;; :email "cashmere@cashmere.rs" ;; :preserve-breaks t ;; :preserve-indent t ;; :with-title t ;; :base-directory "/home/cashmere/wiki/" ;; :base-extension "org" ;; :publishing-directory "/home/cashmere/wiki/html/" ;; :recursive t ;; :publishing-function org-html-publish-to-html ;; :with-toc nil ;; :section-numbers nil ;; :html-head "" ;; :preparation-function (lambda (_) ;; (require 'denote-org) ;; (dolist (file (directory-files-recursively ;; "/home/cashmere/wiki/" "\\.org$")) ;; (with-current-buffer (find-file-noselect file) ;; (ignore-errors ;; (denote-org-extras-convert-links-to-file-type)) ;; (save-buffer))))))) (setq org-publish-project-alist '(("wiki" :base-directory "~/garden" :publishing-directory "~/cashmere.rs/content/wiki" :publishing-function denote-publish-to-md :recursive nil :exclude-tags ("noexport" "draft") :section-numbers nil :with-creator nil :with-toc nil))) (setq org-export-with-broken-links t) (use-package ox-pandoc :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) :config (setq denote-rename-buffer-format "%t" denote-buffer-name-prefix "" denote-directory (expand-file-name "~/org/")) (denote-rename-buffer-mode 1)) (use-package denote-agenda :ensure t :after org :config (setq denote-agenda-include-regexp "_agenda") (denote-agenda-insinuate)) (defun my/denote-toggle-agenda-keyword () "Toggle the \"agenda\" keyword on the current Denote note. Renames the file and rewrites its front matter, saving the buffer." (interactive) (unless (and buffer-file-name (denote-file-is-note-p buffer-file-name)) (user-error "Current buffer is not a Denote note")) (save-buffer) (let* ((keywords (denote-extract-keywords-from-path buffer-file-name)) (denote-rename-confirmations nil) (denote-save-buffers t) (new-keywords (if (member "agenda" keywords) (remove "agenda" keywords) (cons "agenda" keywords)))) (denote-rename-file buffer-file-name 'keep-current new-keywords 'keep-current 'keep-current 'keep-current) (message "%s the agenda" (if (member "agenda" new-keywords) "Added to" "Removed from")))) (use-package denote-journal :ensure t :config (setopt denote-journal-title-format 'day-date-month-year)) ;; (defun my/denote-journal-template () ;; "Insert journal structure into a new journal entry. ;; Only inserts when the buffer has just the front-matter (fresh file)." ;; (when (and (buffer-file-name) ;; (string-match-p "journal" (buffer-file-name)) ;; (<= (count-lines (point-min) (point-max)) 6)) ;; (goto-char (point-max)) ;; (insert ;; "\n* Clockreport \n" ;; "\n#+BEGIN: clocktable :scope file :maxlevel 3 :emphasize nil :link t\n" ;; "#+END:\n" ;; "\n* Agenda \n" ;; "\n** NEXT Plan the day\n" ;; ":LOGBOOK:\n" ;; ":END:\n" ;; "\n* Braindump \n" ;; ":PROPERTIES:\n" ;; ":VISIBILITY: folded\n" ;; ":END:\n"))) ;; (advice-add 'denote-journal-new-or-existing-entry :after ;; (lambda (&rest _) (my/denote-journal-template))) (defcustom my/denote-agenda-filename-component-regexp "\\(?:[-_.]\\|:\\)agenda\\(?:[-_.]\\|:\\|$\\)" "Regexp matching a filename component that denotes the 'agenda' tag. Works on the base filename (without extension), e.g. matches \"-agenda\", \":agenda:\", \"_agenda_\"." :type 'string) (defun my/denote--make-unique-filename (dir base ext) "Return a unique filepath in DIR for BASE+EXT by adding -1, -2... if needed." (let* ((extstr (if (and ext (not (string-empty-p ext))) (concat "." ext) "")) (candidate (expand-file-name (concat base extstr) dir)) (count 1)) (while (file-exists-p candidate) (setq candidate (expand-file-name (format "%s-%d%s" base count extstr) dir)) (setq count (1+ count))) candidate)) (defun my/denote-remove-agenda-filetag-and-silent-rename () "If current headline became DONE in a Denote note, remove :agenda: filetag and remove 'agenda' from filename silently. - Only acts if there are no other non-DONE TODOs in the same file. - Does not call Denote's interactive rename command (no buffers opened)." (when (and (derived-mode-p 'org-mode) (string= (org-get-todo-state) "DONE") (buffer-file-name) ;; detect Denote note by presence of #+identifier: (save-excursion (goto-char (point-min)) (re-search-forward "^#\\+identifier:" nil t))) ;; only proceed if no other active TODOs remain in the file (let ((has-other (catch 'has (org-map-entries (lambda () (let ((s (org-get-todo-state))) (when (and s (not (string= s "DONE"))) (throw 'has t)))) nil 'file) nil))) (unless has-other ;; 1) Remove :agenda: from #+filetags: (save-excursion (goto-char (point-min)) (when (re-search-forward "^#\\+filetags:\\s-*\\(.*\\)$" nil t) (let* ((tags (match-string 1)) (clean (replace-regexp-in-string ":agenda:" "" tags)) (clean (replace-regexp-in-string "::+" ":" clean)) (clean (replace-regexp-in-string "^:\\|:$" "" clean))) (if (string-empty-p clean) (delete-region (line-beginning-position) (1+ (line-end-position))) (beginning-of-line) (kill-line) (insert (concat "#+filetags: :" clean ":")))))) ;; 2) Compute new filename (silent, non-interactive) (let* ((file (buffer-file-name)) (dir (file-name-directory file)) (name (file-name-nondirectory file)) (ext (file-name-extension name)) ;; "org" (base (file-name-sans-extension name)) ;; remove the agenda component from the base (new-base (replace-regexp-in-string my/denote-agenda-filename-component-regexp "" base))) (when (and new-base (not (string= new-base base))) ;; cleanup repeated separators: convert multiple '-'/'_'/'..' to single '-' (setq new-base (replace-regexp-in-string "[-_.:]+" "-" new-base)) ;; strip leading/trailing separators (setq new-base (replace-regexp-in-string "\\`[-_.:-]+" "" new-base)) (setq new-base (replace-regexp-in-string "[-_.:-]+\\'" "" new-base)) ;; ensure unique filename to avoid overwrite without prompt (let ((target (my/denote--make-unique-filename dir new-base ext))) (condition-case err (progn (rename-file file target 1) ;; update current buffer to visit the new filename silently (set-visited-file-name target t t) ;; save changes (filetags update + new filename) (save-buffer) (message "Denote: removed 'agenda' component from filename -> %s" (file-name-nondirectory target))) (error (message "Denote rename failed: %s" (error-message-string err))))))))))) (add-hook 'org-after-todo-state-change-hook #'my/denote-remove-agenda-filetag-and-silent-rename) (use-package denote-menu :ensure nil :after denote :commands (denote-menu)) (use-package denote-org :ensure t) (defun my/convert-all-denote-links-in-directory (directory) "Converts all denote: links to file: links in a specific directory." (interactive "DDirectory: ") (let ((org-files (directory-files directory t "\\.org$"))) (dolist (file org-files) (with-current-buffer (find-file-noselect file) (save-excursion (goto-char (point-min)) (while (re-search-forward "\\[\\[denote:\\([0-9T]+\\)\\]\\(?:\\[\\([^]]*\\)\\]\\)?\\]" nil t) (let* ((id (match-string 1)) (description (match-string 2)) (target-file (seq-find (lambda (f) (string-match-p (regexp-quote id) (file-name-nondirectory f))) org-files))) (when target-file (let* ((target-filename (file-name-nondirectory target-file)) (new-link (if description (format "[[file:./%s][%s]]" target-filename description) (format "[[file:./%s]]" target-filename)))) (replace-match new-link t t)))))) (save-buffer) (kill-buffer))))) (use-package ox-json :ensure t) (use-package ox-gfm :ensure t) (use-package denote-publish :ensure (:repo "https://github.com/vedang/denote-publish")) (setq denote-publish-default-base-dir "~/garden") (setq denote-publish-default-output-dir "~/cashmere.rs/content/wiki") (setq denote-publish-link-class "internal-link") (setq denote-publish-front-matter-fields '(title subtitle identifier date last_updated_at aliases tags category skip_archive has_code og_image og_description og_video_id)) (use-package tmr :ensure (:host github :repo "protesilaos/tmr") :config (setq tmr-sound-file "/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga" tmr-notification-urgency 'normal tmr-description-list 'tmr-description-history) ;; ── Timer persistence across Emacs restarts ────────────── (defvar my/tmr-save-file (expand-file-name "tmr-timers.eld" user-emacs-directory) "File to persist active tmr timers across Emacs restarts.") (defun my/tmr-save-timers () "Save active (non-finished) tmr timers to `my/tmr-save-file'." (let ((active (cl-loop for timer in tmr--timers unless (tmr--timer-finishedp timer) collect (list :end-date (tmr--timer-end-date timer) :creation-date (tmr--timer-creation-date timer) :input (tmr--timer-input timer) :description (tmr--timer-description timer) :acknowledgep (tmr--timer-acknowledgep timer) :paused-remaining (tmr--timer-paused-remaining timer))))) (with-temp-file my/tmr-save-file (prin1 active (current-buffer))))) (defun my/tmr-restore-timers () "Restore tmr timers from `my/tmr-save-file'. Timers that expired while Emacs was closed fire immediately." (require 'tmr) (when (file-exists-p my/tmr-save-file) (let ((saved (with-temp-buffer (insert-file-contents my/tmr-save-file) (read (current-buffer))))) (dolist (entry saved) (let* ((end-date (plist-get entry :end-date)) (creation-date (plist-get entry :creation-date)) (input (plist-get entry :input)) (description (plist-get entry :description)) (acknowledgep (plist-get entry :acknowledgep)) (paused-remaining (plist-get entry :paused-remaining)) (remaining (if paused-remaining paused-remaining (round (- (float-time end-date) (float-time))))) (adjusted-end (if (and (not paused-remaining) (<= remaining 0)) (time-add (current-time) 1) end-date)) (timer (tmr--timer-create :creation-date creation-date :end-date adjusted-end :input input :description description :acknowledgep acknowledgep :paused-remaining paused-remaining))) ;; For active timers, schedule the live Emacs timer and store it ;; in the timer-object slot (index 5 in the cl-defstruct vector). ;; We use aset directly to avoid needing setf expanders at ;; macro-expansion time (tmr may not be loaded when config tangles). (unless paused-remaining (let ((live-timer (run-with-timer (max 1 remaining) nil #'tmr--complete timer))) (aset timer 5 live-timer))) (push timer tmr--timers))) (run-hooks 'tmr--update-hook)) ;; Clear the save file so we don't double-restore (delete-file my/tmr-save-file))) (add-hook 'kill-emacs-hook #'my/tmr-save-timers) (add-hook 'emacs-startup-hook #'my/tmr-restore-timers)) (use-package denote-merge :ensure (:host github :repo "protesilaos/denote-merge") ; not in any package archive ;; You can bind these to keys. They are here so you can learn about them. :commands (denote-merge-file denote-merge-region denote-merge-region-plain denote-merge-region-plain-indented denote-merge-region-org-src denote-merge-region-org-quote denote-merge-region-org-example denote-merge-region-markdown-quote denote-merge-region-markdown-fenced-block)) (use-package denote-solo :ensure (:host github :repo "pavlo/denote-solo") :after denote :config (setq denote-solo-directories '(("org" . "~/org") ("garden" . "~/garden")) denote-solo-display-modeline nil) (denote-solo-mode 1) (defun my/denote-solo-sync-directory (&rest _) (when-let* ((solo denote-solo--current-solo) (path (denote-solo--directory-for-name solo))) (setq-default denote-directory path) (dolist (buf (buffer-list)) (with-current-buffer buf (when (local-variable-p 'denote-directory) (setq denote-directory path)))))) (advice-add 'denote-solo-switch :after #'my/denote-solo-sync-directory)) (use-package company :ensure t :demand t :hook (after-init . global-company-mode) :custom (company-idle-delay 0.25) (company-minimum-prefix-length 2) (company-require-match nil) (company-insertion-on-trigger nil) (company-tooltip-limit 8) (company-tooltip-align-annotations t) (company-selection-wrap-around t) (company-backends '(company-capf)) (company-frontends '(my/company-popup-frontend company-echo-metadata-frontend)) :config (require 'company-childframe) (setq company-selection-default 0)) (defun my/company-popup-frontend (command) "Use an aligned child frame in graphical Emacs and an overlay in terminals." (cond ((eq command 'hide) (company-childframe-hide) (company-pseudo-tooltip-hide)) ((display-graphic-p) (company-childframe-frontend command)) (t (company-pseudo-tooltip-frontend command)))) (defun my/company-abort-and-normal-state () "Close completion and leave Evil insert state." (interactive) (company-abort) (when (bound-and-true-p evil-local-mode) (evil-normal-state))) (defun my/company-tab () "Indent programming buffers or advance an active snippet; otherwise select a candidate." (interactive) (if (derived-mode-p 'prog-mode) (progn (company-abort) (if (and (bound-and-true-p yas-minor-mode) (yas-active-snippets)) (yas-next-field) (indent-for-tab-command))) (company-select-next))) (defun my/company-navigation-keys (&rest _) "Keep completion navigation consistent after Evil Collection loads." (dolist (key '("TAB" "")) (keymap-set company-active-map key #'my/company-tab)) (dolist (key '("C-n" "C-j")) (keymap-set company-active-map key #'company-select-next)) (dolist (key '("" "S-TAB" "C-p" "C-k")) (keymap-set company-active-map key #'company-select-previous)) (keymap-set company-active-map "RET" #'company-complete-selection) (keymap-set company-active-map "" #'company-complete-selection) (keymap-set company-active-map "" #'my/company-abort-and-normal-state)) (with-eval-after-load 'company (my/company-navigation-keys) (add-hook 'evil-collection-setup-hook #'my/company-navigation-keys)) (use-package which-key :ensure t :defer t :hook (after-init . which-key-mode) :custom (which-key-idle-delay 0.3)) (use-package cape :ensure (:wait t) ;; Bind prefix keymap providing all Cape commands under a mnemonic key. ;; Press C-c p ? to for help. ;; Alternatively bind Cape commands individually. ;; :bind (("C-c p d" . cape-dabbrev) ;; ("C-c p h" . cape-history) ;; ("C-c p f" . cape-file) ;; ...) :init ;; Add to the global default value of `completion-at-point-functions' which is ;; used by `completion-at-point'. The order of the functions matters, the ;; first function returning a result wins. Note that the list of buffer-local ;; completion functions takes precedence over the global list. (add-hook 'completion-at-point-functions #'cape-file)) (use-package helm :ensure (:wait t) :after evil-collection :hook (after-init . helm-mode) (helm-minibuffer-set-up . turn-off-evil-mode) :custom (helm-M-x-fuzzy-match t) (helm-occur-buffer-substring-default-mode 'buffer-substring) (helm-buffers-fuzzy-matching t) (helm-recentf-fuzzy-match t) (helm-move-to-line-cycle-in-source nil) (helm-split-window-inside-p t) (helm-autoresize-mode 1) (helm-buffers-show-icons t) (helm-follow-input-idle-delay 0.15) (helm-boring-buffer-regexp-list '("\\` " "\\`\\*helm" "\\`\\*Echo Area" "\\`\\*Minibuf" "\\`\\*Messages\\*\\'" "\\`\\*Warnings\\*\\'" "\\`\\*Compile-Log\\*\\'" "\\`\\*Backtrace\\*\\'" "\\`\\*Help\\*\\'" "\\`\\*scratch\\*\\'" "\\`\\*Completions\\*\\'" "\\`\\*Flymake" "\\`\\*eldoc" "\\`\\*elpaca-log\\*\\'" "\\`\\*Native-compile-Log\\*\\'" "\\`\\*Async-native-compile-log\\*\\'")) ;; (helm-display-function #'helm-display-buffer-in-own-frame) :config (with-eval-after-load 'helm-grep (setf (eieio-oref-default 'helm-grep-ag-class 'follow) 1))) (use-package helm-xref :ensure t :after helm) (use-package citre :ensure t :demand t :init (require 'citre-config) :custom (citre-default-create-tags-file-location 'in-dir) (citre-edit-ctags-options-manually nil) (citre-auto-enable-citre-mode-modes '(prog-mode)) (citre-completion-backends '(my-index)) (citre-find-definition-backends '(my-index)) (citre-identifier-list-backends '(my-index)) (citre-tags-in-buffer-backends '(my-index)) (citre-find-reference-backends '(global)) (citre-capf-optimize-for-popup t) :config (require 'citre-index) (require 'citre-backend) (setq my/project-ctags-program (expand-file-name "bin/project-ctags" user-emacs-directory) my/citre-index-cache-directory (expand-file-name "citre-cache/" user-emacs-directory) my/citre-index-idle-delay 0.1 my/citre-index-refresh-delay 0.2 my/citre-index-check-interval 30 my/citre-index-dependency-check-interval 300 my/citre-index-max-buffer-size (* 2 1024 1024) my/citre-index-max-watches 256 my/citre-index-process-timeout 120 my/citre-completion-deduplication 'name my/citre-completion-context-filter 'safe my/citre-completion-source-order '(current-buffer live-buffer project dependency tags-file) my/citre-completion-annotation '(scope source count) my/citre-index-excluded-directories '(".git" ".hg" ".svn" ".direnv" ".venv" ".pixi" "__pycache__" "node_modules" "target" "build" "dist" "citre-cache" "elpaca" "elpa" "eln-cache" "tree-sitter" "workspaces")) (defun my/citre-completion-sources () "Use file completion before language sources and buffer words as a fallback." (when (derived-mode-p 'prog-mode) (setq-local completion-at-point-functions (cl-subst #'my/citre-completion-at-point #'citre-completion-at-point completion-at-point-functions)) (add-hook 'completion-at-point-functions #'cape-file nil t) (add-hook 'completion-at-point-functions #'cape-dabbrev 90 t))) (defun my/citre-auto-enable () "Let Citre enable unmanaged buffers without toggling an already active mode." (unless (bound-and-true-p citre-mode) (citre-auto-enable-citre-mode))) (remove-hook 'find-file-hook #'citre-auto-enable-citre-mode) (add-hook 'find-file-hook #'my/citre-auto-enable 90) (add-hook 'citre-mode-hook #'my/citre-completion-sources) (my/citre-index-mode 1)) (use-package nov :ensure t :mode ("\\.epub\\'" . nov-mode) :hook (nov-mode . visual-line-mode) :custom (nov-text-width 100)) (defun my/programming-indent-setup () "Use TAB to indent programming buffers without starting completion." (setq-local tab-always-indent t)) (add-hook 'prog-mode-hook #'my/programming-indent-setup) (use-package typst-ts-mode :ensure t :mode "\\.typ\\'") (use-package yasnippet :ensure (:wait t) :demand t :config (yas-global-mode 1)) (use-package yasnippet-snippets :ensure (:wait t) :demand t :after yasnippet :config (yas-reload-all)) (defun my/yas-tab-filter (command) "Offer Yasnippet expansion through TAB outside programming buffers." (unless (derived-mode-p 'prog-mode) (yas-maybe-expand-abbrev-key-filter command))) (use-package nix-ts-mode :ensure t :mode "\\.nix\\'" :config (modify-syntax-entry ?. "_" nix-ts-mode--syntax-table)) (with-eval-after-load 'compile (dolist (rule '((nix-def "^\\s-*- In [`']\\(/[^'\n]+\\)'" 1) (nix-eval "^\\s-+at \\(/[^:\n]+\\):\\([0-9]+\\):\\([0-9]+\\):" 1 2 3) (nix-trace "^\\s-+at \\(/nix/store/[^:\n]+\\):\\([0-9]+\\):\\([0-9]+\\):" 1 2 3 0))) (add-to-list 'compilation-error-regexp-alist (car rule)) (add-to-list 'compilation-error-regexp-alist-alist rule))) (defun nix-compile-goto-option () (interactive) (let (option path) (save-excursion (goto-char (point-min)) (unless (re-search-forward "option `\\([^'\n]+\\)'" nil t) (user-error "No nix option error found")) (setq option (match-string 1)) (unless (re-search-forward "- In `\\(/[^'\n]+\\)'" nil t) (user-error "No definition file found")) (setq path (match-string 1))) (when (file-directory-p path) (setq path (expand-file-name "default.nix" path))) (find-file path) (goto-char (point-min)) (search-forward (car (last (split-string option "\\." t)))))) (use-package go-mode :ensure t :mode (("\\.go\\'" . go-mode) ("\\.templ\\'" . go-mode)) :hook (go-mode . (lambda () (setq-local tab-width 4)))) (defun my/go-citre-bindings (&optional mode _maps &rest _) "Apply Citre navigation after Go or Evil Collection loads." (when (and (boundp 'go-mode-map) (or (null mode) (eq mode 'go-mode))) (keymap-set go-mode-map " " #'citre-jump) (keymap-set go-mode-map " " #'citre-peek) (when (featurep 'evil) (evil-define-key* 'normal go-mode-map (kbd "g d") #'citre-jump (kbd "K") #'citre-peek)))) (with-eval-after-load 'go-mode (my/go-citre-bindings)) (with-eval-after-load 'evil (my/go-citre-bindings)) (add-hook 'evil-collection-setup-hook #'my/go-citre-bindings) (use-package c-ts-mode :ensure nil :mode (("\\.c\\'" . c-ts-mode) ("\\.h\\'" . c-ts-mode)) :custom (c-ts-mode-indent-offset 2) (c-ts-mode-indent-style 'k&r)) (use-package c3-ts-mode :ensure (:host github :repo "c3lang/c3-ts-mode" :ref "d6ad0e39351d4a5430e731d05474a064dcdb2bc6" :files ("c3-ts-mode.el")) :mode "\\.c3[it]?\\'" :init (with-eval-after-load 'treesit (setf (alist-get 'c3 treesit-language-source-alist) '("https://github.com/c3lang/tree-sitter-c3" :commit "56d73880751a3f442296d7042e7f0a3d21967238"))) :custom (c3-ts-mode-indent-offset 2) (c3-ts-mode-highlight-variable t) (c3-ts-mode-highlight-property t) (c3-ts-mode-highlight-assignment t) (c3-ts-mode-highlight-punctuation t)) (use-package rust-ts-mode :ensure nil :mode "\\.rs\\'" :custom (rust-ts-indent-offset 4)) (use-package pyvenv :ensure t :config (setq pyvenv-mode-line-indicator '(pyvenv-virtual-env-name (" [venv:" pyvenv-virtual-env-name "] "))) (add-hook 'python-ts-mode-hook (lambda () (pyvenv-mode 1)))) (use-package xonsh-mode :ensure (:host github :repo "seanfarley/xonsh-mode") :mode ("\\.xsh\\'" "\\.xonshrc\\'")) (define-derived-mode xonsh-ts-mode python-ts-mode "Xonsh[ts]" "Major mode for xonsh, derived from `python-ts-mode'. Xonsh is a python superset; reuse python tree-sitter parser." (setq-local comment-start "# " comment-end "")) (with-eval-after-load 'treesit (when (treesit-language-available-p 'python) (add-to-list 'auto-mode-alist '("\\.xsh\\'" . xonsh-ts-mode)) (add-to-list 'auto-mode-alist '("\\.xonshrc\\'" . xonsh-ts-mode)))) (defvar my/ob-xonsh-installer (lambda () (defun org-babel-execute:xonsh (body params) "Execute a xonsh source block. BODY is the xonsh script. PARAMS may include :dir and :cmdline." (let* ((dir (or (cdr (assq :dir params)) default-directory)) (cmdline (or (cdr (assq :cmdline params)) "")) (tmp (make-temp-file "ob-xonsh-" nil ".xsh"))) (unwind-protect (progn (with-temp-file tmp (insert body)) (let ((default-directory dir)) (shell-command-to-string (format "xonsh %s %s" cmdline (shell-quote-argument tmp))))) (when (file-exists-p tmp) (delete-file tmp)))))) "Defines `org-babel-execute:xonsh'; reused in ob-async child processes.") (with-eval-after-load 'org (funcall my/ob-xonsh-installer) (add-to-list 'org-src-lang-modes '("xonsh" . xonsh))) (with-eval-after-load 'ob-async (add-hook 'ob-async-pre-execute-src-block-hook my/ob-xonsh-installer)) (use-package yaml-ts-mode :ensure nil :mode "\\.ya?ml\\'") (use-package bash-ts-mode :ensure nil :mode (("\\.sh\\'" . bash-ts-mode) ("\\.bash\\'" . bash-ts-mode) ("\\.bashrc\\'" . bash-ts-mode) ("\\.bash_profile\\'" . bash-ts-mode))) (use-package just-mode :ensure t :mode ("[Jj]ustfile\\'" "\\.just\\'")) (use-package olivetti :ensure t :commands (olivetti-mode) :preface (defun my/prose-writing-layout () "Use a centered, comfortably spaced prose-writing layout." (setq-local olivetti-body-width 88 line-spacing 0.14) (display-line-numbers-mode -1) (olivetti-mode 1)) :hook ((org-mode . my/prose-writing-layout) (markdown-mode . my/prose-writing-layout)) :custom (olivetti-style nil) ; Use window margins (fringes disabled in early-init) (olivetti-margin-width 10) ; No side margins ;; (olivetti-shrink t) (olivetti-safe t) :config ;; Guard: vertico-posframe calls `text-scale-set' on *Minibuf-1*, which ;; triggers olivetti's buffer-local text-scale-mode-hook. olivetti then ;; calls window-width on the minibuffer window (nil) -> wrong-type-argument. (add-hook 'minibuffer-setup-hook (lambda () (when (bound-and-true-p olivetti-mode) (olivetti-mode -1)))) (define-advice olivetti-set-window (:around (orig win) my/skip-minibuffer) (unless (window-minibuffer-p win) (funcall orig win))) (defun my/olivetti-rebalance (&rest _) (dolist (win (get-buffer-window-list nil nil 'visible)) (when (buffer-local-value 'olivetti-mode (window-buffer win)) (olivetti-set-window win)))) (defun my/olivetti-hide-continuation-glyph () (let ((dt (or buffer-display-table (make-display-table)))) (set-display-table-slot dt 'wrap (if olivetti-mode ?\s nil)) (setq buffer-display-table dt))) (add-hook 'olivetti-mode-hook #'my/olivetti-hide-continuation-glyph) (with-eval-after-load 'diff-hl-margin (advice-add 'diff-hl-margin-local-mode :after #'my/olivetti-rebalance) (advice-add 'diff-hl-margin-ensure-visible :after #'my/olivetti-rebalance))) (use-package diff-hl :ensure t :hook (((prog-mode text-mode conf-mode) . diff-hl-mode) (magit-post-refresh . diff-hl-magit-post-refresh) (fossil-ui-post-refresh . fossil-ui-diff-hl-refresh)) :custom (diff-hl-margin-symbols-alist '((insert . "┃") (delete . "▁") (change . "┃") (unknown . "?") (ignored . " "))) :config (diff-hl-flydiff-mode 1) (unless (display-graphic-p) (diff-hl-margin-mode 1))) (use-package hl-todo :ensure (:host github :repo "tarsius/hl-todo") :config (global-hl-todo-mode) (setq hl-todo-keyword-faces '(("FIXME" . "#FF4500") ("BUG" . "#FF0000") ("DEBUG" . "#A020F0") ("HACK" . "#E6DB74") ("TODO" . "#FF8C00") ("REVIEW" . "#2D9574") ("OPTIMIZE" . "#1E90FF") ("STUB" . "#87CEEB") ("DEPRECATED" . "#808080")))) (use-package magit :ensure (:wait t) :config (if ek-use-nerd-fonts (setopt magit-format-file-function #'magit-format-file-nerd-icons)) :custom (magit-display-buffer-function #'magit-display-buffer-same-window-except-diff-v1)) (defun my/magit-kill-buffers () (interactive) (let ((buffers (magit-mode-get-buffers))) (magit-restore-window-configuration) (mapc #'kill-buffer buffers))) (setq switch-to-prev-buffer-skip (lambda (window buffer bury-or-kill) (string-match-p "\\*magit" (buffer-name buffer)))) (defun my/magit-uncommit () "Undo the last commit, leaving its changes in the working tree, unstaged." (interactive) (magit-reset-mixed "HEAD^") (message "Uncommitted HEAD^ — changes preserved, unstaged")) (use-package forge :ensure t :after magit) (use-package indent-guide :defer t :ensure t :hook (prog-mode . indent-guide-mode) ;; Activate indent-guide in programming modes. :config (setq indent-guide-char "│") ;; Set the character used for the indent guide. (define-advice indent-guide-show (:around (orig) my/guard-stale-window) (when (and (eq (window-buffer) (current-buffer)) (<= (window-start) (point-max))) (funcall orig)))) (use-package evil :ensure (:wait t) :init (setq evil-want-integration t) (setq evil-want-keybinding nil) (setq evil-want-C-u-delete t) (setq evil-want-C-u-scroll t) (setq evil-undo-system 'undo-redo) (setq evil-split-window-below t) (setq evil-vsplit-window-right t) (setq evil-paste-from-register nil) :config (evil-mode 1)) (modify-syntax-entry ?_ "w") (defun my/eldoc-and-jump () "Show documentation at point. Works in both GUI and terminal." (interactive) ;; (if (display-graphic-p) ;; (eldoc-box-help-at-point) ;; (eldoc-doc-buffer t)) (eldoc-doc-buffer t)) (use-package smartparens :ensure t :init (smartparens-global-mode 1) :config (require 'smartparens-config) (sp-local-pair 'org-mode "~" nil :actions nil)) (use-package evil-smartparens :ensure t :after evil-collection :hook (smartparens-global-mode . evil-smartparens-mode)) (use-package evil-collection :after evil :ensure (:wait t) :config (setq evil-collection-mode-list (remq 'helm evil-collection-mode-list)) (evil-collection-require 'minibuffer) (evil-collection-init)) (use-package evil-surround :ensure t :after evil-collection :config (global-evil-surround-mode 1)) (use-package evil-matchit :ensure t :after evil-collection :config (global-evil-matchit-mode 1)) (use-package evil-textobj-anyblock :ensure t :after evil :config (setq evil-textobj-anyblock-blocks '(("(" . ")") ("{" . "}") ("\\[" . "\\]") ("<" . ">")))) (evil-define-text-object my-evil-textobj-anyblock-inner-quote (count &optional beg end type) "Select the closest outer quote." (let ((evil-textobj-anyblock-blocks '(("'" . "'") ("\"" . "\"") ("`" . "'") ("“" . "”")))) (evil-textobj-anyblock--make-textobj beg end type count nil))) (evil-define-text-object my-evil-textobj-anyblock-a-quote (count &optional beg end type) "Select the closest outer quote." (let ((evil-textobj-anyblock-blocks '(("'" . "'") ("\"" . "\"") ("`" . "'") ("“" . "”")))) (evil-textobj-anyblock--make-textobj beg end type count t))) (use-package evil-mc :ensure t :after evil :config (global-evil-mc-mode 1) (defvar my/evil-mc-visual-map nil) (setq my/evil-mc-visual-map (make-sparse-keymap)) (keymap-set my/evil-mc-visual-map "a" #'evil-mc-make-all-cursors) (keymap-set my/evil-mc-visual-map "q" #'my/evil-mc-quit) (keymap-set my/evil-mc-visual-map "n" #'evil-mc-make-and-goto-next-match) (keymap-set my/evil-mc-visual-map "p" #'evil-mc-make-and-goto-prev-match) (keymap-set my/evil-mc-visual-map "C-n" #'my/evil-mc-skip-next-match) (keymap-set my/evil-mc-visual-map "C-p" #'my/evil-mc-skip-prev-match) (evil-define-command my/evil-mc-visual-block () :keep-visual t (interactive) (call-interactively #'evil-visual-block) (when (evil-visual-state-p) (set-transient-map my/evil-mc-visual-map #'evil-visual-state-p))) (defun my/evil-mc-quit () (interactive) (evil-mc-undo-all-cursors) (evil-exit-visual-state)) (evil-define-command my/evil-mc-skip-next-match () :repeat ignore :evil-mc t (evil-mc-make-and-goto-next-match) (evil-mc-skip-and-goto-next-match)) (evil-define-command my/evil-mc-skip-prev-match () :repeat ignore :evil-mc t (evil-mc-make-and-goto-prev-match) (evil-mc-skip-and-goto-prev-match)) (defun my/evil-visual-block-p () (and (bound-and-true-p evil-visual-selection) (eq evil-visual-selection 'block))) (defun my/evil-mc-visual-block-insert () (interactive) (if (my/evil-visual-block-p) (progn (evil-mc-make-cursor-in-visual-selection-beg) (evil-insert 1)) (call-interactively 'evil-insert))) (defun my/evil-mc-visual-block-append () (interactive) (if (my/evil-visual-block-p) (progn (evil-mc-make-cursor-in-visual-selection-end) (evil-append 1)) (call-interactively 'evil-append)))) (with-eval-after-load 'evil-mc (defun my/evil-mc-keep-visual-selection (original &rest args) (if (evil-visual-state-p) (let* ((range (evil-visual-range)) (length (if (evil-mc-has-pattern-p) (evil-mc-get-pattern-length) (- (cadr range) (car range))))) (prog1 (apply original args) (setq evil-mc-cursor-list (mapcar (lambda (cursor) (save-excursion (goto-char (evil-mc-get-cursor-start cursor)) (evil-mc-delete-region-overlay (evil-mc-get-cursor-region cursor)) (evil-mc-put-cursor-region cursor (evil-mc-create-region (- (1+ (point)) length) (point) 'char)))) evil-mc-cursor-list)) (evil-visual-char (- (1+ (point)) length) (point)))) (apply original args))) (advice-add 'evil-mc-find-and-goto-match :around #'my/evil-mc-keep-visual-selection) (advice-add 'evil-mc-make-all-cursors :around #'my/evil-mc-keep-visual-selection)) (use-package flash :ensure (:host github :repo "Prgebish/flash") :after evil :custom (flash-multi-window t) (flash-backdrop t) (flash-case-fold t) (flash-autojump t) (flash-highlight-matches t) (flash-label-position 'overlay) (flash-jump-position 'start) (flash-char-jump-labels t) (flash-char-multi-line nil) (flash-nohlsearch t) (flash-jumplist t) (flash-rainbow t) (flash-rainbow-shade 5) :config ;; Evil integration: binds gs in normal/visual/operator + enhanced f/t/F/T (require 'flash-evil) (flash-evil-setup t) ;; Search integration: labels during C-s, /, ? (require 'flash-isearch) (flash-isearch-mode 1)) (setq undo-limit 800000 undo-strong-limit 12000000 undo-outer-limit 120000000) (use-package rainbow-delimiters :defer t :ensure t :hook (prog-mode . rainbow-delimiters-mode)) (use-package dotenv-mode :defer t :ensure t :config) (add-hook 'org-agenda-finalize-hook #'org-save-all-org-buffers) (use-package org-agenda :ensure nil :config (setq org-agenda-window-setup 'current-window org-agenda-inhibit-startup t org-agenda-use-tag-inheritance nil org-agenda-dim-blocked-tasks nil org-startup-indented nil org-startup-folded 'overview org-agenda-prefix-format '((agenda . " %i %?-12t% s") (todo . " %i ") (tags . " %i ") (search . " %i ")))) (use-package evil-org :ensure t :after org :hook (org-mode . evil-org-mode) :config (require 'evil-org-agenda) (evil-org-agenda-set-keys)) (use-package nerd-icons-dired :if ek-use-nerd-fonts ;; Load the package only if the user has configured to use nerd fonts. :ensure t ;; Ensure the package is installed. :defer t ;; Load the package only when needed to improve startup time. :hook (dired-mode . nerd-icons-dired-mode)) (use-package diredfl :ensure t :hook ((dired-mode . diredfl-mode) (dirvish-directory-view-mode . diredfl-mode))) (setq display-time-default-load-average nil) (display-time-mode 1) (defvar my/mode-line--format (or (default-value 'mode-line-format) (eval (car (get 'mode-line-format 'standard-value)) t)) "The format restored when the bottom mode line is visible.") (define-minor-mode my/global-mode-line-mode "Show the traditional bottom mode line in regular buffers." :global t :init-value t :group 'mode-line (setq-default mode-line-format (and my/global-mode-line-mode my/mode-line--format)) (force-mode-line-update t)) (defun my/hide-mode-line () "Toggle the traditional bottom mode line globally." (interactive) (my/global-mode-line-mode (if my/global-mode-line-mode -1 1))) ;; The custom header line already carries the status information, so keep the ;; traditional bottom mode line hidden on startup (including `emacs -nw'). (my/global-mode-line-mode -1) (use-package mode-line-maker :ensure (:host github :repo "rougier/mode-line-maker") :config (require 'my-header-line) (my/header-line-install)) (use-package adaptive-wrap :ensure t :hook ((text-mode . adaptive-wrap-prefix-mode)) :config (add-hook 'org-mode-hook (lambda () (when (bound-and-true-p adaptive-wrap-prefix-mode) (adaptive-wrap-prefix-mode -1))))) (setq package-gnupghome-dir "~/.gnupg") (blink-cursor-mode 0) (setq-default cursor-type 'box) (defun my/fontconfig-name (&optional height) "Return the configured monospace Fontconfig name at HEIGHT." (concat cashmere/font-family (when height (format "-%g" (/ height 10.0))) (when cashmere/font-features (concat ":fontfeatures=" (mapconcat #'identity cashmere/font-features ","))))) (use-package fontaine :ensure t :demand t :hook (text-mode . variable-pitch-mode) :custom (fontaine-latest-state-file (locate-user-emacs-file "fontaine-latest-state.eld")) :config (setq fontaine-presets `((regular :default-height ,cashmere/font-height) (comfortable :default-height ,(+ cashmere/font-height 20) :variable-pitch-height 1.08 :line-spacing 0.12) (presentation :default-height ,(+ cashmere/font-height 60) :variable-pitch-height 1.12 :line-spacing 0.16) (t :default-family ,cashmere/font-family :default-weight regular :fixed-pitch-family ,cashmere/font-family :variable-pitch-family "SF Pro Text" :variable-pitch-weight regular :variable-pitch-height 1.04 :mode-line-active-family "SFProText Nerd Font" :mode-line-active-weight regular :mode-line-active-height 0.92 :mode-line-inactive-family "SFProText Nerd Font" :mode-line-inactive-height 0.92 :header-line-family ,cashmere/font-family :header-line-weight regular :line-number-family ,cashmere/font-family :line-number-height 0.9 :bold-weight semibold :line-spacing 0.08))) (defun my/apply-line-number-faces (&optional _theme) "Keep all line numbers typographically quiet and monospaced." (let ((foreground (face-attribute 'line-number :foreground nil t))) (dolist (face '(line-number line-number-current-line)) (set-face-attribute face nil :family cashmere/font-family :height 0.9 :weight 'regular :slant 'normal :width 'normal)) ;; The theme makes the current number bold and accent-colored. Use the ;; regular line-number color so its width and emphasis do not jump. (set-face-attribute 'line-number-current-line nil :foreground foreground :background 'unspecified :box nil :underline nil :overline nil))) (defun my/apply-font-features (&optional _preset) "Apply `cashmere/font-features' without changing the current font size." (dolist (frame (frame-list)) (when (display-graphic-p frame) (set-frame-font (my/fontconfig-name (face-attribute 'default :height frame)) t (list frame) t)))) (add-hook 'fontaine-set-preset-hook #'my/apply-line-number-faces) (add-hook 'fontaine-set-preset-hook #'my/apply-font-features) (add-hook 'enable-theme-functions #'my/apply-line-number-faces) ;; Fontaine 3 implements presets as a global theme, so applying the preset ;; once also covers GUI frames created later by an Emacs daemon. Reapplying ;; it from `server-after-make-frame-hook' can disrupt an active minibuffer ;; (notably Helm) when an emacsclient frame is created or reused. (fontaine-set-preset (or (fontaine-restore-latest-preset) 'regular)) (fontaine-mode 1) (my/apply-line-number-faces) (my/apply-font-features)) ;; Give a daemon's first GUI frame the final base font before its first ;; redisplay. This avoids fallback-font geometry and a visible layout jump; ;; Fontaine's global theme then supplies the remaining face attributes. (add-to-list 'default-frame-alist `(font . ,(my/fontconfig-name cashmere/font-height))) ;; (use-package solarized-theme ;; :ensure t ;; :config ;; (load-theme 'solarized-gruvbox-dark t)) ;; (use-package tokyo-night ;; :ensure t ;; :config ;; (load-theme 'tokyo-night t)) (use-package dirvish :ensure (:host github :repo "latiagertrutis/dirvish" :branch "main") :init (dirvish-override-dired-mode) :custom (dirvish-quick-access-entries '(("h" "~/" "home") ("d" "~/Downloads/" "downloads") ("c" "~/.config/" "config"))) (dirvish-reuse-session 'open) (dirvish-attributes '(file-size)) (dirvish-mode-line-format '(:left (sort file-time symlink) :right (omit yank index))) (dirvish-hide-details '(dirvish dirvish-side)) (dirvish-hide-cursor '(dirvish dirvish-side)) (dired-listing-switches "-l --almost-all --human-readable --group-directories-first --no-group") :config (setq dired-dwim-target t) (setq delete-by-moving-to-trash t) (setq dired-mouse-drag-files t) (define-advice dirvish--preview-dps-validate (:around (fn &optional dps) tty-media-filter) "Drop GUI-only media dispatchers when the session's frame is a TTY." (let ((dps (if (display-graphic-p) dps (cl-remove-if (lambda (d) (memq d '(image gif video video-mtn))) (or dps dirvish-preview-dispatchers))))) (funcall fn dps)))) (use-package zoxide :ensure t :config (defun my/zoxide-add-dired () "Append the current dired/dirvish directory to the zoxide database." (when (and default-directory (not (file-remote-p default-directory))) (zoxide-add default-directory))) (add-hook 'dired-after-readin-hook #'my/zoxide-add-dired)) (use-package croc-ui :ensure nil :commands (croc-ui croc-ui-send-files croc-ui-send-directory croc-ui-send-text croc-ui-receive)) (use-package textui :ensure (:host github :repo "yibie/textui" :ref "v0.8.0" :files ("*.el"))) (use-package bbs-ui :ensure nil :after textui :commands (bbs bbs-open) :custom (bbs-ui-repository-file (expand-file-name "bbs-sandbox.fossil" "/home/cashmere/bbs/")) (bbs-ui-web-base-url "https://bbs.copland.systems") (bbs-ui-pull-on-open nil)) (use-package fossil-ui :ensure nil :demand t :after textui :commands (fossil-ui fossil-ui-status) :custom (fossil-ui-title-alignment 'center) (fossil-ui-text-wrap 'greedy) (fossil-ui-diff-renderer 'auto) (fossil-ui-delta-color-mode 'auto) (fossil-ui-stage-directory (expand-file-name "fossil-stage/" user-emacs-directory)) (fossil-ui-max-stage-bytes (* 5 1024 1024)) (fossil-ui-allow-configured-binary-commits t) (fossil-ui-keyed-timeline t) (fossil-ui-full-frame t) (fossil-ui-confirm-revert t) (fossil-ui-compact-layout t) :config (with-eval-after-load 'zoom (add-to-list 'zoom-ignored-major-modes 'fossil-ui-mode)) (with-eval-after-load 'golden-ratio (add-to-list 'golden-ratio-exclude-modes 'fossil-ui-mode))) (use-package yggdrasil-ui :ensure nil :after textui :commands (yggdrasil-ui)) (use-package sync-ui :ensure nil :commands (sync-ui)) (defun my/reload-config () "Save, tangle and reload config.org in this Emacs process only." (interactive) (require 'ob-tangle) (let* ((org-file (expand-file-name "config.org" user-emacs-directory)) (el-file (expand-file-name "config.el" user-emacs-directory)) (start (current-time))) (let ((inhibit-message t) (message-log-max nil)) (when-let* ((buffer (find-buffer-visiting org-file))) (with-current-buffer buffer (when (buffer-modified-p) (save-buffer)))) (org-babel-tangle-file org-file el-file "emacs-lisp") (load el-file nil 'nomessage)) (message "Config reloaded in %.2fs" (float-time (time-since start))))) (setq-default olivetti-body-width 100) (defun my/olivetti-suitable-buffer-p () (and (buffer-file-name) (not (minibufferp)) (not (derived-mode-p 'special-mode)) (not (string-prefix-p " " (buffer-name))))) (define-globalized-minor-mode my/global-olivetti-mode olivetti-mode (lambda () (when (my/olivetti-suitable-buffer-p) (olivetti-mode 1)))) ;; (my/centered-cursor) ;; (my/global-olivetti-mode) (defvar my-recorded-startup-time-message nil "Stores the formatted string of the Emacs startup metrics.") (defun my-record-startup-time () "Calculate and record the elapsed startup time. This function records the time when `window-setup-hook' runs." (setq my-recorded-startup-time-message (format "Emacs loaded in %.2f seconds (Init time: %.2fs) with %d garbage collections." (float-time (time-since before-init-time)) (float-time (time-subtract after-init-time before-init-time)) gcs-done)) ;; Output to the *Messages* buffer during the initial launch (message "%s" my-recorded-startup-time-message)) (defun my-display-startup-time () "Display the previously recorded Emacs startup time in the echo area." (interactive) (if my-recorded-startup-time-message (message "%s" my-recorded-startup-time-message) (message "Startup time was not recorded."))) (defvar my-recorded-startup-time-message nil "Stores the formatted string of the Emacs startup metrics.") (defun my-record-startup-time () "Calculate and record the elapsed startup time. This function records the time when `window-setup-hook' runs." (setq my-recorded-startup-time-message (format "Emacs loaded in %.2f seconds (Init time: %.2fs) with %d garbage collections." (float-time (time-since before-init-time)) (float-time (time-subtract after-init-time before-init-time)) gcs-done)) ;; Output to the *Messages* buffer during the initial launch (message "%s" my-recorded-startup-time-message)) (my-record-startup-time) (defun my-display-startup-time () "Display the previously recorded Emacs startup time in the echo area." (interactive) (if my-recorded-startup-time-message (message "%s" my-recorded-startup-time-message) (message "Startup time was not recorded."))) (setq ibuffer-never-show-predicates '(;; System buffers ;; "^\\*Messages\\*$" "^\\*scratch\\*$" "^\\*Completions\\*$" "^\\*Help\\*$" "^\\*Apropos\\*$" "^\\*info\\*$" "^\\*Async-native-compile-log\\*$")) (use-package projectile :ensure (:wait t) :demand t :config (projectile-mode +1) (setq projectile-completion-system 'default projectile-enable-caching t projectile-indexing-method 'alien projectile-sort-order 'recentf projectile-require-project-root nil projectile-globally-ignored-buffers '("\\*magit.*")) (add-hook 'after-save-hook #'projectile-cache-current-file)) (defun my/projectile-fossil-command (original vcs &optional directory) "Return a Fossil listing relative to DIRECTORY, delegating other VCS to ORIGINAL." (if (not (eq vcs 'fossil)) (funcall original vcs directory) (let* ((directory (file-name-as-directory (expand-file-name (or directory default-directory)))) (checkout (or (locate-dominating-file directory ".fslckout") (locate-dominating-file directory "_FOSSIL_") (user-error "No Fossil checkout for %s" directory))) (prefix (if (file-equal-p directory checkout) "" (file-relative-name directory checkout)))) (format "fossil ls . | cut -b %d- | tr '\\n' '\\0'" (1+ (string-bytes prefix)))))) (with-eval-after-load 'projectile (advice-add 'projectile-get-ext-command :around #'my/projectile-fossil-command)) (defun my/projectile-dashboard-fullscreen () "Show the Projectile dashboard in the full frame." (interactive) (delete-other-windows) (projectile-dashboard)) (use-package helm-projectile :ensure t :after (helm projectile) :commands (helm-projectile-find-file helm-projectile-switch-project) :custom (helm-projectile-fuzzy-match nil)) (use-package wgrep :ensure t :defer t :custom (wgrep-auto-save-buffer t) (wgrep-change-readonly-file t)) (use-package wgrep-helm :ensure t :after (wgrep helm) :config ;; jump straight into editable wgrep mode in *hgrep*/*hmoccur* buffers (add-hook 'helm-grep-mode-hook #'wgrep-change-to-wgrep-mode) (add-hook 'helm-occur-mode-hook #'wgrep-change-to-wgrep-mode)) (defun my/project-replace () "Project-wide search via ripgrep (`helm-projectile-rg'). Press `C-x C-s' in the helm session to save results to a *hgrep* buffer, then `C-c C-p' (wgrep-change-to-wgrep-mode) to edit in place. `C-c C-c' commits, `C-c C-k' aborts." (interactive) (call-interactively #'helm-projectile-rg)) (defun my/helm-find-in (dir) "Recursively find files below DIR with Helm." (interactive "DFind files in: ") (let ((default-directory (file-name-as-directory (expand-file-name dir)))) (helm-find nil))) (defun my/helm-rg-in (dir) "Search DIR recursively with ripgrep via Helm." (interactive "DRipgrep in: ") (helm-grep-ag (expand-file-name dir) nil)) (defun my/denote-rg () "Search `denote-directory' recursively with ripgrep via Helm." (interactive) (helm-grep-ag (expand-file-name denote-directory) nil)) (use-package persp-mode :ensure t :demand t :init (setq persp-keymap-prefix nil) :config (defvar my/workspaces-master "master") (defvar my/workspace-last nil) (defvar my/workspaces-on-switch-project 'non-empty) (defvar my/workspace-switch-project-function #'my/projectile-dashboard-fullscreen) (setq persp-autokill-buffer-on-remove 'kill-weak persp-reset-windows-on-nil-window-conf nil persp-nil-hidden t persp-auto-save-fname "autosave" persp-save-dir (file-name-concat user-emacs-directory "workspaces/") persp-set-last-persp-for-new-frames t persp-switch-to-added-buffer nil persp-add-buffer-on-after-change-major-mode t persp-kill-foreign-buffer-behaviour 'kill persp-remove-buffers-from-nil-persp-behaviour nil persp-auto-resume-time -1 persp-auto-save-opt 0 uniquify-buffer-name-style nil) (add-to-list 'persp-filter-save-buffers-functions (lambda (buffer) (with-current-buffer buffer (memq major-mode '(eat-mode vterm-mode term-mode shell-mode eshell-mode ghostel-mode))))) (defface my/workspace-tab-selected '((t (:inherit highlight))) "Active workspace tab." :group 'persp-mode) (defface my/workspace-tab '((t (:inherit default))) "Inactive workspace tab." :group 'persp-mode) (defun my/workspace-current-name () (safe-persp-name (get-current-persp))) (defun my/workspace-names () (cl-remove persp-nil-name persp-names-cache :count 1)) (defun my/workspace-protected-p (name) (equal name persp-nil-name)) (defun my/workspace-exists-p (name) (member name (my/workspace-names))) (defun my/workspace-fallback-buffer () (get-buffer-create "*scratch*")) (defun my/workspace-buffer-list (&optional persp) (persp-buffers (or persp (get-current-persp)))) (defun my/workspace-switch (name &optional create) (unless (my/workspace-exists-p name) (if create (persp-add-new name) (user-error "No workspace named '%s'" name))) (let ((old (my/workspace-current-name))) (unless (equal old name) (setq my/workspace-last (if (my/workspace-protected-p old) my/workspaces-master old)) (persp-frame-switch name)))) (defun my/workspace-switch-to-index (index) (let ((name (nth index (my/workspace-names)))) (if name (progn (my/workspace-switch name) (my/workspace-display)) (user-error "No workspace #%d" (1+ index))))) (defun my/workspace--tabline () (let ((current (my/workspace-current-name)) (i 0)) (mapconcat (lambda (name) (setq i (1+ i)) (propertize (format " [%d] %s " i name) 'face (if (equal current name) 'my/workspace-tab-selected 'my/workspace-tab))) (my/workspace-names) " "))) (defun my/workspace-display () (interactive) (let (message-log-max) (message "%s" (my/workspace--tabline)))) (defun my/workspace-new (&optional name) (interactive) (let ((name (or name (read-string "New workspace: " (format "#%d" (1+ (length (my/workspace-names)))))))) (when (my/workspace-protected-p name) (user-error "Reserved workspace name")) (when (my/workspace-exists-p name) (user-error "Workspace '%s' already exists" name)) (my/workspace-switch name t) (minimal-dashboard) (my/workspace-display))) (defun my/workspace-rename (new-name) (interactive (list (read-string "Rename workspace to: " (my/workspace-current-name)))) (when (my/workspace-protected-p (my/workspace-current-name)) (user-error "Can't rename this workspace")) (persp-rename new-name (get-current-persp)) (my/workspace-display)) (defun my/workspace-kill (&optional name) (interactive) (let* ((name (or name (my/workspace-current-name))) (others (remove name (my/workspace-names)))) (when (my/workspace-protected-p name) (user-error "Can't kill this workspace")) (if (null others) (progn (my/workspace-switch my/workspaces-master t) (unless (equal name my/workspaces-master) (persp-kill name))) (when (equal name (my/workspace-current-name)) (my/workspace-switch (if (my/workspace-exists-p my/workspace-last) my/workspace-last (car others)))) (persp-kill name)) (my/workspace-display))) (defun my/workspace-kill-session () (interactive) (when (yes-or-no-p "Clear all workspaces and buffers? ") (let ((persp-autokill-buffer-on-remove t)) (dolist (n (my/workspace-names)) (unless (equal n my/workspaces-master) (persp-kill n)))) (my/workspace-switch my/workspaces-master t) (delete-other-windows) (switch-to-buffer (my/workspace-fallback-buffer)) (my/workspace-display))) (defun my/workspace-switch-to (name) (interactive (list (completing-read "Switch to workspace: " (my/workspace-names)))) (my/workspace-switch name t) (my/workspace-display)) (defun my/workspace-switch-to-final () (interactive) (when-let* ((name (car (last (my/workspace-names))))) (my/workspace-switch name) (my/workspace-display))) (defun my/workspace-other () (interactive) (if (and my/workspace-last (my/workspace-exists-p my/workspace-last)) (progn (my/workspace-switch my/workspace-last) (my/workspace-display)) (user-error "No other workspace"))) (defun my/workspace-cycle (n) (let* ((names (my/workspace-names)) (count (length names)) (index (cl-position (my/workspace-current-name) names :test #'equal))) (if (or (null index) (= count 1)) (user-error "No other workspace") (my/workspace-switch (nth (mod (+ index n) count) names)) (my/workspace-display)))) (defun my/workspace-switch-left () (interactive) (my/workspace-cycle -1)) (defun my/workspace-switch-right () (interactive) (my/workspace-cycle 1)) (defun my/workspace-save-session () (interactive) (persp-save-state-to-file)) (defun my/workspace-load-session () (interactive) (persp-load-state-from-file) (my/workspace-display)) (defun my/workspace-close-window-or-workspace () (interactive) (let ((delete-fn (if (featurep 'evil) #'evil-window-delete #'delete-window))) (if (or (window-dedicated-p) (my/workspace-protected-p (my/workspace-current-name)) (cdr (window-list))) (funcall delete-fn) (my/workspace-kill (my/workspace-current-name))))) (defun my/workspace-init-main (&rest _) (when persp-mode (let (persp-before-switch-functions) (unless (or (persp-get-by-name my/workspaces-master) (> (hash-table-count *persp-hash*) 2)) (persp-add-new my/workspaces-master)) (when (equal (my/workspace-current-name) persp-nil-name) (persp-frame-switch my/workspaces-master))))) (add-hook 'persp-mode-hook #'my/workspace-init-main) (defun my/workspace-switch-to-project (&optional dir) (let* ((dir (or dir default-directory)) (name (projectile-project-name dir))) (when persp-mode (if (or (eq my/workspaces-on-switch-project t) (my/workspace-protected-p (my/workspace-current-name)) (my/workspace-buffer-list)) (progn (my/workspace-switch name t) (switch-to-buffer (my/workspace-fallback-buffer))) (ignore-errors (persp-rename name (get-current-persp)))) (let ((default-directory dir)) (funcall my/workspace-switch-project-function))))) (with-eval-after-load 'projectile (setq projectile-switch-project-action #'my/workspace-switch-to-project)) (add-hook 'persp-filter-save-buffers-functions (lambda (buf) (with-current-buffer buf (derived-mode-p 'erc-mode)))) (defun my/workspace--inhibit-irc-part (orig &rest args) (let ((erc-kill-channel-hook nil) (erc-kill-server-hook nil) (erc-kill-buffer-hook nil)) (apply orig args))) (advice-add 'my/workspace-kill :around #'my/workspace--inhibit-irc-part) (advice-add 'my/workspace-kill-session :around #'my/workspace--inhibit-irc-part) (persp-mode 1)) (with-eval-after-load 'ibuffer (define-ibuffer-filter persp "Limit to buffers in the current workspace." (:description "current workspace") (persp-contain-buffer-p buf))) (defun my/ibuffer-workspace () (interactive) (let ((name (my/workspace-current-name))) (if (equal name my/workspaces-master) (ibuffer nil "*ibuffer: master*") (ibuffer nil (format "*ibuffer: %s*" name) (list (cons 'persp t)))))) (use-package my-quick-access :ensure nil :after persp-mode :demand t :custom (my/quick-access-recent-count 5) ; Recent buffers, excluding the active buffer. (my/quick-access-selection-keys '("f" "j" "d" "k" "g" "h")) ; Recent selection and initial collisions. (my/quick-access-page-size 6) ; Maximum visible entries before paging. (my/quick-access-max-height 0.48) ; At most 48% of the frame height. :config (my/quick-access-mode 1)) (use-package pass :ensure t :defer t :commands (pass) :config (setq pass-view-font-lock-keywords '(("\\(^[^:\t\n]+:\\) " 1 'font-lock-keyword-face))) (add-to-list 'display-buffer-alist '("\\*Pass.*\\*" (display-buffer-full-frame)))) (use-package auth-source :ensure nil ;; This is built-in, no need to fetch it. :defer t :config ;; Enable password-store integration for secure API key retrieval. (when (require 'auth-source-pass nil t) (auth-source-pass-enable) ;; Add password-store explicitly to auth-sources for compatibility. (add-to-list 'auth-sources 'password-store) ;; Optional: Cache expires after 5 minutes for security. (setq auth-source-cache-expiry 300))) (use-package zoom :ensure t) (custom-set-variables '(zoom-size '(0.382 . 0.618))) (use-package golden-ratio :ensure t :config ;; (setq golden-ratio-auto-scale t) ;; (golden-ratio-mode 1) (setq golden-ratio-exclude-buffer-names '(" *which-key*")) (setq golden-ratio-exclude-buffer-regexp '("\\`\\*eldoc"))) (defun my/format-buffer () "Format the current buffer with its mode-specific formatter." (interactive) (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, or select a project first." (interactive) (if (projectile-project-p) (helm-projectile-find-file) (helm-projectile-switch-project))) (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) (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) (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) (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) (keymap-set my/leader-project-map "C" #'my/citre-index-status) (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-map "o" my/leader-open-map) ;; (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 "v" #'yggdrasil-ui) (keymap-set my/leader-open-map "s" #'sync-ui) (keymap-set my/leader-open-map "r" #'rsync-ui) (keymap-set my/leader-open-map "z" #'zfs) ;; (keymap-set my/leader-open-map "c" #'croc-ui) (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) (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) (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 "t" #'justl) (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 "d" #'minimal-dashboard) (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")) (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 "" #'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) (evil-define-motion my/s-key-dispatch (count) "Run Flash with stable viewport margins." :type inclusive :jump t (let ((scroll-margin 0) (maximum-scroll-margin 0)) (flash-evil-jump count))) ;; 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 motion operator) 'global (kbd "s") #'my/s-key-dispatch (kbd "H") #'evil-beginning-of-line (kbd "L") #'evil-end-of-line) (with-eval-after-load 'evil-surround (evil-define-key 'operator evil-surround-mode-map (kbd "s") #'my/s-key-dispatch (kbd "S") #'evil-surround-edit (kbd "g S") #'evil-Surround-edit)) (evil-define-key 'insert 'global (kbd "C-w") evil-window-map) (evil-define-key 'motion 'global (kbd "C-v") #'my/evil-mc-visual-block) (keymap-global-set "M-b" #'my/quick-access) (evil-define-key '(normal visual motion insert emacs) 'global (kbd "M-b") #'my/quick-access) (evil-define-key 'normal 'global (kbd "t") #'my/toggle-header-line-workspaces (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) (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) (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) (defun my/evil-move-visual-lines (direction) "Move the selected lines by DIRECTION, preserving the Visual selection." (let* ((anchor (marker-position evil-visual-mark)) (cursor (marker-position evil-visual-point)) (type (evil-visual-type)) (beg (save-excursion (goto-char (min anchor cursor)) (line-beginning-position))) (end (save-excursion (goto-char (max anchor cursor)) (forward-line 1) (point))) (first-line (line-number-at-pos beg)) (last-line (line-number-at-pos (max beg (1- end)))) (final-newline (eq (char-before (point-max)) ?\n))) (when (if (> direction 0) (< end (point-max)) (> beg (point-min))) (evil-with-single-undo (evil-move beg end (if (> direction 0) (1+ last-line) (- first-line 2))) (unless final-newline (save-excursion (goto-char (point-max)) (when (eq (char-before) ?\n) (delete-char -1)))) (let ((start (save-excursion (goto-char (point-min)) (forward-line (+ first-line direction -1)) (point)))) (evil-visual-make-selection (+ start (- anchor beg)) (+ start (- cursor beg)) type)))) (setq deactivate-mark nil))) (evil-define-command my/evil-move-visual-lines-down () "Move selected lines down one line and remain in Visual state." :keep-visual t (interactive "*") (my/evil-move-visual-lines 1)) (evil-define-command my/evil-move-visual-lines-up () "Move selected lines up one line and remain in Visual state." :keep-visual t (interactive "*") (my/evil-move-visual-lines -1)) (evil-define-key 'visual 'global (kbd "N") #'my/evil-move-visual-lines-down (kbd "P") #'my/evil-move-visual-lines-up) (with-eval-after-load 'yasnippet (define-key yas-minor-mode-map (kbd "TAB") '(menu-item "" yas-expand :filter my/yas-tab-filter))) (with-eval-after-load 'compile (keymap-set compilation-mode-map "C-c o" #'nix-compile-goto-option)) (defun my/dired-view-file-externally () "Open the current Dired file through mailcap." (interactive) (mailcap-view-file (dired-get-filename))) (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 visual) dired-mode-map (kbd "SPC") my/leader-map) (evil-define-key 'normal dired-mode-map (kbd "h") #'dired-up-directory (kbd "l") #'dired-find-file)) (defvar my/dirvish-yank-map (make-sparse-keymap)) (defvar my/dirvish-symlink-map (make-sparse-keymap)) (defun dirvish-next-file (arg) "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 raw lines and land on the filename column." (interactive "^p") (forward-line (- arg)) (dired-move-to-filename)) (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 "SPC") my/leader-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 "SPC") my/leader-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)) (defun my/atuin-history () (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 (my/setup-eshell-bindings)) (add-hook 'evil-collection-setup-hook #'my/eshell-bindings-after-evil-collection) (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)) (with-eval-after-load 'fossil-ui (evil-define-key '(normal motion visual) fossil-ui-mode-map (kbd "SPC") my/leader-map (kbd "V") #'evil-visual-line (kbd "s") #'fossil-ui-stage (kbd "u") #'fossil-ui-unstage (kbd "r") #'fossil-ui-refresh (kbd "x") #'fossil-ui-discard (kbd "d") #'fossil-ui-delete (kbd "G") #'evil-goto-line (kbd "TAB") #'fossil-ui-diff (kbd "") #'fossil-ui-diff (kbd "c") nil (kbd "c c") #'fossil-ui-commit (kbd "S") #'fossil-ui-sync (kbd "F") #'fossil-ui-update (kbd "RET") #'fossil-ui-visit-file (kbd "") #'fossil-ui-visit-file (kbd "]c") #'fossil-ui-next-hunk (kbd "[c") #'fossil-ui-previous-hunk (kbd "j") #'evil-next-line (kbd "k") #'evil-previous-line (kbd "n") #'fossil-ui-next-file (kbd "p") #'fossil-ui-previous-file (kbd "q") #'fossil-ui-quit (kbd "?") #'fossil-ui-help) (evil-define-key 'visual fossil-ui-mode-map (kbd "j") #'evil-next-line (kbd "k") #'evil-previous-line) (evil-define-key '(normal insert visual motion) fossil-ui-commit-mode-map (kbd "C-c C-c") #'fossil-ui-commit-submit (kbd "C-c C-k") #'fossil-ui-commit-cancel)) (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)) (evil-set-initial-state 'help-mode 'emacs) (evil-set-initial-state 'ibuffer-mode 'normal) (evil-set-initial-state 'messages-buffer-mode 'normal) (with-eval-after-load 'justl (evil-set-initial-state 'justl-mode 'normal) (evil-set-initial-state 'justl-module-mode 'normal) (evil-set-initial-state 'justl-compile-mode 'normal) (evil-define-key 'normal justl-mode-map (kbd "q") #'quit-window (kbd "r") #'justl--refresh-buffer (kbd "e") #'justl-exec-recipe (kbd "E") #'justl--exec-recipe-with-args (kbd "m") #'justl--show-modules (kbd "RET") #'justl-go-to-recipe (kbd "?") #'justl-help-popup) (evil-define-key 'normal justl-module-mode-map (kbd "q") #'quit-window (kbd "r") #'justl--module-refresh-buffer (kbd "e") #'justl-exec-module (kbd "o") #'justl--module-open-justl (kbd "RET") #'justl--go-to-module (kbd "?") #'justl-module-help-popup) (evil-define-key 'normal justl-compile-mode-map (kbd "q") #'quit-window (kbd "r") #'justl-recompile)) (with-eval-after-load 'magit (evil-define-key '(normal visual) magit-mode-map (kbd "SPC") my/leader-map)) (defvar my/markdown-leader-map (make-sparse-keymap)) (defvar my/markdown-command-map (make-sparse-keymap)) (defvar my/markdown-insert-map (make-sparse-keymap)) (defvar my/markdown-table-map (make-sparse-keymap)) ;; Recreate these maps on reload so removed bindings cannot linger. (setq my/markdown-leader-map (make-sparse-keymap) my/markdown-command-map (make-sparse-keymap) my/markdown-insert-map (make-sparse-keymap) my/markdown-table-map (make-sparse-keymap)) (set-keymap-parent my/markdown-leader-map my/leader-map) (keymap-set my/markdown-leader-map "m" my/markdown-command-map) (keymap-set my/markdown-command-map "v" #'markdown-toggle-markup-hiding) (keymap-set my/markdown-command-map "p" #'markdown-toggle-inline-images) (keymap-set my/markdown-command-map "j" #'imenu) (keymap-set my/markdown-command-map "b" #'markdown-cycle) (keymap-set my/markdown-command-map "x" #'markdown-toggle-gfm-checkbox) (keymap-set my/markdown-command-map "i" my/markdown-insert-map) (keymap-set my/markdown-command-map "t" my/markdown-table-map) (keymap-set my/markdown-insert-map "h" #'markdown-insert-header-dwim) (keymap-set my/markdown-insert-map "l" #'markdown-insert-link) (keymap-set my/markdown-insert-map "i" #'markdown-insert-image) (keymap-set my/markdown-insert-map "q" #'markdown-insert-blockquote) (keymap-set my/markdown-insert-map "c" #'markdown-insert-code) (keymap-set my/markdown-insert-map "C" #'markdown-insert-gfm-code-block) (keymap-set my/markdown-insert-map "f" #'markdown-insert-footnote) (keymap-set my/markdown-insert-map "r" #'markdown-insert-hr) (keymap-set my/markdown-table-map "i" #'markdown-insert-table) (keymap-set my/markdown-table-map "a" #'markdown-table-align) (keymap-set my/markdown-table-map "r" #'markdown-table-insert-row) (keymap-set my/markdown-table-map "R" #'markdown-table-delete-row) (keymap-set my/markdown-table-map "c" #'markdown-table-insert-column) (keymap-set my/markdown-table-map "C" #'markdown-table-delete-column) (with-eval-after-load 'markdown-mode (evil-define-key '(normal visual) markdown-mode-map (kbd "SPC") my/markdown-leader-map (kbd "RET") #'markdown-follow-thing-at-point) (evil-define-key 'normal markdown-mode-map (kbd "M-") #'markdown-move-up (kbd "M-") #'markdown-move-down (kbd "M-") #'markdown-promote (kbd "M-") #'markdown-demote) (evil-define-key 'insert markdown-mode-map (kbd "M-SPC") my/markdown-leader-map)) (with-eval-after-load 'which-key (which-key-add-keymap-based-replacements my/markdown-leader-map "m" "markdown") (which-key-add-keymap-based-replacements my/markdown-command-map "i" "insert" "t" "table")) (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-command-map "R" #'org-clock-reminder-mode) (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") :defer t :custom (elfeed-db-directory "~/.emacs.d/elfeed") :config (setq elfeed-search-filter "@2-weeks-ago +unread" elfeed-search-title-max-width 110) (setq elfeed-use-libxml 't) (add-hook 'elfeed-search-mode-hook #'elfeed-update) (setq elfeed-search-sort-function (lambda (a b) (let* ((a-date (elfeed-entry-date a)) (b-date (elfeed-entry-date b)) (a-feed (elfeed-feed-title (elfeed-entry-feed a))) (b-feed (elfeed-feed-title (elfeed-entry-feed b))) (a-title (elfeed-entry-title a)) (b-title (elfeed-entry-title b))) (cond ((> a-date b-date) t) ((< a-date b-date) nil) ((string< a-feed b-feed) t) ((string> a-feed b-feed) nil) (t (string< a-title b-title))))))) (use-package elfeed-protocol :ensure (:host github :repo "fasheng/elfeed-protocol") :after elfeed :custom (elfeed-protocol-work-with-others t) (elfeed-protocol-enabled-protocols '(ttrss)) :init (add-to-list 'elfeed-feeds '("ttrss+https://cashmere@rss.cashmere.rs" :password (password-store-get "rss.cashmere.rs")) :append) :config (elfeed-protocol-enable)) (defun my/elfeed-reset () "Delete the local elfeed database and re-fetch everything from tt-rss. Fixes stale feeds that were removed server-side but still show up locally. Note: entries no longer present on the server are lost." (interactive) (when (yes-or-no-p "Delete local elfeed db and re-fetch all feeds from tt-rss? ") (elfeed-db-unload) (delete-directory elfeed-db-directory t) (make-directory elfeed-db-directory t) (elfeed-db-load) (elfeed-update))) (use-package elfeed-goodies :ensure t :after elfeed :config (elfeed-goodies/setup)) (with-eval-after-load 'elfeed (require 'shrface) (add-hook 'elfeed-show-mode-hook (lambda () (visual-line-mode 1) (setq-local shr-width nil shr-use-fonts t shr-max-image-proportion 0.6 shr-indentation 2 line-spacing 0.2) (shrface-mode 1)))) (setq browse-url-browser-function 'eww-browse-url browse-url-secondary-browser-function 'browse-url-generic browse-url-generic-program "helium-browser") (defun my/browse-url-no-frame-guard (orig url &rest args) "Around-advice for `browse-url' that resolves and calls the handler directly, bypassing the upstream `browse-url' body. Emacs 30+ unconditionally calls `(frame-parameter nil \\='display)` inside `browse-url' to set DISPLAY from the selected frame. That raises \"Frames are not in use or not initialized\" in daemon contexts, and in TUI-only setups the native-compiled frame access inside the subr can throw even when `(frame-live-p (selected-frame))' returns non-nil in the caller (the two checks diverge). For the handlers registered here (`eww-browse-url' is in-frame, `browse-url-generic' inherits DISPLAY from the daemon env) the DISPLAY reset is unnecessary, so do the handler resolution ourselves and skip `browse-url' entirely." (interactive (browse-url-interactive-arg "URL: ")) (unless (called-interactively-p 'interactive) (setq args (or args (list browse-url-new-window-flag)))) (let ((function (or (browse-url-select-handler url) browse-url-browser-function))) (if (functionp function) (apply function url args) (error "No suitable browser for URL %s" url)))) (advice-add 'browse-url :around #'my/browse-url-no-frame-guard) (use-package eww :ensure nil :commands (eww eww-search-words) :custom (eww-search-prefix "https://duckduckgo.com/html/?q=") (eww-auto-rename-buffer 'title) (eww-browse-url-new-window-is-tab nil) (eww-download-directory (expand-file-name "~/Downloads")) (eww-bookmarks-directory (locate-user-emacs-file "eww-bookmarks/")) (eww-restore-desktop t) (eww-desktop-remove-duplicates t) (eww-history-limit 150) (eww-suggest-uris '(eww-links-at-point thing-at-point-url-at-point)) (eww-header-line-format "%t — %u") (url-privacy-level '(email os emacs lastloc)) (url-cookie-trusted-urls '()) :config (setq-default shr-inhibit-images nil shr-use-fonts t shr-use-colors t shr-folding-mode t shr-bullet "• " shr-image-animate nil shr-max-image-proportion 0.6 shr-width nil shr-cookie-policy nil) (make-directory eww-bookmarks-directory t)) (defun my/eww-new-buffer (url) "Open URL in a fresh eww buffer (keep current)." (interactive (list (read-from-minibuffer "URL: "))) (eww url 4)) (defun my/eww-toggle-images () "Toggle image rendering and reload page." (interactive) (setq-local shr-inhibit-images (not shr-inhibit-images)) (eww-reload) (message "Images %s" (if shr-inhibit-images "off" "on"))) (defun my/eww-copy-as-org () "Yank visible eww region/page as org-formatted text." (interactive) (require 'ol-eww) (org-eww-copy-for-org-mode)) (use-package shrface :ensure t :hook (eww-after-render . shrface-mode) :custom (shrface-bullets-bullet-list '("◉" "○" "✸" "✿")) (shrface-href-versatile t) :config (shrface-basic) (shrface-trial) (shrface-default-keybindings)) (use-package shr-tag-pre-highlight :ensure (:host github :repo "xuchunyang/shr-tag-pre-highlight.el") :after shr :config (add-to-list 'shr-external-rendering-functions '(pre . shr-tag-pre-highlight))) (use-package ace-link :ensure t :commands (ace-link-eww ace-link-info ace-link-help ace-link-org) :config (ace-link-setup-default)) (use-package link-hint :ensure t :commands (link-hint-open-link link-hint-copy-link)) (use-package mu4e :ensure nil :defer t :commands (mu4e mu4e-compose-new) :init (defun my/mu4e-ensure-running-for-compose () "Start mu4e in the background before constructing a draft. This makes its asynchronous contact cache available even when a draft is opened directly with `mu4e-compose-new'." (require 'mu4e) (unless (bound-and-true-p mu4e--started) (mu4e t))) (defun my/mu4e-compose-tab () "Complete a contact in address fields, otherwise run `message-tab'." (interactive) (if (mu4e--compose-complete-contact-field) (company-complete) (message-tab))) (add-hook 'mu4e-compose-pre-hook #'my/mu4e-ensure-running-for-compose) (with-eval-after-load 'mu4e-compose (keymap-set mu4e-compose-mode-map "TAB" #'my/mu4e-compose-tab) (keymap-set mu4e-compose-mode-map "" #'my/mu4e-compose-tab)) :config (setq mu4e-mu-binary (executable-find "mu")) (setq mu4e-split-view 'vertical) (setq mu4e-thread-mode t) (setq mu4e-maildir "~/Mails") (setq mu4e-get-mail-command (concat (executable-find "mbsync") " -a")) (setq mu4e-update-interval 300) (setq mu4e-attachment-dir "~/Downloads") (setq mu4e-change-filenames-when-moving t) (setq mu4e-contexts `(,(make-mu4e-context :name "riseup" :match-func (lambda (msg) (when msg (string-prefix-p "/riseup/" (mu4e-message-field msg :maildir)))) :vars '((user-mail-address . "cashmere1337@riseup.net") (user-full-name . "cashmere") (mu4e-drafts-folder . "/riseup/Drafts") (mu4e-sent-folder . "/riseup/Sent") (mu4e-trash-folder . "/riseup/Trash") (mu4e-refile-folder . "/riseup/Archive"))) ,(make-mu4e-context :name "autistici" :match-func (lambda (msg) (when msg (string-prefix-p "/autistici" (mu4e-message-field msg :maildir)))) :vars '((user-mail-address . "cashmeresamurai@autistici.org") (user-full-name . "cashmere") (mu4e-drafts-folder . "/autistici/Drafts") (mu4e-sent-folder . "/autistici/Sent") (mu4e-trash-folder . "/autistici/Trash") (mu4e-refile-folder . "/autistici/Archive"))) ,(make-mu4e-context :name "cashmere" :match-func (lambda (msg) (when msg (string-prefix-p "/cashmere/cashmere" (mu4e-message-field msg :maildir)))) :vars '((user-mail-address . "cashmere@cashmere.rs") (user-full-name . "cashmere") (mu4e-drafts-folder . "/cashmere/cashmere/Drafts") (mu4e-sent-folder . "/cashmere/cashmere/Sent") (mu4e-trash-folder . "/cashmere/cashmere/Trash") (mu4e-refile-folder . "/cashmere/cashmere/Archive"))))) (setq mu4e-context-policy 'pick-first) (setq mu4e-compose-context-policy 'ask) (setq sendmail-program (executable-find "msmtp")) (setq send-mail-function 'sendmail-send-it) (setq message-send-mail-function 'message-send-mail-with-sendmail) (setq message-sendmail-envelope-from 'header) (setq message-kill-buffer-on-exit t) (defun my/mu4e-set-msmtp-account () "Select the msmtp account matching the draft's From address." (when (message-mail-p) (save-restriction (message-narrow-to-headers) (let* ((from (message-fetch-field "from")) (address (and from (cadr (mail-extract-address-components from)))) (account (cdr (assoc-string (or address "") '(("cashmere1337@riseup.net" . "riseup") ("cashmeresamurai@autistici.org" . "autistici") ("cashmere@cashmere.rs" . "cashmere-rs")) t)))) (setq-local message-sendmail-extra-arguments nil) (unless account (user-error "No email From: %s" (or from ""))) (setq-local message-sendmail-extra-arguments (list "-a" account)))))) (remove-hook 'message-send-mail-hook 'mu4e-set-msmtp-account) (add-hook 'message-send-mail-hook #'my/mu4e-set-msmtp-account)) (use-package himalaya-ui :ensure nil :commands (himalaya-ui)) (use-package mu4e-alert :ensure t :after mu4e :custom (mu4e-alert-modeline-formatter #'my/header-line-mu4e-formatter) :config ;; Use libnotify for desktop notifications (notify-send / swaync) (mu4e-alert-set-default-style 'libnotify) ;; Interesting mail query: unread, exclude trash and sent folders (setq mu4e-alert-interesting-mail-query (concat "flag:unread AND NOT flag:trashed" " AND NOT maildir:\"/riseup/Sent\"" " AND NOT maildir:\"/autistici/Sent\"" " AND NOT maildir:\"/cashmere/cashmere/Sent\"")) ;; Show both count and per-sender subject notifications (setq mu4e-alert-email-notification-types '(count subjects)) ;; Group desktop notifications by maildir (per-account grouping) (setq mu4e-alert-group-by :maildir) ;; Disable X11 urgency hints (does not work on Wayland) (setq mu4e-alert-set-window-urgency nil) ;; Enable desktop notifications (mu4e-alert-enable-notifications) ;; Enable mode-line data updates (populates mu4e-alert-mode-line variable). ;; We strip the default global-mode-string entry afterwards since we use ;; a custom maple-modeline segment instead. (mu4e-alert-enable-mode-line-display) (setq global-mode-string (delete '(:eval mu4e-alert-mode-line) global-mode-string))) (use-package colorful-mode ;; :diminish :ensure t ; Optional :custom (colorful-use-prefix t) (colorful-only-strings 'only-prog) (css-fontify-colors nil) :config (global-colorful-mode t) (add-to-list 'global-colorful-modes 'helpful-mode)) (use-package envrc :ensure t :defer 1 :init (require 'notifications) (defun my/envrc-project-buffer () "Load the local project environment before terminal startup or task discovery." (unless (file-remote-p default-directory) (require 'envrc) (envrc-mode 1) (when (eq envrc--status 'error) (user-error "Project environment failed; inspect M-x envrc-show-log")))) (defvar my/envrc-notified (make-hash-table :test 'equal) "Directories already notified about, to avoid spam.") (defun my/envrc-notify (dir status) "Desktop notification for DIR with STATUS symbol. Skip when no .envrc (STATUS `none'). Dedupe by .envrc root so opening another file in same project does not re-notify." (when (and dir (memq status '(on error))) (let ((root (or (locate-dominating-file dir ".envrc") dir))) (unless (gethash root my/envrc-notified) (puthash root t my/envrc-notified) (ignore-errors (notifications-notify :title "direnv" :body (format "%s — %s" (abbreviate-file-name root) (pcase status ('on "environment loaded") ('error "load failed") (_ (format "%s" status)))) :app-name "emacs" :urgency (if (eq status 'error) 'critical 'low) :timeout 3000)) (message "envrc: %s [%s]" root status))))) (defun my/envrc-maybe-async () "Enable `envrc-mode' asynchronously for current buffer." (when (and (not (bound-and-true-p envrc-mode)) buffer-file-name (not (file-remote-p buffer-file-name))) (let ((buf (current-buffer))) (run-with-idle-timer 0.2 nil (lambda () (when (buffer-live-p buf) (with-current-buffer buf (envrc-mode 1) (let ((dir (and (boundp 'envrc--status) (car-safe (bound-and-true-p envrc--process-env)) default-directory)) (status (bound-and-true-p envrc--status))) (when status (my/envrc-notify default-directory status)))))))))) :hook ((find-file . my/envrc-maybe-async) (dired-mode . my/envrc-project-buffer) (projectile-dashboard-mode . my/envrc-project-buffer) (ghostel-mode . my/envrc-project-buffer)) :config (when (bound-and-true-p envrc-global-mode) (envrc-global-mode -1))) (use-package tldr :ensure t) (use-package ghostel :ensure t :custom (ghostel-shell "xonsh") (ghostel-ssh-install-terminfo t) (ghostel-timer-delay 0.01) (ghostel-max-scrollback (* 1024 1024)) :config ;; Ghostel's platform detector only knows darwin/linux; teach it FreeBSD ;; so it can download the x86_64-freebsd release asset when the user ;; first invokes a ghostel command. (defun my/ghostel-module-platform-tag (orig) (or (funcall orig) (and (eq system-type 'berkeley-unix) (let* ((raw-arch (car (split-string system-configuration "-"))) (arch (pcase raw-arch ("amd64" "x86_64") ("arm64" "aarch64") (_ raw-arch)))) (format "%s-freebsd" arch))))) (advice-add 'ghostel--module-platform-tag :around #'my/ghostel-module-platform-tag) (require 'ghostel-compile) (ghostel-compile-global-mode 1) (setq-default window-adjust-process-window-size-function #'window-adjust-process-window-size-largest) (defun my/ghostel-transparent-buffer-face (fg _bg) (unless (equal fg ghostel--face-cookie-fg-bg) (when ghostel--face-cookie (face-remap-remove-relative ghostel--face-cookie)) (setq ghostel--face-cookie (face-remap-add-relative 'default :foreground fg)) (setq ghostel--face-cookie-fg-bg fg))) (advice-add 'ghostel--set-buffer-face :override #'my/ghostel-transparent-buffer-face) (advice-add 'enable-theme :after (lambda (&rest _) (when (fboundp 'ghostel-sync-theme) (ghostel-sync-theme))))) (use-package evil-ghostel :ensure (:host github :repo "dakra/ghostel" :files ("extensions/evil-ghostel/*.el")) :after (ghostel evil) :custom (evil-ghostel-escape 'evil) :hook (ghostel-mode . evil-ghostel-mode)) (use-package inheritenv :ensure t :after ghostel :config (inheritenv-add-advice 'ghostel-compile) (inheritenv-add-advice 'ghostel-recompile)) (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) ) (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)) (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") :init (global-sops-mode 1)) (use-package magit-delta :ensure t :after magit :init (when (executable-find "delta") (add-hook 'magit-mode-hook #'magit-delta-mode))) (use-package pulsar :ensure t :config (setq pulsar-pulse t) (setq pulsar-delay 0.025) (setq pulsar-iterations 10) (setq pulsar-face 'evil-ex-lazy-highlight) (setq pulsar-tty-color "white") (let ((orig-face-background (symbol-function 'face-background))) (defun my/pulsar--create-pulse (locus face) "Like `pulsar--create-pulse' but with a smarter TTY colour fallback." (let ((common-fn (lambda (locus face) (let ((pulse-flag t) (pulse-delay pulsar-delay) (pulse-iterations pulsar-iterations) (overlay (make-overlay (car locus) (cdr locus)))) (overlay-put overlay 'pulse-delete t) (overlay-put overlay 'window (frame-selected-window)) (pulse-momentary-highlight-overlay overlay face))))) (if (display-graphic-p) (funcall common-fn locus face) (cl-letf (((symbol-function 'face-background) (lambda (f &optional frame inherit) (let ((bg (funcall orig-face-background f frame inherit))) (cond ((and (eq f 'default) (null bg)) (or (frame-parameter frame 'background-color) (if (eq (frame-parameter frame 'background-mode) 'light) "white" "black"))) ((null bg) pulsar-tty-color) (t bg)))))) (funcall common-fn locus face)))))) (advice-add 'pulsar--create-pulse :override #'my/pulsar--create-pulse) (add-to-list 'pulsar-pulse-functions 'evil-scroll-down) (add-to-list 'pulsar-pulse-functions 'flymake-goto-next-error) (add-to-list 'pulsar-pulse-functions 'flymake-goto-prev-error) (add-to-list 'pulsar-pulse-functions 'evil-yank) (add-to-list 'pulsar-pulse-functions 'evil-yank-line) (add-to-list 'pulsar-pulse-functions 'evil-delete) (add-to-list 'pulsar-pulse-functions 'evil-delete-line) (add-to-list 'pulsar-pulse-functions 'evil-jump-item) (add-to-list 'pulsar-pulse-functions 'diff-hl-next-hunk) (add-to-list 'pulsar-pulse-functions 'diff-hl-previous-hunk) (pulsar-global-mode)) (use-package org-auto-tangle :ensure t :hook (org-mode . org-auto-tangle-mode)) (use-package zfs :ensure nil :commands (zfs) :custom (zfs-privileged-command '("doas" "-n")) (zfs-authentication-command '("doas"))) (use-package minimal-dashboard :ensure t :demand t :init (setq initial-buffer-choice #'minimal-dashboard) ;; set initial buffer as dashboard :custom (minimal-dashboard-buffer-name "Dashboard") ;; (minimal-dashboard-buffer-name #'some-func-that-returns-a-string) (minimal-dashboard-image-path "~/.emacs.d/Emacs-logo.svg") ;; path to image ;; (minimal-dashboard-image-path #'some-func-that-returns-a-valid-image-path) (minimal-dashboard-text "Welcome to Emacs") ;; plain text ;; You can have function returning a string as well (minimal-dashboard-text (lambda () (format "started in %s" (emacs-init-time)))) ;; Multi-line text (with center alignment) is also supported (minimal-dashboard-text "You never quit Emacs\nYou just die at some point") ;; Click support for image ;; (minimal-dashboard-image-click-handler ;; (lambda (event) ;; (pcase (event-basic-type event) ;; ('mouse-1 (message "Left click on image")) ;; ('mouse-2 (message "Middle click on image")) ;; ('mouse-3 (message "Right click on image"))))) ;; Click support for text ;; (minimal-dashboard-text-click-handler ;; (lambda (event) ;; (pcase (event-basic-type event) ;; ('mouse-1 (message "Left click on text")) ;; ('mouse-2 (message "Middle click on text")) ;; ('mouse-3 (message "Right click on text"))))) (minimal-dashboard-image-scale 1.25) (minimal-dashboard-enable-resize-handling t) ;; to refresh when buffer is resized (minimal-dashboard-modeline-shown t)) ;; visibility of the modeline (use-package rsync-ui :ensure nil :commands (rsync-ui rsync-ui-dired)) (defun my/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." (interactive) (unless (derived-mode-p 'org-mode) (user-error "Not in an Org buffer")) (let* ((tmp (make-temp-file "org-preview-" nil ".html")) (org-export-in-background nil) (org-html-head-include-default-style nil) (org-html-head-include-scripts nil) (org-html-head rlr/org-preview-css) (org-html-validation-link nil)) (org-export-to-file 'html tmp) (add-hook 'kill-emacs-hook (lambda () (ignore-errors (delete-file tmp)))) (funcall browse-url-secondary-browser-function (browse-url-file-url tmp)))) (defvar my/org-preview-css " " "Stylesheet injected into throwaway HTML previews of Org buffers.") (use-package clatter :ensure (:host github :repo "parenworks/clatter.el") :commands (clatter clatter-quick-connect) :hook (clatter-mode . my/clatter-company-mode) :custom (clatter-message-order 'oldest-first) (clatter-notify-enabled t) (clatter-notify-on-mention t) (clatter-notify-on-dm t) (clatter-notify-on-invite t) (clatter-notify-on-keyword t) (clatter-notify-keywords '("cashmere")) (clatter-notify-current-buffer nil) (clatter-notify-timeout 5000) (clatter-notify-urgency 'normal) (clatter-suppress-messages '(join part quit away)) (clatter-networks `(("soju" :server "bouncer.cashmere.rs" :port 6699 :tls t :nick "cashmere" ;; Bare bouncer username: fans out to every upstream network. :username "cashmere/libera" :password ,(password-store-get "soju") :sasl plain :bouncer t))) :config (defvar-keymap my/clatter-company-mode-map "TAB" #'company-complete "" #'company-complete) (define-minor-mode my/clatter-company-mode "Use Company for explicit completion in Clatter buffers." :init-value nil :lighter nil :keymap my/clatter-company-mode-map) (require 'gnutls) (clatter-setup) (clatter-dcc-setup) (with-eval-after-load 'org (require 'clatter-org) (clatter-org-setup))) (use-package apheleia :ensure t :hook (nix-ts-mode . apheleia-mode) :custom (apheleia-mode-alist '((nix-ts-mode . nixfmt)))) (use-package justl :ensure t :commands (justl justl-exec-recipe-in-dir justl-exec-default-recipe) :hook ((justl-mode . my/envrc-project-buffer) (justl-module-mode . my/envrc-project-buffer)) :custom (justl-recipe-width 20) (justl-per-recipe-buffer nil) :config (inheritenv-add-advice 'justl--make-process)) (use-package msgpack :ensure t) (use-package tramp-rpc :ensure (:host github :repo "ArthurHeymans/emacs-tramp-rpc" :ref "v0.13.1" :lisp-dir "lisp") :init (setq tramp-rpc-deploy-git-build-policy 'release)) (provide 'init)