Custom mode line
#emacs#theme
Table of Contents
Figure 1: My mode line in light mode
Well, I finally did it. I really sat down and looked at my mode line. Before I was just toggling some configuration on predefined setups like doom-modeline and smart-mode-line, but they are all either too much or too little.
The two packages I use in the mode line are all-the-icons and
minions. For all-the-icons, no extra set up is necessary beyond
installing fonts with M-x all-the-icons-install-fonts
. I have some
sample configuration below for minions using use-package.
Custom mode line
See also
all-the-icons' wiki is a great tool to to learn about configuring the text properties correctly on the mode line, but I found that evaluating each of those functions for every mode line update was taxing my system. Every key press just felt sluggish. I opted to rewrite most of the functions from scratch using stored values, but this wiki was still a great resource for learning the syntax and seeing the possibilities.
Putting it all together
"Putting what all together?" You might ask.
This is the step you should do last once you've configure all the custom variables you want to use. It's good to see the purpose behind all the subsequent elisp by looking at the end product first.
Some items on the mode line should be aligned to right side of the
window, like the time and battery status. These items will go into
the my-mode-line/right
variable. I'm using the built in
time string variable and a custom indicator for the battery. When
in tty mode, I use the built-in variable for both of these
indicators, mode-line-misc-info
.
(defvar my-mode-line/right nil) ;;;###autoload (put 'my-mode-line/right 'risky-local-variable t) (setq my-mode-line/right (if (display-graphic-p) '("" display-time-string my-mode-line/battery) 'mode-line-misc-info))
If a mode line item is surrounded by (:eval)
, emacs will
reevaluate the expression each time the mode line updates. Any
function call here could become very expensive. Only one item on my
custom mode line uses such a function, in order to determine the
correct padding to align my-mode-line/right
to the right:
(defun my-mode-line/padding () (let ((r-length (length (format-mode-line my-mode-line/right)))) (propertize " " 'display `(space :align-to (- right ,r-length)))))
Otherwise, all custom items are actually buffer-local or global variables that are updated based on some hooks, advice, and timers. This frees up the mode line from having to actually compute anything; it just displays the value of the variable.
Here is the rest of the mode line. As you might have already
guessed, any custom item is prefixed by my-mode-line/
.
The first list is for graphical displays and the second is for working in the command line.
(setq-default mode-line-format (if (display-graphic-p) '("%e" my-mode-line/modal-editing my-mode-line/modified my-mode-line/major-mode " %l:%C %o " my-mode-line/dir mode-line-buffer-identification " " minions-mode-line-modes (:eval (my-mode-line/padding)) my-mode-line/right) '("%e" mode-line-front-space mode-line-modified " %l:%C %o " my-mode-line/dir mode-line-buffer-identification " " mode-line-modes (:eval (my-mode-line/padding)) my-mode-line/right)))
Notice the last two lines are the padding and then the right-aligned items I defined for you above.
Now, let's go through each item one by one.
%e
This built-in variable shows an out-of-memory error on the mode line if applicable, nothing otherwise.
Modified indicator
Shows a lock if the buffer is read-only, a broken chain if it is
modified, and a normal chain if the file is saved. All three of
those icons can be configured by changing the icon
variable
below.
If the file is modified, clicking on this icon will save it. Otherwise, it will toggle read-only-mode.
(defvar-local my-mode-line/modified nil) ;;;###autoload (put 'my-mode-line/modified 'risky-local-variable t) (defun my-mode-line/update-modified (&optional arg) (let ((icon (cond (buffer-read-only (propertize (all-the-icons-octicon "lock") 'help-echo 'mode-line-read-only-help-echo 'local-map (purecopy (make-mode-line-mouse-map 'mouse-1 #'mode-line-toggle-read-only)) 'mouse-face 'mode-line-highlight)) ((or (buffer-modified-p) (string= arg "modified")) (propertize (all-the-icons-faicon "chain-broken") 'help-echo 'mode-line-modified-help-echo 'local-map (purecopy (make-mode-line-mouse-map 'mouse-1 #'save-buffer)) 'mouse-face 'mode-line-highlight)) (t (propertize (all-the-icons-faicon "link") 'help-echo 'mode-line-read-only-help-echo 'local-map (purecopy (make-mode-line-mouse-map 'mouse-1 #'mode-line-toggle-read-only)) 'mouse-face 'mode-line-highlight))))) (setq my-mode-line/modified (format " %s " (propertize icon 'display '(raise 0.01)))) (force-mode-line-update))) ;; First change hook runs before buffer is modified. Passing "modified" to my function ;; will override the result of (buffer-modified-p) and the chain-broken icon will be displayed (add-hook 'first-change-hook (lambda () (my-mode-line/update-modified "modified"))) (add-hook 'buffer-list-update-hook #'my-mode-line/update-modified) (add-hook 'after-save-hook #'my-mode-line/update-modified) (add-hook 'read-only-mode-hook #'my-mode-line/update-modified) (advice-add 'undo :after #'my-mode-line/update-modified)
Major mode icon
The major mode icon will give you a quick indicator of which buffer belongs to which context.
(defvar-local my-mode-line/major-mode nil) ;;;###autoload (put 'my-mode-line/major-mode 'risky-local-variable t) (defun my-mode-line/update-major-mode (&rest _) (let ((icon (all-the-icons-icon-for-buffer))) (unless (symbolp icon) (setq my-mode-line/major-mode (format " %s " (propertize icon 'display '(raise 0.0) 'help-echo (format "Major mode: `%s`" major-mode))))))) (add-hook 'buffer-list-update-hook #'my-mode-line/update-major-mode) (add-hook 'after-change-major-mode-hook #'my-mode-line/update-major-mode t)
%l:%C %o
These built in variables will give the line and column number along
with the percent of the way through the buffer. It might look
something like this: 79:20 97%
.
Directory name
This is the system-name followed by a relative path from your home
directory to the current directory. Most of the files I work with
are in my home directory so this is usually the most succinct way
of showing my location. It might look something like this:
tyler-hp:org/
if you are in the folder /home/tyler/org
on the
tyler-hp
machine. The directory is truncated to the last 7
letters.
If you click on the directory name, a dired buffer will open.
(defvar-local my-mode-line/dir nil) ;;;###autoload (put 'my-mode-line/dir 'risky-local-variable t) (setq my-mode-line/dir-length 7) (defun my-mode-line/update-dir () (setq my-mode-line/dir (if buffer-file-name (propertize (format " %s:%s" (or (system-name) (getenv "SYSTEM_NAME")) (reverse (truncate-string-to-width (reverse (replace-regexp-in-string "\\(^\\\./\\)" "" (file-relative-name default-directory "~"))) (+ 1 my-mode-line/dir-length) nil nil "-"))) 'help-echo "Open dired buffer" 'local-map (purecopy (make-mode-line-mouse-map 'mouse-1 (lambda () (interactive) (dired default-directory)))) 'mouse-face 'mode-line-highlight) nil))) (add-hook 'find-file-hook #'my-mode-line/update-dir)
mode-line-buffer-identification
Built-in variable displaying the name of the buffer. Clicking on this will switch to the last buffer.
minions
Minions is a replacement for the mode list. It hides all of your
minor modes by default and you have to enable each one
individually. It also surrounds the mode list with square brackets
for each level of recursive editing. The
minions-mode-line-lighter
is a symbol shown in the mode list that
you can click on to view and toggle all other minor modes. You can
add more minor modes to minions-direct
in order to see them in
the mode line.
(use-package minions :init (setq minions-mode-line-lighter "...") (setq minions-direct '(flycheck-mode boon-local-mode)) (add-hook 'after-init-hook #'minions-mode))
Display time
Time and date formatting. Look up the documentation of this variable to see all available replacement strings.
Clicking on the time will open the calendar.
(setq display-time-string-forms (if (display-graphic-p) '((propertize (format "%s, %s%s %s:%s%s " dayname monthname day 12-hours minutes (upcase am-pm)) 'help-echo "Open calendar" 'local-map (purecopy (make-mode-line-mouse-map 'mouse-1 (lambda () (interactive) (calendar)))) 'mouse-face 'mode-line-highlight)) '((format "%s:%s%s " 12-hours minutes (upcase am-pm))))) (display-time-mode 1)
Battery
The battery icon has five stages of progressively less charge. The
operative lines for this function are the ones setting the
battery-icon
and info
variables. If you hover over the battery
icon in the mode line, the info is shown in the echo
area.
The battery indicator is updated once a minute. You can evaluate
the function my-mode-line/update-battery
manually to update it
immediately.
(require 'battery) (unless (display-graphic-p) (display-battery-mode t)) (defvar my-mode-line/battery nil) ;;;###autoload (put 'my-mode-line/battery 'risky-local-variable t) (defun my-mode-line/update-battery (&optional arg) (let* ((battery-info-alist (funcall battery-status-function)) (status (cdr (assoc ?B battery-info-alist))) (percent (string-to-number (cdr (assoc ?p battery-info-alist)))) (time-remaining (replace-regexp-in-string "\\(N/A\\)" "0:00" (cdr (assoc ?t battery-info-alist)))) (rate (replace-regexp-in-string "\\(N/A\\)" "0" (cdr (assoc ?r battery-info-alist)))) (battery-icon (cond ((string-match "^[Cc]harging" status) "bolt") ((= percent 100) "battery-full") ((>= percent 75) "battery-three-quarters") ((>= percent 50) "battery-half") ((>= percent 25) "battery-quarter") (t "battery-empty"))) (info (format "Battery: %s. %s remaining; rate %sC" status time-remaining rate))) (setq-default my-mode-line/battery (propertize (format " %s %s " (all-the-icons-faicon battery-icon) time-remaining) 'display '(raise 0.0) 'help-echo info)) ;; Display battery status in echo area after evaluating manually info)) (run-with-timer 0 60 'my-mode-line/update-battery)
The battery discharge/charge rate is given in 'C' units, which are proportional to the amperage.
Conclusion
That is the extent of my mode line. It's uncomplicated because I tend to have a lot of windows and splitting going on and I don't need any clutter. But I also don't like the default mule-info and modification indicators and feel that icons deliver more meaningful information with less thought.
I hope this is a good starting place for you to branch out and put whatever you want on your mode line, keeping in mind that creating local variables and updating them from hooks or timers is a lot more efficient than evaluating a custom function each time the mode line updates.