Permalink
Switch branches/tags
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
570 lines (447 sloc) 19.4 KB

Elvish Chain theme prompt

This is my implementation for Elvish of the Chain prompt theme, based on the original fish theme at https://github.com/oh-my-fish/theme-chain.

Ported to Elvish by Diego Zamboni <diego@zzamboni.org>.

This file is written in literate programming style, to make it easy to explain. See chain.elv for the generated file.

Table of Contents

Use

To use this theme, first install the github.com/zzamboni/elvish-themes package using epm:

epm:install github.com/zzamboni/elvish-themes

You can do this interactively or from your ~/.elvish/rc.elv file. If you want to put this in your rc.elv so that the package is automatically installed when needed, and avoid having a message printed every time the shell starts, you can add the &silent-if-installed=$true option to the above command.

Add the following to you ~/.elvish/rc.elv file to load and configure the theme:

use github.com/zzamboni/elvish-themes/chain

If you want the prompt to be shown using bold fonts, set the following:

chain:bold-prompt = $true

The default Elvish prompt settings work fine, but if you want to fine tune them, check the Prompts documentation. For example, if you want to keep the prompt from showing inverted when “stale” (this may happen if you use git segments and are in a large git repo), you could set the stale-prompt transformer to an identity function:

edit:prompt-stale-transform = $all~

If you want the prompt to be grayed out when stale, you can do the following:

edit:prompt-stale-transform = { each [x]{ styled $x[text] "gray" } }

The chains on both sides can be configured by assigning to theme:chain:prompt-segments and theme:chain:rprompt-segments, respectively. These variables must be arrays, and the given segments will be automatically linked by $theme:chain:glyph[chain]. Their default values are:

prompt-segments-defaults = [ su dir git-branch git-combined arrow ]
rprompt-segments-defaults = [ ]

Each element can be any of the following:

  • The name of one of the built-in segments. Available segments: arrow, timestamp, su, dir, session (a glyph with a unique color for the current session, based on its PID), git-branch, git-dirty, git-untracked, git-deleted, git-ahead, git-behind, git-staged, git-combined (which combines in a single segment all the other git status indicators), git-timestamp (timestamp of the last git commit);
  • A string or the output of styled, which will be displayed as-is;
  • A lambda, which will be called and its output displayed;
  • The output of a call to chain:segment <style> <strings>, which returns a “proper” segment, enclosed in square brackets and styled as requested.

You can customize the glyphs and styles used for the different segments by assigning to the corresponding elements of chain:glyph and chain:segment-style, respectively.

The value assigned to an element of chain:segment-style can be:

  • Any valid argument to styled.
  • The string default to indicate no special color (this will show in your regular terminal text color).
  • The string session to produce a color based on the current process ID, which can be used to indicate a unique color for the current session. This is used by the session segment, but can be assigned to any segment.

The module also includes the chain:summary-status function, which provides a useful summary of some predetermined git repositories, using the git-combined, git-branch and git-timestamp segments. I use this to get a quick summary of the status of my most commonly-used repos.

[~]─> chain:summary-repos = [ ~/.elvish ~/.emacs.d ~/.hammerspoon]
[~]─> chain:summary-status
[ ✎ ] [⎇ master] [2018-06-19] ~/.elvish
[ OK ] [⎇ master] [2018-06-13] ~/.emacs.d
[ OK ] [⎇ master] [2018-04-30] ~/.hammerspoon

Implementation

Base code and default values

Load the regular expression library.

use re

We use muesli’s git library for the git functions.

use github.com/muesli/elvish-libs/git

Set up the default values for the chains (all can be configured by assigning to the appropriate variable):

prompt-segments = $prompt-segments-defaults
rprompt-segments = $rprompt-segments-defaults

Set up the default values for the glyphs used in the different chains. Note that some of the Unicode glyphs may need an extra space after them so that the character does not run into the next one in the terminal. This is highly dependent on the font you use, so please fine tune as needed. The default values work fine for the Inconsolata font I use.

default-glyph = [
  &git-branch=    "⎇"
  &git-dirty=     "●"
  &git-ahead=     "⬆"
  &git-behind=    "⬇"
  &git-staged=    "✔"
  &git-untracked= "+"
  &git-deleted=   "-"
  &su=            "⚡"
  &chain=         "─"
  &session=       "▪"
  &arrow=         ">"
]

Styling for each built-in segment. The value must be a valid argument to [[https://elvish.io/ref/edit.html#editstyled][styled]].

default-segment-style = [
  &git-branch=    [ blue    ]
  &git-dirty=     [ yellow  ]
  &git-ahead=     [ red     ]
  &git-behind=    [ red     ]
  &git-staged=    [ green   ]
  &git-untracked= [ red     ]
  &git-deleted=   [ red     ]
  &git-combined=  [ default ]
  &git-timestamp= [ cyan    ]
  &su=            [ yellow  ]
  &chain=         [ default ]
  &arrow=         [ green   ]
  &dir=           [ cyan    ]
  &session=       [ session ]
  &timestamp=     [ gray    ]
]

The $glyph and $segment-style maps are where the user can assign their custom glyphs or styles. Both are empty by default. If an element does not exist in these variables, the corresponding default value is used.

glyph = [&]
segment-style = [&]

To how many letters to abbreviate directories in the path - 0 to show in full.

prompt-pwd-dir-length = 1

Format to use for the timestamp segment, in strftime(3) format.

timestamp-format = "%R"

User ID that will trigger the su segment. Defaults to root (UID 0).

root-id = 0

Whether the prompt should be bold.

bold-prompt = $false

The git-get-timestamp function gets executed to produce the text to be displayed in the git-timestamp module. You can change it if you want to change the format of what gets displayed.

git-get-timestamp = { git log -1 --date=short --pretty=format:%cd }

General utility functions

Function to choose a color based on the current value of $pid, as an indicator of the current session.

fn -session-color {
  valid-colors = [ black red green yellow blue magenta cyan lightgray gray lightred lightgreen lightyellow lightblue lightmagenta lightcyan white ]
  put $valid-colors[(% $pid (count $valid-colors))]
}

Internal function to return a styled string, or plain if color is “default”. If $color is “session”, then a unique color is chosen for the current session using the -session-color function.

fn -colorized [what @color]{
  if (and (not-eq $color []) (eq (kind-of $color[0]) list)) {
    color = [(explode $color[0])]
  }
  if (and (not-eq $color [default]) (not-eq $color [])) {
    if (eq $color [session]) {
      color = [(-session-color)]
    }
    if $bold-prompt {
      color = [ $@color bold ]
    }
    styled $what $@color
  } else {
    put $what
  }
}

We have two auxiliary functions to return the glyph or style corresponding to a given segment. Default values are stored in the module’s $default-glyph and $default-segment-style variables, but the user can provide their own values by setting $glyph and $segment-style respectively.

fn -glyph [segment-name]{
  if (has-key $glyph $segment-name) {
    put $glyph[$segment-name]
  } else {
    put $default-glyph[$segment-name]
  }
}
fn -segment-style [segment-name]{
  if (has-key $segment-style $segment-name) {
    put $segment-style[$segment-name]
  } else {
    put $default-segment-style[$segment-name]
  }
}

The -colorized-glyph returns the glyph for the given segment, with its corresponding style. If extra arguments are given, they are concatenated after the glyph.

fn -colorized-glyph [segment-name @extra-text]{
  -colorized (-glyph $segment-name)(joins "" $extra-text) (-segment-style $segment-name)
}

Build a prompt segment in the given style, surrounded by square brackets. The first argument can be a style argument understood by styled, or the name of one of the predefined segments. In the latter case, the style is taken from the $segment-style map, and if a glyph for that segment name exists in the $glyph map, it is automatically prepended to the given text.

fn prompt-segment [segment-or-style @texts]{
  style = $segment-or-style
  if (has-key $default-segment-style $segment-or-style) {
    style = (-segment-style $segment-or-style)
  }
  if (has-key $default-glyph $segment-or-style) {
    texts = [ (-glyph $segment-or-style) $@texts ]
  }
  text = "["(joins ' ' $texts)"]"
  -colorized $text $style
}

Built-in Segment Definitions

This is where the built-in segments are defined. We assign the corresponding functions to elements of the $segment map, indexed by their segment name. The segment names need to correspond between the $segment, $glyph and $segment-style maps.

segment = [&]

git-related segments

Note that all the git-related segment functions only produce an output if the current directory contains a git repository.

We define a module-level variable which contains the latest git information. It gets populated once-per-prompt by the -parse-git function, and the information is used by all the segments.

last-status = [&]

The -any-staged function indicates whether there are any staged changes (can be files added, deleted, modified, renamed or copied), and is used below to extend the results from git:status.

fn -any-staged {
  count [(each [k]{
        explode $last-status[$k]
  } [staged-modified staged-deleted staged-added renamed copied])]
}

The -parse-git function calls git:status to get the git status of the current directory. It extends the results with the result from -any-staged to have an easy indicator of staged files.

fn -parse-git {
  last-status = (git:status)
  last-status[any-staged] = (-any-staged)
}

The git-branch segment indicates the current branch name. If we are in a detached-branch state, we return the first 6 digits of the commit ID.

segment[git-branch] = {
  branch = $last-status[branch-name]
  if (not-eq $branch "") {
    if (eq $branch '(detached)') {
      branch = $last-status[branch-oid][0:7]
    }
    prompt-segment git-branch $branch
  }
}

The git-timestamp segment shows the last-commit timestamp from the current branch.

segment[git-timestamp] = {
  ts = ($git-get-timestamp)
  prompt-segment git-timestamp $ts
}

The -show-git-indicator function takes a git segment name and returns whether it should be shown, depending on the information stored in $last-status. Since the git segment names do not correspond one-to-one with the elements of $last-status, we do here the mapping between them.

fn -show-git-indicator [segment]{
  status-name = [
    &git-dirty=  local-modified  &git-staged=    any-staged
    &git-ahead=  rev-ahead       &git-untracked= untracked
    &git-behind= rev-behind      &git-deleted=   local-deleted
  ]
  value = $last-status[$status-name[$segment]]
  # The indicator must show if the element is >0 or a non-empty list
  if (eq (kind-of $value) list) {
    not-eq $value []
  } else {
    > $value 0
  }
}

Generic function to display a git prompt segment.

fn -git-prompt-segment [segment]{
  if (-show-git-indicator $segment) {
    prompt-segment $segment
  }
}

We support the following git indicator segments:

-git-indicator-segments = [untracked deleted dirty staged ahead behind]
  • The git-dirty segment indicates whether any files are “dirty” (modified locally).
  • The git-ahead and git-behind segments indicate whether the current repository is ahead or behind of the upstream remote, if any.
  • The git-staged, git-untracked and git-deleted segments indicate whether there are staged-but-uncommited, untracked or deleted-but-still-tracked files, respectively.

Using -git-prompt-segment, we define all these git segments.

each [ind]{
  segment[git-$ind] = { -git-prompt-segment git-$ind }
} $-git-indicator-segments

The git-combined segment combines all the different status indicators in a single segment. The $segment-style[git-combined] value determines the color used for the surrounding brackets.

segment[git-combined] = {
  indicators = [(each [ind]{
        if (-show-git-indicator git-$ind) { -colorized-glyph git-$ind }
  } $-git-indicator-segments)]
  if (> (count $indicators) 0) {
    color = (-segment-style git-combined)
    put (-colorized '[' $color) $@indicators (-colorized ']' $color)
  }
}

dir

For this segment we also need a support function, which returns the current path with each directory name shortened to a maximum of $prompt-pwd-dir-length characters.

fn -prompt-pwd {
  tmp = (tilde-abbr $pwd)
  if (== $prompt-pwd-dir-length 0) {
    put $tmp
  } else {
    re:replace '(\.?[^/]{'$prompt-pwd-dir-length'})[^/]*/' '$1/' $tmp
  }
}
segment[dir] = {
  prompt-segment dir (-prompt-pwd)
}

su

This segment outputs a glyph if the current user has a privileged ID (root by default, with ID 0, but can be configured by changing $root-id).

segment[su] = {
  uid = (id -u)
  if (eq $uid $root-id) {
    prompt-segment su
  }
}

timestamp

This segment simply outputs the current date according to the format defined in $timestamp-format.

segment[timestamp] = {
  prompt-segment timestamp (date +$timestamp-format)
}

session

This segment prints a session indicator in a color unique to the current session, based on its $pid.

segment[session] = {
  prompt-segment session
}

arrow

This segment prints the separator between the other chains and the cursor.

segment[arrow] = {
  -colorized-glyph arrow " "
}

Chain- and prompt-building functions

Given a segment specification, return the appropriate value, depending on whether it’s the name of a built-in segment, a lambda, a string or a styled object.

fn -interpret-segment [seg]{
  k = (kind-of $seg)
  if (eq $k 'fn') {
    # If it's a lambda, run it
    $seg
  } elif (eq $k 'string') {
    if (has-key $segment $seg) {
      # If it's the name of a built-in segment, run its function
      $segment[$seg]
    } else {
      # If it's any other string, return it as-is
      put $seg
    }
  } elif (or (eq $k 'styled') (eq $k 'styled-text')) {
    # If it's a styled object, return it as-is
    put $seg
  }
}

Given a list of segments (which can be built-in segment names, lambdas, strings or styled objects), return the appropriate chain, including the chain connectors.

fn -build-chain [segments]{
  if (eq $segments []) {
    return
  }
  first = $true
  output = ""
  -parse-git
  for seg $segments {
    output = [(-interpret-segment $seg)]
    if (> (count $output) 0) {
      if (not $first) {
        -colorized-glyph chain
      }
      put $@output
      first = $false
    }
  }
}

Finally, we get to the functions that build the left and right prompts, respectively. These are basically wrappers around -build-chain with the corresponding arguments.

fn prompt {
  if (not-eq $prompt-segments []) {
    -build-chain $prompt-segments
  }
}

fn rprompt {
  if (not-eq $rprompt-segments []) {
    -build-chain $rprompt-segments
  }
}

Initialization

Default setup function, assigning our functions to edit:prompt and edit:rprompt

fn init {
  edit:prompt = $prompt~
  edit:rprompt = $rprompt~
}

We call the init function automatically on module load.

init

Bonus useful functions

chain:summary-status provides a summarized list of the git-combined and git-branch indicators for the repositories specified in $chain:summary-repos.

summary-repos = []

fn summary-status {
  prev = $pwd
  each $echo~ $summary-repos | sort | each [r]{
    cd $r
    -parse-git
    status = [($segment[git-combined])]
    if (eq $status []) {
      status = [(-colorized "[" session) (styled OK green) (-colorized "]" session)]
    }
    status = [$@status ' ' ($segment[git-branch]) ' ' ($segment[git-timestamp])]
    echo &sep="" $@status ' ' (styled (tilde-abbr $r) blue)
  } | sort -r +2 -3
  cd $prev
}