ui-config.el Mon, Jul 20, 2026

;;; ui-config.el --- UI configuration -*- lexical-binding: t -*-

;;; Commentary:
;; Visual and interface settings

;;; Code:

;; Custom cleanup function instead of midnight mode
;; run manually like (dws/cleanup-buffers 4)
(defcustom dws/cleanup-buffer-excludes
  '("\\*scratch\\*"
    "\\*Messages\\*"
    "\\.org$"
    "TAGS$"
    "\\*compilation\\*"
    "\\*grep\\*"
    "\\*Backtrace\\*")
  "Regexps of buffer names to exclude from cleanup."
  :type '(repeat regexp)
  :group 'convenience)

(defun dws/cleanup-buffers (hours)
  "Kill stale file buffers not visible and unmodified, older than HOURS.
 Runs on a wall-clock timer so active Emacs use does not block cleanup."
  (interactive "nKill buffers not seen in the last N hours: ")
  (let* ((now (float-time))
         (age-threshold (* hours 3600))
         (killed '()))
    (dolist (buf (buffer-list))
      (with-current-buffer buf
        (when (and (buffer-file-name buf)              ; file buffer only
                   (not (string-prefix-p " " (buffer-name buf))) ; not internal
                   (not (get-buffer-window buf t))     ; not currently visible
                   (not (buffer-modified-p buf))       ; not dirty
                   (not (seq-some (lambda (re) (string-match-p re (buffer-name buf)))
                                  dws/cleanup-buffer-excludes))
                   ;; Age check: use display time if available, else file mod time
                   (let ((last-seen (or (and buffer-display-time
                                            (float-time buffer-display-time))
                                        (float-time
                                         (nth 5 (file-attributes (buffer-file-name buf)))))))
                     (> (- now last-seen) age-threshold)))
          (push (buffer-name buf) killed)
          (kill-buffer buf))))
    (if killed
        (message "[buffer-cleanup %s] Killed %d buffer%s (>%dh): %s"
                 (format-time-string "%H:%M")
                 (length killed)
                 (if (= 1 (length killed)) "" "s")
                 hours
                 (string-join (reverse killed) ", "))
      (message "[buffer-cleanup %s] No stale buffers older than %d hours."
               (format-time-string "%H:%M") hours))))

(defun dws/cleanup-buffers-report (hours)
  "Show why each file buffer would or would not be killed after HOURS.
Useful for diagnosing why buffers survive cleanup. Displays in a buffer."
  (interactive "nReport on buffers not seen in the last N hours: ")
  (let* ((now (float-time))
         (age-threshold (* hours 3600))
         (report-buf (get-buffer-create "*Buffer Cleanup Report*"))
         (rows '()))
    (dolist (buf (buffer-list))
      (with-current-buffer buf
        (when (buffer-file-name buf)
          (let* ((name (buffer-name buf))
                 (visible    (get-buffer-window buf t))
                 (modified   (buffer-modified-p buf))
                 (excluded   (seq-some (lambda (re) (string-match-p re name))
                                       dws/cleanup-buffer-excludes))
                 (last-seen  (or (and buffer-display-time
                                     (float-time buffer-display-time))
                                 (float-time
                                  (nth 5 (file-attributes (buffer-file-name buf))))))
                 (age-hrs    (/ (- now last-seen) 3600.0))
                 (old-enough (> (- now last-seen) age-threshold))
                 (would-kill (and (not visible) (not modified)
                                  (not excluded) old-enough))
                 (reason (cond
                          (visible    "visible in window")
                          (modified   "modified (unsaved)")
                          (excluded   "excluded by pattern")
                          ((not old-enough)
                           (format "too recent (%.1fh ago)" age-hrs))
                          (t          "eligible"))))
            (push (list would-kill name age-hrs
                        (if buffer-display-time "display-time" "file-mtime")
                        reason)
                  rows)))))
    (with-current-buffer report-buf
      (let ((inhibit-read-only t))
        (erase-buffer)
        (insert (format "Buffer Cleanup Report — threshold: %dh — %s\n"
                        hours (format-time-string "%Y-%m-%d %H:%M")))
        (insert (make-string 70 ?-) "\n")
        (insert (format "%-40s %8s %-12s %s\n" "Buffer" "Age(h)" "Time-basis" "Status"))
        (insert (make-string 70 ?-) "\n")
        (dolist (row (sort rows (lambda (a b) (> (nth 2 a) (nth 2 b)))))
          (cl-destructuring-bind (kill name age basis reason) row
            (insert (format "%-40s %8.1f %-12s %s%s\n"
                            (truncate-string-to-width name 40)
                            age basis
                            (if kill "WOULD KILL" "keep")
                            (if kill "" (concat " — " reason))))))
        (insert (make-string 70 ?-) "\n")))
    (display-buffer report-buf)))

;; Run on a wall-clock schedule every 2 hours so active use doesn't block cleanup
(run-at-time (* 2 60 60) (* 2 60 60) (lambda () (dws/cleanup-buffers 5)))

;; Modeline cleanup - simplify while keeping line/column numbers
(setq-default mode-line-mule-info nil)          ; Hides "U" (UTF-8)
(setq-default mode-line-client nil)             ; Hides "@" for emacsclient
(setq-default mode-line-remote nil)             ; Hides "-" for remote
(setq-default mode-line-frame-identification nil) ; Hides " " or "F1"
(setq-default mode-line-percent-position nil)    ; Hides "Bot", "Top", "45%"
(setq vc-handled-backends nil)                  ; Disable VC (hides "Git:..." and speeds up startup)

;; Window system specific configuration
(when window-system
  (server-start)
  (menu-bar-mode 1)
  (bind-key "C-x C-c" 'kill-buffer)
  (bind-key "H-n" 'make-frame)
  (bind-key "H-w" 'delete-frame)
  (bind-key "H-o" 'other-frame)
  (bind-key "H-m" 'lower-frame)
  (bind-key "H-h" 'lower-frame))

;; Load nord theme properly when running as a daemon
(use-package nord-theme
  :ensure t
  :demand t ; Ensure the package code is available in the daemon
  :init
  (defun my-nord-theme-setup (frame)
    "Load Nord theme only when a frame is created."
    ;; with-selected-frame ensures load-theme runs in the new client/frame context.
    (with-selected-frame frame
      ;; Prevent the theme from loading multiple times across frames.
      (unless (member 'nord custom-enabled-themes)
        (load-theme 'nord t))))

  ;; Add the function to the hook that runs when a frame is created
  (add-hook 'after-make-frame-functions #'my-nord-theme-setup))

;; for the function/class hints
(which-function-mode 1)

(use-package vertico
  :ensure t
  :init
  (vertico-mode)
  :config
  (setq vertico-cycle t
        vertico-count 12))

(use-package orderless
  :ensure t
  :defer 10
  :custom
  (completion-styles '(orderless basic))
  (completion-category-overrides '((file (styles basic partial-completion)))))

(use-package marginalia
  :ensure t
  :defer 10
  :init
  (marginalia-mode))

(use-package consult
:ensure t
:defer 1
:bind (;; Modified from your previous ivy bindings
	   ("C-s" . consult-line)  ; replaces swiper
	   ("M-y" . consult-yank-pop)
	   ("M-b" . consult-buffer)))

;; Mac-specific configuration
(when (eq system-type 'darwin)
  (setq mac-option-key-is-meta t
        mac-command-modifier 'hyper
        mac-option-modifier 'meta)
  
  ;; Clipboard integration
  (defun isolate-kill-ring()
    "Isolate Emacs kill ring from OS X system pasteboard."
    (interactive)
    (setq interprogram-cut-function nil)
    (setq interprogram-paste-function nil))

  (defun pasteboard-copy()
    "Copy region to OS X system pasteboard."
    (interactive)
    (shell-command-on-region
     (region-beginning) (region-end) "pbcopy"))

  (defun pasteboard-paste()
    "Paste from OS X system pasteboard via `pbpaste' to point."
    (interactive)
    (shell-command-on-region
     (point) (if mark-active (mark) (point)) "pbpaste" nil t))

  (defun pasteboard-cut()
    "Cut region and put on OS X system pasteboard."
    (interactive)
    (pasteboard-copy)
    (delete-region (region-beginning) (region-end)))

  (isolate-kill-ring)
  (bind-key "H-c" 'pasteboard-copy)
  (bind-key "H-v" 'pasteboard-paste)
  (bind-key "H-x" 'pasteboard-cut))

;; Linux-specific configuration
(when (eq system-type 'linux)
  (setq x-alt-keysym 'meta))

;; Which-key configuration
(use-package which-key
  :ensure t
  :diminish which-key-mode
  :defer 10
  :config
  (which-key-mode))

;; Global diminishes for built-in or already-loaded modes
(with-eval-after-load 'eldoc
  (diminish 'eldoc-mode))
(with-eval-after-load 'autorevert
  (diminish 'auto-revert-mode))

(use-package tramp
  :custom
  (tramp-default-method "ssh")
  :config
  (add-to-list 'tramp-remote-path "~/bin")
  (add-to-list 'tramp-remote-path "~/python/bin")
  (add-to-list 'tramp-remote-path "/usr/local/go/bin/")
  (add-to-list 'tramp-remote-path 'tramp-own-remote-path))

;; Window management functions
(defun dws/rotate-windows ()
  "Rotate your windows."
  (interactive)
  (cond ((not (> (count-windows)1))
         (message "You can't rotate a single window!"))
        (t
         (setq i 1)
         (let ((numWindows (count-windows)))
           (while  (< i numWindows)
             (let* ((w1 (elt (window-list) i))
                    (w2 (elt (window-list) (+ (% i numWindows) 1)))
                    (b1 (window-buffer w1))
                    (b2 (window-buffer w2))
                    (s1 (window-start w1))
                    (s2 (window-start w2)))
               (set-window-buffer w1  b2)
               (set-window-buffer w2 b1)
               (set-window-start w1 s2)
               (set-window-start w2 s1)
               (setq i (1+ i))))))))

(provide 'ui-config)
;;; ui-config.el ends here