Created
June 3, 2026 21:17
-
-
Save wroyca/a4eac82f84a2a40a85577326a4e7277b to your computer and use it in GitHub Desktop.
Minimal custom mode line
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ;;; dotemacs-modeline.el --- Minimal custom mode line -*- lexical-binding: t -*- | |
| ;;; Commentary: | |
| ;; | |
| ;; This library installs a small mode line built on Emacs' standard | |
| ;; `mode-line-format' machinery. It intentionally avoids external rendering | |
| ;; packages. Consult and Magit are used when available, but the mode line | |
| ;; remains usable with built-in libraries only. | |
| ;; | |
| ;; The installed format is: | |
| ;; | |
| ;; left: project, Git branch | |
| ;; right: line/column, indentation, encoding, end-of-line convention, | |
| ;; major mode, notification indicator | |
| ;; | |
| ;; Segments are clickable with mouse-1 when | |
| ;; `dotemacs-modeline-enable-mouse' is non-nil. | |
| ;;; Code: | |
| (require 'cl-lib) | |
| (require 'project) | |
| (require 'seq) | |
| (require 'subr-x) | |
| (defgroup dotemacs-modeline nil | |
| "Minimal custom mode line." | |
| :group 'dotemacs | |
| :prefix "dotemacs-modeline-") | |
| (defcustom dotemacs-modeline-enabled t | |
| "Non-nil means `dotemacs/modeline-setup' enables the custom mode line." | |
| :type 'boolean | |
| :safe #'booleanp | |
| :group 'dotemacs-modeline) | |
| (defcustom dotemacs-modeline-remove-borders t | |
| "Non-nil means remove box, underline, and overline mode-line attributes." | |
| :type 'boolean | |
| :safe #'booleanp | |
| :group 'dotemacs-modeline) | |
| (defcustom dotemacs-modeline-enable-after-theme-load t | |
| "Non-nil means reapply mode-line face attributes after enabling a theme." | |
| :type 'boolean | |
| :safe #'booleanp | |
| :group 'dotemacs-modeline) | |
| (defcustom dotemacs-modeline-enable-mouse t | |
| "Non-nil means make mode-line segments clickable." | |
| :type 'boolean | |
| :safe #'booleanp | |
| :group 'dotemacs-modeline) | |
| (defcustom dotemacs-modeline-mouse-face 'mode-line-highlight | |
| "Face used for a clickable mode-line segment under the mouse pointer." | |
| :type 'face | |
| :group 'dotemacs-modeline) | |
| (defcustom dotemacs-modeline-separator " " | |
| "String inserted between adjacent mode-line segments." | |
| :type 'string | |
| :safe #'stringp | |
| :group 'dotemacs-modeline) | |
| (defcustom dotemacs-modeline-project-icon "" | |
| "String displayed before the project name. | |
| Set this option to an empty string to hide the project icon." | |
| :type 'string | |
| :safe #'stringp | |
| :group 'dotemacs-modeline) | |
| (defcustom dotemacs-modeline-vcs-icon "" | |
| "String displayed before the version-control branch. | |
| Set this option to an empty string to hide the version-control icon." | |
| :type 'string | |
| :safe #'stringp | |
| :group 'dotemacs-modeline) | |
| (defcustom dotemacs-modeline-notification-string "" | |
| "String displayed in the notification segment. | |
| Set this option to an empty string to hide the notification segment." | |
| :type 'string | |
| :safe #'stringp | |
| :group 'dotemacs-modeline) | |
| (defcustom dotemacs-modeline-project-fallback "emacs" | |
| "Project name displayed when the current buffer is not in a project." | |
| :type 'string | |
| :safe #'stringp | |
| :group 'dotemacs-modeline) | |
| (defcustom dotemacs-modeline-indentation-choices | |
| '("Spaces: 2" | |
| "Spaces: 4" | |
| "Spaces: 8" | |
| "Tabs: 2" | |
| "Tabs: 4" | |
| "Tabs: 8") | |
| "Indentation choices offered by `dotemacs/modeline-indentation'." | |
| :type '(repeat string) | |
| :group 'dotemacs-modeline) | |
| (defcustom dotemacs-modeline-language-modes | |
| '(c-mode | |
| c++-mode | |
| c-ts-mode | |
| c++-ts-mode | |
| emacs-lisp-mode | |
| lisp-mode | |
| scheme-mode | |
| python-mode | |
| python-ts-mode | |
| rust-mode | |
| rust-ts-mode | |
| go-mode | |
| go-ts-mode | |
| js-mode | |
| js-ts-mode | |
| typescript-mode | |
| typescript-ts-mode | |
| tsx-ts-mode | |
| lua-mode | |
| lua-ts-mode | |
| sh-mode | |
| bash-ts-mode | |
| cmake-mode | |
| makefile-gmake-mode | |
| conf-mode | |
| yaml-mode | |
| yaml-ts-mode | |
| json-mode | |
| json-ts-mode | |
| markdown-mode | |
| org-mode | |
| text-mode) | |
| "Major modes offered by `dotemacs/modeline-language'." | |
| :type '(repeat symbol) | |
| :group 'dotemacs-modeline) | |
| (defcustom dotemacs-modeline-git-query-remote-branches t | |
| "Non-nil means query Git remotes when reading branch candidates. | |
| The branch reader always includes local branches and fetched | |
| remote-tracking branches. When this option is non-nil, it also runs | |
| `git ls-remote --heads --refs' for each configured remote." | |
| :type 'boolean | |
| :safe #'booleanp | |
| :group 'dotemacs-modeline) | |
| (defcustom dotemacs-modeline-show-project t | |
| "Non-nil means display the project segment." | |
| :type 'boolean | |
| :safe #'booleanp | |
| :group 'dotemacs-modeline) | |
| (defcustom dotemacs-modeline-show-vcs t | |
| "Non-nil means display the version-control branch segment." | |
| :type 'boolean | |
| :safe #'booleanp | |
| :group 'dotemacs-modeline) | |
| (defcustom dotemacs-modeline-show-position t | |
| "Non-nil means display the line and column segment." | |
| :type 'boolean | |
| :safe #'booleanp | |
| :group 'dotemacs-modeline) | |
| (defcustom dotemacs-modeline-show-indentation t | |
| "Non-nil means display the indentation segment." | |
| :type 'boolean | |
| :safe #'booleanp | |
| :group 'dotemacs-modeline) | |
| (defcustom dotemacs-modeline-show-encoding t | |
| "Non-nil means display the buffer encoding segment." | |
| :type 'boolean | |
| :safe #'booleanp | |
| :group 'dotemacs-modeline) | |
| (defcustom dotemacs-modeline-show-eol t | |
| "Non-nil means display the end-of-line convention segment." | |
| :type 'boolean | |
| :safe #'booleanp | |
| :group 'dotemacs-modeline) | |
| (defcustom dotemacs-modeline-show-language t | |
| "Non-nil means display the major mode segment." | |
| :type 'boolean | |
| :safe #'booleanp | |
| :group 'dotemacs-modeline) | |
| (defcustom dotemacs-modeline-show-notification t | |
| "Non-nil means display the notification segment." | |
| :type 'boolean | |
| :safe #'booleanp | |
| :group 'dotemacs-modeline) | |
| (defface dotemacs-modeline | |
| '((t :inherit mode-line)) | |
| "Face used for active mode-line segments." | |
| :group 'dotemacs-modeline) | |
| (defface dotemacs-modeline-inactive | |
| '((t :inherit mode-line-inactive)) | |
| "Face used for inactive mode-line segments." | |
| :group 'dotemacs-modeline) | |
| (defface dotemacs-modeline-project | |
| '((t :inherit dotemacs-modeline)) | |
| "Face used for the project segment." | |
| :group 'dotemacs-modeline) | |
| (defface dotemacs-modeline-vcs | |
| '((t :inherit dotemacs-modeline)) | |
| "Face used for the version-control segment." | |
| :group 'dotemacs-modeline) | |
| (defface dotemacs-modeline-status | |
| '((t :inherit dotemacs-modeline)) | |
| "Face used for right-aligned status segments." | |
| :group 'dotemacs-modeline) | |
| (defconst dotemacs-modeline--saved-format-unset | |
| (make-symbol "dotemacs-modeline--saved-format-unset") | |
| "Sentinel used when no previous mode-line format has been saved.") | |
| (defconst dotemacs-modeline--indentation-variables | |
| '(c-basic-offset | |
| c-ts-mode-indent-offset | |
| c++-ts-mode-indent-offset | |
| js-indent-level | |
| js-jsx-indent-level | |
| typescript-indent-level | |
| typescript-ts-mode-indent-offset | |
| python-indent-offset | |
| rust-mode-indent-offset | |
| go-ts-mode-indent-offset | |
| lua-indent-level) | |
| "Buffer-local variables consulted for indentation width.") | |
| (defconst dotemacs-modeline--faces | |
| '(mode-line | |
| mode-line-active | |
| mode-line-inactive | |
| mode-line-highlight | |
| dotemacs-modeline | |
| dotemacs-modeline-inactive | |
| dotemacs-modeline-project | |
| dotemacs-modeline-vcs | |
| dotemacs-modeline-status) | |
| "Faces controlled by the mode-line face policy.") | |
| (defconst dotemacs-modeline--git-environment | |
| '("GIT_OPTIONAL_LOCKS=0" | |
| "GIT_TERMINAL_PROMPT=0") | |
| "Environment bindings used for synchronous Git commands.") | |
| (defvar dotemacs-modeline--saved-mode-line-format | |
| dotemacs-modeline--saved-format-unset | |
| "Default value of `mode-line-format' saved before enabling the mode line.") | |
| (defvar dotemacs-modeline--mouse-map-cache | |
| (make-hash-table :test #'eq) | |
| "Mouse maps used by clickable mode-line segments.") | |
| (defvar mode-line-format-right-align) | |
| (defvar mode-line-window) | |
| (defvar vc-mode) | |
| (defvar enable-theme-functions) | |
| (declare-function consult--read "consult") | |
| (declare-function consult-goto-line "consult") | |
| (declare-function magit-status "magit") | |
| (declare-function mode-line-window-selected-p "bindings") | |
| (declare-function vc-dir "vc-dir") | |
| (declare-function vc-refresh-state "vc") | |
| (declare-function view-echo-area-messages "simple") | |
| (defun dotemacs//modeline-nonempty-string-p (object) | |
| "Return non-nil when OBJECT is a non-empty string." | |
| (and (stringp object) | |
| (not (string-empty-p object)))) | |
| (defun dotemacs//modeline-window-active-p () | |
| "Return non-nil when the rendered mode line belongs to the selected window." | |
| (if (fboundp 'mode-line-window-selected-p) | |
| (mode-line-window-selected-p) | |
| (and (boundp 'mode-line-window) | |
| (eq mode-line-window (selected-window))))) | |
| (defun dotemacs//modeline-face (&optional face) | |
| "Return FACE for an active window, or the inactive mode-line face." | |
| (if (dotemacs//modeline-window-active-p) | |
| (or face 'dotemacs-modeline) | |
| 'dotemacs-modeline-inactive)) | |
| (defun dotemacs//modeline-mouse-map (command) | |
| "Return a mode-line mouse map that invokes COMMAND." | |
| (or (gethash command dotemacs-modeline--mouse-map-cache) | |
| (puthash command | |
| (make-mode-line-mouse-map 'mouse-1 command) | |
| dotemacs-modeline--mouse-map-cache))) | |
| (defun dotemacs//modeline-propertize (string &optional face command help) | |
| "Return STRING propertized for display in the mode line. | |
| FACE is used when the mode line belongs to the selected window. COMMAND | |
| is installed as the mouse-1 action when mouse support is enabled. HELP is | |
| used as the `help-echo' text for clickable segments." | |
| (let ((properties (list 'face (dotemacs//modeline-face face)))) | |
| (when (and dotemacs-modeline-enable-mouse command) | |
| (setq properties | |
| (append properties | |
| (list 'mouse-face dotemacs-modeline-mouse-face | |
| 'pointer 'hand | |
| 'local-map (dotemacs//modeline-mouse-map command))))) | |
| (when help | |
| (setq properties (append properties (list 'help-echo help)))) | |
| (apply #'propertize string properties))) | |
| (defun dotemacs//modeline-segment (string &optional face command help) | |
| "Return STRING as a mode-line segment. | |
| Return nil when STRING is nil or empty. FACE, COMMAND, and HELP are | |
| passed to `dotemacs//modeline-propertize'." | |
| (when (dotemacs//modeline-nonempty-string-p string) | |
| (dotemacs//modeline-propertize string face command help))) | |
| (defun dotemacs//modeline-prefixed-segment (prefix string &optional face command help) | |
| "Return a mode-line segment containing PREFIX and STRING. | |
| PREFIX is omitted when it is nil or empty. FACE, COMMAND, and HELP are | |
| passed to `dotemacs//modeline-segment'." | |
| (cond | |
| ((not (dotemacs//modeline-nonempty-string-p string)) | |
| nil) | |
| ((dotemacs//modeline-nonempty-string-p prefix) | |
| (dotemacs//modeline-segment | |
| (concat prefix " " string) | |
| face | |
| command | |
| help)) | |
| (t | |
| (dotemacs//modeline-segment string face command help)))) | |
| (defun dotemacs//modeline-join (segments) | |
| "Return SEGMENTS joined with `dotemacs-modeline-separator'." | |
| (string-join | |
| (seq-filter #'dotemacs//modeline-nonempty-string-p segments) | |
| dotemacs-modeline-separator)) | |
| (defun dotemacs//modeline-select-event-window () | |
| "Select the window associated with the current mode-line event." | |
| (let* ((event last-nonmenu-event) | |
| (start (and (consp event) | |
| (ignore-errors (event-start event)))) | |
| (window (and start (posn-window start)))) | |
| (when (window-live-p window) | |
| (select-window window)))) | |
| (defun dotemacs//modeline-read (prompt candidates &optional default group-function) | |
| "Read a completion candidate from CANDIDATES with PROMPT. | |
| DEFAULT is the initial default value. GROUP-FUNCTION is passed to Consult | |
| when Consult is available. Without Consult, GROUP-FUNCTION is attached as | |
| completion metadata for `completing-read'." | |
| (if (and (require 'consult nil t) | |
| (fboundp 'consult--read)) | |
| (consult--read candidates | |
| :prompt prompt | |
| :require-match t | |
| :default default | |
| :group group-function) | |
| (let ((table (if group-function | |
| (completion-table-with-metadata | |
| candidates | |
| `(metadata (group-function . ,group-function))) | |
| candidates))) | |
| (completing-read prompt table nil t nil nil default)))) | |
| (defun dotemacs//modeline-project-root () | |
| "Return the current project root, or nil outside a project." | |
| (when-let* ((project (project-current nil))) | |
| (project-root project))) | |
| (defun dotemacs//modeline-project-name () | |
| "Return the project name for the current buffer." | |
| (if-let* ((root (dotemacs//modeline-project-root))) | |
| (file-name-nondirectory (directory-file-name root)) | |
| dotemacs-modeline-project-fallback)) | |
| (defun dotemacs//modeline-project-segment () | |
| "Return the project mode-line segment." | |
| (when dotemacs-modeline-show-project | |
| (dotemacs//modeline-prefixed-segment | |
| dotemacs-modeline-project-icon | |
| (dotemacs//modeline-project-name) | |
| 'dotemacs-modeline-project | |
| #'dotemacs/modeline-project | |
| "mouse-1: switch project"))) | |
| (defun dotemacs//modeline-vcs-branch () | |
| "Return the current version-control branch name, or nil." | |
| (when vc-mode | |
| (let* ((raw (string-trim | |
| (substring-no-properties (format-mode-line vc-mode)))) | |
| (branch (replace-regexp-in-string | |
| "\\`\\(?:Git\\|Hg\\|SVN\\|Bzr\\)[:-]?" | |
| "" | |
| raw))) | |
| (unless (string-empty-p branch) | |
| branch)))) | |
| (defun dotemacs//modeline-vcs-segment () | |
| "Return the version-control mode-line segment." | |
| (when dotemacs-modeline-show-vcs | |
| (dotemacs//modeline-prefixed-segment | |
| dotemacs-modeline-vcs-icon | |
| (dotemacs//modeline-vcs-branch) | |
| 'dotemacs-modeline-vcs | |
| #'dotemacs/modeline-branch | |
| "mouse-1: switch Git branch"))) | |
| (defun dotemacs//modeline-position-segment () | |
| "Return the line and column mode-line segment." | |
| (when dotemacs-modeline-show-position | |
| (dotemacs//modeline-segment | |
| (format "Ln %d, Col %d" | |
| (line-number-at-pos) | |
| (1+ (current-column))) | |
| 'dotemacs-modeline-status | |
| #'dotemacs/modeline-position | |
| "mouse-1: go to line"))) | |
| (defun dotemacs//modeline-first-integer-variable (variables) | |
| "Return the first integer value found in VARIABLES. | |
| Each element of VARIABLES is a symbol naming a variable." | |
| (catch 'value | |
| (dolist (variable variables) | |
| (when (and (boundp variable) | |
| (integerp (symbol-value variable))) | |
| (throw 'value (symbol-value variable)))) | |
| nil)) | |
| (defun dotemacs//modeline-indentation-width () | |
| "Return the indentation width for the current buffer." | |
| (or (dotemacs//modeline-first-integer-variable | |
| dotemacs-modeline--indentation-variables) | |
| tab-width)) | |
| (defun dotemacs//modeline-indentation-segment () | |
| "Return the indentation mode-line segment." | |
| (when dotemacs-modeline-show-indentation | |
| (dotemacs//modeline-segment | |
| (if indent-tabs-mode | |
| (format "Tabs: %d" tab-width) | |
| (format "Spaces: %d" (dotemacs//modeline-indentation-width))) | |
| 'dotemacs-modeline-status | |
| #'dotemacs/modeline-indentation | |
| "mouse-1: change indentation"))) | |
| (defun dotemacs//modeline-current-coding-system () | |
| "Return the current buffer file coding system." | |
| (or buffer-file-coding-system | |
| default-buffer-file-coding-system | |
| 'utf-8-unix)) | |
| (defun dotemacs//modeline-encoding-name () | |
| "Return the current buffer encoding name." | |
| (upcase | |
| (symbol-name | |
| (coding-system-base (dotemacs//modeline-current-coding-system))))) | |
| (defun dotemacs//modeline-encoding-segment () | |
| "Return the encoding mode-line segment." | |
| (when dotemacs-modeline-show-encoding | |
| (dotemacs//modeline-segment | |
| (dotemacs//modeline-encoding-name) | |
| 'dotemacs-modeline-status | |
| #'dotemacs/modeline-encoding | |
| "mouse-1: change buffer encoding"))) | |
| (defun dotemacs//modeline-eol-name () | |
| "Return the current buffer end-of-line convention." | |
| (pcase (coding-system-eol-type | |
| (dotemacs//modeline-current-coding-system)) | |
| (0 "LF") | |
| (1 "CRLF") | |
| (2 "CR") | |
| (_ ""))) | |
| (defun dotemacs//modeline-eol-segment () | |
| "Return the end-of-line mode-line segment." | |
| (when dotemacs-modeline-show-eol | |
| (dotemacs//modeline-segment | |
| (dotemacs//modeline-eol-name) | |
| 'dotemacs-modeline-status | |
| #'dotemacs/modeline-eol | |
| "mouse-1: change end-of-line convention"))) | |
| (defun dotemacs//modeline-language-name () | |
| "Return the current major mode display name." | |
| (string-trim | |
| (substring-no-properties (format-mode-line mode-name)))) | |
| (defun dotemacs//modeline-language-segment () | |
| "Return the major mode mode-line segment." | |
| (when dotemacs-modeline-show-language | |
| (dotemacs//modeline-segment | |
| (dotemacs//modeline-language-name) | |
| 'dotemacs-modeline-status | |
| #'dotemacs/modeline-language | |
| "mouse-1: change major mode"))) | |
| (defun dotemacs//modeline-notification-segment () | |
| "Return the notification mode-line segment." | |
| (when dotemacs-modeline-show-notification | |
| (dotemacs//modeline-segment | |
| dotemacs-modeline-notification-string | |
| 'dotemacs-modeline-status | |
| #'dotemacs/modeline-notification | |
| "mouse-1: show messages"))) | |
| (defun dotemacs//modeline-left () | |
| "Return the left side of the custom mode line." | |
| (dotemacs//modeline-join | |
| (list (dotemacs//modeline-project-segment) | |
| (dotemacs//modeline-vcs-segment)))) | |
| (defun dotemacs//modeline-right () | |
| "Return the right side of the custom mode line." | |
| (dotemacs//modeline-join | |
| (list (dotemacs//modeline-position-segment) | |
| (dotemacs//modeline-indentation-segment) | |
| (dotemacs//modeline-encoding-segment) | |
| (dotemacs//modeline-eol-segment) | |
| (dotemacs//modeline-language-segment) | |
| (dotemacs//modeline-notification-segment)))) | |
| (defconst dotemacs-modeline--format | |
| '("%e" | |
| " " | |
| (:eval (dotemacs//modeline-left)) | |
| mode-line-format-right-align | |
| (:eval (dotemacs//modeline-right)) | |
| " ") | |
| "Mode-line format installed by `dotemacs-modeline-mode'.") | |
| (defun dotemacs//modeline-set-face (face &rest attributes) | |
| "Apply ATTRIBUTES to FACE when FACE is defined." | |
| (when (facep face) | |
| (apply #'set-face-attribute face nil attributes))) | |
| (defun dotemacs//modeline-configure-faces (&rest _arguments) | |
| "Apply the configured mode-line face policy." | |
| (when dotemacs-modeline-remove-borders | |
| (dolist (face dotemacs-modeline--faces) | |
| (dotemacs//modeline-set-face | |
| face | |
| :box nil | |
| :underline nil | |
| :overline nil)))) | |
| (defun dotemacs//modeline-install-theme-hooks () | |
| "Install hooks used to reapply the mode-line face policy." | |
| (when dotemacs-modeline-enable-after-theme-load | |
| (add-hook 'enable-theme-functions | |
| #'dotemacs//modeline-configure-faces))) | |
| (defun dotemacs//modeline-remove-theme-hooks () | |
| "Remove hooks installed by `dotemacs//modeline-install-theme-hooks'." | |
| (remove-hook 'enable-theme-functions | |
| #'dotemacs//modeline-configure-faces)) | |
| (defun dotemacs//modeline-set-indentation-width (width) | |
| "Set common indentation variables to WIDTH in the current buffer." | |
| (setq-local tab-width width) | |
| (dolist (variable dotemacs-modeline--indentation-variables) | |
| (when (boundp variable) | |
| (set (make-local-variable variable) width)))) | |
| (defun dotemacs//modeline-set-eol-type (type) | |
| "Set the current buffer end-of-line convention to TYPE. | |
| TYPE is passed to `coding-system-change-eol-conversion'." | |
| (let* ((coding-system (dotemacs//modeline-current-coding-system)) | |
| (base (coding-system-base coding-system)) | |
| (coding-system (coding-system-change-eol-conversion base type))) | |
| (set-buffer-file-coding-system coding-system t) | |
| (force-mode-line-update t))) | |
| (defun dotemacs//modeline-git-program () | |
| "Return the Git executable name, or nil if Git is unavailable." | |
| (executable-find "git")) | |
| (defun dotemacs//modeline-git-lines (&rest arguments) | |
| "Run Git with ARGUMENTS and return output lines. | |
| Return nil if Git is unavailable or exits unsuccessfully." | |
| (when-let* ((program (dotemacs//modeline-git-program))) | |
| (with-temp-buffer | |
| (let ((process-environment | |
| (append dotemacs-modeline--git-environment process-environment))) | |
| (when (zerop (apply #'call-process program nil t nil arguments)) | |
| (split-string (buffer-string) "\n" t "[[:space:]\n]+")))))) | |
| (defun dotemacs//modeline-git-lines-in-root (root &rest arguments) | |
| "Run Git in ROOT with ARGUMENTS and return output lines." | |
| (apply #'dotemacs//modeline-git-lines "-C" root arguments)) | |
| (defun dotemacs//modeline-git-run (root &rest arguments) | |
| "Run Git in ROOT with ARGUMENTS. | |
| Signal `user-error' if Git is unavailable or exits unsuccessfully." | |
| (unless (dotemacs//modeline-git-program) | |
| (user-error "Git executable not found")) | |
| (with-temp-buffer | |
| (let* ((process-environment | |
| (append dotemacs-modeline--git-environment process-environment)) | |
| (status (apply #'call-process | |
| (dotemacs//modeline-git-program) | |
| nil | |
| t | |
| nil | |
| "-C" | |
| root | |
| arguments))) | |
| (unless (and (integerp status) | |
| (zerop status)) | |
| (user-error "%s" (string-trim (buffer-string))))))) | |
| (defun dotemacs//modeline-git-root () | |
| "Return the Git worktree root for the current buffer, or nil." | |
| (when-let* ((root (car (dotemacs//modeline-git-lines | |
| "-C" | |
| default-directory | |
| "rev-parse" | |
| "--show-toplevel")))) | |
| (file-name-as-directory root))) | |
| (defun dotemacs//modeline-git-current-branch (root) | |
| "Return the current Git branch in ROOT, or nil." | |
| (or (car (dotemacs//modeline-git-lines-in-root | |
| root | |
| "branch" | |
| "--show-current")) | |
| (car (dotemacs//modeline-git-lines-in-root | |
| root | |
| "rev-parse" | |
| "--short" | |
| "HEAD")))) | |
| (defun dotemacs//modeline-git-local-branches (root) | |
| "Return local Git branches in ROOT." | |
| (sort | |
| (or (dotemacs//modeline-git-lines-in-root | |
| root | |
| "for-each-ref" | |
| "--format=%(refname:short)" | |
| "refs/heads") | |
| nil) | |
| #'string-lessp)) | |
| (defun dotemacs//modeline-git-remotes (root) | |
| "Return Git remotes configured in ROOT." | |
| (or (dotemacs//modeline-git-lines-in-root root "remote") | |
| nil)) | |
| (defun dotemacs//modeline-git-remote-tracking-branches (root) | |
| "Return fetched remote-tracking Git branches in ROOT." | |
| (sort | |
| (seq-remove | |
| (lambda (branch) | |
| (or (string-empty-p branch) | |
| (string-match-p "/HEAD\\'" branch))) | |
| (or (dotemacs//modeline-git-lines-in-root | |
| root | |
| "for-each-ref" | |
| "--format=%(refname:short)" | |
| "refs/remotes") | |
| nil)) | |
| #'string-lessp)) | |
| (defun dotemacs//modeline-git-ls-remote-branches (root remote) | |
| "Return remote Git branch names from REMOTE in ROOT. | |
| Each returned branch has the form REMOTE/BRANCH." | |
| (delq nil | |
| (mapcar | |
| (lambda (line) | |
| (when (string-match | |
| "\\`[0-9a-fA-F]+[[:space:]]+refs/heads/\\(.+\\)\\'" | |
| line) | |
| (concat remote "/" (match-string 1 line)))) | |
| (dotemacs//modeline-git-lines-in-root | |
| root | |
| "ls-remote" | |
| "--heads" | |
| "--refs" | |
| remote)))) | |
| (defun dotemacs//modeline-git-server-branches (root) | |
| "Return remote Git branches queried from servers for ROOT." | |
| (when dotemacs-modeline-git-query-remote-branches | |
| (sort | |
| (delete-dups | |
| (apply #'append | |
| (mapcar | |
| (lambda (remote) | |
| (dotemacs//modeline-git-ls-remote-branches root remote)) | |
| (dotemacs//modeline-git-remotes root)))) | |
| #'string-lessp))) | |
| (defun dotemacs//modeline-git-remote-branches (root) | |
| "Return all known remote Git branches in ROOT." | |
| (sort | |
| (delete-dups | |
| (append (dotemacs//modeline-git-remote-tracking-branches root) | |
| (dotemacs//modeline-git-server-branches root))) | |
| #'string-lessp)) | |
| (defun dotemacs//modeline-git-branch-candidate (branch kind) | |
| "Return BRANCH propertized as a branch candidate of KIND." | |
| (propertize branch 'dotemacs-modeline-branch-kind kind)) | |
| (defun dotemacs//modeline-git-branch-candidates (root) | |
| "Return local and remote Git branch candidates for ROOT." | |
| (append | |
| (mapcar | |
| (lambda (branch) | |
| (dotemacs//modeline-git-branch-candidate branch 'local)) | |
| (dotemacs//modeline-git-local-branches root)) | |
| (mapcar | |
| (lambda (branch) | |
| (dotemacs//modeline-git-branch-candidate branch 'remote)) | |
| (dotemacs//modeline-git-remote-branches root)))) | |
| (defun dotemacs//modeline-git-branch-group (candidate &optional transform) | |
| "Return the completion group for branch CANDIDATE. | |
| When TRANSFORM is non-nil, return CANDIDATE unchanged." | |
| (if transform | |
| candidate | |
| (pcase (get-text-property 0 'dotemacs-modeline-branch-kind candidate) | |
| ('local "local branch") | |
| ('remote "remote branch") | |
| (_ nil)))) | |
| (defun dotemacs//modeline-read-branch (prompt candidates &optional default) | |
| "Read a Git branch from CANDIDATES with PROMPT. | |
| DEFAULT is the initial default value." | |
| (dotemacs//modeline-read | |
| prompt | |
| candidates | |
| default | |
| #'dotemacs//modeline-git-branch-group)) | |
| (defun dotemacs//modeline-git-remote-branch-p (root branch) | |
| "Return non-nil if BRANCH names a remote Git branch in ROOT." | |
| (member branch (dotemacs//modeline-git-remote-branches root))) | |
| (defun dotemacs//modeline-git-fetch-remote-branch (root branch) | |
| "Fetch remote BRANCH into ROOT when it is not present locally." | |
| (unless (member branch (dotemacs//modeline-git-remote-tracking-branches root)) | |
| (unless (string-match "\\`\\([^/]+\\)/\\(.+\\)\\'" branch) | |
| (user-error "Invalid remote branch name: %s" branch)) | |
| (let ((remote (match-string 1 branch)) | |
| (name (match-string 2 branch))) | |
| (dotemacs//modeline-git-run | |
| root | |
| "fetch" | |
| remote | |
| (format "%s:refs/remotes/%s/%s" name remote name))))) | |
| (defun dotemacs//modeline-git-switch-branch (root branch) | |
| "Switch Git worktree ROOT to BRANCH." | |
| (cond | |
| ((member branch (dotemacs//modeline-git-local-branches root)) | |
| (dotemacs//modeline-git-run root "switch" branch)) | |
| ((dotemacs//modeline-git-remote-branch-p root branch) | |
| (dotemacs//modeline-git-fetch-remote-branch root branch) | |
| (dotemacs//modeline-git-run root "switch" "--track" branch)) | |
| (t | |
| (user-error "Unknown Git branch: %s" branch)))) | |
| (defun dotemacs//modeline-language-mode-candidates () | |
| "Return available major mode candidates." | |
| (sort | |
| (delete-dups | |
| (mapcar #'symbol-name | |
| (seq-filter | |
| (lambda (mode) | |
| (and (symbolp mode) | |
| (fboundp mode) | |
| (commandp mode))) | |
| dotemacs-modeline-language-modes))) | |
| #'string-lessp)) | |
| ;;;###autoload | |
| (defun dotemacs/modeline-project () | |
| "Switch to another known project." | |
| (interactive) | |
| (dotemacs//modeline-select-event-window) | |
| (if (fboundp 'project-switch-project) | |
| (call-interactively #'project-switch-project) | |
| (user-error "Project switching is not available"))) | |
| ;;;###autoload | |
| (defun dotemacs/modeline-vcs () | |
| "Open version-control status for the current buffer." | |
| (interactive) | |
| (dotemacs//modeline-select-event-window) | |
| (let ((directory (or (dotemacs//modeline-project-root) | |
| default-directory))) | |
| (cond | |
| ((fboundp 'magit-status) | |
| (magit-status directory)) | |
| ((fboundp 'vc-dir) | |
| (vc-dir directory)) | |
| (t | |
| (user-error "No version-control status command is available"))))) | |
| ;;;###autoload | |
| (defun dotemacs/modeline-branch () | |
| "Switch Git branch in the current worktree." | |
| (interactive) | |
| (dotemacs//modeline-select-event-window) | |
| (let* ((root (or (dotemacs//modeline-git-root) | |
| (user-error "Current buffer is not in a Git worktree"))) | |
| (branches (dotemacs//modeline-git-branch-candidates root))) | |
| (unless branches | |
| (user-error "No Git branches found in %s" root)) | |
| (let ((branch (substring-no-properties | |
| (dotemacs//modeline-read-branch | |
| "Git branch: " | |
| branches | |
| (dotemacs//modeline-git-current-branch root))))) | |
| (dotemacs//modeline-git-switch-branch root branch) | |
| (when (fboundp 'vc-refresh-state) | |
| (vc-refresh-state)) | |
| (force-mode-line-update t)))) | |
| ;;;###autoload | |
| (defun dotemacs/modeline-position () | |
| "Read a line number and move point to that line." | |
| (interactive) | |
| (dotemacs//modeline-select-event-window) | |
| (cond | |
| ((and (require 'consult nil t) | |
| (fboundp 'consult-goto-line)) | |
| (call-interactively #'consult-goto-line)) | |
| (t | |
| (call-interactively #'goto-line)))) | |
| ;;;###autoload | |
| (defun dotemacs/modeline-indentation () | |
| "Change indentation style and width in the current buffer." | |
| (interactive) | |
| (dotemacs//modeline-select-event-window) | |
| (let ((choice (dotemacs//modeline-read | |
| "Indentation: " | |
| dotemacs-modeline-indentation-choices))) | |
| (unless (string-match | |
| "\\`\\(Spaces\\|Tabs\\):[[:space:]]+\\([0-9]+\\)\\'" | |
| choice) | |
| (user-error "Invalid indentation choice: %s" choice)) | |
| (let ((style (match-string 1 choice)) | |
| (width (string-to-number (match-string 2 choice)))) | |
| (setq-local indent-tabs-mode (string-equal style "Tabs")) | |
| (dotemacs//modeline-set-indentation-width width) | |
| (force-mode-line-update t)))) | |
| ;;;###autoload | |
| (defun dotemacs/modeline-encoding () | |
| "Change the file coding system for the current buffer." | |
| (interactive) | |
| (dotemacs//modeline-select-event-window) | |
| (let* ((current (dotemacs//modeline-current-coding-system)) | |
| (current-base (coding-system-base current)) | |
| (current-eol (coding-system-eol-type current)) | |
| (choice (intern | |
| (dotemacs//modeline-read | |
| "Encoding: " | |
| (sort (mapcar #'symbol-name (coding-system-list)) | |
| #'string-lessp) | |
| (symbol-name current-base)))) | |
| (coding-system | |
| (coding-system-change-eol-conversion | |
| (coding-system-base choice) | |
| current-eol))) | |
| (set-buffer-file-coding-system coding-system t) | |
| (force-mode-line-update t))) | |
| ;;;###autoload | |
| (defun dotemacs/modeline-eol () | |
| "Change the end-of-line convention for the current buffer." | |
| (interactive) | |
| (dotemacs//modeline-select-event-window) | |
| (let* ((choice (dotemacs//modeline-read | |
| "End-of-line: " | |
| '("LF" "CRLF" "CR") | |
| (dotemacs//modeline-eol-name))) | |
| (type (pcase choice | |
| ("LF" 0) | |
| ("CRLF" 1) | |
| ("CR" 2) | |
| (_ (user-error "Invalid end-of-line convention: %s" choice))))) | |
| (dotemacs//modeline-set-eol-type type))) | |
| ;;;###autoload | |
| (defun dotemacs/modeline-language () | |
| "Change the major mode of the current buffer." | |
| (interactive) | |
| (dotemacs//modeline-select-event-window) | |
| (let* ((candidates (dotemacs//modeline-language-mode-candidates)) | |
| (current (symbol-name major-mode)) | |
| (choice (dotemacs//modeline-read | |
| "Major mode: " | |
| candidates | |
| current)) | |
| (mode (intern choice))) | |
| (unless (fboundp mode) | |
| (user-error "Mode is not available: %s" choice)) | |
| (funcall mode) | |
| (force-mode-line-update t))) | |
| ;;;###autoload | |
| (defun dotemacs/modeline-notification () | |
| "Display recent echo-area messages." | |
| (interactive) | |
| (dotemacs//modeline-select-event-window) | |
| (if (fboundp 'view-echo-area-messages) | |
| (view-echo-area-messages) | |
| (switch-to-buffer "*Messages*"))) | |
| (defun dotemacs//modeline-start () | |
| "Install the custom mode-line format." | |
| (when (eq dotemacs-modeline--saved-mode-line-format | |
| dotemacs-modeline--saved-format-unset) | |
| (setq dotemacs-modeline--saved-mode-line-format | |
| (default-value 'mode-line-format))) | |
| (setq-default mode-line-format dotemacs-modeline--format) | |
| (dotemacs//modeline-install-theme-hooks) | |
| (dotemacs//modeline-configure-faces) | |
| (force-mode-line-update t)) | |
| (defun dotemacs//modeline-stop () | |
| "Restore the mode-line format saved by `dotemacs//modeline-start'." | |
| (unless (eq dotemacs-modeline--saved-mode-line-format | |
| dotemacs-modeline--saved-format-unset) | |
| (setq-default mode-line-format dotemacs-modeline--saved-mode-line-format) | |
| (setq dotemacs-modeline--saved-mode-line-format | |
| dotemacs-modeline--saved-format-unset)) | |
| (clrhash dotemacs-modeline--mouse-map-cache) | |
| (dotemacs//modeline-remove-theme-hooks) | |
| (force-mode-line-update t)) | |
| ;;;###autoload | |
| (define-minor-mode dotemacs-modeline-mode | |
| "Toggle the custom mode line. | |
| When enabled, install a minimal mode-line format that displays project and | |
| Git branch information on the left, and buffer status information on the | |
| right." | |
| :global t | |
| :init-value nil | |
| :lighter nil | |
| (if dotemacs-modeline-mode | |
| (dotemacs//modeline-start) | |
| (dotemacs//modeline-stop))) | |
| (defvar-keymap dotemacs-modeline-keymap | |
| :doc "Keymap for custom mode-line commands." | |
| "t" #'dotemacs-modeline-mode | |
| "p" #'dotemacs/modeline-project | |
| "b" #'dotemacs/modeline-branch | |
| "g" #'dotemacs/modeline-vcs | |
| "l" #'dotemacs/modeline-position | |
| "i" #'dotemacs/modeline-indentation | |
| "e" #'dotemacs/modeline-encoding | |
| "n" #'dotemacs/modeline-eol | |
| "m" #'dotemacs/modeline-language | |
| "!" #'dotemacs/modeline-notification) | |
| ;;;###autoload | |
| (defun dotemacs/modeline-setup () | |
| "Configure the custom mode line for the current Emacs session." | |
| (interactive) | |
| (when dotemacs-modeline-enabled | |
| (dotemacs-modeline-mode 1))) | |
| (dotemacs/modeline-setup) | |
| (provide 'dotemacs-modeline) | |
| ;;; dotemacs-modeline.el ends here |
Author
wroyca
commented
Jun 3, 2026
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment