intro

Acme has a neat little feature where if you right click on a text like /tmp/a.txt:2, it would open /tmp/a.txt in another buffer/window and highlight the second line.

However, since each program, or more specifically any compile program, outputs different line numbers in a different way, one must write different parsers for editors like acme.

Thankfully, acme’s developer thought about this and made the parsing of right clicks external. The plumber is the program responsible for handling all right clicks done by acme.

Usually people write custom selectors to execute specific programs tailored to specific files. An example of this is matching image extensions such as "png", "jpeg" and "webp" with an image viewer.

In this article, I want to look into the process of creating a custom filter for plumber, how to setup a good development environment for writing plumber rules and finally the tsserver parsing plumb rules.

resources

Since acme and plumber are both software from Plan 9, their documentataion is pretty scarce. With the exception of some few blog posts, there are little to no resources documenting how plumber works.

Generally, the man page for plumber is the place to go. It offers very basic information about plumber and definitions for stuff but not smart uses.

mostlymaths.net’s article is very good. The Plumbing and other utilities paper is also good.

One more resource that was indensible for me was regexr.com. This basically allows you to write regular expressions and test them and as you could probably guess, plumber heavily uses regular expressions.

how to write a plumbing rule

The order of plumbing rules is very simple. First in First out.

Usually, copy pasting this and modifying to match as you fit works.

type is text
data matches '[a-zA-Z0-9]'
data matches '[-_]'
arg isfile $1.ts
data set $file
plumb to edit

If you look at the manpage, type is text is literally the only type there is. The data matches line is a line that should be followed by a regular expression.

You could wrap any regular expression with single quotes ‘. Also, you could use simple variables and define them like so variablename=123 and reference them like so $variablename.

If the regular expression is matched, $n is defined for each group. So, if your rule is data matches '([a-z])([A-Z])' and your text is aA, then $1=a and $2=A and also $0=aA.

The arg isfile $1.ts line validates if the value is a file, i.e. if the $1.ts is a file, the rule continues to being parsed.

Also, the isfile can be swapped with isdir. Both set $file and $dir respectively if they got executed.

data set $file essentially sets the file and is only necessary when plumb to edit. Other rules like matching image viewers/pdf viewers with their respective file extensions are much simpler:

type is text
data matches $filereg
data matches ($filereg)\.(doc|rtf)'
arg isfile	$0
plumb to msword
plumb start wdoc2txt $file

plumb start is a command that starts another command and their arguments. Essentially, you can run any command with plumb start.

One thing I forgot to mention is that if you want to set the address for the file, you just set an attribute using attr add addr=$2. That’s precisely how we get tsserver’s error messages to work with acme.

tsserver’s plumbing rule

Check this rule out:

# support for ts line number and column
type is text
data matches '([a-zA-Z0-9_\-./]+)(\(([0-9].*),\s?([0-9].*)\))'
arg isfile $1
attr add addr=$3
data set $file
plumb to edit

This rule basically says, match anything amount of text that has a ($num, $num) where $num is a number. It doesn’t reference a variable called number but, instead, (0-9.*) references the idea of a number.

Now, we first check if the first section is a file through arg isfile $1 because we don’t want any nonsense followed up by (3, 4) to open a non existing file. Then, we set the address to that of the first number in ($num, $num).

For example, take the following annoying line that gets spammed over and over when I use vercel and svelte.

.svelte-kit/tsconfig.json(50,3): error TS5023: Unknown compiler option 'ignoreDeprecations'.

If we right click from acme on the start of the line until the first space(just before the word error), the variables would be as follows:

$1: .svelte-kit/tsconfig.json
$2: (50,3)
$3: 50
$4: 3

opening js file when it ends with ts

One strange thing about working with Typescript is that this is illegal:

import Whatever from "./ok.ts";

No, the word Whatever is not illegal, nor are there any syntax issues, the thing that is wrong is the file. I did not make any mistake, the file “ok.ts” exists and has valid typescript code, but, the issue is that you shouldn’t import a ts file using its actual name, no, you should import a typescript file with a javascript extension.

That does not mean you should make a Typescript file end with js, no, you make it end with ts. But when you import it, make sure it ends with js.

Sounds counter inuititve, right? It is.

Read the github issue for proof.

Anycase, I am not here to rant about Typescript, maybe another day. When working with acme, I was annoyed that .js didn’t open .ts file and acme was right. It is just I want acme to act wrongly so I can get the job done.

Regardless, here’s a rule I made to check if a .js file exists when right clicking a .ts file.

filereg='([a-zA-Z0-9_\-./\[\]@]+)'
# check for js file when ts file exists
type is text
data matches $filereg'\.js'
arg isfile $1.ts
data set $file
plumb to edit

Now, if you right click ok.js and ok.ts exists, ok.ts will open. Woohoo.

write plumbing rules faster

It is a trend in programming that the fact you get a result, the faster you’ll be able to debug it. Nobody wants to restart a server and wait X amount of seconds to check if their configuration is correct, if reloading is an option, it should be used.

Luckily, the plumber authors thought of this and exposed a 9p server that allows you to directly write the rules onto the plumber server. But be careful as it overwrites any existing rules.

cat ./rules | 9p write plumb/rules

If the rules have some errors, the old rules will remain and 9p will exit with an error.

I have that shell command as a comment that starts with a hash at the end of rules file for easier reloading. At times, I put it in the acme status bar to execute it faster.

I find writing plumber rules to be very straight forward once I obtain the right regular expression. Then it is a matter for checking compatiblity between plumber’s internal regular expression engine and the one I wrote the regular expression in.

is all of this worth it?

YES. Definitely. It is always worth it to make your setup comfortable enough so that you have your things the way you want them.

This is a bit off topic and that’s intentional. I had always procrastinated setting up plumber with my own plumbing rules because I thought the time spent wasn’t worth it. I was wrong, it is so worth it.

xdg-open sucks compared to plumb & plumber. Why only open files? You can also open URLs with their own special program.

Here’s a good example: I have a news reader program that I use called newsboat. This program just fetches my RSS feeds, organizes them, makes them indexable and opens them if necessary. It opens them usually in the browser.

But, with plumb, I can customize how each URL should open. For example, say I want to open a YouTube video but I don’t want to use my browser, I could use a program like mpv to YouTube videos.

Since YouTube comes with its algorithimic layout that optimizes for my maximum amount of consumption, I tend to use mpv or prefer it over regular old YouTube.

Other examples could be parsing tracking numbers and opening their tracking pages, etc. Personally, I prefer using my own programs rather than the often random xdg-open. Here are some of my image and video rules:

# image files go to sxiv
type is text
data matches '[a-zA-Z¡-￿0-9_\-./@]+'
data matches '([a-zA-Z¡-￿0-9_\-./@]+)\.(jpe?g|JPE?G|gif|GIF|tiff?|TIFF?|ppm|bit|png|PNG)'
arg isfile	$0
plumb to image
plumb start sxiv $file

video='mpv-quiet'

# https://www.youtube.com/watch?v=sPc25vd7jSw
type is text
data matches $earl
data matches 'https://(www\.)?youtube\.com.*'
plumb to video
plumb start $video $0

# any file that ends with these file extensinos
type is text
data matches $filereg'\.(mp4|webm|mkv|mov|avi|flv)'
arg isfile $0
plumb to video
plumb start $video $0