summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-07-11 08:50:14 +0200
committercashmere <cashmere@autistici.org>2026-07-11 08:50:14 +0200
commitc74c3526fe719ff8127d17f44b67a33772adc334 (patch)
tree7b05d6cad835f8704430ac38c4f22358601fe846
parentc6579ddf761aaa1c922213310dc1f46347ea06ca (diff)
downloademacs-c74c3526fe719ff8127d17f44b67a33772adc334.tar.gz
2026-07-11T08:50:16+02:00
-rw-r--r--.gitignore11
-rw-r--r--config.el1301
-rw-r--r--config.org395
-rw-r--r--lisp/agent-shell-diff.el268
-rw-r--r--lisp/agent-shell-kimi.el612
5 files changed, 1686 insertions, 901 deletions
diff --git a/.gitignore b/.gitignore
index 766f23c..2adefd4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,6 +25,17 @@
/auto-save-list
/bbdb
/bookmarks
+/bookmarks.eld
+/cursory-latest-state.eld
+/nov-places
+/org-workbench-db.el
+/places.eld
+/projects.eld
+/recentf.eld
+/config.org.tangled
+/denote-solo-last-directory
+/image.jpg
+/tldr/
/eln-cache
/elpa
/emms
diff --git a/config.el b/config.el
index 08ac520..32f561e 100644
--- a/config.el
+++ b/config.el
@@ -26,18 +26,19 @@
(defvar my/local-packages nil
"Alist of (PACKAGE . DIRECTORY) loaded from a local checkout.
Set per-host in the gitignored `local.el'.")
-
- (load (locate-user-emacs-file "local.el") 'noerror 'nomessage)
-
- (defun my/package-recipe (package remote)
- "Elpaca `:ensure' recipe for PACKAGE.
-Return nil when PACKAGE is registered in `my/local-packages', so
-use-package loads it from `load-path'; otherwise return REMOTE."
- (if (assq package my/local-packages) nil remote)))
+ (load (locate-user-emacs-file "local.el") 'noerror 'nomessage))
(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)
+
(use-package emacs
:ensure nil
:custom
@@ -66,7 +67,6 @@ use-package loads it from `load-path'; otherwise return REMOTE."
(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)
(org-mode . display-line-numbers-mode)
@@ -114,10 +114,11 @@ use-package loads it from `load-path'; otherwise return REMOTE."
(scroll-bar-mode -1)
(add-to-list 'default-frame-alist '(vertical-scroll-bars . nil))
(add-to-list 'default-frame-alist '(horizontal-scroll-bars . nil))
- ;; (set-frame-parameter (selected-frame) 'alpha-background 80)
- ;; (add-to-list 'default-frame-alist '(alpha-background . 80))
+ (set-frame-parameter (selected-frame) 'alpha-background 80)
+ (add-to-list 'default-frame-alist '(alpha-background . 80))
(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)
@@ -160,11 +161,15 @@ use-package loads it from `load-path'; otherwise return REMOTE."
(derived-mode-p 'compilation-mode)))
(defun my/compile-or-recompile ()
- "Recompile with the last command if a compilation ran before, else prompt."
+ "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 "*compilation*")
- (recompile)
- (call-interactively #'compile)))
+ (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."
@@ -252,8 +257,6 @@ use-package loads it from `load-path'; otherwise return REMOTE."
(use-package async :ensure t)
-;; Always-available IRC entry points so `M-x run-irc` works from any daemon
-;; (hub, work, standalone). Calling `erc-tls` autoloads ERC on demand.
(defun my/soju-password ()
"Read the Soju bouncer password from the sops-nix secret."
(string-trim
@@ -262,212 +265,84 @@ use-package loads it from `load-path'; otherwise return REMOTE."
"/home/cashmere/.config/sops-nix/secrets/senpai_password")
(buffer-string))))
-(defun run-irc ()
- "Connect to all IRC networks via Soju bouncer."
- (interactive)
- (let ((pw (my/soju-password)))
- (erc-tls :server "bouncer.cashmere.rs"
- :port 6699
- :nick "cashmere1337"
- :user "cashmere/irc.libera.chat@emacs"
- :password pw)
- (erc-tls :server "bouncer.cashmere.rs"
- :port 6699
- :nick "cashmere"
- :user "cashmere/ergo@emacs"
- :password pw)
- (erc-tls :server "bouncer.cashmere.rs"
- :port 6699
- :nick "cashmere"
- :user "cashmere/mansion@emacs"
- :password pw)))
-
-(use-package erc
- :ensure nil
- :defer t
+(use-package clatter
+ :ensure (:host github :repo "parenworks/clatter.el" :files ("*.el"))
+ :commands (clatter clatter-connect)
: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))
+ (clatter-networks
+ '(("libera"
+ :server "bouncer.cashmere.rs" :port 6699 :tls t
+ :nick "cashmere1337"
+ :username "cashmere/irc.libera.chat@emacs")
+ ("ergo"
+ :server "bouncer.cashmere.rs" :port 6699 :tls t
+ :nick "cashmere"
+ :username "cashmere/ergo@emacs")
+ ("mansion"
+ :server "bouncer.cashmere.rs" :port 6699 :tls t
+ :nick "cashmere"
+ :username "cashmere/mansion@emacs")
+ ("bitlbee"
+ :server "bouncer.cashmere.rs" :port 6699 :tls t
+ :nick "cashmere"
+ :username "cashmere/bitlbee@emacs")))
+ (clatter-flyspell-enable nil)
+ (clatter-format-strip-only t)
+ (clatter-message-order 'oldest-first)
+ (clatter-read-marker-enabled nil)
+ (clatter-chathistory-limit 1000)
: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 in modeline
- (setq erc-track-position-in-mode-line t)
-
- ;; keep large buffer for soju history replay
- (setq erc-max-buffer-size 100000)
+ (require 'gnutls)
+ (clatter-setup)
+ (setq clatter-hl-keywords '("cashmere"))
+ (advice-add 'clatter-buffer-name :override #'my/clatter-buffer-name)
+ (add-hook 'clatter-mode-hook (lambda () (setq-local corfu-auto nil)))
+ (general-def 'normal clatter-mode-map
+ "gb" #'my/clatter-switch-buffer
+ "gH" #'clatter-chathistory-request
+ "gh" #'clatter-chathistory-more))
- (erc-update-modules))
+(defun my/clatter-buffer-name (network target)
+ "Name clatter buffers by bare channel/query TARGET, server by NETWORK."
+ (if (string= target "*server*")
+ (format "*%s*" network)
+ target))
-(defvar my/consult-source-erc
- (list :name "IRC"
- :narrow ?i
- :category 'buffer
- :face 'erc-default-face
- :state #'consult--buffer-state
- :items (lambda ()
- (mapcar #'buffer-name
- (cl-remove-if-not
- (lambda (buf)
- (with-current-buffer buf
- (derived-mode-p 'erc-mode)))
- (buffer-list))))))
-
-(defun my/erc-switch-channel ()
- "Switch to an ERC channel buffer via consult."
+(defun my/clatter-switch-buffer ()
+ "Switch to a clatter IRC buffer."
(interactive)
- (consult-buffer (list my/consult-source-erc)))
-
-(defun my/erc-fetch-history (&optional count)
- "Fetch COUNT previous messages from Soju via CHATHISTORY.
-Defaults to 1000 (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: " 1000)
- 1000)
- 1000)))
- (erc-notifications-disable)
- (erc-server-send (format "CHATHISTORY LATEST %s * %d" target n))
- (run-at-time 5 nil #'erc-notifications-enable)))))
+ (let ((bufs (cl-remove-if-not
+ (lambda (b)
+ (with-current-buffer b (derived-mode-p 'clatter-mode)))
+ (buffer-list))))
+ (if bufs
+ (switch-to-buffer
+ (completing-read "IRC buffer: " (mapcar #'buffer-name bufs) nil t))
+ (message "No clatter buffers."))))
-(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))))))
+(defvar my/clatter-inhibit-popup nil
+ "When non-nil, clatter buffers are not auto-displayed by `display-buffer'.")
-(defun my/erc-reconnect ()
- "Reconnect to the current ERC server."
- (interactive)
- (erc-cmd-RECONNECT))
+(defun my/clatter-popup-suppressed-p (buf &rest _)
+ "Non-nil when BUF is a clatter buffer and popups are inhibited."
+ (and my/clatter-inhibit-popup
+ (get-buffer buf)
+ (with-current-buffer (get-buffer buf)
+ (derived-mode-p 'clatter-mode))))
-(defun my/erc-list-channels ()
- "Request channel list from the current ERC server."
- (interactive)
- (erc-cmd-LIST))
+(add-to-list 'display-buffer-alist
+ '(my/clatter-popup-suppressed-p
+ display-buffer-no-window
+ (allow-no-window . t)))
-(defun persp-irc ()
- "Switch to (or create) the IRC frame and show ERC buffers."
+(defun run-irc ()
+ "Connect to all IRC networks via the Soju bouncer, in the background."
(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))))
-
-;; ERC keybindings and hooks — register from any daemon, since `run-irc`
-;; autoloads ERC even outside the hub daemon.
-(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)))
-
- (with-eval-after-load 'general
- (general-def 'normal erc-mode-map
- "q" 'quit-window
- "Q" 'my/erc-quit-all
- "gb" 'my/erc-switch-channel
- "gn" 'erc-track-switch-buffer
- "gH" 'my/erc-fetch-history
- "go" 'erc-channel-names
- "gj" 'erc-join-channel
- "gl" 'my/erc-list-channels
- "gr" 'my/erc-reconnect
- "RET" 'erc-send-current-line)
-
- (general-def '(normal insert) erc-mode-map
- "M-n" 'erc-track-switch-buffer
- "C-k" 'erc-previous-command
- "C-j" 'erc-next-command)))
+ (let ((pw (my/soju-password)))
+ (setq my/clatter-inhibit-popup t)
+ (dolist (net '("libera" "ergo" "mansion" "bitlbee"))
+ (clatter-connect net :password pw))
+ (run-at-time 8 nil (lambda () (setq my/clatter-inhibit-popup nil)))))
(defvar my/centered-cursor-enabled nil)
@@ -489,65 +364,6 @@ Temporarily disables notifications during the fetch."
(setq my/centered-cursor-enabled t)
(message "centered-cursor on"))))
-(use-package cursory
- :ensure t
- :demand t
- :if (display-graphic-p)
- :config
- (setq cursory-presets
- '((box
- :cursor-color success ; will typically be green
- :blink-cursor-interval 1.2)
- (box-no-blink
- :inherit box
- :blink-cursor-mode -1)
- (bar
- :cursor-type (bar . 2)
- :cursor-color error ; will typically be red
- :blink-cursor-interval 0.8)
- (bar-no-other-window
- :inherit bar
- :cursor-in-non-selected-windows nil)
- (bar-no-blink
- :inherit bar
- :blink-cursor-mode -1)
- (underscore
- :cursor-color warning ; will typically be yellow
- :cursor-type (hbar . 3)
- :blink-cursor-interval 0.3
- :blink-cursor-blinks 50)
- (underscore-no-other-window
- :inherit underscore
- :cursor-in-non-selected-windows nil)
- (underscore-thick
- :inherit underscore
- :cursor-type (hbar . 8)
- :cursor-in-non-selected-windows (hbar . 3))
- (t ; the default values
- :cursor-color unspecified ; use the theme's original
- :cursor-type box
- :cursor-in-non-selected-windows hollow
- :blink-cursor-mode 1
- :blink-cursor-blinks 10
- :blink-cursor-interval 0.2
- :blink-cursor-delay 0.2)))
-
- ;; I am using the default value of `cursory-latest-state-file'.
-
- ;; Set last preset or fall back to desired style from
- ;; `cursory-presets'. Alternatively, use the function
- ;; `cursory-set-last-or-fallback' (can be added to the
- ;; `after-init-hook'.
- (cursory-set-preset (or (cursory-restore-latest-preset) 'box))
-
- ;; Persist configurations between Emacs sessions. Also apply the
- ;; :cursor-color again when swithcing to another theme.
- (cursory-mode 1)
- :bind
- ;; We have to use the "point" mnemonic, because C-c c is often the
- ;; suggested binding for `org-capture' and is the one I use as well.
- ("C-c p" . cursory-set-preset))
-
(use-package clipetty
:ensure t
:after evil
@@ -690,7 +506,7 @@ Temporarily disables notifications during the fetch."
(or
(mode . eca-chat-mode)))
- ("ERC" (or (mode . erc-mode)))
+ ("IRC" (or (mode . clatter-mode)))
("Emacs" (or
(mode . emacs-lisp-mode)
@@ -730,9 +546,10 @@ Temporarily disables notifications during the fetch."
(general-evil-setup)
(general-create-definer my-leader
- :states '(normal visual)
+ :states '(normal visual insert)
:keymaps 'override
- :prefix "SPC")
+ :prefix "SPC"
+ :non-normal-prefix "M-SPC")
(general-create-definer my-local-leader
:states '(normal visual)
@@ -810,7 +627,7 @@ Temporarily disables notifications during the fetch."
(claude-code-ide-cli-path "claude")
(claude-code-ide-cli-debug nil)
;; Terminal backend
- (claude-code-ide-terminal-backend 'eat)
+ (claude-code-ide-terminal-backend 'ghostel)
;; Window placement — mirror eca on the right side so the two AI assistants
;; don't fight over the same slot
@@ -863,27 +680,13 @@ Temporarily disables notifications during the fetch."
;; Leader-key bindings (SPC a …) — defined pre-load so :commands autoloads fire.
(with-eval-after-load 'general
+ ;; 2026-06-23: replaced claude-code-ide bindings with pi-coding-agent under `a'.
+ ;; pi-coding-agent only exposes 3 user-facing commands, so the prefix is intentionally sparse.
(my-leader
- "a" '(:ignore t :wk "claude-code")
- "aa" '(claude-code-ide :wk "start / toggle")
- "am" '(claude-code-ide-menu :wk "transient menu")
- "at" '(claude-code-ide-toggle :wk "toggle window")
- "aT" '(claude-code-ide-toggle-recent :wk "toggle recent")
- "ap" '(claude-code-ide-send-prompt :wk "send prompt")
- "a@" '(claude-code-ide-insert-at-mentioned :wk "@mention region")
- "ac" '(claude-code-ide-continue :wk "continue last")
- "ar" '(claude-code-ide-resume :wk "resume session")
- "as" '(claude-code-ide-switch-to-buffer :wk "switch buffer")
- "al" '(claude-code-ide-list-sessions :wk "list sessions")
- "aq" '(claude-code-ide-stop :wk "stop")
- "a?" '(claude-code-ide-check-status :wk "check status")
- "ad" '(claude-code-ide-show-debug :wk "show debug")
- "aD" '(claude-code-ide-clear-debug :wk "clear debug"))
-
- (my-leader 'visual
- "a@" '(claude-code-ide-insert-at-mentioned :wk "@mention region")))
-
- (global-set-key (kbd "C-c C-'") #'claude-code-ide-menu)
+ "aa" '(agent-shell :wk "start")
+ "at" '(agent-shell-toggle :wk "toggle")
+ "am" '(agent-shell-help-menu :wk "open session")
+ "ar" '(agent-shell-send-region :wk "send region")))
:config
;; Runs only on first invocation — not at Emacs startup.
@@ -894,6 +697,85 @@ Temporarily disables notifications during the fetch."
:config
(setopt evil-collection-eca-maps 't))
+(use-package pi-coding-agent
+ :ensure t
+ :commands (pi-coding-agent
+ pi-coding-agent-toggle
+ pi-coding-agent-open-session-file))
+
+(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-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
+ :bind (:map agent-shell-mode-map
+ ("RET" . newline)
+ ("M-RET" . shell-maker-submit))
+ :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 nil)
+ (setq agent-shell-user-message-expand-by-default nil)
+ (setq agent-shell-diff-delta-command
+ (when (executable-find "delta")
+ '("delta" "--no-gitconfig" "--color-only")))
+
+ (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
@@ -903,14 +785,6 @@ Temporarily disables notifications during the fetch."
:init
(global-eldoc-mode))
-(use-package eldoc-box
- :ensure t
- :defer t
- :config
- (setq eldoc-box-max-pixel-width 800)
- (setq eldoc-box-max-pixel-height 600))
-
-
(use-package flymake
:ensure nil ;; This is built-in, no need to fetch it.
:defer t
@@ -927,7 +801,6 @@ Temporarily disables notifications during the fetch."
(use-package flycheck
:ensure t
:config
- (setq flycheck-global-modes '(not org-mode))
(setq flycheck-display-errors-function nil)
(add-hook 'after-init-hook #'global-flycheck-mode))
@@ -960,6 +833,13 @@ Temporarily disables notifications during the fetch."
:ensure t
:hook (flycheck-mode . sideline-flycheck-setup))
+;; (use-package flycheck-vale
+;; :ensure t
+;; :after flycheck
+;; :demand t
+;; :config
+;; (flycheck-vale-setup))
+
(use-package xref
:ensure nil)
@@ -998,6 +878,7 @@ Temporarily disables notifications during the fetch."
(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 t
org-tags-column -80
@@ -1005,7 +886,6 @@ Temporarily disables notifications during the fetch."
org-special-ctrl-a/e t
org-insert-heading-respect-content t)
- (add-hook 'org-mode-hook 'variable-pitch-mode)
;; (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)))
@@ -1061,39 +941,11 @@ Temporarily disables notifications during the fetch."
(kitty-gfx-shr-fit-height 20)
:hook (dired-mode . kitty-gfx-dired-auto-preview-mode)
:config
- (kitty-graphics-setup))
-
-(use-package org-fancy-priorities
- :ensure t
- :hook
- (org-mode . org-fancy-priorities-mode)
- :config
- (setq org-fancy-priorities-list '("⚡" "⬆" "⬇" "☕")))
+ (setq kitty-gfx-enable-browser t
+ kitty-gfx-casty-program "~/projects/casty/bin/casty.js"
+ kitty-gfx-casty-chrome "helium-browser")
-(setq org-priority-faces
- '((?A . (:background "#e91e63" :foreground "#ffffff" :weight bold))
- (?B . (:background "#ff9800" :foreground "#1a1a1a" :weight bold))
- (?C . (:background "#2196f3" :foreground "#ffffff" :weight bold))
- (?D . (:background "#795548" :foreground "#ffffff" :weight bold))))
-
-;; (setq org-agenda-start-on-weekday nil
-;; org-agenda-block-separator nil
-;; org-agenda-remove-tags t)
-
-;; (setq org-agenda-skip-scheduled-if-done t
-;; org-agenda-skip-deadline-if-done t
-;; org-agenda-include-deadlines t
-;; org-agenda-block-separator nil
-;; org-agenda-compact-blocks t
-;; org-agenda-start-day nil
-;; org-agenda-span 1
-;; org-agenda-start-on-weekday nil
-;; org-agenda-hide-tags-regexp "task"
-;; org-agenda-prefix-format
-;; '((agenda . " %i %?-12t% s")
-;; (todo . " %i ")
-;; (tags . " %i ")
-;; (search . " %i ")))
+ (kitty-graphics-setup))
(with-eval-after-load 'org
(setq org-confirm-babel-evaluate nil)
@@ -1130,6 +982,38 @@ Temporarily disables notifications during the fetch."
(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)
@@ -1217,7 +1101,6 @@ and convert it to Org using the pandoc utility."
(kill-region (point-min) (point-max)))
(yank)))
-
(with-eval-after-load 'org
;; Log state changes with timestamps
(setq org-log-done 'time
@@ -1408,102 +1291,61 @@ Skips capture tasks and projects."
(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 ()
- "Capture function: jump to today's journal for a timeblock."
- (require 'denote)
- (require 'denote-journal)
- (let* ((today (format-time-string "%Y-%m-%d"))
- (journal-files (denote-directory-files "journal"))
- (today-journal (seq-find
- (lambda (file)
- (string-match-p today (file-name-nondirectory file)))
- journal-files)))
- (if today-journal
- (find-file today-journal)
- (denote-journal-new-or-existing-entry))
- (goto-char (point-max))
- (insert "\n* Timeblock\n")))
+ (my/org-capture-to-journal "\n* Timeblock\n"))
(defun my/org-capture-recurring-to-journal ()
- "Capture function: jump to today's journal for a recurring task."
- (require 'denote)
- (require 'denote-journal)
- (let* ((today (format-time-string "%Y-%m-%d"))
- (journal-files (denote-directory-files "journal"))
- (today-journal (seq-find
- (lambda (file)
- (string-match-p today (file-name-nondirectory file)))
- journal-files)))
- (if today-journal
- (find-file today-journal)
- (denote-journal-new-or-existing-entry))
- (goto-char (point-max))
- (insert "\n* Recurring\n")))
+ (my/org-capture-to-journal "\n* Recurring\n"))
(defun my/org-capture-meeting-to-journal ()
- "Capture function: jump to today's journal for a meeting note."
- (require 'denote)
- (require 'denote-journal)
- (let* ((today (format-time-string "%Y-%m-%d"))
- (journal-files (denote-directory-files "journal"))
- (today-journal (seq-find
- (lambda (file)
- (string-match-p today (file-name-nondirectory file)))
- journal-files)))
- (if today-journal
- (find-file today-journal)
- (denote-journal-new-or-existing-entry))
- (goto-char (point-max))
- (insert "\n* Meeting: ")))
-
-;; capture templates are merged in the org-capture section below
+ (my/org-capture-to-journal "\n* Meeting: "))
(with-eval-after-load 'eglot
(add-to-list 'eglot-server-programs
'(org-mode . ("harper-ls" "--stdio"))))
-(defun my/org-capture-denote-deadline ()
+(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: "))
- (file-name (denote nil '("agenda"))))
- (find-file file-name)
- (goto-char (point-min))
- (forward-line 4)
- (insert (format "\n* %s\nDEADLINE: <%s>\n\n" context deadline))
- (current-buffer)))
+ (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-denote-scheduled ()
+(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: "))
- (file-name (denote nil '("agenda"))))
- (find-file file-name)
- (goto-char (point-min))
- (forward-line 4)
- (insert (format "\n* %s\nSCHEDULED: <%s>\n\n" context schedule))
- (current-buffer)))
+ (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-denote-task ()
- (let* ((context (read-string "Task: "))
- (todo-state (completing-read "TODO state: "
- '("ACTIVE" "NEXT" "TODO" "WAIT")))
- (file-name (denote nil '("agenda"))))
- (find-file file-name)
- (goto-char (point-min))
- (forward-line 4)
- (insert (format "\n* %s %s\n\n" todo-state context))
- (point)))
+(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-denote-task)
+ (function my/org-capture-task-to-journal)
"" :immediate-finish t :jump-to-captured t)
("d" "Deadline" plain
- (function my/org-capture-denote-deadline)
+ (function my/org-capture-deadline-to-journal)
"" :immediate-finish t :jump-to-captured t)
("s" "Scheduled" plain
- (function my/org-capture-denote-scheduled)
+ (function my/org-capture-scheduled-to-journal)
"" :immediate-finish t :jump-to-captured t)
("b" "Timeblock" plain
(function my/org-capture-timeblock-to-journal)
@@ -1812,27 +1654,6 @@ Skips capture tasks and projects."
(add-hook 'org-agenda-finalize-hook
#'my/org-agenda-fix-block-spacing)
-(defun my/agenda-app ()
- "Launch Emacs as a dedicated agenda kiosk.
-Opens the 'd' (Today) agenda view in a clean, distraction-free frame.
-Usage: emacs --eval '(my/agenda-app)' --no-splash"
- (interactive)
- (set-frame-name "Agenda")
- (delete-other-windows)
- (org-agenda nil "d")
- (delete-other-windows)
- (setq mode-line-format nil)
- (menu-bar-mode -1)
- (tool-bar-mode -1)
- (scroll-bar-mode -1)
- (when (bound-and-true-p tab-bar-mode)
- (tab-bar-mode -1))
- (when (find-font (font-spec :family "Lato"))
- (buffer-face-set '(:family "Lato")))
- (text-scale-increase 2)
- (hide-mode-line-mode 1)
- (winner-mode 1))
-
(use-package org-contrib
:ensure (:wait t))
@@ -1856,7 +1677,6 @@ Usage: emacs --eval '(my/agenda-app)' --no-splash"
(setq org-download-timestamp "%Y%m%d-%H%M%S_")
(setq org-download-heading-lvl nil))
-
;; (setq org-publish-project-alist
;; '(("wiki"
;; :author "cashmere"
@@ -1912,7 +1732,7 @@ Usage: emacs --eval '(my/agenda-app)' --no-splash"
(setq denote-rename-buffer-format "%t"
denote-buffer-name-prefix ""
denote-directory (expand-file-name "~/org/"))
- (denote-rename-buffer-mode 1))
+ (denote-rename-buffer-mode 1))
(use-package denote-agenda
:ensure t
@@ -2091,11 +1911,6 @@ for `denote-directory' and silently drop the file-list filter."
:ensure t
)
-
-
-(use-package denote-explore
- :ensure t)
-
(use-package ox-gfm
:ensure t)
@@ -2182,7 +1997,6 @@ Timers that expired while Emacs was closed fire immediately."
(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.
@@ -2197,6 +2011,27 @@ Timers that expired while Emacs was closed fire immediately."
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 which-key
:ensure t
:defer t
@@ -2231,7 +2066,7 @@ Timers that expired while Emacs was closed fire immediately."
:custom
(corfu-cycle t)
(corfu-auto t)
- (corfu-auto-delay 0.001)
+ (corfu-auto-delay 0.2)
(corfu-auto-prefix 2)
:init
(global-corfu-mode)
@@ -2294,6 +2129,7 @@ Timers that expired while Emacs was closed fire immediately."
("M-." . embark-dwim)
("C-h B" . embark-bindings)
:map minibuffer-local-map
+ ("C-c C-a" . embark-act-all)
("C-c C-e" . embark-export)
("C-c C-o" . embark-collect))
:init
@@ -2332,16 +2168,16 @@ completion-category-overrides '((file (styles partial-completion))))) ;; Customi
(setq eglot-connect-timeout 30)
(setq eglot-events-buffer-size 0)
(setq eglot-report-progress nil)
- (setq jsonrpc-default-request-timeout 10)
+ (setq jsonrpc-default-request-timeout 10))
- (add-hook 'eglot-managed-mode-hook
- (lambda ()
- (eglot-inlay-hints-mode -1)))
+ ;; (add-hook 'eglot-managed-mode-hook
+ ;; (lambda ()
+ ;; (eglot-inlay-hints-mode -1)))
- (setq eglot-code-action-indications '(eldoc-hint))
+ ;; (setq eglot-code-action-indications '(eldoc-hint))
- (setq eglot-ignored-server-capabilities
- '(:inlayHintProvider :documentHighlightProvider)))
+ ;; (setq eglot-ignored-server-capabilities
+ ;; '(:inlayHintProvider :documentHighlightProvider)))
(use-package typst-ts-mode
:ensure t
@@ -2372,6 +2208,42 @@ completion-category-overrides '((file (styles partial-completion))))) ;; Customi
(put 'eglot-workspace-configuration 'safe-local-variable #'listp)
+(use-package c-ts-mode
+ :ensure nil
+ :mode (("\\.c\\'" . c-ts-mode)
+ ("\\.h\\'" . c-ts-mode))
+ :hook (c-ts-mode . eglot-ensure)
+ :custom
+ (c-ts-mode-indent-offset 2)
+ (c-ts-mode-indent-style 'k&r)
+ :config
+ (with-eval-after-load 'eglot
+ (add-to-list 'eglot-server-programs
+ '((c-ts-mode c-mode) .
+ ("clangd"
+ "--background-index"
+ "--clang-tidy"
+ "--header-insertion=never")))))
+
+(use-package c3-ts-mode
+ :ensure (:host github :repo "c3lang/c3-ts-mode")
+ :mode (("\\.c3\\'" . c3-ts-mode)
+ ("\\.c3i\\'" . c3-ts-mode)
+ ("\\.c3t\\'" . c3-ts-mode))
+ :hook (c3-ts-mode . eglot-ensure)
+ :custom
+ (c3-ts-mode-indent-offset 2)
+ (setq treesit-font-lock-level 4)
+ :init
+ (add-to-list 'treesit-language-source-alist
+ '(c3 "https://github.com/c3lang/tree-sitter-c3"))
+ :config
+ (unless (treesit-language-available-p 'c3)
+ (treesit-install-language-grammar 'c3))
+ (with-eval-after-load 'eglot
+ (add-to-list 'eglot-server-programs
+ '(c3-ts-mode . ("c3-lsp")))))
+
(use-package python
:ensure nil
:mode ("\\.py\\'" . python-ts-mode)
@@ -2427,20 +2299,6 @@ completion-category-overrides '((file (styles partial-completion))))) ;; Customi
(add-to-list 'eglot-server-programs
'((typescript-ts-mode tsx-ts-mode) . ("rass" "--" "typescript-language-server" "--stdio" "--" "vscode-eslint-language-server" "--stdio")))))
-(use-package web-mode
- :ensure t
- :mode "\\.vue\\'"
- :hook (web-mode . eglot-ensure)
- :config
- (setq web-mode-markup-indent-offset 2)
- (setq web-mode-css-indent-offset 2)
- (setq web-mode-code-indent-offset 2)
- (setq web-mode-style-padding 2)
- (setq web-mode-script-padding 2)
- (with-eval-after-load 'eglot
- (add-to-list 'eglot-server-programs
- '(web-mode . ("rass" "vuetail")))))
-
(use-package xonsh-mode
:ensure (:host github :repo "seanfarley/xonsh-mode")
:mode ("\\.xsh\\'" "\\.xonshrc\\'"))
@@ -2514,28 +2372,6 @@ BODY is the xonsh script. PARAMS may include :dir and :cmdline."
(with-eval-after-load 'ob-async
(add-hook 'ob-async-pre-execute-src-block-hook my/ob-xonsh-installer))
-(use-package go-mode
- :ensure t
- :mode "\\.go\\'"
- :hook (go-mode . eglot-ensure)
- :config
- (with-eval-after-load 'eglot
- (add-to-list 'eglot-server-programs
- '(go-mode . ("gopls" :initializationOptions
- (:staticcheck t
- :matcher "CaseSensitive"
- :usePlaceholders t)))))
-
- (defun go-mode-setup ()
- (add-hook 'before-save-hook #'eglot-format-buffer -10 t)
- (add-hook 'before-save-hook
- (lambda ()
- (when (eglot-managed-p)
- (eglot-code-action-organize-imports nil t)))
- nil t))
-
- (add-hook 'go-mode-hook #'go-mode-setup))
-
(use-package json-ts-mode
:ensure nil
:mode "\\.json\\'"
@@ -2580,20 +2416,9 @@ BODY is the xonsh script. PARAMS may include :dir and :cmdline."
;; :ensure t
;; :after just-mode)
-(use-package eat
- :ensure t
- :custom
- (eat-shell "xonsh")
- (eat-kill-buffer-on-exit t)
- (eat-enable-shell-prompt-annotation t)
- (eat-term-name "xterm-256color")
- :hook
- (eshell-load . eat-eshell-mode)
- (eshell-load . eat-eshell-visual-command-mode))
-
(use-package olivetti
:ensure t
- :hook (org-mode . olivetti-mode)
+ ;; :hook (org-mode . olivetti-mode) ; no auto-center in org buffers
:custom
(olivetti-style nil) ; Use window margins (fringes disabled in early-init)
(olivetti-margin-width 10) ; No side margins
@@ -2615,6 +2440,11 @@ BODY is the xonsh script. PARAMS may include :dir and :cmdline."
(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)))
@@ -2631,27 +2461,6 @@ BODY is the xonsh script. PARAMS may include :dir and :cmdline."
(unless (display-graphic-p)
(diff-hl-margin-mode 1)))
-(defun my-org-sidecar-left ()
- "Pick an org-mode buffer via consult and display it as a left sidecar."
- (interactive)
- (let* ((bufs (cl-remove-if-not
- (lambda (b)
- (with-current-buffer b
- (derived-mode-p 'org-mode)))
- (buffer-list)))
- (names (mapcar #'buffer-name bufs))
- (choice (consult--read names
- :prompt "Org sidecar: "
- :sort nil
- :require-match t
- :category 'buffer)))
- (when-let* ((buf (get-buffer choice)))
- (display-buffer-in-side-window
- buf
- '((side . left)
- (window-width . 0.25)
- (slot . 0))))))
-
(use-package hl-todo
:ensure (:host github :repo "tarsius/hl-todo")
:config
@@ -2715,13 +2524,23 @@ so the working-tree diff stays visible until the user explicitly stages."
(add-hook 'after-save-hook #'my/auto-git-add-new-file)
+(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 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.
+ (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)
@@ -2740,16 +2559,18 @@ so the working-tree diff stays visible until the user explicitly stages."
(evil-set-initial-state 'messages-buffer-mode 'normal)
(evil-set-initial-state 'dired-mode 'normal)
(evil-set-initial-state 'ibuffer-mode 'normal)
- (evil-set-initial-state 'erc-mode 'normal))
+ (evil-set-initial-state 'clatter-mode 'normal)
+ (define-key evil-insert-state-map (kbd "C-w") 'evil-window-map))
(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)))
+ ;; (if (display-graphic-p)
+ ;; (eldoc-box-help-at-point)
+ ;; (eldoc-doc-buffer t))
+ (eldoc-doc-buffer t))
(define-key evil-normal-state-map (kbd "K") #'my/eldoc-and-jump)
@@ -2959,35 +2780,51 @@ so the working-tree diff stays visible until the user explicitly stages."
(read-key) ;; Wait for the user to press any key.
(kill-emacs)) ;; Close Emacs after installation is complete.
-(use-package hide-mode-line
- :ensure t)
+(display-time-mode 1)
-(use-package svg-line
- :ensure (:host nil :repo "ssh://git.cashmere.rs/svg-line.git"
- :branch "feat/tty-kitty-modeline")
- :config
- (require 'svg-line-segments)
- (setq svg-line-weather-latitude "53.25"
- svg-line-weather-longitude "10.41")
- (svg-line-weather-mode 1)
+(defvar-local my/doom-modeline--buffer-title nil)
+
+(defun my/doom-modeline--update-title-cache ()
+ "Update the cached #+title for the current buffer."
+ (setq-local my/doom-modeline--buffer-title
+ (cadar (org-collect-keywords '("TITLE")))))
+
+(defun my/doom-modeline--buffer-title ()
+ "Return the cached #+title, computing it if necessary."
+ (unless (local-variable-p 'my/doom-modeline--buffer-title)
+ (my/doom-modeline--update-title-cache))
+ my/doom-modeline--buffer-title)
- (svg-line-define 'cashmere-mode-line
- :target 'mode-line
- :layout 'lines
- :active #'mode-line-window-selected-p
- :background (lambda () (face-background 'mode-line nil t))
- :foreground (lambda () (face-foreground 'mode-line nil t))
- :inactive-background (lambda () (face-background 'mode-line-inactive nil t))
- :inactive-foreground (lambda () (face-foreground 'mode-line-inactive nil t))
- :content
- (lambda ()
- (list (cons (list #'svg-line-segment-evil " "
- #'svg-line-segment-buffer #'svg-line-segment-vc)
- (list #'svg-line-segment-weather #'svg-line-segment-battery
- #'svg-line-segment-position " "
- #'svg-line-segment-major-mode)))))
+(defun my/doom-modeline-set-buffer-title (&rest _)
+ "Override doom-modeline's cached file name with the Org #+title.
+If no TITLE keyword is found, leave doom-modeline's default name."
+ (when-let* ((title (my/doom-modeline--buffer-title)))
+ (setq doom-modeline--buffer-file-name
+ (propertize title
+ 'face 'doom-modeline-buffer-file
+ 'mouse-face 'mode-line-highlight
+ 'help-echo (concat (or buffer-file-truename (buffer-name))
+ "\nmouse-1: Previous buffer\nmouse-3: Next buffer")
+ 'local-map mode-line-buffer-identification-keymap))))
- (svg-line-activate 'cashmere-mode-line))
+(defun my/doom-modeline--invalidate-title-cache ()
+ "Invalidate the cached title so it gets recomputed on next update."
+ (kill-local-variable 'my/doom-modeline--buffer-title))
+
+(use-package doom-modeline
+ :ensure t
+ :init (doom-modeline-mode 1)
+ :config
+ (setq doom-modeline-hud t
+ doom-modeline-irc t
+ doom-modeline-irc-buffers nil
+ doom-modeline-mu4e t
+ doom-modeline-buffer-file-name-style 'relative-to-project
+ doom-modeline-buffer-encoding nil
+ doom-modeline-time t
+ doom-modeline-time-live-icon t)
+ (advice-add #'doom-modeline-update-buffer-file-name :after #'my/doom-modeline-set-buffer-title)
+ (add-hook 'before-save-hook #'my/doom-modeline--invalidate-title-cache))
(use-package adaptive-wrap
:ensure t
@@ -3011,42 +2848,36 @@ Runs per-frame so a TTY-only daemon never touches fonts while GUI frames
created later still get them."
(when (display-graphic-p frame)
(set-face-attribute 'default frame
- :family "MapleMono NF"
+ :family "Maple Mono NF"
:height 180
:font (font-spec
- :family "MapleMono NF"
- :features '(cv04 ss05 zero)))
- (set-face-attribute 'fixed-pitch frame :family "MapleMono NF" :weight 'regular)
- (set-face-attribute 'variable-pitch frame :family "MapleMono NF" :weight 'regular :height 1.1)))
+ :family "Maple Mono NF"
+ :features '(cv04 ss05 zero)
+ ))
+ (set-face-attribute 'fixed-pitch frame :family "Maple Mono NF" :weight 'regular)
+ (set-face-attribute 'variable-pitch frame :family "Maple Mono NF" :weight 'regular :height 1.1)))
(add-hook 'server-after-make-frame-hook #'cashmere/set-fonts)
(cashmere/set-fonts)
-(use-package mixed-pitch
- :ensure t
- :defer t
- :hook ((org-mode . mixed-pitch-mode)
- (LaTeX-mode . mixed-pitch-mode)))
-
(use-package grip-mode
:ensure t
:config
(setq grip-command 'auto)) ;; auto, grip, go-grip or mdopen
(add-to-list 'custom-theme-load-path "~/.emacs.d/themes/")
-(load-theme 'noctalia t)
+(load-theme 'tinty t)
(use-package dirvish
- :ensure t
+ :ensure (:host github :repo "latiagertrutis/dirvish" :branch "main")
:init
(dirvish-override-dired-mode)
:custom
(dirvish-quick-access-entries
- '(("h" "~/" "Home")
- ("d" "~/Downloads/" "Downloads")
- ;; ("p" "~/projects/" "Projects")
- ))
+ '(("h" "~/" "home")
+ ("d" "~/Downloads/" "downloads")
+ ("c" "~/.config/" "config")))
(dirvish-reuse-session 'open)
(dirvish-attributes '(file-size))
@@ -3085,7 +2916,7 @@ created later still get them."
dirvish-preview-dispatchers)))))
(use-package zoxide
- :ensure t)
+ :ensure t)
(use-package dired-rsync
:ensure t)
@@ -3183,6 +3014,14 @@ activates automatically. Edit, then `C-c C-c' commits, `C-c C-k' aborts."
(with-eval-after-load 'embark
(advice-add 'embark-export :after #'my/embark-export-auto-wgrep))
+(defun my/consult-fd-in (dir)
+ (interactive "DFind files in: ")
+ (consult-fd dir))
+
+(defun my/consult-ripgrep-in (dir)
+ (interactive "DGrep in: ")
+ (consult-ripgrep dir))
+
(use-package persp-mode
:ensure t
:demand t
@@ -3204,8 +3043,8 @@ activates automatically. Edit, then `C-c C-c' commits, `C-c C-k' aborts."
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.0
- persp-auto-save-opt 1
+ persp-auto-resume-time -1
+ persp-auto-save-opt 0
uniquify-buffer-name-style nil)
(add-to-list 'persp-filter-save-buffers-functions
@@ -3379,7 +3218,6 @@ activates automatically. Edit, then `C-c C-c' commits, `C-c C-k' aborts."
(defun my/workspace-switch-to-project (&optional dir)
(let* ((dir (or dir default-directory))
- (default-directory dir)
(name (projectile-project-name dir)))
(when persp-mode
(if (or (eq my/workspaces-on-switch-project t)
@@ -3389,7 +3227,8 @@ activates automatically. Edit, then `C-c C-c' commits, `C-c C-k' aborts."
(my/workspace-switch name t)
(switch-to-buffer (my/workspace-fallback-buffer)))
(ignore-errors (persp-rename name (get-current-persp))))
- (funcall my/workspace-switch-project-function))))
+ (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))
@@ -3399,30 +3238,17 @@ activates automatically. Edit, then `C-c C-c' commits, `C-c C-k' aborts."
(define-key persp-mode-map [remap evil-window-delete]
#'my/workspace-close-window-or-workspace))
- (defun my/workspace-restore-services (&rest _)
- (run-with-idle-timer
- 1 nil
- (lambda ()
- (unless (and (fboundp 'mu4e-running-p) (mu4e-running-p))
- (mu4e t)))))
- (add-hook 'persp-after-load-state-functions #'my/workspace-restore-services)
-
(add-hook 'persp-filter-save-buffers-functions
(lambda (buf)
- (with-current-buffer buf (derived-mode-p 'erc-mode))))
-
- (run-with-timer (* 10 60) (* 10 60)
- (lambda () (when persp-mode (persp-save-state-to-file))))
+ (with-current-buffer buf (derived-mode-p 'clatter-mode))))
- (defun my/workspace--inhibit-erc-part (orig &rest args)
- (let ((erc-kill-channel-hook nil)
- (erc-kill-server-hook nil)
- (erc-kill-buffer-hook nil))
+ (defun my/workspace--inhibit-irc-part (orig &rest args)
+ (cl-letf (((symbol-function 'clatter--on-kill-buffer) #'ignore))
(apply orig args)))
- (advice-add 'my/workspace-kill :around #'my/workspace--inhibit-erc-part)
- (advice-add 'my/workspace-kill-session :around #'my/workspace--inhibit-erc-part)
+ (advice-add 'my/workspace-kill :around #'my/workspace--inhibit-irc-part)
+ (advice-add 'my/workspace-kill-session :around #'my/workspace--inhibit-irc-part)
- (when (daemonp) (persp-mode 1)))
+ (persp-mode 1))
(with-eval-after-load 'consult
(defvar my/workspace-consult-source
@@ -3450,8 +3276,11 @@ activates automatically. Edit, then `C-c C-c' commits, `C-c C-k' aborts."
(defun my/ibuffer-workspace ()
(interactive)
- (ibuffer nil (format "*ibuffer: %s*" (my/workspace-current-name))
- (list (cons 'persp t))))
+ (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))))))
(my-leader
"TAB" '(:ignore t :wk "workspace")
@@ -3551,7 +3380,8 @@ activates automatically. Edit, then `C-c C-c' commits, `C-c C-k' aborts."
: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")))
(use-package pdf-tools
:ensure nil
@@ -3588,10 +3418,13 @@ activates automatically. Edit, then `C-c C-c' commits, `C-c C-k' aborts."
:wk "find file/switch project")
"sp" '(consult-projectile :wk "search project")
"ss" '(consult-line :wk "search project")
+ "sd" '(my/consult-ripgrep-in :wk "grep in dir")
+ "sf" '(my/consult-fd-in :wk "find file in dir")
"/" '(consult-ripgrep :wk "search project")
"." '(find-file :wk "find file")
"," '(consult-buffer :wk "switch buffer")
":" (lambda () (interactive) (execute-extended-command nil))
+ "u" '(universal-argument :wk "universal argument")
"d" '(:ignore t :wk "denote")
"dj" '(denote-journal-new-or-existing-entry :wk "journal")
@@ -3603,6 +3436,7 @@ activates automatically. Edit, then `C-c C-c' commits, `C-c C-k' aborts."
"dl" '(denote-link-or-create t :wk "Link Note")
"dn" '(denote t :wk "Create a new note")
"dr" '(denote-rename-file t :wk "Rename Note")
+ "ds" '(denote-solo-switch :wk "switch silo")
"dtl" '(tmr-list-timers :wk "list timer")
"dtt" '(tmr :wk "set timer")
@@ -3649,6 +3483,7 @@ activates automatically. Edit, then `C-c C-c' commits, `C-c C-k' aborts."
"gt" '(eglot-find-typeDefinition :wk "go to type definition")
"gr" '(xref-find-references :wk "find references")
"gs" '(magit-file-stage :wk "stage file")
+ "gu" '(my/magit-uncommit :wk "uncommit (keep & unstage)")
"gb" '(vc-annotate :wk "blame")
"gT" '(my/code-todos-harvest :wk "harvest code TODOs")
@@ -3656,6 +3491,7 @@ activates automatically. Edit, then `C-c C-c' commits, `C-c C-k' aborts."
"os" '(my/snip-upload :wk "snip buffer/region")
"oS" '(my/snip-upload-file :wk "snip file")
"op" '(pass :wk "pass")
+ "ot" '(ghostel :wk "ghostel")
"h" '(:ignore t :wk "help")
"hm" '(describe-mode :wk "mode")
@@ -3681,8 +3517,8 @@ activates automatically. Edit, then `C-c C-c' commits, `C-c C-k' aborts."
"w L" '(buf-move-right :wk "Buffer move right")
"c" '(:ignore t :wk "code")
- "cc" '(my/compile-or-recompile :wk "compile / recompile")
- "cC" '(compile :wk "compile (new command)")
+ "cc" '(my/compile-or-recompile :wk "compile")
+ "cC" '(ghostel-compile :wk "recompile")
"ca" '(eglot-code-actions :wk "code actions")
"cr" '(eglot-rename :wk "lsp rename")
"cf" '(eglot-format :wk "format buffer")
@@ -3728,17 +3564,19 @@ activates automatically. Edit, then `C-c C-c' commits, `C-c C-k' aborts."
(call-interactively 'flash-evil-jump)))))
(general-def '(normal visual operator) 'override
+ :predicate '(not (derived-mode-p 'mu4e-main-mode 'mu4e-headers-mode
+ 'mu4e-view-mode 'mu4e-compose-mode))
"s" 'my/s-key-dispatch)
(general-def 'normal 'override
"K" 'my/eldoc-and-jump
- "'d" 'flycheck-next-error
- ";d" 'flycheck-previous-error
+ "]d" 'flycheck-next-error
+ "[d" 'flycheck-previous-error
"]c" 'diff-hl-next-hunk
"[c" 'diff-hl-previous-hunk
- "'b" 'switch-to-next-buffer
- ";b" 'switch-to-prev-buffer
+ "]b" 'switch-to-next-buffer
+ "[b" 'switch-to-prev-buffer
"]t" 'tab-next
"[t" 'tab-previous
"P" 'consult-yank-from-kill-ring
@@ -3767,6 +3605,7 @@ activates automatically. Edit, then `C-c C-c' commits, `C-c C-k' aborts."
"mC" '(org-capture :wk "capture")
"mc" '(:wk "set" :ignore)
+ "mk" '(kitty-gfx-org-heading-sizes :wk "kgfx headlines")
"mcd" '(org-deadline :wk "deadline")
"mcs" '(org-schedule :wk "schedule")
"mce" '(org-set-effort :wk "effort")
@@ -3929,13 +3768,19 @@ date) and the dired header are never skipped."
((string> a-feed b-feed) nil)
(t (string< a-title b-title)))))))
-(use-package elfeed-org
- :ensure t
+(use-package elfeed-protocol
+ :ensure (:host github :repo "fasheng/elfeed-protocol")
:after elfeed
:custom
- (rmh-elfeed-org-files (list "~/org/rss.org"))
+ (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-org))
+ (elfeed-protocol-enable))
(use-package elfeed-goodies
:ensure t
@@ -3955,29 +3800,36 @@ date) and the dired header are never skipped."
line-spacing 0.2)
(shrface-mode 1))))
-(defun elfeed-export-from-index-to-csv (output-file)
- (interactive "FExport CSV nach: ")
- (require 'elfeed)
- (require 'elfeed-db)
-
- (elfeed-db-load)
-
- (let ((entries (hash-table-values elfeed-db-entries)))
- (with-temp-file output-file
- (insert "id,title,link,date,feed,tags\n")
- (dolist (entry entries)
- (insert (format "\"%s\",\"%s\",\"%s\",%s,\"%s\",\"%s\"\n"
- (elfeed-entry-id entry)
- (replace-regexp-in-string "\"" "\"\"" (elfeed-entry-title entry))
- (elfeed-entry-link entry)
- (elfeed-entry-date entry)
- (elfeed-feed-title (elfeed-entry-feed entry))
- (mapconcat #'symbol-name (elfeed-entry-tags entry) " "))))
- (message "Export abgeschlossen: %d Einträge" (length entries)))))
-
-(setq browse-url-browser-function 'browse-url-generic
+(setq browse-url-browser-function 'eww-browse-url
+ browse-url-secondary-browser-function 'browse-url-generic
browse-url-generic-program "qutebrowser")
+(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)
@@ -4061,6 +3913,12 @@ date) and the dired header are never skipped."
:ensure t
:commands (link-hint-open-link link-hint-copy-link))
+(with-eval-after-load 'general
+ (general-def :states '(normal visual)
+ :keymaps '(eww-mode-map elfeed-show-mode-map elfeed-search-mode-map)
+ "f" #'link-hint-open-link
+ "F" #'link-hint-copy-link))
+
(use-package engine-mode
:ensure t
:commands (engine/search-duckduckgo
@@ -4184,18 +4042,6 @@ date) and the dired header are never skipped."
(setq global-mode-string
(delete '(:eval mu4e-alert-mode-line) global-mode-string)))
-(use-package denote-project-notes
- :ensure t
- :after org
- :config
- (setq denote-project-notes-identifier '(project stayem)))
-
-(use-package arrow
- :ensure t
- :elpaca (arrow :host github :repo "vmargb/arrow.el")
- :config
- (setq arrow-mode 1))
-
(use-package colorful-mode
;; :diminish
:ensure t ; Optional
@@ -4276,12 +4122,6 @@ opening another file in same project does not re-notify."
(setq eshell-command-aliases-list
'(("nh-switch" "nh os switch --quiet $*"))))
-(use-package eshell-atuin
- :ensure t
- :after eshell
- :config
- (eshell-atuin-mode))
-
(use-package el-fetch
:ensure t)
@@ -4291,6 +4131,7 @@ opening another file in same project does not re-notify."
(ghostel-shell "xonsh")
(ghostel-ssh-install-terminfo t)
:config
+ (require 'ghostel-compile)
(setq-default window-adjust-process-window-size-function
#'window-adjust-process-window-size-largest)
(add-to-list 'ghostel-tramp-shells '("rpc" login-shell))
@@ -4317,8 +4158,17 @@ opening another file in same project does not re-notify."
: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))
+
(when-let* ((garden-dir (expand-file-name
(or (alist-get 'garden my/local-packages) "~/garden")))
((file-directory-p garden-dir)))
@@ -4331,17 +4181,24 @@ opening another file in same project does not re-notify."
(garden-auto-index-mode 1)
(autoload 'garden "garden-dashboard" nil t)
(autoload 'garden-sidecar "garden-dashboard" nil t)
+ (autoload 'garden-search "garden-dashboard" nil t)
+ (autoload 'garden-fleet "garden-fleet" nil t)
(autoload 'garden-connect "garden-connect" nil t)
- (autoload 'garden-connect-pick "garden-connect" nil t))
-;; (autoload 'garden-player "garden-player" nil t)
+ (autoload 'garden-connect-pick "garden-connect" nil t)
+ (autoload 'garden-publish "garden-publish" nil t)
+ (autoload 'garden-publish-all "garden-publish" nil t)
+ (autoload 'garden-publish-wiki "garden-publish" nil t)
+ (autoload 'garden-publish-blog "garden-publish" nil t)
+ (autoload 'garden-publish-deploy "garden-publish" nil t)
+ (autoload 'denote-capf-setup "denote-capf" nil t)
+ (with-eval-after-load 'denote-capf
+ (setq denote-capf-directories '("~/org/")))
+ (add-hook 'org-mode-hook #'denote-capf-setup)
-(autoload 'denote-capf-setup "denote-capf" nil t)
-(add-hook 'org-mode-hook #'denote-capf-setup)
-(autoload 'garden-publish "garden-publish" nil t)
-(autoload 'garden-publish-all "garden-publish" nil t)
-(autoload 'garden-publish-wiki "garden-publish" nil t)
-(autoload 'garden-publish-blog "garden-publish" nil t)
-(autoload 'garden-publish-deploy "garden-publish" nil t)
+ (defun my/denote-capf-tab-in-insert ()
+ (when (featurep 'evil)
+ (evil-local-set-key 'insert (kbd "TAB") #'indent-for-tab-command)))
+ (add-hook 'org-mode-hook #'my/denote-capf-tab-in-insert))
(use-package sops
:ensure (:type git :host github :repo "djgoku/sops")
@@ -4359,12 +4216,6 @@ opening another file in same project does not re-notify."
remote-file-name-inhibit-locks t
tramp-verbose 1))
-(use-package difftastic
- :ensure t
- :defer t
- :init
- (difftastic-bindings-mode))
-
(use-package magit-delta
:ensure t
:after magit
@@ -4373,99 +4224,57 @@ opening another file in same project does not re-notify."
(add-hook 'magit-mode-hook #'magit-delta-mode)))
(use-package pretty-sha-path
- :ensure t)
-
-(use-package grid
- :ensure (:host github :repo "ichernyshovvv/grid.el")
- :demand t)
-
-(use-package enlight
- :ensure (:host github :repo "ichernyshovvv/enlight")
- :demand t
- :after grid
+ :ensure t
:config
- (require 'enlight-menu)
- (defface my/enlight-header
- '((t (:inherit font-lock-keyword-face :weight bold)))
- "Enlight banner face.")
-
- (defvar my/enlight-banner
- (propertize
- "███████ ███ ███ █████ ██████ ███████
-██ ████ ████ ██ ██ ██ ██
-█████ ██ ████ ██ ███████ ██ ███████
-██ ██ ██ ██ ██ ██ ██ ██
-███████ ██ ██ ██ ██ ██████ ███████"
- 'face 'my/enlight-header))
-
- (defun my/enlight-stats ()
- (concat
- (format "%d packages" (length (elpaca--queued)))
- (when (bound-and-true-p after-init-time)
- (format " · %s" (emacs-init-time "%.2fs")))
- (format " · %s" (format-time-string "%a %d %b %H:%M"))))
-
- (defun my/enlight-recent ()
- (if (bound-and-true-p recentf-list)
- (mapconcat
- (lambda (f)
- (let ((s (abbreviate-file-name f)))
- (if (> (length s) 32)
- (concat "…" (substring s (- (length s) 31)))
- s)))
- (seq-take recentf-list 8)
- "\n")
- (propertize "no recent files" 'face 'shadow)))
+ (setopt global-pretty-sha-path-mode 't))
- (defun my/enlight-content ()
- (concat
- (grid-make-box `(:align center :width 80 :content ,my/enlight-banner))
- "\n"
- (grid-make-box `(:align center :width 80
- :content ,(propertize (my/enlight-stats) 'face 'shadow)))
- "\n\n"
- (grid-make-row
- (list
- (grid-make-box
- `(:width 40 :border t :padding 1
- :content ,(enlight-menu
- '(("Quick actions"
- ("Agenda" (org-agenda nil "d") "a")
- ("Recent file" consult-recent-file "r")
- ("Switch project" projectile-switch-project "p")
- ("Find note" denote-open-or-create "n")
- ("Journal entry" denote-journal-new-or-existing-entry "j")
- ("Magit" magit-status "g")
- ("Files" dirvish "f")
- ("Mail" mu4e "m")
- ("IRC" run-irc "i")
- ("Switch workspace" my/workspace-switch-to "w")
- ("Restore session" my/workspace-load-session "S"))))))
- " "
- (grid-make-box
- `(:width 38 :border t :padding 1
- :content ,(concat (propertize "Recent files\n\n" 'face 'enlight-menu-section)
- (my/enlight-recent)))))))))
-
-(defun my/enlight-refresh (&rest _)
- (enlight--update 'enlight-content (my/enlight-content)))
-(advice-add 'enlight :before #'my/enlight-refresh)
-
-(defun my/session-exists-p ()
- (file-exists-p (file-name-concat persp-save-dir persp-auto-save-fname)))
+(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")
-(setq initial-buffer-choice
- (lambda ()
- (if (and (fboundp 'enlight) (not (my/session-exists-p)))
- (enlight)
- (get-buffer-create "*scratch*"))))
+ (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-hook 'elpaca-after-init-hook
- (lambda ()
- (when (and (fboundp 'enlight-open) (not (my/session-exists-p)))
- (enlight-open))))
+ (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)
-(with-eval-after-load 'evil
- (evil-set-initial-state 'enlight-mode 'emacs))
+ (pulsar-global-mode))
(provide 'init)
diff --git a/config.org b/config.org
index 8a717ab..eeb4d82 100644
--- a/config.org
+++ b/config.org
@@ -33,10 +33,6 @@
* local packages
-Per-host development overrides. Any package registered in the gitignored =local.el= is loaded from a local checkout on =load-path= instead of its remote source, so I can hack on it in place (for example =kitty-graphics.el= on its =testing= branch) while =config.org= stays identical across every machine. No per-package recipe wrapper is needed: an =:around= advice on the elpaca =:ensure= normalizer rewrites the recipe to =nil= for anything in =my/local-packages=, so the =use-package= forms downstream use the plain =:ensure (:host github :repo "...")= they always have and =local.el= overrides.
-
-=local.el= is loaded inside =eval-and-compile= so the advice sees the same =my/local-packages= whether =config.el= runs as source or native-compiled. After editing =local.el=, re-evaluate the file (or restart) so the new override takes effect. A minimal =local.el= looks like:
-
#+begin_example
;;; -*- lexical-binding: t -*-
(setq my/local-packages
@@ -53,6 +49,8 @@ Set per-host in the gitignored `local.el'.")
(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
@@ -138,8 +136,8 @@ Set per-host in the gitignored `local.el'.")
(scroll-bar-mode -1)
(add-to-list 'default-frame-alist '(vertical-scroll-bars . nil))
(add-to-list 'default-frame-alist '(horizontal-scroll-bars . nil))
- ;; (set-frame-parameter (selected-frame) 'alpha-background 80)
- ;; (add-to-list 'default-frame-alist '(alpha-background . 80))
+ (set-frame-parameter (selected-frame) 'alpha-background 80)
+ (add-to-list 'default-frame-alist '(alpha-background . 80))
(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
@@ -288,6 +286,8 @@ Selects the compilation window so the cursor lands in it."
(setq insert-directory-program gls)))))
#+end_src
+* async
+
#+begin_src emacs-lisp
(use-package async :ensure t)
#+end_src
@@ -781,7 +781,8 @@ Defined early so later =with-eval-after-load 'general= callbacks find =my-leader
(my-leader
"aa" '(agent-shell :wk "start")
"at" '(agent-shell-toggle :wk "toggle")
- "am" '(agent-shell-help-menu :wk "open session")))
+ "am" '(agent-shell-help-menu :wk "open session")
+ "ar" '(agent-shell-send-region :wk "send region")))
:config
;; Runs only on first invocation — not at Emacs startup.
@@ -814,7 +815,27 @@ let's give agent-shell another try
#+begin_src emacs-lisp
(defun my/agent-shell-autoscroll (&rest _)
(dolist (win (get-buffer-window-list (current-buffer) 'visible))
- (set-window-point win (point-max))))
+ (when (>= (window-point win)
+ (save-excursion (goto-char (point-max)) (point)))
+ (set-window-point win (point-max)))))
+
+(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
@@ -823,10 +844,51 @@ let's give agent-shell another try
("M-RET" . shell-maker-submit))
:config
(advice-add 'shell-maker-submit :after #'my/agent-shell-autoscroll)
- (setq agent-shell-anthropic-claude-environment
- (agent-shell-make-environment-variables
- "ANTHROPIC_BASE_URL" "https://api.minimax.io/anthropic"
- "ANTHROPIC_AUTH_TOKEN" (password-store-get "minimax"))))
+ (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 nil)
+ (setq agent-shell-user-message-expand-by-default nil)
+ (setq agent-shell-diff-delta-command
+ (when (executable-find "delta")
+ '("delta" "--no-gitconfig" "--color-only")))
+
+ ;; 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))
#+end_src
* eldoc
@@ -1063,6 +1125,10 @@ Only activates when running in a Kitty terminal (not in GUI Emacs).
(kitty-gfx-shr-fit-height 20)
:hook (dired-mode . kitty-gfx-dired-auto-preview-mode)
:config
+ (setq kitty-gfx-enable-browser t
+ kitty-gfx-casty-program "~/projects/casty/bin/casty.js"
+ kitty-gfx-casty-chrome "helium-browser")
+
(kitty-graphics-setup))
#+end_src
@@ -1477,55 +1543,22 @@ Skips capture tasks and projects."
*** org-capture helpers
#+begin_src emacs-lisp
-(defun my/org-capture-timeblock-to-journal ()
- "Capture function: jump to today's journal for a timeblock."
- (require 'denote)
+(defun my/org-capture-to-journal (heading)
(require 'denote-journal)
- (let* ((today (format-time-string "%Y-%m-%d"))
- (journal-files (denote-directory-files "journal"))
- (today-journal (seq-find
- (lambda (file)
- (string-match-p today (file-name-nondirectory file)))
- journal-files)))
- (if today-journal
- (find-file today-journal)
- (denote-journal-new-or-existing-entry))
- (goto-char (point-max))
- (insert "\n* Timeblock\n")))
+ (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 ()
- "Capture function: jump to today's journal for a recurring task."
- (require 'denote)
- (require 'denote-journal)
- (let* ((today (format-time-string "%Y-%m-%d"))
- (journal-files (denote-directory-files "journal"))
- (today-journal (seq-find
- (lambda (file)
- (string-match-p today (file-name-nondirectory file)))
- journal-files)))
- (if today-journal
- (find-file today-journal)
- (denote-journal-new-or-existing-entry))
- (goto-char (point-max))
- (insert "\n* Recurring\n")))
+ (my/org-capture-to-journal "\n* Recurring\n"))
(defun my/org-capture-meeting-to-journal ()
- "Capture function: jump to today's journal for a meeting note."
- (require 'denote)
- (require 'denote-journal)
- (let* ((today (format-time-string "%Y-%m-%d"))
- (journal-files (denote-directory-files "journal"))
- (today-journal (seq-find
- (lambda (file)
- (string-match-p today (file-name-nondirectory file)))
- journal-files)))
- (if today-journal
- (find-file today-journal)
- (denote-journal-new-or-existing-entry))
- (goto-char (point-max))
- (insert "\n* Meeting: ")))
+ (my/org-capture-to-journal "\n* Meeting: "))
-;; capture templates are merged in the org-capture section below
#+end_src
@@ -1541,48 +1574,42 @@ Skips capture tasks and projects."
*** org-capture combined with denote
-#+begin_src emacs-lisp
-(defun my/org-capture-denote-deadline ()
+#+begin_src emacs-lisp
+(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: "))
- (file-name (denote nil '("agenda"))))
- (find-file file-name)
- (goto-char (point-min))
- (forward-line 4)
- (insert (format "\n* %s\nDEADLINE: <%s>\n\n" context deadline))
- (current-buffer)))
+ (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-denote-scheduled ()
+(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: "))
- (file-name (denote nil '("agenda"))))
- (find-file file-name)
- (goto-char (point-min))
- (forward-line 4)
- (insert (format "\n* %s\nSCHEDULED: <%s>\n\n" context schedule))
- (current-buffer)))
+ (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-denote-task ()
- (let* ((context (read-string "Task: "))
- (todo-state (completing-read "TODO state: "
- '("ACTIVE" "NEXT" "TODO" "WAIT")))
- (file-name (denote nil '("agenda"))))
- (find-file file-name)
- (goto-char (point-min))
- (forward-line 4)
- (insert (format "\n* %s %s\n\n" todo-state context))
- (point)))
+(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-denote-task)
+ (function my/org-capture-task-to-journal)
"" :immediate-finish t :jump-to-captured t)
("d" "Deadline" plain
- (function my/org-capture-denote-deadline)
+ (function my/org-capture-deadline-to-journal)
"" :immediate-finish t :jump-to-captured t)
("s" "Scheduled" plain
- (function my/org-capture-denote-scheduled)
+ (function my/org-capture-scheduled-to-journal)
"" :immediate-finish t :jump-to-captured t)
("b" "Timeblock" plain
(function my/org-capture-timeblock-to-journal)
@@ -2350,6 +2377,34 @@ Timers that expired while Emacs was closed fire immediately."
denote-merge-region-markdown-fenced-block))
#+end_src
+** denote-solo
+
+Auto switch silos (different workspaces/folders) based on preconfigured directories.
+
+#+begin_src emacs-lisp
+(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))
+#+end_src
+
+
* which-key
#+begin_src emacs-lisp
@@ -3434,9 +3489,44 @@ Native =undo-redo= (via =evil-undo-system='undo-redo=); just raise the undo limi
* modelines
+** display-time
+
+#+begin_src emacs-lisp
+(display-time-mode 1)
+#+end_src
+
** doom-modeline
#+begin_src emacs-lisp
+(defvar-local my/doom-modeline--buffer-title nil)
+
+(defun my/doom-modeline--update-title-cache ()
+ "Update the cached #+title for the current buffer."
+ (setq-local my/doom-modeline--buffer-title
+ (cadar (org-collect-keywords '("TITLE")))))
+
+(defun my/doom-modeline--buffer-title ()
+ "Return the cached #+title, computing it if necessary."
+ (unless (local-variable-p 'my/doom-modeline--buffer-title)
+ (my/doom-modeline--update-title-cache))
+ my/doom-modeline--buffer-title)
+
+(defun my/doom-modeline-set-buffer-title (&rest _)
+ "Override doom-modeline's cached file name with the Org #+title.
+If no TITLE keyword is found, leave doom-modeline's default name."
+ (when-let* ((title (my/doom-modeline--buffer-title)))
+ (setq doom-modeline--buffer-file-name
+ (propertize title
+ 'face 'doom-modeline-buffer-file
+ 'mouse-face 'mode-line-highlight
+ 'help-echo (concat (or buffer-file-truename (buffer-name))
+ "\nmouse-1: Previous buffer\nmouse-3: Next buffer")
+ 'local-map mode-line-buffer-identification-keymap))))
+
+(defun my/doom-modeline--invalidate-title-cache ()
+ "Invalidate the cached title so it gets recomputed on next update."
+ (kill-local-variable 'my/doom-modeline--buffer-title))
+
(use-package doom-modeline
:ensure t
:init (doom-modeline-mode 1)
@@ -3448,7 +3538,9 @@ Native =undo-redo= (via =evil-undo-system='undo-redo=); just raise the undo limi
doom-modeline-buffer-file-name-style 'relative-to-project
doom-modeline-buffer-encoding nil
doom-modeline-time t
- doom-modeline-time-live-icon t))
+ doom-modeline-time-live-icon t)
+ (advice-add #'doom-modeline-update-buffer-file-name :after #'my/doom-modeline-set-buffer-title)
+ (add-hook 'before-save-hook #'my/doom-modeline--invalidate-title-cache))
#+end_src
* soph
@@ -4153,6 +4245,7 @@ activates automatically. Edit, then `C-c C-c' commits, `C-c C-k' aborts."
"dl" '(denote-link-or-create t :wk "Link Note")
"dn" '(denote t :wk "Create a new note")
"dr" '(denote-rename-file t :wk "Rename Note")
+ "ds" '(denote-solo-switch :wk "switch silo")
"dtl" '(tmr-list-timers :wk "list timer")
"dtt" '(tmr :wk "set timer")
@@ -4207,6 +4300,7 @@ activates automatically. Edit, then `C-c C-c' commits, `C-c C-k' aborts."
"os" '(my/snip-upload :wk "snip buffer/region")
"oS" '(my/snip-upload-file :wk "snip file")
"op" '(pass :wk "pass")
+ "ot" '(ghostel :wk "ghostel")
"h" '(:ignore t :wk "help")
"hm" '(describe-mode :wk "mode")
@@ -4279,17 +4373,19 @@ activates automatically. Edit, then `C-c C-c' commits, `C-c C-k' aborts."
(call-interactively 'flash-evil-jump)))))
(general-def '(normal visual operator) 'override
+ :predicate '(not (derived-mode-p 'mu4e-main-mode 'mu4e-headers-mode
+ 'mu4e-view-mode 'mu4e-compose-mode))
"s" 'my/s-key-dispatch)
(general-def 'normal 'override
"K" 'my/eldoc-and-jump
- "'d" 'flycheck-next-error
- ";d" 'flycheck-previous-error
+ "]d" 'flycheck-next-error
+ "[d" 'flycheck-previous-error
"]c" 'diff-hl-next-hunk
"[c" 'diff-hl-previous-hunk
- "'b" 'switch-to-next-buffer
- ";b" 'switch-to-prev-buffer
+ "]b" 'switch-to-next-buffer
+ "[b" 'switch-to-prev-buffer
"]t" 'tab-next
"[t" 'tab-previous
"P" 'consult-yank-from-kill-ring
@@ -4325,6 +4421,7 @@ activates automatically. Edit, then `C-c C-c' commits, `C-c C-k' aborts."
"mC" '(org-capture :wk "capture")
"mc" '(:wk "set" :ignore)
+ "mk" '(kitty-gfx-org-heading-sizes :wk "kgfx headlines")
"mcd" '(org-deadline :wk "deadline")
"mcs" '(org-schedule :wk "schedule")
"mce" '(org-set-effort :wk "effort")
@@ -5027,7 +5124,6 @@ opening another file in same project does not re-notify."
(autoload 'garden-fleet "garden-fleet" nil t)
(autoload 'garden-connect "garden-connect" nil t)
(autoload 'garden-connect-pick "garden-connect" nil t)
- ;; (autoload 'garden-player "garden-player" nil t)
(autoload 'garden-publish "garden-publish" nil t)
(autoload 'garden-publish-all "garden-publish" nil t)
(autoload 'garden-publish-wiki "garden-publish" nil t)
@@ -5036,34 +5132,13 @@ opening another file in same project does not re-notify."
(autoload 'denote-capf-setup "denote-capf" nil t)
(with-eval-after-load 'denote-capf
(setq denote-capf-directories '("~/org/")))
- (add-hook 'org-mode-hook #'denote-capf-setup))
-#+end_src
-
-** garden-player
-
-=garden-player= (=M-x garden-player=, buffer =*navi*=) is a terminal Navidrome client: it streams through a background =mpv= over a unix socket, renders a TUI with =vui=, and shows album art inline through the kitty graphics protocol. It opens on your starred library; press =u= to flip the song list to the live up-next queue and =u= again to flip back.
-
-The buffer installs =garden-player-map= as a vim-like keymap (bindings live in =garden-player.el=, this is just the reference):
+ (add-hook 'org-mode-hook #'denote-capf-setup)
-| key | action |
-|-----+--------------------------------------------------------------|
-| =SPC= | play / pause |
-| =h= | previous track |
-| =l= | next track |
-| =j= | restart the current song from the beginning |
-| =k= | cycle repeat mode: off → repeat-all → repeat-one |
-| =s= | reshuffle the queue |
-| =n= | search the library and play the pick next (no playlist made) |
-| =N= | search the library and append the pick to the end |
-| =/= | same as =N= |
-| =u= | toggle between the starred library and the up-next queue |
-| =f= | favorite the current song |
-| =r= | radio: queue songs similar to the current one |
-| =g= | reload the starred library |
-| =q= | quit |
-| =RET= | play the song on the current row |
-
-Interactive commands: =garden-player= opens the player; =garden-player-play-next= and =garden-player-enqueue= search and queue without touching server playlists; =garden-player-toggle-view= flips the list; =garden-player-repeat=, =garden-player-shuffle=, =garden-player-radio=, =garden-player-like=, =garden-player-refresh= and =garden-player-quit= back the matching keys.
+ (defun my/denote-capf-tab-in-insert ()
+ (when (featurep 'evil)
+ (evil-local-set-key 'insert (kbd "TAB") #'indent-for-tab-command)))
+ (add-hook 'org-mode-hook #'my/denote-capf-tab-in-insert))
+#+end_src
* sops
@@ -5104,52 +5179,62 @@ Interactive commands: =garden-player= opens the player; =garden-player-play-next
#+begin_src emacs-lisp
(use-package pretty-sha-path
- :ensure t)
+ :ensure t
+ :config
+ (setopt global-pretty-sha-path-mode 't))
#+end_src
-
-* COMMENT empv
+* pulsar-mode
#+begin_src emacs-lisp
-(use-package empv
+(use-package pulsar
:ensure t
- :defer t
:config
- (setq empv-subsonic-url (password-store-get-field "navidrome" "url"))
- (setq empv-subsonic-username (password-store-get-field "navidrome" "username"))
- (setq empv-subsonic-password (password-store-get "navidrome"))
-
- (defun my/empv-subsonic-starred-songs ()
- "List every starred song in Navidrome and play the selected one."
- (interactive)
- (empv--subsonic-request
- "getStarred2.view"
- (empv--subsonic-result-handler "Starred song:")))
-
- (defun my/empv-subsonic-starred-songs-urls ()
- "Return a list of playable URLs for every starred song."
- (thread-last
- (empv--subsonic-request "getStarred2.view")
- (alist-get 'results)
- (seq-filter (lambda (item) (eq (alist-get 'kind item) 'song)))
- (mapcar #'empv--subsonic-item-extract-url)))
+ (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")
- (defun my/empv-subsonic-play-all-starred-songs ()
- "Clear the playlist and play every starred song."
- (interactive)
- (if-let ((urls (my/empv-subsonic-starred-songs-urls)))
- (empv-play urls)
- (message "No starred songs found")))
+ (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)
- (defun my/empv-subsonic-enqueue-all-starred-songs ()
- "Append every starred song to the current playlist."
- (interactive)
- (if-let ((urls (my/empv-subsonic-starred-songs-urls)))
- (empv-enqueue urls)
- (message "No starred songs found")))
+ (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)
- (with-eval-after-load 'embark
- (empv-embark-initialize-extra-actions)))
+ (pulsar-global-mode))
#+end_src
* provide init
diff --git a/lisp/agent-shell-diff.el b/lisp/agent-shell-diff.el
new file mode 100644
index 0000000..b78bdf1
--- /dev/null
+++ b/lisp/agent-shell-diff.el
@@ -0,0 +1,268 @@
+;;; agent-shell-diff.el --- A quick way to query/display a diff. -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2024 Alvaro Ramirez
+
+;; Author: Alvaro Ramirez https://xenodium.com
+;; URL: https://github.com/xenodium/agent-shell
+
+;; This package is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 3, or (at your option)
+;; any later version.
+
+;; This package is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; Report issues at https://github.com/xenodium/agent-shell/issues
+;;
+;; ✨ Please support this work https://github.com/sponsors/xenodium ✨
+
+;;; Code:
+
+(eval-when-compile
+ (require 'cl-lib))
+(require 'ansi-color)
+(require 'diff)
+(require 'diff-mode)
+
+(defcustom agent-shell-diff-delta-command
+ '("delta" "--no-gitconfig" "--color-only")
+ "Command and arguments used to colorize diffs with delta.
+
+Set to nil to disable delta and fall back to the standard `diff-no-select'
+output. delta must be installed and on `exec-path'."
+ :type '(choice (const :tag "Disable delta" nil)
+ (repeat string))
+ :group 'agent-shell)
+
+(defvar-local agent-shell-diff--on-exit nil
+ "Function to call when the diff buffer is killed.
+
+This variable is automatically set by :on-exit from `agent-shell-diff'
+and can be temporarily let-bound to nil to prevent the
+on-exit callback from running when the buffer is killed.")
+
+(defvar-local agent-shell-diff--file nil
+ "Buffer-local file path associated with the diff.")
+
+(defvar-local agent-shell-diff--accept-all-command nil
+ "Buffer-local command to accept all changes in the diff.")
+
+(defvar-local agent-shell-diff--reject-all-command nil
+ "Buffer-local command to reject all changes in the diff.")
+
+(defvar-local agent-shell-diff--used-delta nil
+ "Non-nil when the current buffer's diff was generated using delta.")
+
+(defvar agent-shell-diff-mode-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map (kbd "n") #'diff-hunk-next)
+ (define-key map (kbd "p") #'diff-hunk-prev)
+ (define-key map (kbd "y") #'agent-shell-diff-accept-all)
+ (define-key map (kbd "C-c C-c") #'agent-shell-diff-reject-all)
+ (define-key map (kbd "f") #'agent-shell-diff-open-file)
+ (define-key map (kbd "q") #'kill-current-buffer)
+ map)
+ "Keymap for `agent-shell-diff-mode'.")
+
+(define-derived-mode agent-shell-diff-mode diff-mode "Agent-Shell-Diff"
+ "Major mode for `agent-shell' diff buffers.
+Derives from `diff-mode'. Provides `agent-shell-diff-accept-all'
+and `agent-shell-diff-reject-all' commands that can be rebound
+via `agent-shell-diff-mode-map'."
+ :group 'agent-shell
+ ;; Don't inherit diff-mode-map (some bindings can be destructive).
+ (set-keymap-parent agent-shell-diff-mode-map nil)
+ (setq buffer-read-only t))
+
+(defun agent-shell-diff-kill-buffer (buffer)
+ "Kill diff BUFFER, suppressing any `agent-shell-diff--on-exit' callback.
+If BUFFER is not live, do nothing."
+ (when (buffer-live-p buffer)
+ (with-current-buffer buffer
+ (setq agent-shell-diff--on-exit nil))
+ (kill-buffer buffer)))
+
+(defun agent-shell-diff-accept-all ()
+ "Accept all changes in the current diff buffer."
+ (interactive)
+ (if agent-shell-diff--accept-all-command
+ (let ((buf (current-buffer)))
+ (funcall agent-shell-diff--accept-all-command)
+ (when (buffer-live-p buf)
+ (let ((agent-shell-diff--on-exit nil))
+ (kill-buffer buf))))
+ (user-error "No accept command available in this buffer")))
+
+(defun agent-shell-diff-reject-all ()
+ "Reject all changes in the current diff buffer."
+ (interactive)
+ (if agent-shell-diff--reject-all-command
+ (let ((buf (current-buffer)))
+ (when (funcall agent-shell-diff--reject-all-command)
+ (when (buffer-live-p buf)
+ (let ((agent-shell-diff--on-exit nil))
+ (kill-buffer buf)))))
+ (user-error "No reject command available in this buffer")))
+
+(cl-defun agent-shell-diff (&key old new on-exit on-accept on-reject title file)
+ "Display a diff between OLD and NEW strings in a buffer.
+
+Creates a new buffer showing the differences between OLD and NEW
+using `agent-shell-diff-mode'. The buffer is read-only.
+
+When the buffer is killed, calls ON-EXIT with no arguments.
+
+Returns the newly created diff buffer.
+
+Arguments:
+ :OLD - Original string content
+ :NEW - Modified string content
+ :ON-EXIT - Function called with no arguments when buffer is killed
+ :ON-ACCEPT - Command to accept all changes
+ :ON-REJECT - Command to reject all changes
+ :TITLE - Optional title to display in header line
+ :FILE - File path"
+ (let* ((diff-buffer (generate-new-buffer "*agent-shell-diff*"))
+ (calling-window (selected-window))
+ (calling-buffer (current-buffer))
+ (interrupt-key (where-is-internal 'agent-shell-interrupt
+ (current-local-map) t)))
+ (unwind-protect
+ (progn
+ (with-current-buffer diff-buffer
+ (let ((inhibit-read-only t)
+ (diff-mode-read-only nil))
+ (erase-buffer)
+ ;; Set mode before inserting diff so diff-no-select
+ ;; doesn't reset font-lock (see #316).
+ (agent-shell-diff-mode)
+ (agent-shell-diff--insert-diff old new file diff-buffer)
+ ;; Add overlays to hide scary text.
+ (save-excursion
+ (goto-char (point-min))
+ ;; `diff-no-select' adds a command line at the top and
+ ;; "Diff finished." at the bottom. delta output does not,
+ ;; so only strip those when delta was not used.
+ (unless agent-shell-diff--used-delta
+ (delete-region (point) (progn (forward-line 1) (point)))
+ (delete-region (progn (goto-char (point-max)) (forward-line -1) (forward-line 0) (point))
+ (point-max))
+ (goto-char (point-min)))
+ (goto-char (point-min))
+ ;; Hide --- and +++ lines
+ (while (re-search-forward "^\\(---\\|\\+\\+\\+\\).*\n" nil t)
+ (let ((overlay (make-overlay (match-beginning 0) (match-end 0))))
+ (overlay-put overlay 'category 'diff-header)
+ (overlay-put overlay 'display "")
+ (overlay-put overlay 'evaporate t)))
+ ;; Replace @@ lines with "Changes"
+ (goto-char (point-min))
+ (while (re-search-forward "^@@.*@@.*\n" nil t)
+ (let ((overlay (make-overlay (match-beginning 0) (match-end 0)))
+ (face 'diff-hunk-header)) ; or any face you prefer
+ (overlay-put overlay 'category 'diff-header)
+ ;; Intended display is:
+ ;; ╭─────────╮
+ ;; │ changes │
+ ;; ╰─────────╯
+ ;; Using before-string so diff-hunk-next
+ ;; lands on "│" instead of "╭".
+ (overlay-put overlay 'before-string
+ (propertize "\n╭─────────╮\n" 'face face))
+ (overlay-put overlay 'display
+ (propertize "│ changes │\n╰─────────╯\n\n" 'face face))
+ (overlay-put overlay 'evaporate t)))))
+ (goto-char (point-min))
+ (ignore-errors (diff-hunk-next))
+ (setq agent-shell-diff--file file
+ agent-shell-diff--accept-all-command on-accept
+ agent-shell-diff--reject-all-command on-reject)
+ (when on-exit
+ (setq agent-shell-diff--on-exit on-exit)
+ (add-hook 'kill-buffer-hook
+ (lambda ()
+ (when (and agent-shell-diff--on-exit
+ (buffer-live-p calling-buffer))
+ (with-current-buffer calling-buffer
+ (funcall on-exit)))
+ ;; Give focus back to calling buffer.
+ (when (buffer-live-p calling-buffer)
+ (ignore-errors
+ (when (window-live-p calling-window)
+ (unless (eq (window-buffer calling-window) calling-buffer)
+ (set-window-buffer calling-window calling-buffer))
+ (select-window calling-window)))))
+ nil t))
+ (let ((map (copy-keymap agent-shell-diff-mode-map)))
+ (when (and interrupt-key
+ (not (lookup-key map interrupt-key)))
+ (define-key map interrupt-key #'agent-shell-diff-reject-all))
+ (use-local-map map))
+ (setq header-line-format
+ (substitute-command-keys
+ (concat
+ " "
+ (when title
+ (concat (propertize title 'face 'mode-line-emphasis) " "))
+ "\\[diff-hunk-next] next hunk "
+ "\\[diff-hunk-prev] previous hunk "
+ "\\[agent-shell-diff-accept-all] accept "
+ "\\[agent-shell-diff-reject-all] reject "
+ "\\[agent-shell-diff-open-file] open "
+ "\\[kill-current-buffer] quit"))))
+ diff-buffer)
+ (pop-to-buffer diff-buffer '((display-buffer-use-some-window
+ display-buffer-same-window))))))
+
+(defun agent-shell-diff-open-file ()
+ "Open the file associated with the current diff buffer."
+ (interactive)
+ (if agent-shell-diff--file
+ (find-file agent-shell-diff--file)
+ (user-error "No file associated with this diff buffer")))
+
+(defun agent-shell-diff--insert-diff (old new file buf)
+ "Insert diff from FILE between OLD and NEW strings in buffer BUF.
+
+When `agent-shell-diff-delta-command' is non-nil and delta is available
+on `exec-path', pipe the unified diff through delta for syntax
+highlighting; otherwise fall back to `diff-no-select'."
+ (let* ((suffix (format ".%s" (file-name-extension file)))
+ (old-file (make-temp-file "old" nil suffix))
+ (new-file (make-temp-file "new" nil suffix))
+ (use-delta (and agent-shell-diff-delta-command
+ (executable-find (car agent-shell-diff-delta-command)))))
+ (with-temp-file old-file (insert old))
+ (with-temp-file new-file (insert new))
+ (with-current-buffer buf
+ (setq agent-shell-diff--used-delta use-delta)
+ (if use-delta
+ (let ((coding-system-for-read 'utf-8)
+ (coding-system-for-write 'utf-8)
+ (command (concat "diff -U3 "
+ (shell-quote-argument old-file)
+ " "
+ (shell-quote-argument new-file)
+ " | "
+ (mapconcat #'shell-quote-argument
+ agent-shell-diff-delta-command
+ " "))))
+ (call-process-shell-command command nil t nil)
+ ;; `ansi-color-apply-on-region' can drop properties in some
+ ;; Emacs builds, so apply to the string and re-insert.
+ (let ((colored (ansi-color-apply (buffer-string))))
+ (erase-buffer)
+ (insert colored)))
+ (diff-no-select old-file new-file "-U3" t buf)))))
+
+(provide 'agent-shell-diff)
+
+;;; agent-shell-diff.el ends here
diff --git a/lisp/agent-shell-kimi.el b/lisp/agent-shell-kimi.el
new file mode 100644
index 0000000..3a9d948
--- /dev/null
+++ b/lisp/agent-shell-kimi.el
@@ -0,0 +1,612 @@
+;;; agent-shell-kimi.el --- Kimi Code agent configurations -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2026 Nicolai Singh
+
+;; This package is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation; either version 3, or (at your option)
+;; any later version.
+
+;; This package is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; This file includes Kimi Code configuration using `kimi acp'.
+;;
+;; It exposes the Kimi-specific flags that are available when starting an
+;; ACP server (`--yolo', `--auto', `--plan', `--model', `--add-dir',
+;; `--skills-dir') and integrates with Kimi's session index so recent
+;; sessions can be resumed from Emacs.
+
+;;; Code:
+
+(eval-when-compile
+ (require 'cl-lib))
+(require 'json)
+(require 'shell-maker)
+(require 'acp)
+
+(declare-function agent-shell--indent-string "agent-shell")
+(declare-function agent-shell--make-acp-client "agent-shell")
+(declare-function agent-shell-make-agent-config "agent-shell")
+(autoload 'agent-shell-make-agent-config "agent-shell")
+(declare-function agent-shell--dwim "agent-shell")
+(declare-function agent-shell-start "agent-shell")
+(declare-function shell-maker-welcome-message "shell-maker")
+(defvar shell-maker-prompt)
+(defvar shell-maker-prompt-regexp)
+
+(defcustom agent-shell-kimi-acp-command
+ '("kimi" "acp")
+ "Command and parameters for the Kimi Code CLI client.
+
+The first element is the command name, and the rest are command parameters.
+This is the base command; flags controlled by the other custom variables
+in this file are prepended before the `acp' subcommand."
+ :type '(repeat string)
+ :group 'agent-shell)
+
+(defcustom agent-shell-kimi-default-model-id
+ nil
+ "Default Kimi model ID.
+
+Must be one of the model ID's displayed under \"Available models\"
+when starting a new shell.
+
+Can be set to either a string or a function that returns a string."
+ :type '(choice (const nil) string function)
+ :group 'agent-shell)
+
+(defcustom agent-shell-kimi-default-session-mode-id
+ nil
+ "Default Kimi Code session mode ID.
+
+Must be one of the mode ID's displayed under \"Available modes\"
+when starting a new shell.
+
+Can be set to either a string or a function that returns a string."
+ :type '(choice (const nil) string function)
+ :group 'agent-shell)
+
+(defcustom agent-shell-kimi-environment
+ nil
+ "Environment variables for the Kimi Code CLI client.
+
+This should be a list of environment variables to be used when
+starting the Kimi client process."
+ :type '(repeat string)
+ :group 'agent-shell)
+
+(defcustom agent-shell-kimi-yolo
+ nil
+ "When non-nil, pass `--yolo' to `kimi' so all actions are auto-approved.
+
+See `kimi --help' for the exact semantics."
+ :type 'boolean
+ :group 'agent-shell)
+
+(defcustom agent-shell-kimi-auto
+ nil
+ "When non-nil, pass `--auto' to `kimi' to start in auto permission mode."
+ :type 'boolean
+ :group 'agent-shell)
+
+(defcustom agent-shell-kimi-plan
+ nil
+ "When non-nil, pass `--plan' to `kimi' to start in plan mode."
+ :type 'boolean
+ :group 'agent-shell)
+
+(defcustom agent-shell-kimi-model
+ nil
+ "Kimi model alias passed to `--model'.
+
+Can be either a string or a function that returns a string. When nil,
+no `--model' flag is added and Kimi uses its configured default."
+ :type '(choice (const nil) string function)
+ :group 'agent-shell)
+
+(defcustom agent-shell-kimi-additional-dirs
+ nil
+ "List of additional workspace directories passed via `--add-dir'.
+
+Each entry should be an expanded directory path."
+ :type '(repeat string)
+ :group 'agent-shell)
+
+(defcustom agent-shell-kimi-skills-dirs
+ nil
+ "List of skills directories passed via `--skills-dir'.
+
+Each entry should be an expanded directory path."
+ :type '(repeat string)
+ :group 'agent-shell)
+
+(defcustom agent-shell-kimi-extra-command-params
+ nil
+ "Extra command-line parameters passed to `kimi' before the `acp' subcommand.
+
+Use this for flags that do not yet have a dedicated custom variable."
+ :type '(repeat string)
+ :group 'agent-shell)
+
+(defcustom agent-shell-kimi-session-index
+ (expand-file-name "~/.kimi-code/session_index.jsonl")
+ "Path to Kimi's session index file.
+
+Used by `agent-shell-kimi-resume-session' to list recent sessions."
+ :type 'file
+ :group 'agent-shell)
+
+(cl-defun agent-shell-kimi-make-authentication (&key api-key login none)
+ "Create a Kimi authentication configuration.
+
+API-KEY is a Kimi API key string or a function that returns one.
+LOGIN when non-nil indicates login-based (device-code) authentication.
+NONE when non-nil indicates no authentication method is requested.
+
+Only one of API-KEY, LOGIN, or NONE should be provided."
+ (when (> (seq-count #'identity (list api-key login none)) 1)
+ (error "Cannot specify multiple authentication methods - choose one"))
+ (unless (> (seq-count #'identity (list api-key login none)) 0)
+ (error "Must specify one of :api-key, :login, or :none"))
+ (cond
+ (api-key `((:api-key . ,api-key)))
+ (login `((:login . t)))
+ (none `((:none . t)))))
+
+(defcustom agent-shell-kimi-authentication
+ (agent-shell-kimi-make-authentication :none t)
+ "Configuration for Kimi authentication.
+
+For no explicit authentication (default):
+
+ (setq agent-shell-kimi-authentication
+ (agent-shell-kimi-make-authentication :none t))
+
+For login-based authentication:
+
+ (setq agent-shell-kimi-authentication
+ (agent-shell-kimi-make-authentication :login t))
+
+For API key (string):
+
+ (setq agent-shell-kimi-authentication
+ (agent-shell-kimi-make-authentication :api-key \"your-key\"))
+
+For API key (function):
+
+ (setq agent-shell-kimi-authentication
+ (agent-shell-kimi-make-authentication :api-key (lambda () ...)))"
+ :type 'alist
+ :group 'agent-shell)
+
+(defcustom agent-shell-kimi-login-command
+ '("kimi" "login")
+ "Command used by `agent-shell-kimi-login' to run the device-code flow."
+ :type '(repeat string)
+ :group 'agent-shell)
+
+(defcustom agent-shell-kimi-show-model-in-prompt
+ t
+ "When non-nil, include the active model/mode in the Kimi shell prompt."
+ :type 'boolean
+ :group 'agent-shell)
+
+;;;; Faces
+
+(defface agent-shell-kimi-prompt-face
+ '((t (:foreground "#81c8be" :weight bold :inherit fixed-pitch)))
+ "Face for the Kimi shell prompt."
+ :group 'agent-shell)
+
+(defface agent-shell-kimi-user-face
+ '((t (:inherit default)))
+ "Face for user messages in the Kimi shell."
+ :group 'agent-shell)
+
+(defface agent-shell-kimi-assistant-face
+ '((t (:foreground "#8caaee")))
+ "Face for assistant messages in the Kimi shell."
+ :group 'agent-shell)
+
+(defface agent-shell-kimi-tool-face
+ '((t (:foreground "#ca9ee6")))
+ "Face for tool-use sections in the Kimi shell."
+ :group 'agent-shell)
+
+(defface agent-shell-kimi-thought-face
+ '((t (:foreground "#a5adce" :slant italic)))
+ "Face for thought-process sections in the Kimi shell."
+ :group 'agent-shell)
+
+(defface agent-shell-kimi-error-face
+ '((t (:foreground "#e78284" :weight bold)))
+ "Face for errors in the Kimi shell."
+ :group 'agent-shell)
+
+;;;; Authentication & helpers
+
+(defun agent-shell-kimi-key ()
+ "Get the Kimi API key from `agent-shell-kimi-authentication'."
+ (cond ((stringp (map-elt agent-shell-kimi-authentication :api-key))
+ (map-elt agent-shell-kimi-authentication :api-key))
+ ((functionp (map-elt agent-shell-kimi-authentication :api-key))
+ (condition-case _err
+ (funcall (map-elt agent-shell-kimi-authentication :api-key))
+ (error
+ "API key not found. Check out `agent-shell-kimi-authentication'")))
+ (t
+ nil)))
+
+(defun agent-shell-kimi-login ()
+ "Run `kimi login' to authenticate via the device-code flow."
+ (interactive)
+ (unless agent-shell-kimi-login-command
+ (user-error "`agent-shell-kimi-login-command' is not set"))
+ (let ((command (mapconcat #'shell-quote-argument agent-shell-kimi-login-command " ")))
+ (async-shell-command command "*Kimi login*")))
+
+(defun agent-shell-kimi--resolve-string-or-function (value)
+ "Resolve VALUE if it is a function, otherwise return it as-is."
+ (if (functionp value)
+ (funcall value)
+ value))
+
+(defun agent-shell-kimi--command-params ()
+ "Return the complete list of parameters passed to `kimi' before `acp'."
+ (let ((params nil))
+ (when agent-shell-kimi-yolo
+ (push "--yolo" params))
+ (when agent-shell-kimi-auto
+ (push "--auto" params))
+ (when agent-shell-kimi-plan
+ (push "--plan" params))
+ (when-let* ((model (agent-shell-kimi--resolve-string-or-function agent-shell-kimi-model))
+ ((not (string-empty-p model))))
+ (push "--model" params)
+ (push model params))
+ (dolist (dir (reverse agent-shell-kimi-additional-dirs))
+ (push "--add-dir" params)
+ (push dir params))
+ (dolist (dir (reverse agent-shell-kimi-skills-dirs))
+ (push "--skills-dir" params)
+ (push dir params))
+ (dolist (arg (reverse agent-shell-kimi-extra-command-params))
+ (push arg params))
+ (nreverse params)))
+
+(defun agent-shell-kimi--active-model-string ()
+ "Return the currently configured Kimi model alias, or nil if none."
+ (when-let* ((model (agent-shell-kimi--resolve-string-or-function
+ agent-shell-kimi-model))
+ ((not (string-empty-p model))))
+ model))
+
+(defun agent-shell-kimi--active-mode-string ()
+ "Return the currently configured Kimi session mode ID, or nil if none."
+ (when-let* ((mode (agent-shell-kimi--resolve-string-or-function
+ agent-shell-kimi-default-session-mode-id))
+ ((not (string-empty-p mode))))
+ mode))
+
+(defun agent-shell-kimi--prompt-string ()
+ "Return the propertized Kimi shell prompt string."
+ (let ((extra nil))
+ (when agent-shell-kimi-show-model-in-prompt
+ (let ((model (agent-shell-kimi--active-model-string))
+ (mode (agent-shell-kimi--active-mode-string)))
+ (when (or model mode)
+ (setq extra (concat "("
+ (or model "default")
+ (when mode (concat "|" mode))
+ ")")))))
+ (propertize (concat "Kimi" extra "> ")
+ 'font-lock-face 'agent-shell-kimi-prompt-face)))
+
+(defun agent-shell-kimi-update-prompt ()
+ "Update the prompt in the current Kimi shell buffer."
+ (interactive)
+ (when (eq major-mode 'agent-shell-mode)
+ (setq-local shell-maker-prompt (agent-shell-kimi--prompt-string))
+ (setq-local shell-maker-prompt-regexp
+ (concat "^" (regexp-quote (substring-no-properties shell-maker-prompt))))))
+
+(defun agent-shell-kimi-make-config ()
+ "Create a Kimi Code agent configuration.
+
+Returns an agent configuration alist using `agent-shell-make-agent-config'."
+ (agent-shell-make-agent-config
+ :identifier 'kimi
+ :mode-line-name "Kimi"
+ :buffer-name "Kimi"
+ :shell-prompt (agent-shell-kimi--prompt-string)
+ :shell-prompt-regexp "Kimi[^>]*> "
+ :welcome-function #'agent-shell-kimi--welcome-message
+ :client-maker (lambda (buffer)
+ (agent-shell-kimi-make-client :buffer buffer))
+ :default-model-id (lambda () (agent-shell-kimi--resolve-string-or-function
+ agent-shell-kimi-default-model-id))
+ :default-session-mode-id (lambda () (agent-shell-kimi--resolve-string-or-function
+ agent-shell-kimi-default-session-mode-id))
+ :install-instructions "See https://www.kimi.com/code for installation."))
+
+(defun agent-shell-kimi-start-agent ()
+ "Start an interactive Kimi Code agent shell."
+ (interactive)
+ (agent-shell--dwim :config (agent-shell-kimi-make-config)
+ :new-shell t))
+
+(defun agent-shell-kimi-start-coding ()
+ "Start a Kimi shell for normal coding.
+
+This is equivalent to `agent-shell-kimi-start-agent' but explicitly
+resets `--plan' and `--yolo' so the shell starts in plain coding mode."
+ (interactive)
+ (let ((agent-shell-kimi-plan nil)
+ (agent-shell-kimi-yolo nil))
+ (agent-shell-kimi-start-agent)))
+
+(defun agent-shell-kimi-start-plan ()
+ "Start a Kimi shell in plan mode (`--plan')."
+ (interactive)
+ (let ((agent-shell-kimi-plan t)
+ (agent-shell-kimi-yolo nil))
+ (agent-shell-kimi-start-agent)))
+
+(defun agent-shell-kimi-start-yolo ()
+ "Start a Kimi shell in yolo/auto-approve mode (`--yolo')."
+ (interactive)
+ (let ((agent-shell-kimi-yolo t))
+ (agent-shell-kimi-start-agent)))
+
+(defun agent-shell-kimi-start-review ()
+ "Start a Kimi shell tuned for code review.
+
+Enables `--plan' and, if no model is set, falls back to
+`kimi-for-coding'. Customize `agent-shell-kimi-model' to change the
+review model."
+ (interactive)
+ (let ((agent-shell-kimi-plan t)
+ (agent-shell-kimi-yolo nil)
+ (agent-shell-kimi-model (or agent-shell-kimi-model "kimi-for-coding")))
+ (agent-shell-kimi-start-agent)))
+
+(cl-defun agent-shell-kimi-make-client (&key buffer)
+ "Create a Kimi Code ACP client with BUFFER as context."
+ (unless buffer
+ (error "Missing required argument: :buffer"))
+ (let* ((command (car agent-shell-kimi-acp-command))
+ (base-params (cdr agent-shell-kimi-acp-command))
+ (kimi-params (agent-shell-kimi--command-params))
+ ;; Global `kimi' flags go before the `acp' subcommand.
+ (command-params (if (string= (car base-params) "acp")
+ (append kimi-params base-params)
+ (append base-params kimi-params)))
+ (env-vars (append agent-shell-kimi-environment
+ (when-let* ((api-key (agent-shell-kimi-key))
+ ((not (map-elt agent-shell-kimi-authentication :none))))
+ (list (format "KIMI_API_KEY=%s" api-key))))))
+ (agent-shell--make-acp-client :command command
+ :command-params command-params
+ :environment-variables env-vars
+ :context-buffer buffer)))
+
+(defun agent-shell-kimi--read-jsonl (path)
+ "Return a list of parsed JSON objects from PATH (one per line)."
+ (when (file-exists-p path)
+ (with-temp-buffer
+ (insert-file-contents path)
+ (let ((result nil))
+ (goto-char (point-min))
+ (while (not (eobp))
+ (let ((line (string-trim (thing-at-point 'line t))))
+ (unless (string-empty-p line)
+ (push (json-parse-string line :object-type 'alist) result)))
+ (forward-line 1))
+ (nreverse result)))))
+
+(defun agent-shell-kimi--session-mtime (session)
+ "Return the modification time of SESSION's directory, or 0 if missing."
+ (let ((dir (cdr (assq 'sessionDir session))))
+ (if (file-directory-p dir)
+ (time-to-seconds (file-attribute-modification-time (file-attributes dir)))
+ 0)))
+
+(defun agent-shell-kimi--relative-time (seconds)
+ "Return a human-readable string for SECONDS since the epoch."
+ (let ((delta (max 0 (- (float-time) seconds))))
+ (cond ((< delta 60) "just now")
+ ((< delta 3600) (format "%d min ago" (floor delta 60)))
+ ((< delta 86400) (format "%d hr ago" (floor delta 3600)))
+ ((< delta 604800) (format "%d days ago" (floor delta 86400)))
+ (t (format "%d wks ago" (floor delta 604800))))))
+
+(defun agent-shell-kimi--session-project-name (session)
+ "Return a project-like name for SESSION's working directory."
+ (let ((dir (abbreviate-file-name (or (cdr (assq 'workDir session)) ""))))
+ (if (string-empty-p dir)
+ "?"
+ (file-name-nondirectory (directory-file-name dir)))))
+
+(defun agent-shell-kimi--session-label (session)
+ "Return a display label for SESSION suitable for `completing-read'."
+ (let ((id (cdr (assq 'sessionId session)))
+ (dir (abbreviate-file-name (or (cdr (assq 'workDir session)) "")))
+ (project (agent-shell-kimi--session-project-name session))
+ (mtime (agent-shell-kimi--session-mtime session)))
+ (format "%s %s %s %s"
+ project
+ (agent-shell-kimi--relative-time mtime)
+ dir
+ (or id ""))))
+
+(defun agent-shell-kimi--session-current-p (session)
+ "Return non-nil if SESSION's workDir is under `default-directory'."
+ (let ((dir (cdr (assq 'workDir session)))
+ (cwd (expand-file-name default-directory)))
+ (and dir
+ (string-prefix-p cwd (expand-file-name dir)))))
+
+(defun agent-shell-kimi-clean-session-index ()
+ "Remove session index entries whose working directory no longer exists.
+
+Returns the number of removed entries. The original index file is
+renamed with a timestamp backup before rewriting."
+ (interactive)
+ (unless (file-exists-p agent-shell-kimi-session-index)
+ (user-error "Session index not found: %s" agent-shell-kimi-session-index))
+ (let* ((sessions (agent-shell-kimi--read-jsonl agent-shell-kimi-session-index))
+ (valid (seq-filter (lambda (s)
+ (let ((dir (cdr (assq 'workDir s))))
+ (and dir (file-directory-p dir))))
+ sessions))
+ (removed (- (length sessions) (length valid))))
+ (when (> removed 0)
+ (rename-file agent-shell-kimi-session-index
+ (format "%s.bak-%s" agent-shell-kimi-session-index
+ (format-time-string "%Y%m%d-%H%M%S"))
+ t)
+ (with-temp-file agent-shell-kimi-session-index
+ (dolist (s valid)
+ (insert (json-encode s) "\n"))))
+ (message "Removed %d stale Kimi session(s)" removed)
+ removed))
+
+(defun agent-shell-kimi-resume-session (cleanup)
+ "Resume a recent Kimi session by selecting from the session index.
+
+With prefix arg CLEANUP, first remove index entries whose working
+directory no longer exists."
+ (interactive "P")
+ (when cleanup
+ (agent-shell-kimi-clean-session-index))
+ (let* ((sessions (agent-shell-kimi--read-jsonl agent-shell-kimi-session-index))
+ (sessions (seq-filter (lambda (s)
+ (let ((dir (cdr (assq 'workDir s))))
+ (and dir (file-directory-p dir))))
+ sessions))
+ (sessions (seq-sort-by #'agent-shell-kimi--session-mtime #'> sessions))
+ ;; Surface sessions from the current project first.
+ (sessions (append (seq-filter #'agent-shell-kimi--session-current-p sessions)
+ (seq-remove #'agent-shell-kimi--session-current-p sessions)))
+ (choices (mapcar (lambda (s)
+ (cons (agent-shell-kimi--session-label s) s))
+ sessions))
+ (choice (completing-read "Resume Kimi session: " choices nil t))
+ (session (cdr (assoc choice choices)))
+ (dir (cdr (assq 'workDir session)))
+ (id (cdr (assq 'sessionId session))))
+ (unless session
+ (user-error "No session selected"))
+ (unless (and dir (file-directory-p dir))
+ (user-error "Session working directory no longer exists: %s" dir))
+ (let ((default-directory dir))
+ (agent-shell-start :config (agent-shell-kimi-make-config)
+ :session-id id))))
+
+(defun agent-shell-kimi--welcome-status-line ()
+ "Return a short status line for the Kimi welcome message."
+ (let ((model (or (agent-shell-kimi--active-model-string) "default"))
+ (mode (or (agent-shell-kimi--active-mode-string) "default")))
+ (concat
+ (propertize "model" 'font-lock-face 'agent-shell-kimi-tool-face)
+ ": " model " "
+ (propertize "mode" 'font-lock-face 'agent-shell-kimi-tool-face)
+ ": " mode " "
+ (propertize "send" 'font-lock-face 'agent-shell-kimi-thought-face) " M-RET "
+ (propertize "interrupt" 'font-lock-face 'agent-shell-kimi-thought-face) " C-c C-c "
+ (propertize "menu" 'font-lock-face 'agent-shell-kimi-thought-face) " SPC a k")))
+
+(defun agent-shell-kimi--welcome-message (config)
+ "Return Kimi ASCII art using `shell-maker' CONFIG."
+ (let ((art (agent-shell--indent-string 4 (agent-shell-kimi--ascii-art)))
+ (message (string-trim-left (shell-maker-welcome-message config) "\n")))
+ (concat "\n\n"
+ art
+ "\n\n"
+ " " (agent-shell-kimi--welcome-status-line) "\n\n"
+ message)))
+
+(defun agent-shell-kimi--ascii-art ()
+ "Kimi ASCII art."
+ (let* ((is-dark (eq (frame-parameter nil 'background-mode) 'dark))
+ (text (string-trim "
+ ██╗ ██╗ ██╗ ███╗ ███╗ ██╗
+ ██║ ██╔╝ ██║ ████╗ ████║ ██║
+ █████╔╝ ██║ ██╔████╔██║ ██║
+ ██╔═██╗ ██║ ██║╚██╔╝██║ ██║
+ ██║ ██╗ ██║ ██║ ╚═╝ ██║ ██║
+ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
+" "\n")))
+ (propertize text 'font-lock-face (if is-dark
+ '(:foreground "#dddddd" :inherit fixed-pitch)
+ '(:foreground "#000000" :inherit fixed-pitch)))))
+
+(defcustom agent-shell-kimi-model-choices
+ '("kimi-for-coding" "kimi-k2" "kimi-k2-5")
+ "List of model aliases offered by `agent-shell-kimi-set-model'.
+
+Set this to the model aliases your Kimi CLI installation supports."
+ :type '(repeat string)
+ :group 'agent-shell)
+
+(defun agent-shell-kimi-set-model (model)
+ "Interactively set `agent-shell-kimi-model' to MODEL.
+
+Use `agent-shell-kimi-model-choices' for completion. An empty choice
+clears the model and uses Kimi's configured default."
+ (interactive
+ (list (completing-read "Kimi model (empty for default): "
+ agent-shell-kimi-model-choices
+ nil nil
+ (or (agent-shell-kimi--active-model-string) ""))))
+ (setq agent-shell-kimi-model (if (string-empty-p model) nil model))
+ (message "Kimi model set to %s" (or agent-shell-kimi-model "default")))
+
+(defun agent-shell-kimi-set-directory (dir)
+ "Add DIR to `agent-shell-kimi-additional-dirs'.
+
+Existing entries are preserved; duplicates are removed."
+ (interactive "DAdditional workspace directory: ")
+ (setq agent-shell-kimi-additional-dirs
+ (seq-uniq (append agent-shell-kimi-additional-dirs
+ (list (expand-file-name dir)))))
+ (message "Kimi additional dirs: %s" agent-shell-kimi-additional-dirs))
+
+(defcustom agent-shell-kimi-pulse-on-done
+ t
+ "When non-nil, pulse the mode-line when Kimi finishes responding.
+
+Only applies when the Kimi buffer is not the selected window; the
+pulse is meant as a subtle TUI notification."
+ :type 'boolean
+ :group 'agent-shell)
+
+(declare-function shell-maker--announce-response "shell-maker")
+
+(defun agent-shell-kimi-response-done-pulse (buffer)
+ "Pulse the mode-line to notify that BUFFER has finished responding.
+
+Meant to be added as :after advice to `shell-maker--announce-response'."
+ (when (and agent-shell-kimi-pulse-on-done
+ (buffer-live-p buffer)
+ (not (eq buffer (window-buffer (selected-window)))))
+ (let ((face 'agent-shell-kimi-prompt-face)
+ (original-face (face-attribute 'mode-line :inherit)))
+ (set-face-attribute 'mode-line nil :inherit face)
+ (run-with-timer 0.3 nil
+ (lambda ()
+ (set-face-attribute 'mode-line nil :inherit original-face))))))
+
+(advice-add 'shell-maker--announce-response :after #'agent-shell-kimi-response-done-pulse)
+
+(provide 'agent-shell-kimi)
+
+;;; agent-shell-kimi.el ends here