~kris/dots

srice

ref: 347c183a3d4728b74c9a3be765bb8e97386b2ae8 srice/doc/plan9-acme.md -rw-r--r-- 23.2 KiB
347c183a — Kris Yotam wallpapermenu: make CAPS call optional so it doesn't error where CAPS is absent 2 months ago

#Plan 9, Acme, and Structural Regex Reference

A practical reference for Plan 9 concepts, the acme editor, structural regular expressions, and how to apply these ideas on Linux.


#1. Acme Interaction Model

Acme is a mouse-centric editor built around three buttons and a uniform interaction model. Every window is text. Every command is text. The editor is a file server.

#Mouse Buttons

Button Name Action
1 Left Select text (click to place, drag to select)
2 Middle Execute: run the selected/clicked text as a command
3 Right Plumb or search: opens file, jumps to line, or searches

Chording: hold button 1 then click 2 to cut, hold button 1 then click 3 to paste.

#The Tag Bar

Each window has a tag bar at the top containing the filename and a set of built-in commands: Del Snarf Get Put Look. Clicking any of these with button 2 executes them. You can type additional commands into the tag bar.

#Middle-Click to Execute

Any text in any window can be executed by middle-clicking. Acme looks up the text as a command in $PATH. Output appears in a new window or the scratch column.

middle-click "ls -la"     -> runs ls -la, output in new window
middle-click "go build"   -> runs go build in the current directory
middle-click "Edit ,d"    -> deletes all text in current window

Right-clicking text sends it to the plumber. The plumber inspects the text and decides what to do: open a file, jump to a line, load a URL, open a man page. If no plumber rule matches, acme searches for the text in the current window.

right-click "main.go:42"        -> opens main.go, jumps to line 42
right-click "http://example.com" -> opens browser
right-click "open"               -> searches for "open" in current file
right-click "fork(2)"            -> opens man page for fork section 2

#2. Key Commands

These are typed into the tag bar or any window and executed with button 2.

:42           jump to line 42 in current window
:/pattern/    jump to next match of pattern
Edit =        show current filename and line number

#Window and Session Management

win           open a new shell window inside acme
Dump          save the current acme session layout to a file
Load          restore a saved session layout
New           open a new empty window
Del           close current window

#Display

Font /lib/font/bit/lucsans/unicode.8.font   set font
Font /usr/local/plan9/font/fixed/unicode.6x13.font

#File Operations

Get           reload file from disk (button 2 on "Get" in tag)
Put           write file to disk (button 2 on "Put" in tag)

#3. Keyboard Shortcuts

Acme uses mostly standard Unix line-editing keys plus a few additions.

Shortcut Action
Ctrl-U Delete from cursor to start of line
Ctrl-W Delete previous word
Ctrl-A Move to start of line
Ctrl-E Move to end of line
Ctrl-F Fetch/complete filename
Ctrl-H Backspace
Ctrl-I Tab
Ctrl-J Newline
Escape Select text typed since last mouse click (press again to delete it)

Escape is the undo primitive in acme. Typing then pressing Escape selects what you typed. Pressing Escape again deletes it.

There is no conventional undo stack. The workflow is: type text, verify it, keep or Escape-delete it. For larger operations, use Edit commands which are composable and reversible by design.


#4. Acme Filesystem API (9P)

Acme exposes itself as a 9P file server. Every window is a directory under the mount point. This is how shell scripts and programs interact with acme.

#Listing Windows

9 ls acme          # list all acme window directories
ls $NAMESPACE/acme # direct mount point access

Each window directory contains:

acme/
  <id>/
    addr       current address (read/write)
    body       window content (read/write)
    ctl        control messages (write)
    data       window data at addr (read/write)
    errors     error output destination
    event      mouse/keyboard events (read)
    tag        tag bar content (read/write)
    xdata      extended data

#Reading and Writing

# read window content
cat acme/3/body

# write to a window
echo 'hello world' > acme/3/body

# set address then read at that address
echo '0,.' > acme/3/addr
cat acme/3/data

# append text to body
echo ', a/new line/' | 9p write acme/3/ctl

#Control Messages

Write these strings to the ctl file:

clean       mark file as unmodified
dirty       mark file as modified
del         delete window
get         reload from file
put         write to file
name foo    rename window
show        bring window to front

Example:

echo 'name /tmp/scratch' > acme/$winid/ctl

#The $winid Variable

When acme runs a command, it sets $winid to the ID of the window that executed the command. Scripts use this to target the correct window.

# append text to the window that ran this script
echo 'done' >> acme/$winid/body

# show current file and line
cat acme/$winid/ctl | head -1

#Event Loop (acme scripting)

Programs can read from acme/<id>/event to receive keyboard and mouse events. This is how tools like acmego, acme-lsp, and language servers attach to acme.

Event format: <origin><type><q0> <q1> <flag> <n> <text>

Mx 10 20 0 4 test   # mouse execution of "test" from position 10 to 20

#5. The Edit Command Language

Edit is acme's structural editor, derived from sam. Type Edit <commands> and execute it with button 2.

#Address Syntax

An address specifies a region of text to operate on.

Address Meaning
0 before first character
$ after last character
. current selection
1 line 1
42 line 42
1,42 lines 1 through 42
, entire file (shorthand for 0,$)
/pattern/ next match of pattern after .
?pattern? previous match of pattern before .
+/pattern/ next match after . (explicit forward)
-/pattern/ previous match before .
/foo/,/bar/ from next "foo" to next "bar"
2,/end/ from line 2 to next "end"

#Basic Commands

Edit s/old/new/         substitute first match in .
Edit s/old/new/g        substitute all matches in .
Edit ,s/old/new/g       substitute all matches in file
Edit d                  delete .
Edit ,d                 delete entire file
Edit a/text/            append text after .
Edit i/text/            insert text before .
Edit c/text/            change . to text
Edit p                  print . to acme output window
Edit =                  print filename and line number of .

Using external commands:

Edit ,<cat -n          replace file with cat -n output (number lines)
Edit ,>wc -l           pipe file to wc -l (count lines, no change)
Edit ,|sort            replace file with sorted content
Edit ,|sort -u         replace file with sorted unique content
Edit /TODO/+1,/DONE/-1|sort   sort lines between TODO and DONE markers

#Structural Regular Expressions

The key insight: instead of matching characters, you match structure. x and y let you loop over matches and non-matches.

#x -- Extract and Apply

x/pattern/command -- for each match of pattern in the address, set . to that match and run command.

Edit ,x/TODO.*\n/p           print all TODO lines
Edit ,x/TODO.*\n/d           delete all TODO lines
Edit ,x/^/a/> /              prepend "> " to every line (quote a file)
Edit ,x/\n/a/,/              replace newlines with commas
Edit ,x/[A-Z][a-z]+/         select each capitalized word (. cycles through them)
#y -- Non-Matching Regions

y/pattern/command -- for each region between matches, set . to that region and run command.

Edit ,y/\t/s/ /\t/g          in non-tab regions, replace spaces with tabs
#g and v -- Conditionals

g/pattern/command -- if . contains a match, run command. v/pattern/command -- if . does not contain a match, run command.

Edit ,x/.*\n/g/TODO/p        print lines containing TODO
Edit ,x/.*\n/v/TODO/p        print lines not containing TODO
Edit ,x/.*\n/g/^$/d          delete blank lines
Edit ,x/.*\n/v/^#/d          delete lines that are not comments
#Nesting and Composition

Commands compose. The output address of one command feeds the next.

# Delete all blank lines in a function body
Edit /^func/,/^}/x/.*\n/g/^$/d

# In all struct definitions, sort the fields
Edit ,x/^type.*struct \{/,/^\}/x/^\t[A-Z]/,/\n/|sort

# Replace all occurrences of "foo" that appear on lines containing "bar"
Edit ,x/.*\n/g/bar/s/foo/baz/g

# Number only non-blank lines
Edit ,x/^.+$/=

# Uppercase all words in comments
Edit ,x/(#|\/\/).*\n/x/[a-z]+/|tr a-z A-Z
#Swapping Two Strings

Use a temporary placeholder to avoid double-substitution:

Edit ,s/alpha/PLACEHOLDER/g
Edit ,s/beta/alpha/g
Edit ,s/PLACEHOLDER/beta/g

Or with braces (compound command -- all applied to same address):

Edit ,x/alpha|beta/{
  s/alpha/PLACEHOLDER/
  s/beta/alpha/
  s/PLACEHOLDER/beta/
}

The brace form applies each sub-command to . sequentially within the same address context.

#Cross-File Commands: X and Y

X/pattern/command -- for each open window whose filename matches pattern, run command. Y/pattern/command -- for each open window whose filename does not match, run command.

Edit X/\.go$/,s/fmt\.Print\b/fmt.Println/g    replace in all open .go files
Edit X/\.md$/,x/.*\n/g/TODO/p                 print TODO lines from all open markdown files
Edit Y/\.go$/Del                               close all windows that are not .go files

#6. Plumber Rules

The plumber is Plan 9's dispatcher. It reads rules from $HOME/lib/plumbing and decides what to open or do based on text you right-click.

#Default Rules (plan9port)

# compiler errors: file.go:42:5: message
type is text
data matches '([a-zA-Z0-9_\-./]+):([0-9]+).*'
arg isfile $1
plumb to edit
plumb client window acme
data set $file
attr add addr=$2
# URLs
type is text
data matches 'https?://[a-zA-Z0-9_\-./~:?#&=+%@,;]+'
plumb to web
plumb start chrome $0
# man pages: fork(2), open(2)
type is text
data matches '([a-zA-Z0-9_]+)\(([0-9])\)'
plumb to man
plumb start sh -c 'man '$2' '$1
# git SHA: 40-char hex
type is text
data matches '[0-9a-f]{40}'
plumb to git
plumb start sh -c 'git show '$0' | acme -'
# image files
type is text
data matches '.*\.(png|jpg|jpeg|gif|bmp)'
arg isfile $0
plumb to image
plumb start display $0

#Custom Rule Example

To open .json files in a formatter:

type is text
data matches '.*\.json'
arg isfile $0
plumb to edit
plumb client window acme
data set $0
plumb start window jq . $0

#Reloading Plumber Rules

cat $HOME/lib/plumbing | 9p write plumb/rules

#7. Acme + Git Workflow

These are conventional shell scripts used inside acme. Middle-click to execute.

#gl -- Git Log in Acme Window

#!/usr/bin/env rc
# gl: show git log, clickable SHAs
git log --oneline | sed 's/^/    /' | acme -

#glo -- Git Log with Details

#!/usr/bin/env rc
# glo: full log
git log --format='%H %ad %s' --date=short | acme -

#gv -- Git Show a Commit

#!/usr/bin/env rc
# gv: git show $1
git show $1 | acme -

#gbl -- Git Blame Current File

#!/usr/bin/env rc
# gbl: blame current file
git blame $% | acme -

#gd -- Git Diff

#!/usr/bin/env rc
# gd: diff working tree
git diff | acme -

#Interactive Rebase with Acme as EDITOR

EDITOR=E git rebase -i HEAD~5

E is the plan9port editor wrapper that opens a file in the current acme instance and waits for it to close. This lets you edit the rebase todo list inside acme.

#Committing from Acme

# create commit message file, edit in acme, then commit
echo '' > /tmp/commitmsg
E /tmp/commitmsg
git commit -F /tmp/commitmsg

#8. sam -d for Batch Editing

sam is the predecessor to acme. sam -d runs without any display, reading commands from stdin. This is ideal for scripted file editing.

#Basic Usage

sam -d file.go <<'EOF'
,s/oldname/newname/g
w
EOF

#Piped Workflow

echo ',s/http:/https:/g
w' | sam -d *.md

#Structural Edits via sam -d

# delete all blank lines from file.txt
sam -d file.txt <<'EOF'
,x/^$/d
w
EOF

# prepend shebang to all .sh files missing one
for f in *.sh; do
  sam -d $f <<'EOF'
0g/^#!/ i/#!/usr/bin/env bash\n/
w
EOF
done

#ssam -- Stream sam

ssam is a wrapper that applies sam commands to stdin, outputting to stdout. Useful in pipelines.

# remove trailing whitespace from all lines
cat file.go | ssam ',x/[ \t]+$/d'

# add semicolons to end of lines not already ending in one
cat file.go | ssam ',x/[^;]\n/a/;/'

#9. rc Shell

rc is the Plan 9 shell. It is simpler, more consistent, and more composable than sh or bash.

#Lists Are First-Class

Variables hold lists natively. No word splitting, no glob rescan.

files = (main.go util.go server.go)
for (f in $files) echo $f

# append to a list
files = ($files new.go)

Compare with bash where files="main.go util.go" is just a string and requires careful quoting.

#No Rescan

In bash, $var undergoes glob expansion after assignment. In rc, it does not.

pattern = '*.go'
echo $pattern      # prints *.go literally, not expanded
ls $pattern        # expands here, where you want it

#Simpler Quoting

Single quotes quote literally. Double quotes do not exist as a quoting mechanism. '' inside single quotes produces a literal single quote.

echo 'it''s fine'    # prints: it's fine
echo 'no $expansion' # prints: no $expansion

#String Concatenation with ^

The ^ operator concatenates strings and lists.

base = /usr/local
echo $base^/bin       # /usr/local/bin

prefix = foo
names = (bar baz)
echo $prefix^$names   # foobar foobaz (list expansion)

#File Descriptor Manipulation

cmd >[2=1]         # redirect stderr to stdout (equivalent of 2>&1)
cmd >[2]/dev/null  # discard stderr
cmd <[0]/dev/null  # read stdin from /dev/null

#Function Definitions

fn greet {
    echo hello $1
}

greet world    # prints: hello world

#Environment and Path

path = (/usr/local/bin /usr/bin /bin)   # path is a list
home = /home/kris

#Comparison with sh

Feature sh/bash rc
Lists Strings with splitting Native first-class
Glob rescan Yes (dangerous) No
Quoting Complex Simple
Concatenation Juxtaposition ^ operator
stderr redirect 2>&1 >[2=1]
Functions foo() { } fn foo { }

#10. Rob Pike's 5 Rules of Programming

These rules appear in Pike's notes and talks. They are about systems, not just code.

  1. You can't tell where a program is going to spend its time. Bottlenecks occur in surprising places. Don't guess. Measure.

  2. Measure. Don't tune for speed until you've measured, and even then don't unless one part of the code overwhelms the rest. Premature optimization is wasted effort on the wrong problem.

  3. Fancy algorithms are slow when n is small, and n is usually small. Simple algorithms win in practice. Fancy algorithms have big constants.

  4. Fancy algorithms are buggier than simple ones, and they're much harder to implement. Use simple algorithms and simple data structures. A hash table or a linked list is almost always enough.

  5. Data dominates. If you've chosen the right data structures and organized things well, the algorithms will almost always be self-evident. Program in terms of your data structures. Code follows data.

Rule 5 is the deepest. When the data structure is right, the program writes itself.


#11. cat -v Considered Harmful

This is the title of a 1983 paper by Rob Pike and Brian Kernighan. The principle applies far beyond cat.

#The Argument

cat -v makes non-printing characters visible by converting them to printable representations. It seems helpful. It is harmful because:

  • It destroys data: the output of cat -v is not the same bytes as the input
  • Programs that need visible control characters should handle them themselves
  • Using cat -v in a pipeline corrupts the data for downstream programs
  • It teaches a wrong model: that control characters are inherently problems to be escaped

#The Broader Principle

Tools that silently transform data violate composability. Unix pipes work because tools pass data through unchanged. When a tool modifies data "for display", it breaks the pipeline.

Applied more broadly: do not add smarts to transport layers. The file should contain what the file contains. The display layer can render it however it wants. Do not conflate storage and display.

#Practical Implications

  • Do not use cat -A, cat -e, cat -t in scripts. Use xxd or od for binary inspection.
  • Do not use tools that silently mangle line endings, encodings, or whitespace.
  • Prefer tools that are transparent about their transforms.
  • When writing tools, separate the concerns: read data, process data, present data. Do not mix presentation into processing.

#12. Factotum Credential Management Philosophy

Factotum is the Plan 9 credential manager and authentication agent. It embodies a specific philosophy about how credentials should work.

#The Model

Factotum is a file server (/mnt/factotum). Programs authenticate by reading from and writing to factotum's files. The kernel mediates all access. No program ever sees a raw password; they see tokens that factotum provides.

#Core Principles

Credentials belong to the agent, not the process. A program does not hold a password. It asks factotum "can I authenticate as kris to this host?" and factotum says yes or no, or asks the user.

Authentication is a protocol. Programs speak a challenge-response protocol with factotum. This works for passwords, keys, tickets, and anything else that can be plugged in.

Single point of trust. You type your password once to factotum at login. Everything else delegates to it. No password ever appears in environment variables, files, or process arguments.

Delegation, not copying. A process can be granted the ability to authenticate on your behalf without being given the credential itself. Factotum can grant limited, scoped authority.

#Linux Analogy: ssh-agent

ssh-agent follows the same philosophy for SSH keys. You add your key once (ssh-add), and then all SSH connections negotiate through the agent. The private key never leaves the agent.

The difference is that ssh-agent only does SSH. Factotum is general: it handles any protocol you plug in.

#Applying This Philosophy on Linux

  • Use ssh-agent and ssh-add. Never put private keys in environment variables.
  • Use pass or gopass for passwords. They never appear in shell history.
  • Use gpg-agent for GPG operations. Keys live in the agent.
  • Avoid tools that require --password flags on the command line (they appear in ps output).
  • Prefer tools that read credentials from sockets or files with mode 600.

#13. Plan 9 Ideas on Linux

Plan 9 is not widely deployed, but its ideas are available on Linux through plan9port and disciplined practice.

#plan9port

plan9port is a port of Plan 9 userland tools to Unix.

# install on Arch Linux
paru -S plan9port

# or from source
git clone https://github.com/9fans/plan9port
cd plan9port && ./INSTALL

Key tools it provides: acme, sam, 9p, plumber, rc, mk, 9, factotum (partial).

#Running Acme on Linux

# set PLAN9 environment variable
export PLAN9=/usr/local/plan9
export PATH=$PLAN9/bin:$PATH

# start acme
acme

#sam -d on Linux

Available immediately after installing plan9port. Excellent for scripted edits.

# edit multiple files with structural regex
for f in src/*.go; do
    sam -d $f <<'EOF'
,x/.*\n/g/FIXME/s/FIXME/TODO/g
w
EOF
done

#ssam on Linux

Also available via plan9port. Use it for stream processing:

git diff --stat | ssam ',x/[0-9]+/p' | sort -n

#The Plumber on Linux

The plumber requires a running plumb daemon.

# start plumber
plumber

# reload rules
cat $HOME/lib/plumbing | 9p write plumb/rules

Acme sends right-click events to the plumber automatically when both are running.

#Structural Thinking Without Plan 9 Tools

Even without acme, you can apply structural regex thinking:

With ripgrep:

# find all lines with TODO in .go files, then process
rg 'TODO' --files-with-matches | xargs sam -d <<'EOF'
,x/.*\n/g/TODO/s/TODO(\([^)]+\))?/FIXME/g
w
EOF

With awk:

# awk can do g/v logic: print blocks between markers
awk '/BEGIN/,/END/' file.txt
awk '!/^#/ && NF > 0' file.txt    # non-comment, non-blank lines

With perl -0777:

# perl with -0777 slurps whole file: enables cross-line regex
perl -0777 -i -pe 's/foo.*?bar/REPLACED/gs' file.txt

sed with ranges:

sed -n '/^func /,/^}/p' main.go    # print function bodies
sed '/^func /,/^}/s/TODO/FIXME/g' main.go

#mk vs make

mk is the Plan 9 build tool. It is simpler than make: no implicit rules, no magic variables, consistent behavior.

plan9port ships mk. Using it on Linux:

# Mkfile (note: capital M)
all:V:
    go build ./...

test:V:
    go test ./...

clean:V:
    rm -f bin/*

The :V: suffix marks a target as a virtual target (like .PHONY in make).

#Namespace Manipulation

Plan 9 has per-process namespaces. On Linux, containers and unshare provide a rough equivalent.

# run a command in a new mount namespace
unshare --mount sh

# Linux namespaces are Plan 9 namespaces, implemented later and less cleanly

#The Discipline

The Plan 9 approach applied to Linux+dwm:

  • Keep tools small and composable. Prefer pipelines over monoliths.
  • Write programs that read stdin and write stdout. Make them scriptable.
  • Use text as the universal interface. Avoid binary formats for configuration.
  • Prefer convention over configuration. Agree on file locations and formats.
  • Name things well. A good name is documentation.
  • When something is hard to explain, it probably has the wrong interface.
  • The editor is a tool, not a religion. But use one that can be scripted.
  • Prefer mk over make, rc over bash, sam over sed for structural edits.
  • Measure before optimizing. Data structures before algorithms.

#Quick Reference Card

#Edit Commands Cheat Sheet

,p              print whole file
,d              delete whole file
,s/a/b/g        replace a with b globally
,x/pat/cmd      for each match of pat, run cmd
,x/.*\n/g/pat/p print lines matching pat
,x/.*\n/v/pat/d delete lines not matching pat
,|cmd           pipe whole file through cmd
,<cmd           replace file with cmd output
,>cmd           pipe file to cmd (no replace)
/pat/+1         line after next match of pat
/pat/-1         line before next match of pat
X/\.go$/cmd     run cmd in all open .go windows

#sam -d Cheat Sheet

sam -d file <<'EOF'
,s/old/new/g
w
q
EOF

echo ',s/old/new/g
w' | sam -d file

#rc Cheat Sheet

files = (a b c)          # list
$#files                  # count: 3
$files(2)                # second element: b
fn f { echo $* }         # function
cmd >[2=1]               # stderr to stdout
path = (/usr/bin /bin)   # set path

#Plumber Rule Structure

type is text
data matches 'regex'
arg isfile $0            # optional: verify it's a file
plumb to destination
plumb start command $0