Skip to content

Walheimat/whale-line

Repository files navigation

whale-line

Coverage Status

This package provides a highly modular mode-line. The whale-line is a list of segments divided into two halves (left and right). Each segment is positioned and enabled by its inclusion and positioning in whale-line-segments (wherein | symbolizes the dividing line).

The package ships with a wide array of segments and some augments to enhance those segments.

You may also create your own segments and augments using the whale-line-create-* macros. In fact, you could disregard the pre-shipped segments entirely and just use this package as a library to build your own mode-line.

This package takes inspiration from two other great custom mode-lines: mood-line and doom-modeline.

Preview

Modeline in a split frame (theme is doom-vibrant).

assets/mode-line.png

On the left window, the segment son the left are

  • major-mode (using all-the-icons)
  • buffer-identification
  • flycheck augmenting the prior to indicate errors and warnings (an equivalent flymake augment exists)
  • org segment to show the current top heading and its parent
  • buffer-status segment to indicate the buffer is edited

Not shown are segments for LSP and debug sessions, as well as window-status that each show an icon when they’re active. Also not shown is process that shows mode-line-process as well as client showing mode-line-client.

On the right you can see

  • minor-mode-alist segment augmented by minions
  • project segment
  • vc segment
  • tab-bar segment
  • animation segment

Not shown is the misc-info segment that shows mode-line-misc-info.

The right window shows fewer segments. That’s because many segments are defined to only show on the current window or if space isn’t tight.

GIFs

Showing segments

./assets/whaleline.gif

Hiding low priority segments

./assets/strategy.gif

Installation

If you use straight or quelpa, you know what to do.

If you’re on Emacs >29, I recommend using package-vc-install.

Alternatively, provided you have Cask, you can install the package with make package-install.

Configuration

The position and order of the segments can be controlled by customizing whale-line-segments. You can freely re-arrange them, leave some out, add your custom ones.

If you move, add or remove a segment after loading the package and enabling the mode, you will need to call whale-line-rebuild to see the change.

(use-package whale-line
  :config
  (whale-line-mode +1)

  ;; If you want to use icons.
  (whale-line-iconify-mode)

  :custom
  (whale-line-segments '(major-mode
                         buffer-identification
                         org
                         buffer-status
                         position
                         selection
                         client
                         lsp
                         debug
                         window-status
                         process
                         | ;; Divides into left and right.
                         misc-info
                         minor-modes
                         project
                         vc
                         tab-bar
                         animation))

  ;; Strategy to use when there's not enough space to render all segments.
  (whale-line-segment-strategy 'prioritize) ; Other options are `elide' and `ignore'.

  ;; Animation.
  (whale-line-segments-animation-key-frames [
                                             "   "
                                             ".  "
                                             ".. "
                                             "..."
                                             " .."
                                             "  ."
                                             ])
  (whale-line-segments-animation-speed 0.4)

  ;; Buffer identification.
  (whale-line-segments-buffer-identification-path-segments 1)

  ;; Org.
  (whale-line-segments-org-separator ">")
  (whale-line-segments-org-ellispis "")
  (whale-line-segments-org-elision "*")
  (whale-line-segments-org-max-count 2)

  ;; Window status.
  (whale-line-segments-window-status-separator "·")

  ;; Specs for used icons.
  (whale-line-iconify-specs
   '((project . (:name "package" :font octicon :face whale-line-emphasis))
     (vc . (:name "code-fork" :face whale-line-contrast))
     (major-mode . (:function all-the-icons-icon-for-buffer))
     (buffer-read-only . (:name "lock" :face whale-line-contrast :parent buffer-status))
     (buffer-file-name . (:name "sticky-note-o" :face whale-line-shadow :parent buffer-status))
     (buffer-modified . (:name "pencil" :face whale-line-emphasis :parent buffer-status))
     (window-dedicated . (:name "link" :face whale-line-shadow :parent window-status))
     (window-no-other . (:name "low-vision" :face whale-line-shadow :parent window-status))
     (window-no-delete . (:name "deaf" :face whale-line-shadow :parent window-status))
     (buffer-fallback . (:name "question-circle" :face whale-line-contrast :no-defaults t))
     (lsp . (:name "plug" :face whale-line-contrast))
     (debug . (:name "bug" :face whale-line-urgent)))

  ;; List of icons to disable.
  (whale-line-iconify-disabled nil))

Customizing Priorities

All segments are created with a priority that determines on what condition the segment is shown. The possible values are:

  • t to always show
  • current to always show for the selected window
  • current-low to show for current window if space allows it
  • low to show if space allows it

If you’re unhappy with the default settings, you can use whale-line-with-priorities to change them in bulk after loading the package.

(whale-line-with-priorities
  ;; Make `major-mode' and `buffer-status' segment show only for
  ;; current window.
  major-mode
  buffer-status
  current

  ;; Make `project' segment show only if space allows it.
  project
  low

  ;; Always show `lsp' segment.
  lsp
  t)

Re-ordering of Segments

You can use whale-line-edit to edit segment positioning and inclusion. It will pop to a buffer where you can move, add and delete segments. The empty line represents the divider. If your reordering is valid (single divider, only including known segments), you can apply the change with C-c C-c. To persist the change you need to use C-c C-w.

Custom Segments

You may create your own segments and augments using macros whale-line-create-stateless-segment, whale-line-create-stateful-segment and whale-line-create-augment. Note however that their signature is not finalized and may change at any time. (Be sure to add your segment to whale-line-segments at the desired position.)

As the macro names suggest, there are three things you can create:

  1. Stateless segments
  2. Stateful segments
  3. Augments for either segment type

Be sure to create your segments before activating whale-line-mode.

Stateless segments

A stateless segment is just that: a segment without a state. This simply means that the segment will be re-rendered on every mode-line update.

This is ideal for segments that are not costly to render and should be up-to-date at all times.

Stateless segments use either a variable or a function to yield their representation on a mode-line. If you’re familiar with mode-line constructs, this would be the simplest stateless segment definition.

(defvar my-stateless-segment '((:propertize "hello" face success)))

(whale-line-create-stateless-segment stateless
  :var my-stateless-segment)

This would create segment stateless that would render “hello” propertized with face success on every mode-line update.

This is fine in most cases but if the construction of your segment is a bit more involved than a mode-line construct allows, you might want a function.

(defun my-stateless-getter ()
  "Construct my segment."
  (if (org-before-first-heading-p)
      "before"
    "after"))

(whale-line-create-stateless-segment stateless
  :getter my-stateless-getter)

Stateless segments accept :condition which should be a form that is evaluated before the getter. If it returns nil an empty string is returned instead.

(whale-line-create-stateless-segment conditional
  :var my-stateless-segment
  :condition (derived-mode-p 'text-mode))

Stateful segments

A stateful segment is a segment with a state. This means that it will return its state unchanged on every mode-line update and only re-calculate that state when it’s necessary.

This type makes sense when processing the segment takes a lot of time or resources even though the result of the processing itself only changes at certain known junctures.

There are two ways to tell such a segment to re-calculate: by providing a list of hooks, a list functions to advise or both.

The recalculation is defined as the segment’s getter.

Let’s have a look.

(defun my-stateful-fun ()
  "Return the major mode."
  (let ((calculated ;; Do some heavy stuff here.
         ))

    calculated))

(whale-line-create-stateful-segment stateful
  :getter my-stateful-fun
  :hooks (change-major-mode-hook))

This would call my-stateless-fun only on the first mode-line update and then store it. On each subsequent update the stored value is returned. The value is updated whenever change-major-mode-hook is run.

You may also want to use :after to advise a list of functions after which the state should be updated.

(whale-line-create-stateful-segment advised
  :getter my-stateful-fun
  :after (undo redo))

Whenever undo or redo are called, my-stateful-fun would be called afterwards (with the same arguments) to updated the state.

You can also specify your own advice combinator.

(whale-line-create-stateful-segment before-advised
  :advice (:before . (undo redo)))

Augments

Sometimes a segment you want already exists in a basic form but you want to enhance it when certain criteria are met. This is where augments come into play.

The definition of augments is similar to that of stateful segments. You define either hooks or functions to advise. Other than stateful segments, these hooks being run (or functions being called) do not update another segment directly, instead they just call an action.

The relationship between a segment and its augment is therefore somewhat tenuous in that you need to define how exactly the augmentation is to take place.

The easiest way here is using :after-while in combination with a stateful segment or a stateless getter-based segment.

(defun my-augment-fun (calculated)
  "Enhance CALCULATED value."
  (concat calculated ":augmented"))

(defun my-augment-should-augment-p ()
  "Only augment on Linux."
  (eq system-type 'gnu/linux))

(whale-line-create-augment my-augment
  :verify my-augment-should-augment-p
  :action my-augment-fun
  ;; You may also provide a list.
  :after-while whale-line-stateful--render
  ;; Or equivalent and more readable.
  :wraps stateful)

If you don’t set :verify always will be used for augments. More on this below.

You can also use :after or specify your own advice combinator.

(whale-line-create-augment before-augment
  ;; :after (whale-line-staetful--get-segment)
  :advice (:before-while . (whale-line-stateful--render)))

The function whale-line-stateful--get-segment is created by previous declaration for segment stateful. It is called when the state is updated so our augment advises it to return an augmented value.

If the segment provides a port (see below), you can also use :plugs-into.

(defvar slot-var nil)

(defvar slot-construct '(("fe" slot-var)))

(defun slot-port (a b)
  "Concat A and B."
  (setq slot-var (concat a b))

(whale-line-create-stateless-segment slot
  :var slot-construct
  :port slot-port)

(defun plug-action ()
  "Return values to concatenate."
  (list "male" "female"))

(whale-line-create-augment plug
  :action plug-action
  :plugs-into slot
  :hooks (change-major-mode-hook))

Whenever change-major-mode-hook is run, plug-action would be called and its result passed to slot-port (with some indirection), setting slot-var to “malefemale”. The segment would now show “femalefemale”.

Optional shared properties of all types

setup and teardown

If your segment (or augment) requires a setup or teardown routine, you can pass a lambda or function symbol to :setup and/or :teardown. These functions will be called whenever whale-line-{setup,teardown}-hook is run. This is the case when whale-line-mode is enabled/disabled or when segments are re-built using whale-line-rebuild (provided the segment was added/removed since the previous build).

Note that whether you provide such a routine or not, there’s always a setup and a teardown function (used for :hooks or :advice for example).

verify

Mostly makes sense for augments. This function is called before a setup or teardown happens. If it yields nil, no setup/teardown will take place. Note that for segments this function will replace the default check of (memq '<segment> whale-line-segments).

Optional shared properties of segments

priority

The default priority of your segment (see section Customizing Priorities).

dense

You can use this to disallow padding the segment. Normally, depending on its position, the segment will have padding to its left/right.

padded

If your segment comes pre-padded (for example if you use an external construct that already adds whitespace on the left or right), you can pass left, right or all here. This will ensure that the segment won’t get superfluous padding on that side or both sides, no matter how it’s positioned.

port

This is a function that augments using :plug-into call with their result. This function should set some variable used during internal rendering for augmentation.

The nitty gritty

If you want to build something more complicated, you might need to know what functions and variables are created during macro expansion. So here’s a summary.

Stateless segments define a function whale-line-<name>--render. This function is called to render the segment. If they use :var this function will just return that variable’s value. If they use :getter this function will call additionally created whale-line-<name>--getter that in turn will finally call the passed function. The reason for this nesting and indirection is that you may pass either a function symbol or an anonymous function to :getter.

Stateful segments hold their state in local variable (not function!) whale-line-<name>--render. This variable is set to symbol initial at first to make sure the the value is set at least once during format-mode-line. The function that does this uses pattern whale-line-<name>--setter. It calls the passed getter to set the variable.

Augments also create whale-line-<name>--setter to do their thing.

If you’re using :port and :plugs-into, the segment with :port will create function whale-line-<name>--port that will be called with the result of the augment’s action. That means the return value of the action should match the arity of the port function.