├── LICENSE ├── README.org ├── chain.elv ├── chain.org ├── images └── screenshot.jpg └── metadata.json /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Diego Zamboni 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * Elvish themes 2 | 3 | This package contains Elvish themes I have developed. Install it using [[https://elvish.io/ref/epm.html][epm]]: 4 | 5 | #+begin_src elvish 6 | use epm 7 | epm:install github.com/zzamboni/elvish-themes 8 | #+end_src 9 | 10 | *Please note:* These modules are only guaranteed to be fully compatible with [[https://elv.sh/get/][Elvish HEAD]], which is what I use. This means that occasionally, they will not work even with the latest official release, when breaking changes are introduced. Since Elvish is in active development, I highly recommend you use the latest commit too. 11 | 12 | Currently the following themes are included: 13 | 14 | ** chain 15 | 16 | Chain prompt theme, based on the fish theme at [[https://github.com/oh-my-fish/theme-chain][https://github.com/oh-my-fish/theme-chain]]. 17 | 18 | See the install and use instructions in [[file:chain.org][chain.org]]. 19 | -------------------------------------------------------------------------------- /chain.elv: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE DIRECTLY 2 | # This is a file generated from a literate programing source file located at 3 | # https://github.com/zzamboni/elvish-themes/blob/master/chain.org. 4 | # You should make any changes there and regenerate it from Emacs org-mode using C-c C-v t 5 | 6 | var prompt-segments-defaults = [ su dir git-branch git-combined arrow ] 7 | var rprompt-segments-defaults = [ ] 8 | 9 | use re 10 | use str 11 | use path 12 | 13 | use github.com/href/elvish-gitstatus/gitstatus 14 | use github.com/zzamboni/elvish-modules/spinners 15 | 16 | var prompt-segments = $prompt-segments-defaults 17 | var rprompt-segments = $rprompt-segments-defaults 18 | 19 | var default-glyph = [ 20 | &git-branch= "⎇" 21 | &git-dirty= "●" 22 | &git-ahead= "⬆" 23 | &git-behind= "⬇" 24 | &git-staged= "✔" 25 | &git-untracked= "+" 26 | &git-deleted= "-" 27 | &su= "⚡" 28 | &chain= "─" 29 | &session= "▪" 30 | &arrow= ">" 31 | ] 32 | 33 | var default-segment-style = [ 34 | &git-branch= [ blue ] 35 | &git-dirty= [ yellow ] 36 | &git-ahead= [ red ] 37 | &git-behind= [ red ] 38 | &git-staged= [ green ] 39 | &git-untracked= [ red ] 40 | &git-deleted= [ red ] 41 | &git-combined= [ default ] 42 | &git-timestamp= [ cyan ] 43 | &git-repo= [ blue ] 44 | &su= [ yellow ] 45 | &chain= [ default ] 46 | &arrow= [ green ] 47 | &dir= [ cyan ] 48 | &session= [ session ] 49 | ×tamp= [ bright-black ] 50 | ] 51 | 52 | var glyph = [&] 53 | var segment-style = [&] 54 | 55 | var prompt-pwd-dir-length = 1 56 | 57 | var timestamp-format = "%R" 58 | 59 | var root-id = 0 60 | 61 | var bold-prompt = $false 62 | 63 | var show-last-chain = $true 64 | 65 | var space-after-arrow = $true 66 | 67 | var git-get-timestamp = { git log -1 --date=short --pretty=format:%cd } 68 | 69 | var prompt-segment-delimiters = "[]" 70 | # prompt-segment-delimiters = [ "<<" ">>" ] 71 | 72 | fn -session-color { 73 | var valid-colors = [ red green yellow blue magenta cyan white bright-black bright-red bright-green bright-yellow bright-blue bright-magenta bright-cyan bright-white ] 74 | put $valid-colors[(% $pid (count $valid-colors))] 75 | } 76 | 77 | fn -colorized {|what @color| 78 | if (and (not-eq $color []) (eq (kind-of $color[0]) list)) { 79 | set color = [(all $color[0])] 80 | } 81 | if (and (not-eq $color [default]) (not-eq $color [])) { 82 | if (eq $color [session]) { 83 | set color = [(-session-color)] 84 | } 85 | if $bold-prompt { 86 | set color = [ $@color bold ] 87 | } 88 | styled $what $@color 89 | } else { 90 | put $what 91 | } 92 | } 93 | 94 | fn -glyph {|segment-name| 95 | if (has-key $glyph $segment-name) { 96 | put $glyph[$segment-name] 97 | } else { 98 | put $default-glyph[$segment-name] 99 | } 100 | } 101 | 102 | fn -segment-style {|segment-name| 103 | if (has-key $segment-style $segment-name) { 104 | put $segment-style[$segment-name] 105 | } else { 106 | put $default-segment-style[$segment-name] 107 | } 108 | } 109 | 110 | fn -colorized-glyph {|segment-name @extra-text| 111 | -colorized (-glyph $segment-name)(str:join "" $extra-text) (-segment-style $segment-name) 112 | } 113 | 114 | fn prompt-segment {|segment-or-style @texts| 115 | var style = $segment-or-style 116 | if (or (has-key $default-segment-style $segment-or-style) (has-key $segment-style $segment-or-style)) { 117 | set style = (-segment-style $segment-or-style) 118 | } 119 | if (or (has-key $default-glyph $segment-or-style) (has-key $glyph $segment-or-style)) { 120 | set texts = [ (-glyph $segment-or-style) $@texts ] 121 | } 122 | var text = $prompt-segment-delimiters[0](str:join ' ' $texts)$prompt-segment-delimiters[1] 123 | -colorized $text $style 124 | } 125 | 126 | var segment = [&] 127 | 128 | var last-status = [&] 129 | 130 | fn -parse-git {|&with-timestamp=$false| 131 | set last-status = (gitstatus:query $pwd) 132 | if $with-timestamp { 133 | set last-status[timestamp] = ($git-get-timestamp) 134 | } 135 | } 136 | 137 | set segment[git-branch] = { 138 | var branch = $last-status[local-branch] 139 | if (not-eq $branch $nil) { 140 | if (eq $branch '') { 141 | set branch = $last-status[commit][0..7] 142 | } 143 | prompt-segment git-branch $branch 144 | } 145 | } 146 | 147 | set segment[git-timestamp] = { 148 | var ts = $nil 149 | if (has-key $last-status timestamp) { 150 | set ts = $last-status[timestamp] 151 | } else { 152 | set ts = ($git-get-timestamp) 153 | } 154 | prompt-segment git-timestamp $ts 155 | } 156 | 157 | fn -show-git-indicator {|segment| 158 | var status-name = [ 159 | &git-dirty= unstaged &git-staged= staged 160 | &git-ahead= commits-ahead &git-untracked= untracked 161 | &git-behind= commits-behind &git-deleted= unstaged 162 | ] 163 | var value = $last-status[$status-name[$segment]] 164 | # The indicator must show if the element is >0 or a non-empty list 165 | if (eq (kind-of $value) list) { 166 | not-eq $value [] 167 | } else { 168 | and (not-eq $value $nil) (> $value 0) 169 | } 170 | } 171 | 172 | fn -git-prompt-segment {|segment| 173 | if (-show-git-indicator $segment) { 174 | prompt-segment $segment 175 | } 176 | } 177 | 178 | #-git-indicator-segments = [untracked deleted dirty staged ahead behind] 179 | var -git-indicator-segments = [untracked dirty staged ahead behind] 180 | 181 | each {|ind| 182 | set segment[git-$ind] = { -git-prompt-segment git-$ind } 183 | } $-git-indicator-segments 184 | 185 | set segment[git-combined] = { 186 | var indicators = [(each {|ind| 187 | if (-show-git-indicator git-$ind) { -colorized-glyph git-$ind } 188 | } $-git-indicator-segments)] 189 | if (> (count $indicators) 0) { 190 | var color = (-segment-style git-combined) 191 | put (-colorized $prompt-segment-delimiters[0] $color) $@indicators (-colorized $prompt-segment-delimiters[1] $color) 192 | } 193 | } 194 | 195 | fn -prompt-pwd { 196 | var tmp = (tilde-abbr $pwd) 197 | if (== $prompt-pwd-dir-length 0) { 198 | put $tmp 199 | } else { 200 | re:replace '(\.?[^/]{'$prompt-pwd-dir-length'})[^/]*/' '$1/' $tmp 201 | } 202 | } 203 | 204 | set segment[dir] = { 205 | prompt-segment dir (-prompt-pwd) 206 | } 207 | 208 | var uid = (id -u) 209 | set segment[su] = { 210 | if (eq $uid $root-id) { 211 | prompt-segment su 212 | } 213 | } 214 | 215 | set segment[timestamp] = { 216 | prompt-segment timestamp (date +$timestamp-format) 217 | } 218 | 219 | set segment[session] = { 220 | prompt-segment session 221 | } 222 | 223 | set segment[arrow] = { 224 | var end-text = '' 225 | if $space-after-arrow { set end-text = ' ' } 226 | -colorized-glyph arrow $end-text 227 | } 228 | 229 | fn -interpret-segment {|seg| 230 | var k = (kind-of $seg) 231 | if (eq $k 'fn') { 232 | # If it's a lambda, run it 233 | $seg 234 | } elif (eq $k 'string') { 235 | if (has-key $segment $seg) { 236 | # If it's the name of a built-in segment, run its function 237 | $segment[$seg] 238 | } else { 239 | # If it's any other string, return it as-is 240 | put $seg 241 | } 242 | } elif (or (eq $k 'styled') (eq $k 'styled-text')) { 243 | # If it's a styled object, return it as-is 244 | put $seg 245 | } else { 246 | fail "Invalid segment of type "(kind-of $seg)": "(to-string $seg)". Must be fn, string or styled." 247 | } 248 | } 249 | 250 | fn -build-chain {|segments| 251 | if (eq $segments []) { 252 | return 253 | } 254 | for seg $segments { 255 | if (str:has-prefix (to-string $seg) "git-") { 256 | -parse-git 257 | break 258 | } 259 | } 260 | var first = $true 261 | var output = "" 262 | for seg $segments { 263 | set output = [(-interpret-segment $seg)] 264 | if (> (count $output) 0) { 265 | if (not $first) { 266 | if (or $show-last-chain (not-eq $seg $segments[-1])) { 267 | -colorized-glyph chain 268 | } 269 | } 270 | put $@output 271 | set first = $false 272 | } 273 | } 274 | } 275 | 276 | fn prompt { 277 | if (not-eq $prompt-segments []) { 278 | -build-chain $prompt-segments 279 | } 280 | } 281 | 282 | fn rprompt { 283 | if (not-eq $rprompt-segments []) { 284 | -build-chain $rprompt-segments 285 | } 286 | } 287 | 288 | fn init { 289 | set edit:prompt = $prompt~ 290 | set edit:rprompt = $rprompt~ 291 | } 292 | 293 | var find-all-user-repos = { 294 | fd -H -I -t d '^.git$' ~ | each $path:dir~ 295 | } 296 | 297 | var summary-repos-file = ~/.elvish/package-data/elvish-themes/chain-summary-repos.json 298 | 299 | var summary-repos = [] 300 | 301 | fn -write-summary-repos { 302 | mkdir -p (path:dir $summary-repos-file) 303 | to-json [$summary-repos] > $summary-repos-file 304 | } 305 | 306 | fn -read-summary-repos { 307 | try { 308 | set summary-repos = (from-json < $summary-repos-file) 309 | } catch { 310 | set summary-repos = [] 311 | } 312 | } 313 | 314 | fn summary-data {|repos| 315 | each {|r| 316 | try { 317 | cd $r 318 | -parse-git &with-timestamp 319 | var status = [($segment[git-combined])] 320 | put [ 321 | &repo= (tilde-abbr $r) 322 | &status= $status 323 | &ts= $last-status[timestamp] 324 | ×tamp= ($segment[git-timestamp]) 325 | &branch= ($segment[git-branch]) 326 | ] 327 | } catch e { 328 | put [ 329 | &repo= (tilde-abbr $r) 330 | &status= [(styled '['(to-string $e)']' red)] 331 | &ts= "" 332 | ×tamp= "" 333 | &branch= "" 334 | ] 335 | } 336 | } $repos 337 | } 338 | 339 | fn summary-status {|@repos &all=$false &only-dirty=$false| 340 | var prev = $pwd 341 | 342 | # Determine how to sort the output. This only happens in newer 343 | # versions of Elvish (where the order function exists) 344 | use builtin 345 | var order-cmd~ = $all~ 346 | if (has-key $builtin: order~) { 347 | set order-cmd~ = { order &less-than={|a b| . 17 | 18 | [[file:images/screenshot.jpg]] 19 | 20 | This file is written in [[https://leanpub.com/lit-config][literate programming style]], to make it easy to explain. See [[file:chain.elv][chain.elv]] for the generated file. 21 | 22 | * Table of Contents :TOC_3:noexport: 23 | - [[#use][Use]] 24 | - [[#prompt-theme-configuration][Prompt theme configuration]] 25 | - [[#git-repo-summary-display][Git repo summary display]] 26 | - [[#implementation][Implementation]] 27 | - [[#base-code-and-default-values][Base code and default values]] 28 | - [[#general-utility-functions][General utility functions]] 29 | - [[#built-in-segment-definitions][Built-in Segment Definitions]] 30 | - [[#git-related-segments][git-related segments]] 31 | - [[#dir][dir]] 32 | - [[#su][su]] 33 | - [[#timestamp][timestamp]] 34 | - [[#session][session]] 35 | - [[#arrow][arrow]] 36 | - [[#chain--and-prompt-building-functions][Chain- and prompt-building functions]] 37 | - [[#initialization][Initialization]] 38 | - [[#bonus-displaying-the-status-of-several-git-repos-at-once][Bonus: displaying the status of several git repos at once]] 39 | 40 | * Use 41 | 42 | To use this theme, first install the [[https://github.com/zzamboni/elvish-themes][github.com/zzamboni/elvish-themes]] package using [[https://elvish.io/ref/epm.html][epm]]: 43 | 44 | #+begin_src elvish :tangle no 45 | epm:install github.com/zzamboni/elvish-themes 46 | #+end_src 47 | 48 | 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. 49 | 50 | Add the following to you =~/.elvish/rc.elv= file to load and configure the theme: 51 | 52 | *Note:* You have to call =chain:init= to set up the prompt, just loading the module does not configure it. 53 | 54 | #+begin_src elvish :tangle no 55 | use github.com/zzamboni/elvish-themes/chain 56 | chain:init 57 | #+end_src 58 | 59 | ** Prompt theme configuration 60 | 61 | If you want the prompt to be shown using bold fonts, set the following: 62 | 63 | #+begin_src elvish :tangle no 64 | chain:bold-prompt = $true 65 | #+end_src 66 | 67 | The default Elvish prompt settings work fine, but if you want to fine tune them, check the [[https://elvish.io/ref/edit.html#prompts][Prompts]] documentation. For example, if you want to keep the prompt from appearing inverted when "stale" (this may happen if you use git segments and are in a large git repo), you could set =$edit:prompt-stale-transform= to an identity function: 68 | 69 | #+begin_src elvish :tangle no 70 | edit:prompt-stale-transform = $all~ 71 | #+end_src 72 | 73 | On the other hand, if you want the prompt to be grayed out when stale, you can do the following: 74 | 75 | #+begin_src elvish :tangle no 76 | edit:prompt-stale-transform = { each [x]{ styled $x[text] "bright-black" } } 77 | #+end_src 78 | 79 | The prompt 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: 80 | 81 | #+begin_src elvish :tangle (concat (file-name-sans-extension (buffer-file-name)) ".elv") :comments no 82 | var prompt-segments-defaults = [ su dir git-branch git-combined arrow ] 83 | var rprompt-segments-defaults = [ ] 84 | #+end_src 85 | 86 | Each element can be any of the following: 87 | 88 | - 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-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); 89 | - A string or the output of [[https://elvish.io/ref/edit.html#editstyled][styled]], which will be displayed as-is; 90 | - A lambda, which will be called and its output displayed; 91 | - The output of a call to =chain:segment