diff options
Diffstat (limited to 'lisp/my-header-line.el')
| -rw-r--r-- | lisp/my-header-line.el | 281 |
1 files changed, 281 insertions, 0 deletions
diff --git a/lisp/my-header-line.el b/lisp/my-header-line.el new file mode 100644 index 0000000..0b0bc75 --- /dev/null +++ b/lisp/my-header-line.el @@ -0,0 +1,281 @@ +;;; my-header-line.el --- Custom header line built with mode-line-maker -*- lexical-binding: t; -*- + +(require 'mode-line-maker) + +(defface my/header-line-evil-normal + '((t (:inherit font-lock-keyword-face :weight bold))) + "Face for the evil normal state indicator.") + +(defface my/header-line-evil-insert + '((t (:inherit success :weight bold))) + "Face for the evil insert state indicator.") + +(defface my/header-line-evil-visual + '((t (:inherit warning :weight bold))) + "Face for the evil visual state indicator.") + +(defface my/header-line-evil-replace + '((t (:inherit error :weight bold))) + "Face for the evil replace state indicator.") + +(defface my/header-line-evil-motion + '((t (:inherit font-lock-constant-face :weight bold))) + "Face for the evil motion state indicator.") + +(defface my/header-line-evil-emacs + '((t (:inherit font-lock-builtin-face :weight bold))) + "Face for the evil Emacs state indicator.") + +(defface my/header-line-buffer-icon-face + '((t (:inherit mode-line-buffer-id))) + "Face for the current buffer's file or mode icon.") + +(defface my/header-line-irc-face + '((t (:inherit font-lock-builtin-face :weight bold))) + "Face for the IRC activity indicator.") + +(defface my/header-line-clatter-face + '((t (:inherit font-lock-builtin-face :weight bold))) + "Face for the Clatter joined-channel indicator.") + +(defface my/header-line-mu4e-face + '((t (:inherit warning :weight bold))) + "Face for the unread mail indicator.") + +(defface my/header-line-time-face + '((t (:inherit font-lock-constant-face))) + "Face for the time segment.") + +(defface my/header-line-state-saved-face + '((t (:inherit success))) + "Face for the saved buffer state icon.") + +(defface my/header-line-state-modified-face + '((t (:inherit warning :weight bold))) + "Face for the modified buffer state icon.") + +(defface my/header-line-state-read-only-face + '((t (:inherit error))) + "Face for the read-only buffer state icon.") + +(defvar-local my/header-line--buffer-title nil) + +(defconst my/header-line--missing (make-symbol "missing") + "Sentinel used when checking membership in package hash tables.") + +(defvar my/header-line--format nil + "The installed global header-line format.") + +(defun my/header-line--update-title-cache () + "Update the cached #+title for the current buffer." + (setq-local my/header-line--buffer-title + (and (derived-mode-p 'org-mode) + (cadar (org-collect-keywords '("TITLE")))))) + +(defun my/header-line--buffer-title () + "Return the cached #+title, computing it if necessary." + (unless (local-variable-p 'my/header-line--buffer-title) + (my/header-line--update-title-cache)) + my/header-line--buffer-title) + +(defun my/header-line--invalidate-title-cache () + "Invalidate the cached title so it gets recomputed on next update." + (kill-local-variable 'my/header-line--buffer-title)) + +(defun my/header-line-buffer-name () + "Buffer name for the header line. +Uses the Org #+title if present, otherwise the file path relative to +the project root, falling back to the plain buffer name." + (or (my/header-line--buffer-title) + (if-let* ((file buffer-file-name) + (project (project-current)) + (root (project-root project))) + (file-relative-name file root) + (buffer-name)))) + +(defun my/header-line--nerd-icons-p () + "Return non-nil when Nerd Icons are enabled and available." + (and (bound-and-true-p ek-use-nerd-fonts) + (require 'nerd-icons nil t))) + +(defun my/header-line--icon (name face &optional fallback) + "Render Nerd icon NAME with FACE, or FALLBACK when unavailable." + (if (my/header-line--nerd-icons-p) + (propertize (nerd-icons-mdicon name) 'face face) + (propertize (or fallback "") 'face face))) + +(defun my/header-line-buffer-icon () + "Nerd icon for the current buffer, or nil." + (when (my/header-line--nerd-icons-p) + (or (and buffer-file-name + (ignore-errors + (nerd-icons-icon-for-file + buffer-file-name :face 'my/header-line-buffer-icon-face))) + (ignore-errors + (nerd-icons-icon-for-mode + major-mode :face 'my/header-line-buffer-icon-face))))) + +(defconst my/header-line--evil-states + '((normal "N" my/header-line-evil-normal) + (insert "I" my/header-line-evil-insert) + (visual "V" my/header-line-evil-visual) + (replace "R" my/header-line-evil-replace) + (motion "M" my/header-line-evil-motion) + (emacs "E" my/header-line-evil-emacs)) + "Display labels and theme-derived faces for Evil states.") + +(defun my/header-line-evil-state () + "Compact, theme-aware indicator for the current Evil state." + (when (bound-and-true-p evil-state) + (let* ((entry (assq evil-state my/header-line--evil-states)) + (tag (or (nth 1 entry) + (upcase (substring (symbol-name evil-state) 0 1)))) + (face (or (nth 2 entry) 'my/header-line-evil-normal))) + (propertize (format " %s " tag) 'face face)))) + +(defun my/header-line-buffer-state () + "Render a clean lock, edit, or saved indicator for the current buffer." + (cond + (buffer-read-only + (my/header-line--icon "nf-md-lock" + 'my/header-line-state-read-only-face "RO")) + ((buffer-modified-p) + (my/header-line--icon "nf-md-pencil" + 'my/header-line-state-modified-face "*")) + (t + (my/header-line--icon "nf-md-check" + 'my/header-line-state-saved-face "=")))) + +(defun my/header-line-remote () + "Show a remote indicator only for an actual remote directory. +Unlike the built-in `mode-line-remote', local buffers render nothing instead +of a literal dash." + (when (file-remote-p default-directory) + (my/header-line--icon "nf-md-cloud_outline" + 'font-lock-constant-face "@"))) + +(defun my/header-line-irc () + "IRC activity indicator: icon plus number of buffers with unread activity." + (when (bound-and-true-p erc-modified-channels-alist) + (let ((icon (my/header-line--icon "nf-md-forum_outline" + 'my/header-line-irc-face "irc"))) + (propertize (format "%s %d" icon + (length erc-modified-channels-alist)) + 'face 'my/header-line-irc-face)))) + +(defun my/header-line-clatter () + "Clatter indicator showing the number of joined channels." + (when (featurep 'clatter) + (let ((count 0)) + (dolist (buffer (clatter-all-buffers)) + (when (with-current-buffer buffer + (and (eq clatter--buffer-type 'channel) + (hash-table-p clatter--nick-list) + (when-let* ((connection + (clatter-get-connection clatter--network)) + ((eq (clatter-connection-state connection) + :connected)) + (nick (clatter-connection-nick connection))) + (not (eq (gethash (downcase nick) clatter--nick-list + my/header-line--missing) + my/header-line--missing))))) + (setq count (1+ count)))) + (when (> count 0) + (let ((icon (my/header-line--icon "nf-md-forum_outline" + 'my/header-line-clatter-face "irc"))) + (propertize (format "%s %d" icon count) + 'face 'my/header-line-clatter-face)))))) + +(defun my/header-line-mu4e-formatter (mail-count) + "Format MAIL-COUNT for the custom header line without an extra icon." + (unless (zerop mail-count) + (number-to-string mail-count))) + +(defun my/header-line-mu4e () + "Unread mail indicator from mu4e-alert, or nil when mu4e is not in use." + (when (bound-and-true-p mu4e-alert-mode-line) + (let ((icon (my/header-line--icon "nf-md-email_outline" + 'my/header-line-mu4e-face "mail"))) + (propertize (format "%s %s" icon + (string-trim mu4e-alert-mode-line)) + 'face 'my/header-line-mu4e-face)))) + +(defvar my/header-line--clock-icons + ["nf-md-clock_time_twelve" "nf-md-clock_time_one" "nf-md-clock_time_two" + "nf-md-clock_time_three" "nf-md-clock_time_four" "nf-md-clock_time_five" + "nf-md-clock_time_six" "nf-md-clock_time_seven" "nf-md-clock_time_eight" + "nf-md-clock_time_nine" "nf-md-clock_time_ten" "nf-md-clock_time_eleven"] + "Nerd icon clock faces indexed by hour modulo 12.") + +(defun my/header-line-time () + "Time with a live clock icon showing the current hour." + (when (bound-and-true-p display-time-string) + (let* ((hour (% (string-to-number (format-time-string "%I")) 12)) + (icon (and (my/header-line--nerd-icons-p) + (nerd-icons-mdicon (aref my/header-line--clock-icons hour)))) + (time (string-trim display-time-string))) + (propertize (if icon (concat icon " " time) time) + 'face 'my/header-line-time-face)))) + +(defun my/header-line-sync-theme (&optional _theme) + "Make helper padding inherit the live header-line theme face. +Optional THEME is ignored so this function also fits +`enable-theme-functions'." + (set-face-attribute 'mode-line-maker-padding-face nil + :inherit 'header-line + :foreground 'unspecified + :background 'unspecified + :box nil + :overline nil + :underline nil + :inverse-video nil + :strike-through nil)) + +(define-minor-mode my/global-header-line-mode + "Show the custom header line globally. + +When disabled, local informational headers, such as Elfeed's column +headings, remain visible." + :global t + :init-value t + :group 'mode-line + (setq-default header-line-format + (and my/global-header-line-mode my/header-line--format)) + (force-mode-line-update t)) + +(defun my/toggle-header-line () + "Toggle the custom header line globally." + (interactive) + (my/global-header-line-mode (if my/global-header-line-mode -1 1))) + +(defun my/header-line-install () + "Install the custom header line built with mode-line-maker." + (setq global-mode-string (delq 'display-time-string global-mode-string)) + (my/header-line-sync-theme) + (add-hook 'enable-theme-functions #'my/header-line-sync-theme) + (setq my/header-line--format + (mode-line-maker + '((:eval (my/header-line-evil-state)) " " + (:eval (my/header-line-buffer-state)) + (:eval (my/header-line-remote)) " " + (:eval (my/header-line-buffer-icon)) " " + (:eval (propertize (my/header-line-buffer-name) + 'face 'mode-line-buffer-id + 'mouse-face 'mode-line-highlight + 'help-echo (concat (or buffer-file-truename (buffer-name)) + "\nmouse-1: Previous buffer\nmouse-3: Next buffer") + 'local-map mode-line-buffer-identification-keymap)) " " + mode-line-position) + '("" (:eval (my/header-line-irc)) " " + (:eval (my/header-line-clatter)) " " + (:eval (my/header-line-mu4e)) " " + mode-line-process " " + (:eval (my/header-line-time)) " " + mode-name " "))) + ;; The header line is visible on every startup, regardless of its previous + ;; interactive state in a long-running session. + (my/global-header-line-mode 1) + (add-hook 'before-save-hook #'my/header-line--invalidate-title-cache)) + +(provide 'my-header-line) +;;; my-header-line.el ends here |
