Skip to content

Instantly share code, notes, and snippets.

@wroyca
Created June 3, 2026 21:17
Show Gist options
  • Select an option

  • Save wroyca/a4eac82f84a2a40a85577326a4e7277b to your computer and use it in GitHub Desktop.

Select an option

Save wroyca/a4eac82f84a2a40a85577326a4e7277b to your computer and use it in GitHub Desktop.
Minimal custom mode line
;;; 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
@wroyca
Copy link
Copy Markdown
Author

wroyca commented Jun 3, 2026

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment