aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modules/auto-dim-config.el180
-rw-r--r--tests/test-auto-dim-config.el103
2 files changed, 283 insertions, 0 deletions
diff --git a/modules/auto-dim-config.el b/modules/auto-dim-config.el
index 5ce426d2..e538e523 100644
--- a/modules/auto-dim-config.el
+++ b/modules/auto-dim-config.el
@@ -21,6 +21,177 @@
;;; Code:
+(require 'cl-lib)
+(require 'color)
+
+(declare-function auto-dim-other-buffers-mode "auto-dim-other-buffers")
+(declare-function adob--update "auto-dim-other-buffers")
+(declare-function vterm--get-color "vterm")
+(declare-function vterm--invalidate "vterm")
+(declare-function vterm--set-size "vterm")
+(declare-function vterm--get-margin-width "vterm")
+(defvar vterm-min-window-width)
+(defvar vterm--term)
+
+(defvar cj/auto-dim--last-selected-window nil
+ "Most recent selected window seen by `cj/auto-dim--refresh-vterm-on-command'.")
+
+(defvar cj/auto-dim--vterm-refresh-timer nil
+ "Timer used to defer vterm redraws until after auto-dim updates.")
+
+(defcustom cj/auto-dim-vterm-foreground-blend 0.45
+ "Blend amount for dimmed vterm foreground colors.
+
+0 keeps the original vterm color; 1 uses the
+`auto-dim-other-buffers' foreground color."
+ :type 'number
+ :group 'auto-dim-other-buffers)
+
+(defcustom cj/auto-dim-vterm-background-blend 0.7
+ "Blend amount for dimmed vterm background colors.
+
+0 keeps the original vterm color; 1 uses the
+`auto-dim-other-buffers' background color."
+ :type 'number
+ :group 'auto-dim-other-buffers)
+
+(defun cj/auto-dim--vterm-buffer-dimmed-p ()
+ "Return non-nil when the current vterm buffer should render dimmed.
+
+Vterm resolves terminal colors to concrete color strings while redrawing the
+buffer, so this integration is buffer-level. If the same vterm buffer is shown
+in multiple windows and any one of those windows is selected/undimmed, keep the
+buffer bright."
+ (and (eq major-mode 'vterm-mode)
+ (let ((windows (get-buffer-window-list (current-buffer) nil 'visible)))
+ (and windows
+ (not (catch 'undimmed
+ (dolist (window windows)
+ (unless (window-parameter window 'adob--dim)
+ (throw 'undimmed t)))))))))
+
+(defun cj/auto-dim--face-color (face attribute fallback-face)
+ "Return FACE ATTRIBUTE, falling back to FALLBACK-FACE."
+ (let ((color (face-attribute face attribute nil 'default)))
+ (if (or (null color) (eq color 'unspecified))
+ (face-attribute fallback-face attribute nil 'default)
+ color)))
+
+(defun cj/auto-dim--color-rgb (color)
+ "Return COLOR as a list of RGB floats, or nil if COLOR is unknown."
+ (cond
+ ((and (stringp color)
+ (string-match
+ "\\`#\\([[:xdigit:]]\\{2\\}\\)\\([[:xdigit:]]\\{2\\}\\)\\([[:xdigit:]]\\{2\\}\\)\\'"
+ color))
+ (mapcar (lambda (index)
+ (/ (string-to-number (match-string index color) 16) 255.0))
+ '(1 2 3)))
+ ((and (stringp color)
+ (string-match
+ "\\`#\\([[:xdigit:]]\\)\\([[:xdigit:]]\\)\\([[:xdigit:]]\\)\\'"
+ color))
+ (mapcar (lambda (index)
+ (/ (* 17 (string-to-number (match-string index color) 16)) 255.0))
+ '(1 2 3)))
+ (t
+ (ignore-errors
+ (mapcar (lambda (component) (/ component 65535.0))
+ (color-values color))))))
+
+(defun cj/auto-dim--blend-color (color target amount)
+ "Blend COLOR toward TARGET by AMOUNT and return a hex color string."
+ (if-let* ((rgb (cj/auto-dim--color-rgb color))
+ (target-rgb (cj/auto-dim--color-rgb target)))
+ (apply #'color-rgb-to-hex
+ (append
+ (cl-mapcar
+ (lambda (source dest)
+ (+ (* source (- 1 amount)) (* dest amount)))
+ rgb target-rgb)
+ '(2)))
+ color))
+
+(defun cj/auto-dim--vterm-dim-color (color foreground-p)
+ "Return dimmed vterm COLOR.
+
+When FOREGROUND-P is non-nil, blend toward the dimmed foreground face; otherwise
+blend toward the dimmed background face."
+ (let* ((attribute (if foreground-p :foreground :background))
+ (target (cj/auto-dim--face-color 'auto-dim-other-buffers attribute 'default))
+ (amount (if foreground-p
+ cj/auto-dim-vterm-foreground-blend
+ cj/auto-dim-vterm-background-blend)))
+ (cj/auto-dim--blend-color color target amount)))
+
+(defun cj/auto-dim--vterm-get-color (orig-fun index &rest args)
+ "Advise vterm color lookup ORIG-FUN for dimmed windows.
+
+INDEX and ARGS are passed through to `vterm--get-color'."
+ (let ((color (apply orig-fun index args)))
+ (if (and color (cj/auto-dim--vterm-buffer-dimmed-p))
+ (cj/auto-dim--vterm-dim-color color (memq :foreground args))
+ color)))
+
+(defun cj/auto-dim--refresh-vterm-windows (&optional frame)
+ "Refresh visible vterm buffers in FRAME after dim state changes."
+ (when (or (fboundp 'vterm--set-size) (fboundp 'vterm--invalidate))
+ (dolist (window (window-list frame 'no-minibuf))
+ (with-current-buffer (window-buffer window)
+ (when (eq major-mode 'vterm-mode)
+ (let ((inhibit-read-only t))
+ (if (and (bound-and-true-p vterm--term)
+ (window-live-p window)
+ (fboundp 'vterm--get-margin-width))
+ (let* ((height (max 2 (window-body-height window)))
+ (min-width (if (boundp 'vterm-min-window-width)
+ vterm-min-window-width
+ 80))
+ (width (max min-width
+ (- (window-body-width window)
+ (vterm--get-margin-width)))))
+ ;; `vterm--redraw' only repaints rows libvterm marked dirty.
+ ;; A resize marks the whole terminal grid dirty, so briefly
+ ;; nudge height and restore it to force a full repaint after
+ ;; dim-state changes.
+ (vterm--set-size vterm--term (1+ height) width)
+ (vterm--set-size vterm--term height width))
+ (when (fboundp 'vterm--invalidate)
+ (vterm--invalidate)))))))))
+
+(defun cj/auto-dim--refresh-vterm-after-auto-dim (&optional frame)
+ "Update auto-dim state, then refresh visible vterm buffers in FRAME."
+ (setq cj/auto-dim--vterm-refresh-timer nil)
+ (when (fboundp 'adob--update)
+ (adob--update))
+ (cj/auto-dim--refresh-vterm-windows frame))
+
+(defun cj/auto-dim--schedule-vterm-refresh (&optional frame)
+ "Schedule a deferred vterm refresh for FRAME.
+
+The delay lets selection-changing commands finish before we recompute
+auto-dim state and invalidate vterm."
+ (when cj/auto-dim--vterm-refresh-timer
+ (cancel-timer cj/auto-dim--vterm-refresh-timer))
+ (setq cj/auto-dim--vterm-refresh-timer
+ (run-with-timer 0 nil #'cj/auto-dim--refresh-vterm-after-auto-dim frame)))
+
+(defun cj/auto-dim--refresh-vterm-on-command ()
+ "Refresh visible vterm buffers when selected window changes.
+
+`window-selection-change-functions' does not catch every selection path used by
+windmove/Shift-arrow focus changes in this config, so this post-command hook is
+the fallback that makes vterm repaint after auto-dim changes window state."
+ (let ((window (selected-window)))
+ (unless (eq window cj/auto-dim--last-selected-window)
+ (setq cj/auto-dim--last-selected-window window)
+ (cj/auto-dim--schedule-vterm-refresh))))
+
+(defun cj/auto-dim--after-select-window (&rest _)
+ "Schedule vterm refresh after `select-window'."
+ (setq cj/auto-dim--last-selected-window (selected-window))
+ (cj/auto-dim--schedule-vterm-refresh))
+
(use-package auto-dim-other-buffers
:load-path "~/code/auto-dim-other-buffers.el"
:ensure nil
@@ -73,5 +244,14 @@
(dupre-org-priority-d . (dupre-org-priority-d-dim . nil))))
(auto-dim-other-buffers-mode 1))
+(with-eval-after-load 'vterm
+ (unless (advice-member-p #'cj/auto-dim--vterm-get-color #'vterm--get-color)
+ (advice-add #'vterm--get-color :around #'cj/auto-dim--vterm-get-color))
+ (unless (advice-member-p #'cj/auto-dim--after-select-window #'select-window)
+ (advice-add #'select-window :after #'cj/auto-dim--after-select-window))
+ (add-hook 'window-selection-change-functions
+ #'cj/auto-dim--schedule-vterm-refresh)
+ (add-hook 'post-command-hook #'cj/auto-dim--refresh-vterm-on-command))
+
(provide 'auto-dim-config)
;;; auto-dim-config.el ends here
diff --git a/tests/test-auto-dim-config.el b/tests/test-auto-dim-config.el
index 45e1db5f..f33a475a 100644
--- a/tests/test-auto-dim-config.el
+++ b/tests/test-auto-dim-config.el
@@ -10,6 +10,7 @@
;;; Code:
(require 'ert)
+(require 'cl-lib)
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
(defconst test-auto-dim--fork
@@ -29,5 +30,107 @@
(when (fboundp 'auto-dim-other-buffers-mode)
(auto-dim-other-buffers-mode -1))))
+(ert-deftest test-auto-dim-config-vterm-dimmed-p-all-windows-dimmed ()
+ "Normal: a vterm buffer is dimmed when all displayed windows are dimmed."
+ (skip-unless (file-directory-p test-auto-dim--fork))
+ (require 'auto-dim-config)
+ (let ((major-mode 'vterm-mode))
+ (cl-letf (((symbol-function 'get-buffer-window-list)
+ (lambda (&rest _) '(left right)))
+ ((symbol-function 'window-parameter)
+ (lambda (window parameter)
+ (and (eq parameter 'adob--dim)
+ (memq window '(left right))))))
+ (should (cj/auto-dim--vterm-buffer-dimmed-p)))))
+
+(ert-deftest test-auto-dim-config-vterm-dimmed-p-undimmed-window-keeps-buffer-bright ()
+ "Normal: a selected/undimmed vterm window keeps the buffer bright."
+ (skip-unless (file-directory-p test-auto-dim--fork))
+ (require 'auto-dim-config)
+ (let ((major-mode 'vterm-mode))
+ (cl-letf (((symbol-function 'get-buffer-window-list)
+ (lambda (&rest _) '(left right)))
+ ((symbol-function 'window-parameter)
+ (lambda (window parameter)
+ (and (eq parameter 'adob--dim)
+ (eq window 'right)))))
+ (should-not (cj/auto-dim--vterm-buffer-dimmed-p)))))
+
+(ert-deftest test-auto-dim-config-vterm-get-color-dims-only-dimmed-vterm-buffers ()
+ "Normal: vterm color advice dims only buffers marked dimmed."
+ (skip-unless (file-directory-p test-auto-dim--fork))
+ (require 'auto-dim-config)
+ (let ((major-mode 'vterm-mode)
+ (cj/auto-dim-vterm-foreground-blend 1.0))
+ (cl-letf (((symbol-function 'cj/auto-dim--vterm-buffer-dimmed-p)
+ (lambda () t))
+ ((symbol-function 'cj/auto-dim--face-color)
+ (lambda (&rest _) "#555555")))
+ (should (equal "#555555"
+ (cj/auto-dim--vterm-get-color
+ (lambda (&rest _) "#ffffff") 7 :foreground))))
+ (cl-letf (((symbol-function 'cj/auto-dim--vterm-buffer-dimmed-p)
+ (lambda () nil)))
+ (should (equal "#ffffff"
+ (cj/auto-dim--vterm-get-color
+ (lambda (&rest _) "#ffffff") 7 :foreground))))))
+
+(ert-deftest test-auto-dim-config-vterm-post-command-schedules-refresh-on-window-change ()
+ "Normal: post-command vterm refresh schedules only after selection changes."
+ (skip-unless (file-directory-p test-auto-dim--fork))
+ (require 'auto-dim-config)
+ (let ((cj/auto-dim--last-selected-window 'old)
+ (calls 0))
+ (cl-letf (((symbol-function 'selected-window)
+ (lambda () 'new))
+ ((symbol-function 'cj/auto-dim--schedule-vterm-refresh)
+ (lambda (&optional _) (setq calls (1+ calls)))))
+ (cj/auto-dim--refresh-vterm-on-command)
+ (cj/auto-dim--refresh-vterm-on-command))
+ (should (eq cj/auto-dim--last-selected-window 'new))
+ (should (= calls 1))))
+
+(ert-deftest test-auto-dim-config-vterm-refresh-runs-auto-dim-before-invalidate ()
+ "Normal: deferred vterm refresh updates auto-dim before invalidating vterm."
+ (skip-unless (file-directory-p test-auto-dim--fork))
+ (require 'auto-dim-config)
+ (let (events)
+ (cl-letf (((symbol-function 'adob--update)
+ (lambda () (push 'adob events)))
+ ((symbol-function 'cj/auto-dim--refresh-vterm-windows)
+ (lambda (&optional _) (push 'vterm events))))
+ (cj/auto-dim--refresh-vterm-after-auto-dim))
+ (should (equal events '(vterm adob)))))
+
+(ert-deftest test-auto-dim-config-vterm-refresh-nudges-size-for-full-redraw ()
+ "Normal: vterm refresh nudges size to force full-grid redraw."
+ (skip-unless (file-directory-p test-auto-dim--fork))
+ (require 'auto-dim-config)
+ (let ((calls nil)
+ (vterm-min-window-width 80))
+ (with-temp-buffer
+ (setq major-mode 'vterm-mode)
+ (setq-local vterm--term 'term)
+ (let ((buffer (current-buffer)))
+ (cl-letf (((symbol-function 'window-list)
+ (lambda (&rest _) '(vterm-window)))
+ ((symbol-function 'window-buffer)
+ (lambda (_) buffer))
+ ((symbol-function 'window-live-p)
+ (lambda (_) t))
+ ((symbol-function 'window-body-height)
+ (lambda (_) 24))
+ ((symbol-function 'window-body-width)
+ (lambda (_) 100))
+ ((symbol-function 'vterm--get-margin-width)
+ (lambda () 3))
+ ((symbol-function 'vterm--set-size)
+ (lambda (term height width)
+ (push (list term height width) calls))))
+ (cj/auto-dim--refresh-vterm-windows))))
+ (should (equal (nreverse calls)
+ '((term 25 97)
+ (term 24 97))))))
+
(provide 'test-auto-dim-config)
;;; test-auto-dim-config.el ends here