summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config.el306
-rw-r--r--config.org315
-rw-r--r--themes/tinty-theme.el46
3 files changed, 486 insertions, 181 deletions
diff --git a/config.el b/config.el
index dd45f30..c2ad92d 100644
--- a/config.el
+++ b/config.el
@@ -114,8 +114,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 90)
+ (add-to-list 'default-frame-alist '(alpha-background . 90))
(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
@@ -276,6 +276,10 @@ Selects the compilation window so the cursor lands in it."
(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.
+;; Soju routes to the upstream network via the USER string, so we still need
+;; one connection per network. We connect to Libera and Ergo only.
(defun my/soju-password ()
"Read the Soju bouncer password from the sops-nix secret."
(string-trim
@@ -284,84 +288,211 @@ Selects the compilation window so the cursor lands in it."
"/home/cashmere/.config/sops-nix/secrets/senpai_password")
(buffer-string))))
-(use-package clatter
- :ensure (:host github :repo "parenworks/clatter.el" :files ("*.el"))
- :commands (clatter clatter-connect)
+(defun run-irc ()
+ "Connect to Libera and Ergo via the Soju bouncer."
+ (interactive)
+ (let ((pw (my/soju-password)))
+ (erc-tls :server "bouncer.cashmere.rs"
+ :port 6699
+ :id 'libera
+ :nick "cashmere1337"
+ :user "cashmere/irc.libera.chat@emacs"
+ :password pw)
+ (erc-tls :server "bouncer.cashmere.rs"
+ :port 6699
+ :id 'ergo
+ :nick "cashmere"
+ :user "cashmere/ergo@emacs"
+ :password pw)))
+
+(use-package erc
+ :ensure nil
+ :defer t
:custom
- (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)
+ ;; connection
+ (erc-nick "cashmere1337")
+ (erc-user-full-name "cashmere")
+
+ ;; behaviour
+ (erc-join-buffer 'buffer)
+ (erc-kill-buffer-on-part nil)
+ (erc-kill-queries-on-quit t)
+ (erc-kill-server-buffer-on-quit t)
+
+ ;; visuals
+ (erc-fill-function 'erc-fill-wrap)
+ (erc-timestamp-format "[%H:%M]")
+ (erc-timestamp-format-left "[%H:%M]")
+ (erc-timestamp-format-right "[%H:%M]")
+ (erc-hide-list '("JOIN" "PART" "QUIT" "NICK" "MODE"))
+ (erc-track-exclude-types '("JOIN" "PART" "QUIT" "NICK" "MODE" "324" "329" "332" "333" "353" "477"))
+ (erc-track-shorten-start 4)
+ (erc-track-visibility 'visible)
+
+ ;; prompt
+ (erc-prompt (lambda () (concat (buffer-name) ">")))
+
+ ;; modules
+ (erc-modules '(autojoin
+ button
+ completion
+ fill
+ irccontrols
+ list
+ match
+ move-to-prompt
+ netsplit
+ networks
+ nicks
+ noncommands
+ notifications
+ readonly
+ ring
+ scrolltobottom
+ stamp
+ track))
:config
- (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))
+ ;; highlight own nick
+ (setq erc-current-nick-highlight-type 'all)
+ (setq erc-keywords '("cashmere"))
+ (setq erc-pals '("cashmere"))
+
+ ;; nicks module (built-in nick coloring in Emacs 30)
+ (setq erc-nicks-contrast-range '(40 . 90))
+
+ ;; scrolltobottom -- keep input always at bottom
+ (setq erc-scrolltobottom-all t)
+ (erc-scrolltobottom-mode 1)
+
+ ;; fill-wrap -- modern chat-like wrapping
+ (setq erc-fill-wrap-align-prompt nil)
+ (setq erc-fill-static-center 14)
+
+ ;; track -- activity NOT in modeline (channels list clutters doom-modeline)
+ (setq erc-track-position-in-mode-line nil)
+
+ ;; keep large buffer for soju history replay
+ (setq erc-max-buffer-size 100000)
+
+ (erc-update-modules))
-(defun my/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/clatter-switch-buffer ()
- "Switch to a clatter IRC buffer."
+(defun my/erc-switch-channel ()
+ "Switch to an ERC channel buffer via consult."
(interactive)
- (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."))))
+ (consult-buffer (list my/consult-source-erc)))
-(defvar my/clatter-inhibit-popup nil
- "When non-nil, clatter buffers are not auto-displayed by `display-buffer'.")
+(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)))))
-(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-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))))))
-(add-to-list 'display-buffer-alist
- '(my/clatter-popup-suppressed-p
- display-buffer-no-window
- (allow-no-window . t)))
+(defun my/erc-reconnect ()
+ "Reconnect to the current ERC server."
+ (interactive)
+ (erc-cmd-RECONNECT))
-(defun run-irc ()
- "Connect to all IRC networks via the Soju bouncer, in the background."
+(defun my/erc-list-channels ()
+ "Request channel list from the current ERC server."
(interactive)
- (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)))))
+ (erc-cmd-LIST))
+
+(defun persp-irc ()
+ "Switch to (or create) the IRC frame and show ERC buffers."
+ (interactive)
+ (let* ((erc-bufs (cl-remove-if-not
+ (lambda (buf)
+ (with-current-buffer buf
+ (derived-mode-p 'erc-mode)))
+ (buffer-list)))
+ (irc-frame (cl-find-if
+ (lambda (f)
+ (string= (frame-parameter f 'name) "irc"))
+ (frame-list))))
+ (if irc-frame
+ (select-frame-set-input-focus irc-frame)
+ (select-frame-set-input-focus (make-frame '((name . "irc")))))
+ (if erc-bufs
+ (switch-to-buffer (car erc-bufs))
+ (letrec ((hook-fn (lambda ()
+ (switch-to-buffer (current-buffer))
+ (remove-hook 'erc-join-hook hook-fn))))
+ (add-hook 'erc-join-hook hook-fn))
+ (run-irc))))
+
+;; ERC keybindings and hooks — register from any daemon, since `run-irc`
+;; autoloads ERC even outside the hub daemon.
+;; `general' is required eagerly (not via with-eval-after-load) so the
+;; `general-def' macro is expanded when config.el is compiled; inside a
+;; deferred lambda it would survive as a function call and fail at runtime.
+(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)))
+
+ (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))
(defvar my/centered-cursor-enabled nil)
@@ -397,7 +528,12 @@ Selects the compilation window so the cursor lands in it."
(when (and (not register)
(memq this-command '(evil-yank evil-yank-line)))
(my/send-to-clipboard (car kill-ring))))
- (advice-add 'evil-yank :after #'my/evil-yank-to-clipboard))
+ (advice-add 'evil-yank :after #'my/evil-yank-to-clipboard)
+ ;; elfeed-search-yank bypasses evil-yank (uses gui-set-selection directly),
+ ;; so route it through the same clipboard path — needed in TTY frames.
+ (defun my/elfeed-yank-to-clipboard (&rest _)
+ (my/send-to-clipboard (car kill-ring)))
+ (advice-add 'elfeed-search-yank :after #'my/elfeed-yank-to-clipboard))
(use-package isearch
:ensure nil ;; This is built-in, no need to fetch it.
@@ -525,7 +661,7 @@ Selects the compilation window so the cursor lands in it."
(or
(mode . eca-chat-mode)))
- ("IRC" (or (mode . clatter-mode)))
+ ("IRC" (or (mode . erc-mode)))
("Emacs" (or
(mode . emacs-lisp-mode)
@@ -702,7 +838,7 @@ Selects the compilation window so the cursor lands in it."
;; 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
- "aa" '(agent-shell :wk "start")
+ "aa" '(agent-shell-sidebar-toggle :wk "start")
"at" '(agent-shell-toggle :wk "toggle")
"am" '(agent-shell-help-menu :wk "open session")
"ar" '(agent-shell-send-region :wk "send region")))
@@ -799,6 +935,10 @@ Selects the compilation window so the cursor lands in it."
(advice-add 'agent-shell-yank-dwim :around
#'my/agent-shell-yank-dwim-in-terminal))
+(use-package agent-shell-sidebar
+ :after agent-shell
+ :ensure (:host github :repo "cmacrae/agent-shell-sidebar"))
+
(use-package eldoc
:ensure nil
:config
@@ -825,6 +965,8 @@ Selects the compilation window so the cursor lands in it."
:ensure t
:config
(setq flycheck-display-errors-function nil)
+ (setq flycheck-global-modes
+ '(not ghostel-mode vterm-mode term-mode shell-mode eshell-mode eat-mode))
(add-hook 'after-init-hook #'global-flycheck-mode))
(use-package flycheck-rust
@@ -960,7 +1102,7 @@ Selects the compilation window so the cursor lands in it."
:custom
(kitty-gfx-enable-video t)
(kitty-gfx-shr-scale 'fit)
- (kitty-gfx-shr-fit-width 0.6)
+ (kitty-gfx-shr-fit-width 0.4)
(kitty-gfx-shr-fit-height 20)
:hook (dired-mode . kitty-gfx-dired-auto-preview-mode)
:config
@@ -2567,7 +2709,7 @@ 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 'clatter-mode 'normal)
+ (evil-set-initial-state 'erc-mode 'normal)
(define-key evil-insert-state-map (kbd "C-w") 'evil-window-map))
(modify-syntax-entry ?_ "w")
@@ -2831,6 +2973,12 @@ If no TITLE keyword is found, leave doom-modeline's default name."
doom-modeline-buffer-encoding nil
doom-modeline-time t
doom-modeline-time-live-icon t)
+
+ ;; Replace the channel-list segment with the icon-only segment in ERC buffers.
+ (doom-modeline-def-modeline 'special
+ '(eldoc bar window-state window-number modals matches buffer-info remote-host buffer-position word-count parrot selection-info)
+ '(compilation objed-state misc-info battery irc debug minor-modes input-method indent-info buffer-encoding major-mode process time))
+
(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))
@@ -2873,7 +3021,7 @@ created later still get them."
:config
(setq grip-command 'auto)) ;; auto, grip, go-grip or mdopen
-(add-to-list 'custom-theme-load-path "~/.emacs.d/themes/")
+;; (add-to-list 'custom-theme-load-path "~/.emacs.d/themes/")
;; (load-theme 'tinty t)
(use-package dirvish
@@ -3248,10 +3396,12 @@ activates automatically. Edit, then `C-c C-c' commits, `C-c C-k' aborts."
(add-hook 'persp-filter-save-buffers-functions
(lambda (buf)
- (with-current-buffer buf (derived-mode-p 'clatter-mode))))
+ (with-current-buffer buf (derived-mode-p 'erc-mode))))
(defun my/workspace--inhibit-irc-part (orig &rest args)
- (cl-letf (((symbol-function 'clatter--on-kill-buffer) #'ignore))
+ (let ((erc-kill-channel-hook nil)
+ (erc-kill-server-hook nil)
+ (erc-kill-buffer-hook nil))
(apply orig args)))
(advice-add 'my/workspace-kill :around #'my/workspace--inhibit-irc-part)
(advice-add 'my/workspace-kill-session :around #'my/workspace--inhibit-irc-part)
diff --git a/config.org b/config.org
index 509d79a..d68e29a 100644
--- a/config.org
+++ b/config.org
@@ -136,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 90)
+ (add-to-list 'default-frame-alist '(alpha-background . 90))
(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
@@ -318,9 +318,13 @@ Selects the compilation window so the cursor lands in it."
(use-package async :ensure t)
#+end_src
-* clatter
+* erc
#+begin_src emacs-lisp
+;; Always-available IRC entry points so `M-x run-irc` works from any daemon
+;; (hub, work, standalone). Calling `erc-tls` autoloads ERC on demand.
+;; Soju routes to the upstream network via the USER string, so we still need
+;; one connection per network. We connect to Libera and Ergo only.
(defun my/soju-password ()
"Read the Soju bouncer password from the sops-nix secret."
(string-trim
@@ -329,84 +333,211 @@ Selects the compilation window so the cursor lands in it."
"/home/cashmere/.config/sops-nix/secrets/senpai_password")
(buffer-string))))
-(use-package clatter
- :ensure (:host github :repo "parenworks/clatter.el" :files ("*.el"))
- :commands (clatter clatter-connect)
+(defun run-irc ()
+ "Connect to Libera and Ergo via the Soju bouncer."
+ (interactive)
+ (let ((pw (my/soju-password)))
+ (erc-tls :server "bouncer.cashmere.rs"
+ :port 6699
+ :id 'libera
+ :nick "cashmere1337"
+ :user "cashmere/irc.libera.chat@emacs"
+ :password pw)
+ (erc-tls :server "bouncer.cashmere.rs"
+ :port 6699
+ :id 'ergo
+ :nick "cashmere"
+ :user "cashmere/ergo@emacs"
+ :password pw)))
+
+(use-package erc
+ :ensure nil
+ :defer t
:custom
- (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)
+ ;; connection
+ (erc-nick "cashmere1337")
+ (erc-user-full-name "cashmere")
+
+ ;; behaviour
+ (erc-join-buffer 'buffer)
+ (erc-kill-buffer-on-part nil)
+ (erc-kill-queries-on-quit t)
+ (erc-kill-server-buffer-on-quit t)
+
+ ;; visuals
+ (erc-fill-function 'erc-fill-wrap)
+ (erc-timestamp-format "[%H:%M]")
+ (erc-timestamp-format-left "[%H:%M]")
+ (erc-timestamp-format-right "[%H:%M]")
+ (erc-hide-list '("JOIN" "PART" "QUIT" "NICK" "MODE"))
+ (erc-track-exclude-types '("JOIN" "PART" "QUIT" "NICK" "MODE" "324" "329" "332" "333" "353" "477"))
+ (erc-track-shorten-start 4)
+ (erc-track-visibility 'visible)
+
+ ;; prompt
+ (erc-prompt (lambda () (concat (buffer-name) ">")))
+
+ ;; modules
+ (erc-modules '(autojoin
+ button
+ completion
+ fill
+ irccontrols
+ list
+ match
+ move-to-prompt
+ netsplit
+ networks
+ nicks
+ noncommands
+ notifications
+ readonly
+ ring
+ scrolltobottom
+ stamp
+ track))
:config
- (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))
+ ;; highlight own nick
+ (setq erc-current-nick-highlight-type 'all)
+ (setq erc-keywords '("cashmere"))
+ (setq erc-pals '("cashmere"))
+
+ ;; nicks module (built-in nick coloring in Emacs 30)
+ (setq erc-nicks-contrast-range '(40 . 90))
+
+ ;; scrolltobottom -- keep input always at bottom
+ (setq erc-scrolltobottom-all t)
+ (erc-scrolltobottom-mode 1)
+
+ ;; fill-wrap -- modern chat-like wrapping
+ (setq erc-fill-wrap-align-prompt nil)
+ (setq erc-fill-static-center 14)
+
+ ;; track -- activity NOT in modeline (channels list clutters doom-modeline)
+ (setq erc-track-position-in-mode-line nil)
-(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))
+ ;; keep large buffer for soju history replay
+ (setq erc-max-buffer-size 100000)
-(defun my/clatter-switch-buffer ()
- "Switch to a clatter IRC buffer."
+ (erc-update-modules))
+
+(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."
(interactive)
- (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."))))
+ (consult-buffer (list my/consult-source-erc)))
-(defvar my/clatter-inhibit-popup nil
- "When non-nil, clatter buffers are not auto-displayed by `display-buffer'.")
+(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)))))
-(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-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))))))
-(add-to-list 'display-buffer-alist
- '(my/clatter-popup-suppressed-p
- display-buffer-no-window
- (allow-no-window . t)))
+(defun my/erc-reconnect ()
+ "Reconnect to the current ERC server."
+ (interactive)
+ (erc-cmd-RECONNECT))
-(defun run-irc ()
- "Connect to all IRC networks via the Soju bouncer, in the background."
+(defun my/erc-list-channels ()
+ "Request channel list from the current ERC server."
(interactive)
- (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)))))
+ (erc-cmd-LIST))
+
+(defun persp-irc ()
+ "Switch to (or create) the IRC frame and show ERC buffers."
+ (interactive)
+ (let* ((erc-bufs (cl-remove-if-not
+ (lambda (buf)
+ (with-current-buffer buf
+ (derived-mode-p 'erc-mode)))
+ (buffer-list)))
+ (irc-frame (cl-find-if
+ (lambda (f)
+ (string= (frame-parameter f 'name) "irc"))
+ (frame-list))))
+ (if irc-frame
+ (select-frame-set-input-focus irc-frame)
+ (select-frame-set-input-focus (make-frame '((name . "irc")))))
+ (if erc-bufs
+ (switch-to-buffer (car erc-bufs))
+ (letrec ((hook-fn (lambda ()
+ (switch-to-buffer (current-buffer))
+ (remove-hook 'erc-join-hook hook-fn))))
+ (add-hook 'erc-join-hook hook-fn))
+ (run-irc))))
+
+;; ERC keybindings and hooks — register from any daemon, since `run-irc`
+;; autoloads ERC even outside the hub daemon.
+;; `general' is required eagerly (not via with-eval-after-load) so the
+;; `general-def' macro is expanded when config.el is compiled; inside a
+;; deferred lambda it would survive as a function call and fail at runtime.
+(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)))
+
+ (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))
#+end_src
* centered cursors
@@ -435,7 +566,7 @@ Selects the compilation window so the cursor lands in it."
* clipetty
-Only =y= (evil-yank) reaches the system clipboard; deletes (=d=, =x=, =c=) and word kills (M-DEL, C-w in insert) stay in the kill-ring only. Clipetty's global mode would sync every kill via OSC 52, so instead the OSC 52 escape is emitted explicitly after each yank.
+Only =y= (evil-yank) reaches the system clipboard; deletes (=d=, =x=, =c=) and word kills (M-DEL, C-w in insert) stay in the kill-ring only. Clipetty's global mode would sync every kill via OSC 52, so instead the OSC 52 escape is emitted explicitly after each yank. =elfeed-search-yank= (which bypasses evil-yank via =gui-set-selection=) gets the same treatment for TTY frames.
#+begin_src emacs-lisp
(use-package clipetty
@@ -452,7 +583,12 @@ Only =y= (evil-yank) reaches the system clipboard; deletes (=d=, =x=, =c=) and w
(when (and (not register)
(memq this-command '(evil-yank evil-yank-line)))
(my/send-to-clipboard (car kill-ring))))
- (advice-add 'evil-yank :after #'my/evil-yank-to-clipboard))
+ (advice-add 'evil-yank :after #'my/evil-yank-to-clipboard)
+ ;; elfeed-search-yank bypasses evil-yank (uses gui-set-selection directly),
+ ;; so route it through the same clipboard path — needed in TTY frames.
+ (defun my/elfeed-yank-to-clipboard (&rest _)
+ (my/send-to-clipboard (car kill-ring)))
+ (advice-add 'elfeed-search-yank :after #'my/elfeed-yank-to-clipboard))
#+end_src
* isearch
@@ -600,7 +736,7 @@ I adjusted the config accordingly to my modes which I use on a frequent.
(or
(mode . eca-chat-mode)))
- ("IRC" (or (mode . clatter-mode)))
+ ("IRC" (or (mode . erc-mode)))
("Emacs" (or
(mode . emacs-lisp-mode)
@@ -805,7 +941,7 @@ Defined early so later =with-eval-after-load 'general= callbacks find =my-leader
;; 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
- "aa" '(agent-shell :wk "start")
+ "aa" '(agent-shell-sidebar-toggle :wk "start")
"at" '(agent-shell-toggle :wk "toggle")
"am" '(agent-shell-help-menu :wk "open session")
"ar" '(agent-shell-send-region :wk "send region")))
@@ -917,6 +1053,15 @@ let's give agent-shell another try
#'my/agent-shell-yank-dwim-in-terminal))
#+end_src
+*** agent-shell-sidebar
+
+#+begin_src emacs-lisp
+(use-package agent-shell-sidebar
+ :after agent-shell
+ :ensure (:host github :repo "cmacrae/agent-shell-sidebar"))
+#+end_src
+
+
* eldoc
#+begin_src emacs-lisp
@@ -954,6 +1099,8 @@ let's give agent-shell another try
:ensure t
:config
(setq flycheck-display-errors-function nil)
+ (setq flycheck-global-modes
+ '(not ghostel-mode vterm-mode term-mode shell-mode eshell-mode eat-mode))
(add-hook 'after-init-hook #'global-flycheck-mode))
#+end_src
@@ -1147,7 +1294,7 @@ Only activates when running in a Kitty terminal (not in GUI Emacs).
:custom
(kitty-gfx-enable-video t)
(kitty-gfx-shr-scale 'fit)
- (kitty-gfx-shr-fit-width 0.6)
+ (kitty-gfx-shr-fit-width 0.4)
(kitty-gfx-shr-fit-height 20)
:hook (dired-mode . kitty-gfx-dired-auto-preview-mode)
:config
@@ -3209,7 +3356,7 @@ 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 'clatter-mode 'normal)
+ (evil-set-initial-state 'erc-mode 'normal)
(define-key evil-insert-state-map (kbd "C-w") 'evil-window-map))
(modify-syntax-entry ?_ "w")
@@ -3553,6 +3700,12 @@ If no TITLE keyword is found, leave doom-modeline's default name."
doom-modeline-buffer-encoding nil
doom-modeline-time t
doom-modeline-time-live-icon t)
+
+ ;; Replace the channel-list segment with the icon-only segment in ERC buffers.
+ (doom-modeline-def-modeline 'special
+ '(eldoc bar window-state window-number modals matches buffer-info remote-host buffer-position word-count parrot selection-info)
+ '(compilation objed-state misc-info battery irc debug minor-modes input-method indent-info buffer-encoding major-mode process time))
+
(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
@@ -3611,7 +3764,7 @@ created later still get them."
** theme load
#+begin_src emacs-lisp
-(add-to-list 'custom-theme-load-path "~/.emacs.d/themes/")
+;; (add-to-list 'custom-theme-load-path "~/.emacs.d/themes/")
;; (load-theme 'tinty t)
#+end_src
@@ -4033,10 +4186,12 @@ activates automatically. Edit, then `C-c C-c' commits, `C-c C-k' aborts."
(add-hook 'persp-filter-save-buffers-functions
(lambda (buf)
- (with-current-buffer buf (derived-mode-p 'clatter-mode))))
+ (with-current-buffer buf (derived-mode-p 'erc-mode))))
(defun my/workspace--inhibit-irc-part (orig &rest args)
- (cl-letf (((symbol-function 'clatter--on-kill-buffer) #'ignore))
+ (let ((erc-kill-channel-hook nil)
+ (erc-kill-server-hook nil)
+ (erc-kill-buffer-hook nil))
(apply orig args)))
(advice-add 'my/workspace-kill :around #'my/workspace--inhibit-irc-part)
(advice-add 'my/workspace-kill-session :around #'my/workspace--inhibit-irc-part)
diff --git a/themes/tinty-theme.el b/themes/tinty-theme.el
index dac7528..65f3cc8 100644
--- a/themes/tinty-theme.el
+++ b/themes/tinty-theme.el
@@ -21,52 +21,52 @@
(let* (;; Tinted8 UI surface colors
(ui-bg "#1e1e2e")
(ui-fg "#cdd6f4")
- (ui-fg-dim "#a6adc8")
+ (ui-fg-dim "#97abed")
;; Use dim foreground for borders/muted chrome so they stay visible in
;; light variants (the raw border color is nearly white on latte).
- (ui-border "#a6adc8")
- (ui-line-highlight "#232336")
+ (ui-border "#97abed")
+ (ui-line-highlight "#505365")
(ui-bg-dark "#181825")
- (ui-bg-light "#45475a")
+ (ui-bg-light "#353554")
;; Tinted8 UI accent/status colors
(ui-accent "#f38ba8")
- (ui-link "#89b4fa")
+ (ui-link "#c6ede6")
(ui-info "#fab387")
(ui-error "#f38ba8")
(ui-success "#a6e3a1")
(ui-warning "#f9e2af")
;; Tinted8 UI container colors (for BACKGROUNDS only)
- (ui-selection-bg "#45475a")
+ (ui-selection-bg "#353554")
(ui-selection-fg "#cdd6f4")
- (ui-button-bg "#45475a")
+ (ui-button-bg "#353554")
(ui-button-fg "#cdd6f4")
- (ui-tooltip-bg "#313244")
+ (ui-tooltip-bg "#181825")
(ui-tooltip-fg "#cdd6f4")
- (ui-active-text-bg "#9399b2")
+ (ui-active-text-bg "#6c7086")
(ui-active-text-fg "#cdd6f4")
- (ui-search-bg "#94e2d5")
- (ui-search-fg "#1e1e2e")
+ (ui-search-bg "#353554")
+ (ui-search-fg "#f9e2af")
;; Tinted8 syntax colors
;; Comments use dim foreground instead of the raw syntax-comment gray,
;; which becomes almost invisible in light variants.
- (syntax-comment "#a6adc8")
+ (syntax-comment "#97abed")
(syntax-constant "#fab387")
- (syntax-function "#89b4fa")
+ (syntax-function "#94e2d5")
(syntax-keyword "#cba6f7")
(syntax-string "#a6e3a1")
- (syntax-type "#f9e2af")
+ (syntax-type "#c7dafa")
;; Give variables/imported names a distinct hue instead of blending into
;; the default foreground.
- (syntax-variable "#aa69f7")
+ (syntax-variable "#cdd6f4")
(syntax-warning "#f6c5d3")
(syntax-preprocessor "#488dfd")
(syntax-annotation "#f9e2af")
- (syntax-attribute "#c7dafa")
- (syntax-number "#fab387")
- (syntax-operator "#89dceb")
+ (syntax-attribute "#ece0fb")
+ (syntax-number "#cba6f7")
+ (syntax-operator "#94e2d5")
;; Raw palette colors for rainbow headings, delimiters, terminal faces,
;; and anywhere else we need a predictable hue cycle.
@@ -77,7 +77,7 @@
(pal-cyan "#94e2d5")
(pal-blue "#89b4fa")
(pal-magenta "#cba6f7")
- (pal-gray "#9399b2")
+ (pal-gray "#6c7086")
(pal-white "#cdd6f4")
(pal-black "#1e1e2e"))
@@ -509,14 +509,14 @@
`(ansi-color-magenta ((t (:foreground ,pal-magenta :background ,pal-magenta))))
`(ansi-color-cyan ((t (:foreground ,pal-cyan :background ,pal-cyan))))
`(ansi-color-white ((t (:foreground ,pal-white :background ,pal-white))))
- `(ansi-color-bright-black ((t (:foreground "#45475a" :background "#45475a"))))
+ `(ansi-color-bright-black ((t (:foreground "#353554" :background "#353554"))))
`(ansi-color-bright-red ((t (:foreground "#f6c5d3" :background "#f6c5d3"))))
`(ansi-color-bright-green ((t (:foreground "#d4efd2" :background "#d4efd2"))))
`(ansi-color-bright-yellow ((t (:foreground "#fcf6e9" :background "#fcf6e9"))))
`(ansi-color-bright-blue ((t (:foreground "#c7dafa" :background "#c7dafa"))))
`(ansi-color-bright-magenta ((t (:foreground "#ece0fb" :background "#ece0fb"))))
`(ansi-color-bright-cyan ((t (:foreground "#c6ede6" :background "#c6ede6"))))
- `(ansi-color-bright-white ((t (:foreground "#bac2de" :background "#bac2de"))))
+ `(ansi-color-bright-white ((t (:foreground "#ffffff" :background "#ffffff"))))
;; === Anzu ===
`(anzu-replace-highlight ((t (:background ,ui-line-highlight :foreground ,ui-error :weight bold :strike-through t))))
@@ -1167,14 +1167,14 @@
;; === Term (extended) ===
`(term ((t (:foreground ,ui-fg :background ,ui-bg))))
`(term-bold ((t (:weight bold))))
- `(term-color-bright-black ((t (:foreground "#45475a" :background "#45475a"))))
+ `(term-color-bright-black ((t (:foreground "#353554" :background "#353554"))))
`(term-color-bright-red ((t (:foreground "#f6c5d3" :background "#f6c5d3"))))
`(term-color-bright-green ((t (:foreground "#d4efd2" :background "#d4efd2"))))
`(term-color-bright-yellow ((t (:foreground "#fcf6e9" :background "#fcf6e9"))))
`(term-color-bright-blue ((t (:foreground "#c7dafa" :background "#c7dafa"))))
`(term-color-bright-magenta ((t (:foreground "#ece0fb" :background "#ece0fb"))))
`(term-color-bright-cyan ((t (:foreground "#c6ede6" :background "#c6ede6"))))
- `(term-color-bright-white ((t (:foreground "#bac2de" :background "#bac2de"))))
+ `(term-color-bright-white ((t (:foreground "#ffffff" :background "#ffffff"))))
;; === Transient ===
`(transient-heading ((t (:foreground ,pal-blue :weight bold))))