UP | HOME

Custom mode line
#emacs#theme

Table of Contents

light-mode.png

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.