diff options
| -rw-r--r-- | modules/auto-dim-config.el | 180 | ||||
| -rw-r--r-- | tests/test-auto-dim-config.el | 103 |
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 |
