;;; 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