alexkarle.com

Source for alexkarle.com
git clone git://git.alexkarle.com/alexkarle.com.git
Log | Files | Refs | README | LICENSE

plan9-acme-git-gui.txt (8143B) [raw]


      1 # Using the Plan 9 Plumber to Turn Acme into a Git GUI
      2 
      3 _Published: August 31, 2022_
      4 
      5 ## Backstory
      6 
      7 I first gave Acme a [somewhat serious spin](/blog/exploring-plan9.html)
      8 ~4 months ago and decided that the mouse-driven editor was a bit too
      9 unwieldy coming from my keyboard focused tool suite.
     10 
     11 
     12 However, a couple weeks ago I watched a snippet of the Lex Fridman podcast
     13 where he [interviewed John Carmack](https://www.youtube.com/watch?v=tzr7hRXcwkw)
     14 on IDEs (the tldr being that an IDE with a powerful debugger is crucial).
     15 Carmack advocating for a heavy IDE naturally inspired me to revisit one
     16 of the most spartan editors I've worked with: Acme.
     17 
     18 My previous post covered some of my first impressions and basics of what
     19 Acme is, so in this post I wanted to do a deep dive into one of my favorite
     20 features: the "look"/"plumb" behavior. To illustrate it, I'll walk through
     21 how I was able to use it to turn Acme into a Git GUI.
     22 
     23 ## The Power of Button 3
     24 
     25 Button 3, or "right click", will "look" in Acme. More specifically, it
     26 will take either the selected text (via Button 1) or the word under the
     27 cursor and try to:
     28 
     29 1. Identify if it's a file/directory that exists, and open it in a buffer
     30 2. Send the snippet to the `plumber(4)`
     31 3. If all else fails, search for the word in the buffer
     32 
     33 Number (3) can be a great way to quickly click through usages of a
     34 variable in a function, and (1) is super helpful in exploring the
     35 filesystem and navigating to specific portions of files from tool
     36 output, but (2) is where the magic begins.
     37 
     38 
     39 The plumber gets its name from being a message bus between Plan 9
     40 programs.  In a Plan 9 ecosystem, it appears (I haven't tested this)
     41 that individual servers for things like "web" or "mail" run and
     42 programs like Acme can interact with them by sending messages with
     43 `plumb(1)` through the plumber.
     44 
     45 In practice on a UNIX-like system, I'm running very few servers
     46 (just the plumber itself and `fontsrv(4)` for nicer fonts in Acme),
     47 so the plumbing rules usually have a fallback option of running a
     48 command (rather than passing it to a server).
     49 
     50 The plumber deciphers the messages from `plumb` by matching the
     51 data it receives (in this case the text under our click) against a
     52 series of patterns in a set of rules outlined in `plumb(7)`. By
     53 adding your own rules, you can configure button 3 to open any
     54 external program. Simple rules might match URLs and open a browser,
     55 but given that Acme itself exposes effectively an API through the
     56 filesystem, we can write little programs that in turn modify the
     57 contents of Acme.
     58 
     59 ## Plumbing Rules for Git
     60 
     61 In Git, all objects (primarily commits, blobs, and trees) have a
     62 SHA, which is a hexidecimal string, making it easy to match on. Git
     63 likes to shorten them in tool output, so a primitive regex to match
     64 them might be:
     65 
     66 
     67 	[a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9]+
     68 
     69 That's 6 or more hex digits in a row (an astute reader might warn
     70 that this would also match, say, CSS color codes, but for our
     71 purposes it'll work!).
     72 
     73 The full plumbing rule looks like so (in `~/lib/plumbing`):
     74 
     75 	type is text
     76 	data matches '[a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9]+'
     77 	plumb start rc -c 'git -C '$wdir' show '$0' >[2=1] | plumb -i -d edit -a ''action=showdata filename=/'$wdir'/'$0''''
     78 
     79 We tell the plumber that if the type of the message is text, and if
     80 it matches our pattern, that it should start the external program `rc(1)`
     81 (the plan9 shell) with the command:
     82 
     83 	git -C $wdir show $0 >[2=1]
     84 
     85 To run git in the working directory of the buffer the click was
     86 made on and run `git-show(1)` on the full text that was matched
     87 (with stderr duped to stdout).
     88 
     89 This command of course doesn't do much by itself--it will output
     90 to stdout.  So to make use of the output, we send it back to
     91 `plumb(1)`, this time telling it to send to the `-d` (destination)
     92 `edit` (editor) with the action of showing data. This will open a
     93 new Acme buffer with the contents of the git object (whether it's
     94 a commit, the file for the blob, etc).
     95 
     96 
     97 ## Going Full GUI
     98 
     99 At this point, we could run `git-log(1)` in a `win(1)` buffer (a
    100 shell session in Acme) and start clicking SHAs to see the diffs,
    101 but that's not super helpful.
    102 
    103 Looking at how I like to interact with Git in Vim, I commonly do
    104 the following:
    105 
    106 
    107 1. Look at the full git log, with ability to dive into commits
    108 2. View the short git log for a specific file to see when things changed at a high level
    109 3. View the `git-blame(1)` to see when things changed at a line-level
    110 
    111 The first can be solved by writing a small wrapper around git-log, `gl`:
    112 
    113 	#!/bin/sh
    114 	# git-log, but piped through plumb so that it:
    115 	#   1. doesn't scroll to the end
    116 	#   2. has the proper "filename" so that plumbing SHA's works
    117 	git log "$@" | plumb -i -d edit -a "action=showdata filename=/$(pwd)/git-log"
    118 
    119 For a shortened version (easier to browse), we can simply call this
    120 with `--oneline` (`glo`):
    121 
    122 
    123 	#!/bin/sh
    124 	exec gl --oneline "$@"
    125 
    126 The output on this repo looks like so:
    127 
    128 	[/path/to/alexkarle.com/git-log Del Snarf Redo | Look]
    129 	28a5f1c blog: Edit typos and small rephrasings for wggen post
    130 	85b54e2 blog: Add post on wggen tool to manage wg creds
    131 	6c6d8e0 blog: Add post about garbash.com
    132 	da01dba blog: Add post about mandoc resume
    133 	...
    134 
    135 Finally, we can get a per-file history by just filtering `glo` (`gv`):
    136 
    137 	#!/bin/sh
    138 	# gv -- imitation of :GV! vim plugin
    139 	file=${1:-$samfile}
    140 	glo -- "$file"
    141 
    142 Here we see a nice trick: `gv` takes in an argument (so that we can
    143 specify the file to log), but if it's empty it defaults to `$samfile`,
    144 which is set by Acme to be the current buffer being edited. So
    145 putting `gv` in the tag of the README and middle clicking to execute
    146 gives the same output as before, but filtered!
    147 
    148 
    149 Git blame uses the same concepts (`gbl`):
    150 
    151 	#!/bin/sh
    152 	# git blame, in acme!
    153 	file=${1:-$samfile}
    154 	git blame --date="short" "$file" | plumb -i -d edit -a "action=showdata filename=$file:BLAME"
    155 
    156 Calling `gbl` on the README pops up a new buffer with the following:
    157 
    158 	[/path/to/alexkarle.com/README.md:BLAME Del Snarf Redo | Look]
    159 	5e9783a6 (Alex Karle 2020-06-18  1) alexkarle.com
    160 	5e9783a6 (Alex Karle 2020-06-18  2) =============
    161 	016a5d70 (Alex Karle 2020-07-14  3) My small corner of the internet.
    162 
    163 This may seem unhelpful, but remember that clicking any of those SHA's will
    164 open then in a new buffer with the full diff! Clicking 5e9783a6 gives:
    165 
    166 	[/path/to/alexkarle.com/5e9783a6 Del Snarf Redo | Look]
    167 	commit 5e9783a6ce559f76da83b2530059c9664f43306e
    168 	Author: Alex Karle <email @ domain>
    169 	Date:   Thu Jun 18 00:18:48 2020 -0400
    170 	...
    171 	...
    172 	diff --git a/.gitignore b/.gitignore
    173 	new file mode 100644
    174 	index 0000000..84c048a
    175 	--- /dev/null
    176 	+++ b/.gitignore
    177 	@@ -0,0 +1 @@
    178 	+/build/
    179 
    180 
    181 And what's this? More SHA's to click? 84c048a will give the full
    182 contents of .gitignore at this commit (if the diff doesn't show
    183 it).
    184 
    185 The most incredible bit is that Acme doesn't need to do much to
    186 support this use case--the flexibility of choosing an external (and
    187 extensible) program to match the text and the ability to spawn new
    188 buffers in Acme via any language externally enables this beautifully.
    189 Compare this to the [~500L of VimL in
    190 vim-fugitive](https://github.com/tpope/vim-fugitive/blob/9fcac5b/autoload/fugitive.vim#L6712)
    191 to convert the Vim sidebar into an interactive blame viewer. (No
    192 shade towards Tim Pope--if you're reading this, huge thanks for
    193 everything you've ever written, it's inspired me and shaped my
    194 career!).
    195 
    196 
    197 
    198 ## Conclusion
    199 
    200 I'm really glad I chose to revisit Acme. While it's probably never
    201 going to replace Vim in my toolkit, it was such an incredible
    202 experience to be able to turn the tool into a pretty solid Git GUI
    203 (minus syntax highlighting of course) in just about an hour or so
    204 (including time to learn plumbing rules).
    205 
    206 The concept of a universal yet personally customizable plumber is
    207 beyond cool, and the fact that Acme has one of the 3 buttons reserved
    208 to plumb text allows for developers to easily hyperlink
    209 plaintext, which is awesome.
    210 
    211 If you'd like to give it a spin, all my scripts (including my starter
    212 script for Acme) are Open Source and live in my
    213 [dotfiles](https://sr.ht/~akarle/dotfiles).
    214