├── LICENSE ├── Makefile ├── README.md ├── TODO ├── _notes ├── 9fans-go.md ├── README.md ├── acme-itself.md └── event-examples-and-notes.txt ├── acorp ├── edit.go ├── event.go └── util.go ├── dirtree ├── README.md └── dirtree.go ├── go.mod ├── go.sum ├── gq ├── README.md └── gq.go ├── pick ├── README.md └── pick.go ├── punt ├── README.md └── punt.go ├── scripts ├── a+ ├── a- ├── acme-command-line.sh ├── acme-fuzzy-window-search.sh ├── afocused ├── alinum ├── c-astyle ├── fmtoff ├── fmton ├── gg ├── json-format ├── spell-check └── start-acme └── snoop-acme ├── README.md ├── afmt.go ├── constants.go ├── ftype.go ├── snoop-acme └── main.go ├── snooper.go └── tcp.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Innes Anderson-Morrison 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 | 23 | © 2020 GitHub, Inc. 24 | Terms 25 | Privacy 26 | Security 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | go install ./... 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ACME-CORP: Utilities and extension programs for the acme text editor 2 | ==================================================================== 3 | 4 | This repo is a collection of scripts, extension programs and helpers for use 5 | with the [acme][0] text editor from [plan9][1]. I highly recommend installing the 6 | wonderful [plan9 from userspace][2] port of plan9 software to unix, optionally 7 | using [my fork][3] for a few tweaks such as additional key bindings for acme 8 | and a suckless style `config.h` file for setting a custom color scheme and 9 | default fonts. 10 | 11 | Each top level directory is a stand alone go binary that should be possible to 12 | run with a vanilla acme installation independently from one another. That said, 13 | the intention is to run all of these as a suite of programs that compliment one 14 | another. 15 | 16 | 17 | ### Inspired by Suckless 18 | I love the [suckless][4] programs. On my home machine I run dwm, st and dmenu 19 | and I have my own [patched versions][5] of all three that I tinker with from time 20 | to time. I'm pretty satisfied with my current setup: despite running on an old, 21 | cheap HP laptop I get pretty snappy performance and I'm yet to see any random 22 | crashes (other than while I'm actively tinkering of course). 23 | 24 | With that in mind, acme-corp is written to be as simple as possible while still 25 | being readable. So, little to no magic hacks, comments and links around any of 26 | the more esoteric pieces of code (in particular the use of `sam` expressions 27 | to manipulate window content) and configuration by modifying the source. As much 28 | as I love writing parsers and command line utilities, it really is far easier to 29 | just pull simple things from environment variables and hard code the rest. 30 | 31 | 32 | ### Installation 33 | As far as I can tell, `go get` -ing this repo and running `go install ./...` in 34 | the root should be all you need to grab the utilities themselves. For the scripts, 35 | you will need to add them to your `$PATH` (I tend to conditionally add them when 36 | starting acme so that I don't clutter up my `$PATH`). Some of the scripts are 37 | simple triggers for the `snooper` so you will need to kick that off in order for 38 | them to do anything. I haven't provided any sort of install script for the 39 | dependencies as they tend to fluctuate a bit as I work on things. Make sure to 40 | read the source of anything you intend to run to see what it expects to be on 41 | your system. 42 | 43 | 44 | ### Current utilities 45 | For more in depth information, please see the individual README files in each 46 | directory and obviously the source code itself. 47 | 48 | * dirtree 49 | * A directory viewer for acme. The built in support for navigating the filesystem 50 | can quickly get out of hand if you are jumping around directories a lot. This 51 | allows you to have a single window that acts as a file tree, allowing you to 52 | move the root when needed. 53 | 54 | * gq 55 | * Mimic the Vim `gq` key sequence. Yes I know that `fmt` exists but I wanted to 56 | write something that did what I wanted out of the box. Essentially this is wrap 57 | lines to a column count (default 80) and preserve any common prefix that is found 58 | in order to give a language agnostic way to tidy up comment blocks. 59 | 60 | * punt 61 | * Quickly open the current window in an external program. Originally intended 62 | for punting things over to Vim if I needed to do some more complicated editing 63 | that I can currently get my head around using `sam` commands. It can also send 64 | window content to GUI based programs (default is to expect that the program 65 | will run in a terminal) via the `-g` flag. 66 | 67 | * snoop-acme 68 | * A local TCP server that tails all acme event streams and runs hooks such as 69 | auto formatting and linting (for known filetypes and tools) and quickly allowing 70 | programs to access the currently focused window. As far as I can tell, the 71 | latter is simply emitted on the event log rather than being a piece of state 72 | that you can pull from the acme virtual file system. My main reason for wanting 73 | to get at this is to drive things like the `acme-fuzzy-window-search.sh` 74 | script. 75 | 76 | 77 | [0]: http://acme.cat-v.org/ 78 | [1]: https://9p.io/plan9/ 79 | [2]: https://9fans.github.io/plan9port/ 80 | [3]: https://github.com/sminez/plan9port/ 81 | [4]: https://suckless.org/ 82 | [5]: https://github.com/sminez/suckless/ 83 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | -- TODO -- 2 | 3 | * Read up on, practice and then write up some more of the `sam` editing 4 | language. Structured regular expressions really do look wonderfully powerful 5 | and it would be good to be able to make proper use of them in general rather 6 | than simply as heavily commented "magic" lines in a couple of the utilities. * 7 | Combined with the point above about having a sort of 'command mode' similar to 8 | vim's Normal mode would allow me to do a lot of what I am after I think. 9 | 10 | * Port over the logic in blobfish to work nicely with acme 11 | * There are probably a number of things that would be nice as a modified version of 'pick' 12 | that allowed for a much larger header? 13 | * That or something that let you do an "info + place to type commands" type interface 14 | 15 | 16 | -- TO LOOK AT -- 17 | 18 | * Look at some other acme resources online: 19 | * Setting plumber rules: (https://github.com/karahobny/acme-utils/blob/master/acme-start.sh) 20 | * GitHub FS: (https://github.com/sirnewton01/ghfs) 21 | * Jira client: (https://github.com/hdonnay/Jira) 22 | * Github client: (https://pkg.go.dev/rsc.io/github/issue?utm_source=godoc) 23 | * Script ideas: (https://github.com/ianzhang366/acmescripts) 24 | * Cross platform hotkey solution? (https://github.com/golang-design/hotkey) 25 | * or this (https://github.com/robotn/gohook) 26 | 27 | 28 | -- TO FIX / IMPROVE -- 29 | 30 | * snooper 31 | * rework the snooper window to instead act more like a menu than a log 32 | * just send the output to +Errors like everything else 33 | * show more of the available commands in the window 34 | * auto add based on the handlers that are registered? 35 | 36 | * dirtree 37 | * Fuzzy file search 38 | * Bookmarked directories 39 | * Jump to dir based on edited line 1 40 | * keep focused line (where possible) when re-rendering 41 | 42 | * pick 43 | * Make linePicker public so it can be used in other programs 44 | * Sort results by line number when enabled 45 | * Jumping to line is a little too fiddly at the moment 46 | 47 | * gq 48 | * better detection of indentation styles (like these nested bullet points) 49 | * auto-set line lenth based on file type 50 | 51 | * command-line 52 | * rewrite as a go program 53 | * Edit commands are not running correctly 54 | * store a command history as well maybe? 55 | * if this is integrated with the snooper then that can just be in memory 56 | 57 | * window search 58 | * making this part of the snooper might be worthwhile as well? 59 | * would still need a script that send the correct message to the snooper 60 | -------------------------------------------------------------------------------- /_notes/9fans-go.md: -------------------------------------------------------------------------------- 1 | Library code for interacting with acme from go 2 | ---------------------------------------------- 3 | source: (https://github.com/9fans/go) 4 | local docs: (go doc 9fans.net/go/acme) 5 | (go doc 9fans.net/go/draw) 6 | (go doc 9fans.net/go/plan9) 7 | (go doc 9fans.net/go/plumb) 8 | 9 | 9fans is (mostly) the work of Russ Cox and Rob Pike, wrapping the the virtual 10 | file system api presented by acme along with interaction with the plumber and 11 | other plan9 system stuff. There is also some work around `draw` which I've not 12 | experimented _too_ much with yet but seems interesting for kicking off some 13 | of your own GUI plan9 goodness if that's your thing. 14 | 15 | This is what is powering pretty much all of `acme-corp`, so thanks! -------------------------------------------------------------------------------- /_notes/README.md: -------------------------------------------------------------------------------- 1 | README 2 | ------ 3 | 4 | These are my assorted notes and thoughts on what I've learned about `acme` 5 | and how I tend to make use of it as "A user interface for programmers". 6 | Acme really is amazingly powerful and while you do give up some more modern 7 | comforts such as syntax highlighting and autocomplete, you actually gain a 8 | huge amount by going "full bell labs" and really embracing the paradigm of 9 | "_everything_ is text". 10 | 11 | I will also freely admit that I absolutely love hacking on things and acme 12 | is a wonderful playground for this sort of thing. I'll also admit that, for 13 | now, time critical stuff at work I still do in vim as that's what I am 14 | most efficient in. Lets see if we can change that! 15 | 16 | If you are viewing these files within acme itself then you should be able 17 | to use acme mouse actions to open up and local docs references or URLs. In 18 | the case of `go doc` commands, the simplest thing to do is double click just 19 | after the leading paren and then imediately middle click (button 2) once the 20 | text is highlighted. For man page references, provided you haven't completely 21 | messed with your plumbing rules, a simple right (button 3) click should 22 | suffice. -------------------------------------------------------------------------------- /_notes/acme-itself.md: -------------------------------------------------------------------------------- 1 | acme itself 2 | ----------- 3 | web links: (http://doc.cat-v.org/plan_9/4th_edition/papers/acme/) 4 | (http://doc.cat-v.org/plan_9/4th_edition/papers/plumb) 5 | (http://doc.cat-v.org/bell_labs/sam_lang_tutorial/sam_tut.pdf) 6 | (https://github.com/jinyangustc/acme-editor/blob/master/sam.md) 7 | (https://github.com/jinyangustc/acme-editor/blob/master/acme-sam-tips.txt) 8 | local docs: acme(1) 9 | acme(4) 10 | sam(1) 11 | regexp(7) 12 | 9p(1) 13 | 9pclient(3) 14 | 15 | ### Personal motivation for using acme 16 | `acme` really is, as described by Rob Pike in what I think is his original 17 | paper on it, "a user interface for programmers". Since finding it I've been 18 | slow to realise that it is the missing component (along with the plumber) 19 | between writing your own shell scripts and utility programs and having an 20 | editing / work environment that you are in complete control over. I have, 21 | over time, attempted to use a wide range of editors / IDEs as my daily go-to 22 | tool: light-table, sublime-text, atom, intellij, emacs, spacemacs, vim, neovim, 23 | idle, kate, gedit...you get the idea. Ultimately, for the majority of my time 24 | working as a programmer I have settled on vim from within the terminal rather 25 | than gVim or some sort of electron skin. 26 | The main reasons for this are twofold: 27 | 1) vim's editing language is simply wonderful for structurally editing files 28 | and it can be extended with your own macros and key bindings _relatively_ 29 | simply. (Writing plugins on the other hand?...) 30 | 2) being able to quickly jump in and out of files while navigating around my 31 | file system and then drop back out to the shell when needed is a workflow 32 | that I find infinitely more flexible than hunting around in a gui menu 33 | system for a hand-rolled git implementation / terminal emulator / kitchen 34 | sink. 35 | 36 | So, based on that, why acme? Well, while it does feel like I need to give up 37 | vim's file navigation and structural editing (see footnote) by moving to acme, 38 | what I gain is the ability to modify file contents and editor state via _any 39 | language I chose_ and, more than that, tail the editor state and inject my 40 | own actions from helper programs. 41 | 42 | Now what self respecting hacker (in the original sense) can say no to that? 43 | 44 | (footnote: yes I know that acme has `sam` structural editing embedded but the 45 | need to jump up to the tag to type a command rather than bind common commands 46 | to key sequences/chords is a real pain.) 47 | 48 | 49 | ### Some useful acme commands 50 | * Select with button 3: 51 | * `:n` 52 | * jump to (and select) line n of the file 53 | * `:0` 54 | * jump to the start of the file 55 | *`:$` 56 | * jump to the end of the file 57 | * `:,` 58 | * select the entire file 59 | 60 | * Select with button 2 61 | * `Edit ,` 62 | * select the entire file 63 | * `Edit , d` 64 | * clear the window 65 | * `Edit , < SOME_EXTERNAL_COMMAND` 66 | * replace body with the output of the external command 67 | * `Edit , > SOME_EXTERNAL_COMMAND` 68 | * pipe the current window content to an external command 69 | * `Edit ,s/foo/bar/g` 70 | * equivalent to `:%s/foo/bar/g` in vim 71 | * `Edit ,x/foo/c/bar/` 72 | * a more idiomatic version of the above 73 | 74 | * `$%` is the current window name (normally the abspath of the file being edited) 75 | * `$winid` is the current window ID -------------------------------------------------------------------------------- /_notes/event-examples-and-notes.txt: -------------------------------------------------------------------------------- 1 | [ NOTES ON WORKING WITH ACME WINDOW EVENTS ] 2 | 3 | -- (go doc 9fans.net/go/acme Event) 4 | -- (go doc 9fans.net/go/acme Win) 5 | -- acme(1) 6 | -- acme(4) 7 | 8 | 9 | -- Event fields and what they all mean 10 | * C1: 'M' if this is a mouse event 11 | 'K' if this is a keyboard event 12 | 'E' for writes to the body or tag file 13 | 'F' for actions through the window's other files 14 | 15 | * C2: For mouse events this is Ll/Xx for load/execute (button 3/2). 16 | Upper case means the event was in the body, lower case means it 17 | was in the tag. For keyboard events this is Ii/Dd for insert/delete 18 | with the same upper/lower case semantics as for mouse events. 19 | 20 | * Q0,Q1: Character offsets into the file for where the event began and 21 | ended. For a keyboard action this is usually a single character 22 | difference but in some cases (e.g. C-w) it shows how many 23 | chars were removed. (Presumably the same thing for paste?) 24 | * There are some odd rules around these values if there was 25 | chording: see the godoc output or acme(4) 26 | * OrigQ[0,1] show the original event positions in the same 27 | format as Q0,Q1 but before expansion was applied. (In the case 28 | of expanding within parens, to a full word etc...) 29 | 30 | * Flag: "The flag bits". The following is copied verbatim from the 31 | acme(4) man page: 32 | For D, d, I, and i the flag is always zero. For X and x, the 33 | flag is a bitwise OR (reported decimally) of the following: 34 | 1 if the text indicated is recognized as an acme built-in 35 | command; 2 if the text indicated is a null string that has a 36 | non-null expansion; if so, another complete message will follow 37 | describing the expansion exactly as if it had been indicated 38 | explicitly (its flag will always be 0); 8 if the command has 39 | an extra (chorded) argument; if so, two more complete messages 40 | will follow reporting the argument (with all numbers 0 except 41 | the character count) and where it originated, in the form of a 42 | fully-qualified button 3 style address. 43 | For L and l, the flag is the bitwise OR of the following: 1 if 44 | acme can interpret the action without loading a new file; 2 if 45 | a second (post-expansion) message follows, analogous to that 46 | with X messages; 4 if the text is a file or window name (perhaps 47 | with address) rather than plain literal text. 48 | For messages with the 1 bit on in the flag, writing the message 49 | back to the event file, but with the flag, count, and text 50 | omitted, will cause the action to be applied to the file exactly 51 | as it would have been if the event file had not been open. 52 | 53 | * Nb: The number of bytes present in the optional text 54 | * Nr: The number of UTF-8 characters in the optional text 55 | * Text: The text of the event itself (large selections will need to be 56 | fetched via xData and the Q0/1 values) 57 | * Arg: The chorded argument, if present (the 8 bit is set in the flag). 58 | * Loc: The chorded location, if present (the 8 bit is set in the flag). 59 | 60 | 61 | -- Some test events, snarfed while working on 'pick' using the go API 62 | -- NOTE: I tried tailing the event file in the terminal at the same time and 63 | -- it looks like while you _can_ have multiple programs listening in on 64 | -- events, you have to pass the events back through to acme from each 65 | -- one and the event flow seems to be a stack with acme at the bottom 66 | -- and the newest listener at the top. 67 | 68 | >> INPUTS: a,b, backspace 69 | ------------------------- 70 | &acme.Event{C1:75, C2:73, Q0:0, Q1:1, OrigQ0:0, OrigQ1:1, Flag:0, Nb:0, Nr:1, Text:[]uint8{0x61}, Arg:[]uint8(nil), Loc:[]uint8(nil)} 71 | &acme.Event{C1:75, C2:73, Q0:1, Q1:2, OrigQ0:1, OrigQ1:2, Flag:0, Nb:0, Nr:1, Text:[]uint8{0x62}, Arg:[]uint8(nil), Loc:[]uint8(nil)} 72 | &acme.Event{C1:75, C2:68, Q0:1, Q1:2, OrigQ0:1, OrigQ1:2, Flag:0, Nb:0, Nr:0, Text:[]uint8{}, Arg:[]uint8(nil), Loc:[]uint8(nil)} 73 | 74 | 75 | >> INPUTS: t,h,i,s,' ',C-w 76 | ----------------------- 77 | &acme.Event{C1:75, C2:73, Q0:0, Q1:1, OrigQ0:0, OrigQ1:1, Flag:0, Nb:0, Nr:1, Text:[]uint8{0x74}, Arg:[]uint8(nil), Loc:[]uint8(nil)} 78 | &acme.Event{C1:75, C2:73, Q0:1, Q1:2, OrigQ0:1, OrigQ1:2, Flag:0, Nb:0, Nr:1, Text:[]uint8{0x68}, Arg:[]uint8(nil), Loc:[]uint8(nil)} 79 | &acme.Event{C1:75, C2:73, Q0:2, Q1:3, OrigQ0:2, OrigQ1:3, Flag:0, Nb:0, Nr:1, Text:[]uint8{0x69}, Arg:[]uint8(nil), Loc:[]uint8(nil)} 80 | &acme.Event{C1:75, C2:73, Q0:3, Q1:4, OrigQ0:3, OrigQ1:4, Flag:0, Nb:0, Nr:1, Text:[]uint8{0x73}, Arg:[]uint8(nil), Loc:[]uint8(nil)} 81 | &acme.Event{C1:75, C2:73, Q0:4, Q1:5, OrigQ0:4, OrigQ1:5, Flag:0, Nb:0, Nr:1, Text:[]uint8{0x20}, Arg:[]uint8(nil), Loc:[]uint8(nil)} 82 | &acme.Event{C1:75, C2:68, Q0:1, Q1:5, OrigQ0:1, OrigQ1:5, Flag:0, Nb:0, Nr:0, Text:[]uint8{}, Arg:[]uint8(nil), Loc:[]uint8(nil)} 83 | 84 | 85 | >> INPUTS: Return 86 | &acme.Event{C1:75, C2:73, Q0:4, Q1:5, OrigQ0:4, OrigQ1:5, Flag:0, Nb:0, Nr:1, Text:[]uint8{0xa}, Arg:[]uint8(nil), Loc:[]uint8(nil)} 87 | 88 | 89 | 90 | -- Dealing with control characters 91 | The simplest thing to do (in terms of readability) is to either define the 92 | following as constants or, when a rune is < 26, add on 96 to offset it to the 93 | correct value to parse as the character in the control sequence (i, j, h etc) 94 | 95 | 0x8 - bckspc ctrl+h 96 | 0x9 - \t ctrl+i 97 | 0xa - \n ctrl+j 98 | 0xd - \r ctrl+M -------------------------------------------------------------------------------- /acorp/edit.go: -------------------------------------------------------------------------------- 1 | package acorp 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "9fans.net/go/acme" 8 | ) 9 | 10 | // SetCursorEOL will position the current window cursor at the end of line. 11 | func SetCursorEOL(w *acme.Win, line int) { 12 | w.Addr(fmt.Sprintf("%d-#1", line+1)) 13 | w.Ctl("dot=addr") 14 | w.Ctl("show") 15 | } 16 | 17 | // SetCursorBOL will position the current window cursor at the beginning of line. 18 | func SetCursorBOL(w *acme.Win, line int) { 19 | w.Addr(fmt.Sprintf("%d-#0", line)) 20 | w.Ctl("dot=addr") 21 | w.Ctl("show") 22 | } 23 | 24 | // WindowBody reads the body of the current window as single string 25 | func WindowBody(w *acme.Win) (string, error) { 26 | var ( 27 | body []byte 28 | err error 29 | ) 30 | 31 | // TODO: stash and restore current addr 32 | w.Addr(",") 33 | if body, err = w.ReadAll("data"); err != nil { 34 | return "", err 35 | } 36 | return string(body), nil 37 | } 38 | 39 | // WindowBodyLines reads the body of the current window as an array of strings split on newline 40 | func WindowBodyLines(w *acme.Win) ([]string, error) { 41 | body, err := WindowBody(w) 42 | if err != nil { 43 | return nil, err 44 | } 45 | return strings.Split(body, "\n"), nil 46 | } 47 | 48 | // EventLineNumber returns the line that a e occurred on in w 49 | func EventLineNumber(w *acme.Win, e *acme.Event) (int, error) { 50 | body, err := WindowBody(w) 51 | if err != nil { 52 | return -1, err 53 | } 54 | 55 | upToCursor := body[:e.Q0] 56 | return strings.Count(upToCursor, "\n"), nil 57 | } 58 | -------------------------------------------------------------------------------- /acorp/event.go: -------------------------------------------------------------------------------- 1 | package acorp 2 | 3 | import ( 4 | "fmt" 5 | 6 | "9fans.net/go/acme" 7 | ) 8 | 9 | // A handler fucntion that processes an Acme event and takes an action. Passthrough must be explicitly 10 | // carried out by the handler function itself. 11 | type handler = func(*acme.Win, *acme.Event, func() error) error 12 | 13 | // An EventFilter takes hold of an acme window's event file and passes all events 14 | // it sees through a set of filter functions if they are defined. Unmatched events 15 | // are passed through to acme. 16 | type EventFilter struct { 17 | complete bool 18 | KeyboardInputBody handler 19 | KeyboardDeleteBody handler 20 | KeyboardInputTag handler 21 | KeyboardDeleteTag handler 22 | Mouse2Body handler 23 | Mouse3Body handler 24 | Mouse2Tag handler 25 | Mouse3Tag handler 26 | } 27 | 28 | func (ef *EventFilter) markComplete() error { 29 | ef.complete = true 30 | return nil 31 | } 32 | 33 | func (ef *EventFilter) applyOrPassthrough(f handler, w *acme.Win, e *acme.Event) error { 34 | if f != nil { 35 | return f(w, e, ef.markComplete) 36 | } 37 | 38 | return w.WriteEvent(e) 39 | } 40 | 41 | // Filter runs the event filter, releasing the window event file on the first error encountered 42 | func (ef *EventFilter) Filter(w *acme.Win) error { 43 | for e := range w.EventChan() { 44 | if err := ef.filterSingle(w, e); err != nil { 45 | return err 46 | } 47 | 48 | if ef.complete { 49 | return nil 50 | } 51 | } 52 | 53 | return fmt.Errorf("lost event channel") 54 | } 55 | 56 | // Currently dropping E and F events that are generated by writes from other programs to the acme 57 | // control files. 58 | func (ef *EventFilter) filterSingle(w *acme.Win, e *acme.Event) error { 59 | switch e.C1 { 60 | case 'K': 61 | switch e.C2 { 62 | case 'I': 63 | ef.applyOrPassthrough(ef.KeyboardInputBody, w, e) 64 | case 'D': 65 | ef.applyOrPassthrough(ef.KeyboardDeleteBody, w, e) 66 | case 'i': 67 | ef.applyOrPassthrough(ef.KeyboardInputTag, w, e) 68 | case 'd': 69 | ef.applyOrPassthrough(ef.KeyboardDeleteTag, w, e) 70 | } 71 | return nil 72 | 73 | case 'M': 74 | switch e.C2 { 75 | case 'X': 76 | return ef.applyOrPassthrough(ef.Mouse2Body, w, e) 77 | case 'L': 78 | return ef.applyOrPassthrough(ef.Mouse3Body, w, e) 79 | case 'x': 80 | return ef.applyOrPassthrough(ef.Mouse2Tag, w, e) 81 | case 'l': 82 | return ef.applyOrPassthrough(ef.Mouse3Tag, w, e) 83 | } 84 | } 85 | 86 | w.WriteEvent(e) 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /acorp/util.go: -------------------------------------------------------------------------------- 1 | package acorp 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "net" 7 | "os" 8 | "strconv" 9 | 10 | "9fans.net/go/acme" 11 | ) 12 | 13 | const ( 14 | activeWindowQuery = "active / .\n" 15 | snooperAddr = "127.0.0.1:2009" 16 | ) 17 | 18 | func winIDFromSnooper() (string, error) { 19 | conn, _ := net.Dial("tcp", snooperAddr) 20 | fmt.Fprintf(conn, activeWindowQuery) 21 | message, _ := bufio.NewReader(conn).ReadString('\n') 22 | if message == "-1" { 23 | return "", fmt.Errorf("unable to determine current window ID") 24 | } 25 | return message, nil 26 | } 27 | 28 | // GetCurrentWindow finds the current active window in acme, using the snooper if this is not called from 29 | // inside of an acme window directly. 30 | func GetCurrentWindow() (*acme.Win, error) { 31 | var err error 32 | 33 | winStr := os.Getenv("winid") 34 | if len(winStr) == 0 { 35 | winStr, err = winIDFromSnooper() 36 | if err != nil { 37 | return nil, fmt.Errorf("unable to determine current acme window") 38 | } 39 | } 40 | 41 | winID, err := strconv.Atoi(winStr) 42 | if err != nil { 43 | return nil, fmt.Errorf("non numeric winid: %s", winStr) 44 | } 45 | 46 | w, err := acme.Open(winID, nil) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | return w, nil 52 | } 53 | -------------------------------------------------------------------------------- /dirtree/README.md: -------------------------------------------------------------------------------- 1 | dirtree - a simple file tree for acme 2 | ===================================== 3 | 4 | Dirtree is a minimal, tree based file explorer for acme. (I don't like the way 5 | that acme's default handling of folders opens a new folder for each directory 6 | you click on.) 7 | 8 | When you run dirtree it will open a new window displaying the current working 9 | directory. If you pass a directory as an argument it will use that as the 10 | starting directory. 11 | 12 | The current directory is shown at the top of the window, wrapped in parens for 13 | easy 1-1-3 clicking to highlight, then open the directory in the normal acme 14 | style (in case you want to do file operations with the listing). 15 | 16 | 17 | ### Tag commands 18 | - Hidden 19 | - Toggle the display of dotfiles in the current file tree 20 | 21 | - UpDir 22 | - Move the root of the file tree up to the parent directory then redraw the 23 | window. 24 | 25 | - Reset 26 | - Collapse all expanded directories and re-fetch their contents 27 | 28 | 29 | ### Mouse Actions 30 | - Button 2 31 | - directory: Set that as the new root directory of the tree and redraw 32 | 33 | - Button 3 34 | - directory: Toggle the expand / collapse of the directory contents 35 | - file: Open that file via using the plumber 36 | - user typed text: attempt to execute in the shell as per normal acme windows. 37 | - NOTE: the directory used for execution will be the current root of the tree. 38 | 39 | 40 | ### Known Bugs 41 | - Spaces in file names _sometimes_ causes the plumber to fail...not sure why. 42 | - The entire tree is redrawn on each expansion / collapse of a node. If you have 43 | expanded a lot of nodes then you will see some noticeable redraw. -------------------------------------------------------------------------------- /dirtree/dirtree.go: -------------------------------------------------------------------------------- 1 | // A directory tree viewer for acme 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | "strings" 11 | 12 | "9fans.net/go/acme" 13 | "9fans.net/go/plan9" 14 | "9fans.net/go/plumb" 15 | "github.com/sminez/acme-corp/acorp" 16 | ) 17 | 18 | const ( 19 | indentStr = " " 20 | dirCollapsed = "+ " 21 | dirExpanded = "- " 22 | file = " " 23 | buffSize = 1024 24 | sepSize = 2 25 | ) 26 | 27 | type node struct { 28 | name string 29 | fullPath string 30 | depth int 31 | isDir bool 32 | isExpanded bool 33 | isHidden bool 34 | contents []*node 35 | } 36 | 37 | type fileTree struct { 38 | w *acme.Win 39 | root string 40 | showHidden bool 41 | rootNodes []*node 42 | nodeMap map[string]*node 43 | } 44 | 45 | func main() { 46 | root := determineRoot() 47 | f := newFileTree(root) 48 | f.redraw(nil) 49 | f.runEventLoop() 50 | } 51 | 52 | func determineRoot() string { 53 | cwd, _ := os.Getwd() 54 | 55 | if len(os.Args) == 2 { 56 | dir := os.Args[1] 57 | if dir[0] != '/' { 58 | dir, _ = filepath.Abs(dir) 59 | } 60 | if _, err := os.Stat(dir); !os.IsNotExist(err) { 61 | return dir 62 | } 63 | } 64 | 65 | if cwd == "/" { 66 | cwd, _ = os.UserHomeDir() 67 | } 68 | 69 | return cwd 70 | } 71 | 72 | func newFileTree(root string) *fileTree { 73 | win, err := acme.New() 74 | if err != nil { 75 | fmt.Printf("Unable to initialise new acme window: %s\n", err) 76 | os.Exit(1) 77 | } 78 | 79 | win.Name("+dirtree") 80 | win.Write("tag", []byte("UpDir Hidden Reset")) 81 | rootNodes, _ := getNodes(root, 0) 82 | 83 | f := fileTree{ 84 | w: win, 85 | root: root, 86 | showHidden: false, 87 | rootNodes: rootNodes, 88 | nodeMap: make(map[string]*node), 89 | } 90 | 91 | f.registerNodes(rootNodes) 92 | return &f 93 | } 94 | 95 | // Essentially just run 'ls' for the given root directory. We lazy list contents 96 | // of directories as we expand them so we tag the nodes with their depth as they 97 | // are created in order to track their path relative to the +dirtree window root. 98 | func getNodes(root string, depth int) ([]*node, error) { 99 | var fileInfo []os.FileInfo 100 | var nodes []*node 101 | var err error 102 | 103 | if fileInfo, err = ioutil.ReadDir(root); err != nil { 104 | return nil, err 105 | } 106 | 107 | for _, f := range fileInfo { 108 | name := f.Name() 109 | n := node{ 110 | name: name, 111 | fullPath: filepath.Join(root, name), 112 | depth: depth, 113 | isDir: f.IsDir(), 114 | isHidden: name[0] == '.', 115 | contents: []*node{}, 116 | } 117 | nodes = append(nodes, &n) 118 | } 119 | 120 | return nodes, nil 121 | } 122 | 123 | // Generate a string representation for this node and all child nodes if this is 124 | // an expanded directory. Otherwise, just correctly indent this node for the tree. 125 | func (n *node) stringifyRecursive(showHidden bool) string { 126 | if n.isHidden && !showHidden { 127 | return "" 128 | } 129 | 130 | prefix := file 131 | if n.isDir { 132 | if n.isExpanded { 133 | prefix = dirExpanded 134 | } else { 135 | prefix = dirCollapsed 136 | } 137 | } 138 | 139 | prefix = prefix + strings.Repeat(indentStr, n.depth) 140 | s := fmt.Sprintf("%s%s\n", prefix, n.name) 141 | 142 | if n.isDir && n.isExpanded { 143 | for _, m := range n.contents { 144 | s += m.stringifyRecursive(showHidden) 145 | } 146 | } 147 | 148 | return s 149 | } 150 | 151 | func (n *node) plumb() error { 152 | port, err := plumb.Open("send", plan9.OWRITE) 153 | if err != nil { 154 | return err 155 | } 156 | defer port.Close() 157 | 158 | msg := &plumb.Message{ 159 | Src: "dirtree", 160 | Dst: "", 161 | Dir: "/", 162 | Type: "text", 163 | Data: []byte(strings.Replace(n.fullPath, " ", "\\ ", -1)), 164 | } 165 | 166 | return msg.Send(port) 167 | } 168 | 169 | // We clear & refetch the nodes on expand/collapse in order to allow the user to 170 | // refresh the contents of a single directory. 171 | func (f *fileTree) toggleDirectory(n *node) { 172 | var err error 173 | 174 | if n.isExpanded { 175 | for _, child := range n.contents { 176 | delete(f.nodeMap, child.fullPath) 177 | } 178 | n.contents = []*node{} 179 | } else { 180 | if n.contents, err = getNodes(n.fullPath, n.depth+1); err != nil { 181 | f.w.Write("error", []byte(err.Error())) 182 | return 183 | } 184 | f.registerNodes(n.contents) 185 | } 186 | 187 | n.isExpanded = !n.isExpanded 188 | } 189 | 190 | func (f *fileTree) registerNodes(ns []*node) { 191 | for _, n := range ns { 192 | f.nodeMap[n.fullPath] = n 193 | } 194 | } 195 | 196 | // Recursively stringify the current state of the entire tree. We also 197 | // include the abspath to the current root node at the head of the 198 | // window in order to make it easy to quickly perform other actions. 199 | func (f *fileTree) String() string { 200 | s := "" 201 | for _, n := range f.rootNodes { 202 | s += n.stringifyRecursive(f.showHidden) 203 | } 204 | 205 | return fmt.Sprintf("(%s)\n\n%s", f.root, s) 206 | } 207 | 208 | func (f *fileTree) redraw(e *acme.Event) { 209 | f.w.Clear() 210 | f.w.Write("body", []byte(f.String())) 211 | 212 | if e != nil { 213 | f.w.Addr(fmt.Sprintf("%d-#0", e.OrigQ0)) 214 | } else { 215 | f.w.Addr("1-1#0") 216 | } 217 | 218 | f.w.Ctl("dot=addr") 219 | f.w.Ctl("clean") 220 | f.w.Ctl("show") 221 | } 222 | 223 | func (f *fileTree) resetRoot(root string) { 224 | f.root = root 225 | f.nodeMap = make(map[string]*node) 226 | f.rootNodes, _ = getNodes(f.root, 0) 227 | f.registerNodes(f.rootNodes) 228 | f.w.Name("+dirtree") 229 | f.redraw(nil) 230 | } 231 | 232 | // When pasing events through to the plumber, acme sets the execution directory 233 | // based on the current window name. I've tried manually composing the plumbing 234 | // message for this and I can't get it to work: so for now, setting the name of 235 | // the window correctly for long enought to plumb the message seems to work. 236 | func (f *fileTree) plumbEventAtCurrentRoot(e *acme.Event) { 237 | f.w.Name("%s/+dirtree", f.root) 238 | f.w.WriteEvent(e) 239 | f.w.Name("+dirtree") 240 | f.w.Ctl("clean") 241 | } 242 | 243 | // loop over events we get from '+dirtree' until the user closes the window 244 | func (f *fileTree) runEventLoop() { 245 | var knownNode bool 246 | var n *node 247 | 248 | ef := &acorp.EventFilter{ 249 | Mouse2Tag: func(w *acme.Win, e *acme.Event, done func() error) error { 250 | switch strings.TrimSpace(string(e.Text)) { 251 | case "Del": 252 | w.Ctl("delete") 253 | done() 254 | case "Reset": 255 | f.resetRoot(f.root) 256 | case "Hidden": 257 | f.showHidden = !f.showHidden 258 | f.redraw(nil) 259 | case "UpDir": 260 | f.resetRoot(path.Dir(f.root)) 261 | default: 262 | return w.WriteEvent(e) 263 | } 264 | return nil 265 | }, 266 | 267 | Mouse2Body: func(w *acme.Win, e *acme.Event, done func() error) error { 268 | if n, knownNode = f.nodeFromEvent(e); !knownNode { 269 | f.plumbEventAtCurrentRoot(e) 270 | return nil 271 | } 272 | if n.isDir { 273 | f.resetRoot(n.fullPath) 274 | } 275 | return nil 276 | }, 277 | 278 | Mouse3Body: func(w *acme.Win, e *acme.Event, done func() error) error { 279 | if n, knownNode = f.nodeFromEvent(e); !knownNode { 280 | w.WriteEvent(e) 281 | return nil 282 | } 283 | if n.isDir { 284 | f.toggleDirectory(n) 285 | f.redraw(e) 286 | return nil 287 | } 288 | return n.plumb() 289 | }, 290 | } 291 | 292 | ef.Filter(f.w) 293 | } 294 | 295 | // use 'sam' addressing via the addr and xdata files for this window to extract the line that 296 | // we just clicked on so that we can then rebuild the complete filepath we need. 297 | func (f *fileTree) getPath(e *acme.Event) (string, bool) { 298 | // Fetch the entire line from acme using addr. The acme address syntax here is 299 | // going to the character at the begining of the event selection text (#e.Orig0), 300 | // jumping back to the start of the line (-) and selecting to the end (+). 301 | f.w.Addr(fmt.Sprintf("#%d-+", e.OrigQ0)) 302 | b := make([]byte, buffSize) 303 | n, _ := f.w.Read("xdata", b) 304 | 305 | s := string(b[:n-1]) 306 | if len(s)-1 < sepSize { 307 | return "", false 308 | } 309 | 310 | line := s[sepSize:] 311 | j := 0 312 | 313 | for i := 0; i < len(line); i++ { 314 | if line[i] != ' ' { 315 | j = i 316 | break 317 | } 318 | } 319 | 320 | indent := len(line[:j]) / sepSize 321 | p := []string{line[j:]} 322 | 323 | // Now that we have the line, get it's indentation level and walk 324 | // our way back up the window to get the rest of the path components. 325 | for i := indent - 1; i >= 0; i-- { 326 | // Reverse search (-/regexp/) for the first line that is a directory 327 | // (starts with -/+) and is at the correct indentation level. Then 328 | // select the entire line. 329 | f.w.Addr(fmt.Sprintf(`-/[\-\+] %s[^ ]+/-+`, strings.Repeat(indentStr, i))) 330 | b := make([]byte, buffSize) 331 | n, _ := f.w.Read("xdata", b) 332 | comp := strings.TrimSpace(string(b[:n-1])[sepSize:]) 333 | p = append(p, comp) 334 | } 335 | 336 | // Reverse to get everything in path order 337 | comps := []string{f.root} 338 | for i := len(p) - 1; i >= 0; i-- { 339 | comps = append(comps, p[i]) 340 | } 341 | 342 | return path.Join(comps...), true 343 | } 344 | 345 | // Attempt to interperate the contents of this event as a filename that we can rebuild 346 | // using the current state of the '+dirtree' window and then look up in our known nodes. 347 | func (f *fileTree) nodeFromEvent(e *acme.Event) (*node, bool) { 348 | path, ok := f.getPath(e) 349 | if !ok { 350 | return nil, false 351 | } 352 | 353 | n, ok := f.nodeMap[path] 354 | return n, ok 355 | } 356 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sminez/acme-corp 2 | 3 | require ( 4 | 9fans.net/go v0.0.2 5 | github.com/fsnotify/fsnotify v1.4.7 6 | ) 7 | 8 | require golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a // indirect 9 | 10 | go 1.17 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | 9fans.net/go v0.0.2 h1:RYM6lWITV8oADrwLfdzxmt8ucfW6UtP9v1jg4qAbqts= 2 | 9fans.net/go v0.0.2/go.mod h1:lfPdxjq9v8pVQXUMBCx5EO5oLXWQFlKRQgs1kEkjoIM= 3 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 4 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 5 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= 6 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 7 | -------------------------------------------------------------------------------- /gq/README.md: -------------------------------------------------------------------------------- 1 | gq - line wrapping with prefixes 2 | ================================ 3 | 4 | A common edit I make in vim is to run `gq` on a visual selection, or `gqap` for 5 | a block, in order to quickly line wrap comments or formatted markup. There is 6 | the GNU core-utils `fmt` program but you need to specify the prefix yourself and 7 | it doesn't always do what I want. `gq` will wrap lines to 80 characters by 8 | default with the option to set the column count using the `-c` flag. Prefixes 9 | are maximally determined and stop on the first alpha-numeric character unless 10 | the `-a` flag is given. 11 | 12 | ### Use within acme 13 | Until I get this hooked in to the rest of acme-corp, `gq` is simply intended to 14 | be used in the tag as a filter for you to pipe a selection through. 15 | - For example, if you add `|gq -c 100` to the tag, select a comment block with 16 | button 1 and then select the command with button 2, you will wrap your comment 17 | to 100 characters. 18 | 19 | ### Known bugs 20 | - Leading whitespace will be stripped. 21 | - Indentation within a comment block will be lost. 22 | 23 | ### TODO 24 | - Safe auto wrapping for files that don't currently have a formatter available 25 | for use with `afmt` (shell for example). 26 | - This means no breaking of pre-existing indentation or stripping leading 27 | whitespace. 28 | - Hook into `snoop-acme` as a triggerable key binding (will need shkd or 29 | similar, such as my compiled dwm bindings) in order to get "wrap the current 30 | paragraph" or "wrap current selection". 31 | - This will involve storing some state in the snooper so that it can select 32 | the current window / text block / maximal text block with matching prefix etc 33 | -------------------------------------------------------------------------------- /gq/gq.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Aiming to emulate the behaviour of typing 'gq' with a visual selection in 4 | // vim, wrap lines to a specified column count and retain any leading prefix on 5 | // the newly wrapped lines. This is primarily for markup / comments but it 6 | // might prove useful for other things as well. Who knows! 7 | 8 | import ( 9 | "bufio" 10 | "flag" 11 | "fmt" 12 | "os" 13 | "strings" 14 | "unicode" 15 | ) 16 | 17 | var ( 18 | columns = flag.Int("c", 80, "number of columns to wrap to") 19 | alphaNumeric = flag.Bool("a", false, "allow alphanumeric characters in the prefix") 20 | ) 21 | 22 | // Determine the largest prefix that we can find for all of the lines that we 23 | // have. By default, we stop at the first alphanumeric character encountered but 24 | // this can be overriden using the '-a' flag. 25 | func maximalPrefix(lines []string, allowAlphaNumeric bool) string { 26 | first := lines[0] 27 | prefix := "" 28 | 29 | k := len(first) 30 | if !allowAlphaNumeric { 31 | k = 0 32 | for _, c := range first { 33 | if unicode.IsLetter(c) || unicode.IsNumber(c) { 34 | break 35 | } 36 | k++ 37 | } 38 | } 39 | 40 | for i := 1; i < k; i++ { 41 | s := first[:i] 42 | for _, l := range lines[1:] { 43 | if !strings.HasPrefix(l, s) { 44 | return strings.TrimSpace(prefix) 45 | } 46 | } 47 | prefix = s 48 | } 49 | 50 | return strings.TrimSpace(prefix) 51 | } 52 | 53 | func linesToWords(lines []string, prefix string) chan string { 54 | ch := make(chan string) 55 | 56 | go func() { 57 | for _, l := range lines { 58 | s := strings.Trim(strings.TrimPrefix(l, prefix), " \t") 59 | for _, w := range strings.Split(s, " ") { 60 | ch <- w 61 | } 62 | } 63 | close(ch) 64 | }() 65 | 66 | return ch 67 | } 68 | 69 | func wrapLinesWithPrefix(lines []string, prefix string, columns int) { 70 | var wrapped []string 71 | var l, m, w string 72 | 73 | l = prefix 74 | for w = range linesToWords(lines, prefix) { 75 | if w == "" { 76 | wrapped = append(wrapped, fmt.Sprintf("%s\n%s\n", l, prefix)) 77 | l = prefix + " " 78 | continue 79 | } 80 | 81 | m = w 82 | if len(l) > 0 { 83 | m = fmt.Sprintf("%s %s", l, w) 84 | } 85 | 86 | if len(m) <= columns { 87 | l = m 88 | } else { 89 | wrapped = append(wrapped, l) 90 | l = fmt.Sprintf("%s %s", prefix, w) 91 | } 92 | } 93 | 94 | if len(l) > 0 { 95 | wrapped = append(wrapped, l) 96 | } 97 | 98 | fmt.Print(strings.Join(wrapped, "\n")) 99 | } 100 | 101 | func main() { 102 | var lines []string 103 | flag.Parse() 104 | 105 | s := bufio.NewScanner(os.Stdin) 106 | for s.Scan() { 107 | lines = append(lines, s.Text()) 108 | } 109 | 110 | prefix := maximalPrefix(lines, *alphaNumeric) 111 | wrapLinesWithPrefix(lines, prefix, *columns) 112 | } 113 | -------------------------------------------------------------------------------- /pick/README.md: -------------------------------------------------------------------------------- 1 | pick - cross platform dmenu replacement 2 | ======================================= 3 | 4 | I really want dmenu on my work OSX machine. I've found similar tools that are 5 | OSX specific but nothing that works as easily as `dmenu` or that I can hack on 6 | as most are written in Swift. 7 | 8 | If you know `dmenu` you know the drill: pipe in a newline delimited text, select 9 | the line you want and it will be returned to you on stdout. That's it. 10 | -------------------------------------------------------------------------------- /pick/pick.go: -------------------------------------------------------------------------------- 1 | /* 2 | pick - a minimalist input selector for the acme text editor modelled after dmenu 3 | 4 | If launched from within acme itself, pick will use the current acme window as defined by the 'winid' 5 | environment variable. Otherwise, it will attempt to query a running snooper instance to fetch the 6 | focused window id. 7 | * To mimic dmenu behaviour of reading input from stdin, pass the '-s' flag. 8 | * To return the index of the selected line in the input instead of the line itself, pass the '-n' flag. 9 | * To override the default prompt ('> ') pass the '-p' flag followed by the string to use as the prompt. 10 | 11 | +pick window actions 12 | * character input will be interpreted as a regex for filtering lines 13 | * button 3: select a line to return 14 | * Return: select the line the curesor is currently on 15 | */ 16 | package main 17 | 18 | import ( 19 | "bufio" 20 | "flag" 21 | "fmt" 22 | "os" 23 | "strings" 24 | 25 | "9fans.net/go/acme" 26 | "github.com/sminez/acme-corp/acorp" 27 | ) 28 | 29 | const ( 30 | lineOffset = 1 // assuming single line prompt 31 | windowName = "+pick" 32 | ) 33 | 34 | var ( 35 | readFromStdIn = flag.Bool("s", false, "read input from stdin instead of the current acme window") 36 | returnLineNum = flag.Bool("n", false, "return the line number of the selected line, not the line itself") 37 | numberLines = flag.Bool("N", false, "prefix each line with its line number") 38 | prompt = flag.String("p", "> ", "prompt to present to the user when taking input") 39 | ) 40 | 41 | type linePicker struct { 42 | w *acme.Win 43 | rawLines []string 44 | lineMap map[int]string // original line numbers 45 | selectedLines map[int]int // window line numbers -> input line number 46 | currentInput string 47 | selectionEvent *acme.Event 48 | } 49 | 50 | func newLinePicker(rawLines []string) *linePicker { 51 | var w *acme.Win 52 | var err error 53 | 54 | if w, err = acme.New(); err != nil { 55 | fmt.Printf("Unable to initialise new acme window: %s\n", err) 56 | os.Exit(1) 57 | } 58 | 59 | w.Name(windowName) 60 | lineMap := make(map[int]string) 61 | 62 | for n, l := range rawLines { 63 | lineMap[n+1] = l 64 | } 65 | 66 | return &linePicker{ 67 | w: w, 68 | rawLines: rawLines, 69 | lineMap: lineMap, 70 | selectedLines: make(map[int]int), 71 | currentInput: "", 72 | } 73 | } 74 | 75 | func (lp *linePicker) selectedLine() (int, string, error) { 76 | var ( 77 | windowLineNumber int 78 | err error 79 | ) 80 | 81 | if windowLineNumber, err = acorp.EventLineNumber(lp.w, lp.selectionEvent); err != nil { 82 | return -1, "", err 83 | } 84 | 85 | // hitting enter on the input line selects top match if there is at least one, otherwise 86 | // it returns the current input text 87 | if windowLineNumber == 0 { 88 | if len(lp.selectedLines) == 0 { 89 | return -1, lp.currentInput, nil 90 | } 91 | windowLineNumber = 1 92 | } 93 | 94 | lineNumber := lp.selectedLines[windowLineNumber-lineOffset] 95 | return lineNumber, lp.lineMap[lineNumber], nil 96 | } 97 | 98 | func (lp *linePicker) filter() (int, string, error) { 99 | ef := &acorp.EventFilter{ 100 | KeyboardInputBody: func(w *acme.Win, e *acme.Event, done func() error) error { 101 | r := e.Text[0] 102 | 103 | if r <= 26 { 104 | switch fmt.Sprintf("C-%c", r+96) { 105 | case "C-j": // (Enter) 106 | lp.selectionEvent = e 107 | return done() 108 | case "C-d": 109 | lp.w.Del(true) 110 | os.Exit(0) 111 | } 112 | } 113 | 114 | lp.currentInput += string(r) 115 | return lp.reRender() 116 | }, 117 | 118 | KeyboardDeleteBody: func(w *acme.Win, e *acme.Event, done func() error) error { 119 | if l := len(lp.currentInput); l > 0 { 120 | removed := e.Q1 - e.Q0 121 | lp.currentInput = lp.currentInput[:l-removed] 122 | } 123 | 124 | return lp.reRender() 125 | }, 126 | 127 | Mouse3Body: func(w *acme.Win, e *acme.Event, done func() error) error { 128 | lp.selectionEvent = e 129 | return done() 130 | }, 131 | } 132 | 133 | if err := lp.reRender(); err != nil { 134 | return -1, "", err 135 | } 136 | 137 | if err := ef.Filter(lp.w); err != nil { 138 | return -1, "", err 139 | } 140 | 141 | return lp.selectedLine() 142 | } 143 | 144 | func (lp *linePicker) reRender() error { 145 | lines := lp.rawLines 146 | lp.w.Clear() 147 | 148 | if len(lp.currentInput) > 0 { 149 | fragments := strings.Split(lp.currentInput, " ") 150 | lp.selectedLines = make(map[int]int) 151 | lines = []string{} 152 | k := 0 153 | 154 | for ix, line := range lp.lineMap { 155 | if containsAll(line, fragments) { 156 | lp.selectedLines[k] = ix 157 | lines = append(lines, line) 158 | k++ 159 | } 160 | } 161 | } 162 | 163 | lp.w.Write("body", []byte(fmt.Sprintf("%s%s\n", *prompt, lp.currentInput))) 164 | lp.w.Write("body", []byte(strings.Join(lines, "\n"))) 165 | acorp.SetCursorEOL(lp.w, 1) 166 | return nil 167 | } 168 | 169 | func containsAll(s string, ts []string) bool { 170 | for _, t := range ts { 171 | if !strings.Contains(s, t) { 172 | return false 173 | } 174 | } 175 | return true 176 | } 177 | 178 | func readFromAcme() ([]string, error) { 179 | var ( 180 | w *acme.Win 181 | err error 182 | ) 183 | 184 | if w, err = acorp.GetCurrentWindow(); err != nil { 185 | return nil, err 186 | } 187 | defer w.CloseFiles() 188 | 189 | return acorp.WindowBodyLines(w) 190 | } 191 | 192 | func numberedLines(lines []string) []string { 193 | for ix, line := range lines { 194 | lines[ix] = fmt.Sprintf("%3d | %s", ix+1, line) 195 | } 196 | 197 | return lines 198 | } 199 | 200 | func main() { 201 | var ( 202 | lines []string 203 | err error 204 | n int 205 | selection string 206 | ) 207 | 208 | flag.Parse() 209 | 210 | if *readFromStdIn { 211 | s := bufio.NewScanner(os.Stdin) 212 | for s.Scan() { 213 | lines = append(lines, s.Text()) 214 | } 215 | } else { 216 | if lines, err = readFromAcme(); err != nil { 217 | fmt.Println(err) 218 | os.Exit(1) 219 | } 220 | } 221 | 222 | if *numberLines { 223 | lines = numberedLines(lines) 224 | } 225 | 226 | lp := newLinePicker(lines) 227 | defer lp.w.Del(true) 228 | 229 | if n, selection, err = lp.filter(); err != nil { 230 | fmt.Println(err) 231 | os.Exit(1) 232 | } 233 | 234 | if *returnLineNum { 235 | fmt.Println(n) 236 | } else { 237 | fmt.Println(selection) 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /punt/README.md: -------------------------------------------------------------------------------- 1 | punt - a quick detour from the land of acme 2 | =========================================== 3 | 4 | Can't bare to leave your favourite editor behind? Punt your current window 5 | contents to it, make your edits and then load it back into acme. Or, 6 | alternatively, punt the file you're working on to an external program that can 7 | do something with it: updating some HTML? Punt it to the browser to see what it 8 | looks like. 9 | 10 | ### How does it work? 11 | `punt` will copy your current acme window content (_not_ the underlying file!) 12 | to a temporary file on your local file system before passing that file path off 13 | to the external program that you designate. When the external program closes, 14 | the contents of that temporary file will be read back in to acme and overwrite 15 | the window that you were in before. Initially I wrote this as a way to quickly 16 | open up my current buffer (window, old habits die hard) back to vim if there was 17 | something I needed to do that I couldn't wrap my head around in acme. As I'm 18 | getting more comfortable with acme, `punt` is less of a "must have" tool and 19 | more of "useful in unexpected situations". 20 | -------------------------------------------------------------------------------- /punt/punt.go: -------------------------------------------------------------------------------- 1 | /* 2 | `punt` allows you to edit the contents of an acme window in an external editor. 3 | 4 | It creates a new temp file to work with rather than opening the underlying source 5 | file again inside the new editor and changes are written to the acme window itself 6 | not the underlying file. 7 | 8 | File extensions are parsed when creating thetemp file so that things like plugins 9 | and syntax highlighting trigger correctly in the spawned editor. At present, the 10 | launch of the editor is done via spawning a tilix terminal session first so that 11 | terminal based editors (vim, emacs, nano...) can be used. If the editor in 12 | question is GUI based then you will see an additional terminal window as well. 13 | */ 14 | package main 15 | 16 | import ( 17 | "flag" 18 | "fmt" 19 | "io/ioutil" 20 | "log" 21 | "os" 22 | "os/exec" 23 | "strconv" 24 | "strings" 25 | 26 | "9fans.net/go/acme" 27 | "github.com/fsnotify/fsnotify" 28 | ) 29 | 30 | var ( 31 | terminal = "st" 32 | defaultEditor = "nvim" 33 | tmpName = "acme-punt" 34 | isGUI = flag.Bool("g", false, "The called program is a GUI") 35 | ) 36 | 37 | // Default to `defaultEditor` running in `terminal` or use the user provided details 38 | func parseArgs() (string, bool) { 39 | flag.Parse() 40 | args := flag.Args() 41 | 42 | if len(args) == 0 { 43 | return defaultEditor, false 44 | } 45 | 46 | return args[0], *isGUI 47 | } 48 | 49 | func main() { 50 | editor, isGUI := parseArgs() 51 | 52 | winStr := os.Getenv("winid") 53 | if len(winStr) == 0 { 54 | log.Fatal("Need to be called from inside acme") 55 | } 56 | 57 | winID, err := strconv.Atoi(winStr) 58 | if err != nil { 59 | log.Fatalf("Non numeric winid: %s\n", winStr) 60 | } 61 | 62 | w, err := acme.Open(winID, nil) 63 | if err != nil { 64 | log.Print(err) 65 | } 66 | defer w.CloseFiles() 67 | 68 | winInfo, err := acme.Windows() 69 | if err != nil { 70 | log.Print(err) 71 | return 72 | } 73 | 74 | for _, i := range winInfo { 75 | if i.ID == winID { 76 | comps := strings.SplitAfter(i.Name, ".") 77 | if n := len(comps); n > 1 { 78 | suffix := comps[n-1] 79 | tmpName = fmt.Sprintf("acme-punt.*.%s", suffix) 80 | } 81 | break 82 | } 83 | } 84 | 85 | w.Addr(",") 86 | body, err := w.ReadAll("data") 87 | if err != nil { 88 | log.Print(err) 89 | return 90 | } 91 | 92 | // Set up a temp file, copy the contents of the current window to it and 93 | // then open it up in that editor. 94 | tmpFile, err := ioutil.TempFile("", tmpName) 95 | if err != nil { 96 | log.Print(err) 97 | return 98 | } 99 | 100 | if _, err := tmpFile.Write(body); err != nil { 101 | tmpFile.Close() 102 | log.Fatal(err) 103 | } 104 | tmpFile.Close() 105 | 106 | // Kick off a file watcher to track when the file is saved 107 | watcher, err := fsnotify.NewWatcher() 108 | if err != nil { 109 | log.Print(err) 110 | return 111 | } 112 | 113 | go func() { 114 | defer os.Remove(tmpFile.Name()) // clean up 115 | defer watcher.Close() 116 | 117 | if err := watcher.Add(tmpFile.Name()); err != nil { 118 | log.Printf("fsnotify-error: %s\n", err) 119 | return 120 | } 121 | 122 | for { 123 | select { 124 | // watch for events 125 | case event := <-watcher.Events: 126 | // log.Printf("fsnotify: %#v\n", event) 127 | 128 | if event.Op&fsnotify.Write == fsnotify.Write { 129 | // Read the tempfile contents back in and replace the 130 | // window content with them. 131 | // TODO: Stash cursor position? Can't guarentee that we'll 132 | // still have a similar structure when we come back... 133 | edited, err := ioutil.ReadFile(tmpFile.Name()) 134 | if err != nil { 135 | log.Print(err) 136 | return 137 | } 138 | 139 | w.Clear() 140 | w.Write("data", edited) 141 | os.Remove(tmpFile.Name()) 142 | return 143 | } 144 | 145 | if event.Op&fsnotify.Remove == fsnotify.Remove { 146 | // If the file was removed then kill the goro 147 | return 148 | } 149 | 150 | case err := <-watcher.Errors: 151 | log.Printf("fsnotify-error: %s\n", err) 152 | } 153 | } 154 | }() 155 | 156 | // spawn the editor 157 | if isGUI { 158 | _, err = exec.Command(editor, tmpFile.Name()).CombinedOutput() 159 | } else { 160 | _, err = exec.Command(terminal, "-e", editor, tmpFile.Name()).CombinedOutput() 161 | } 162 | if err != nil { 163 | log.Print(err) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /scripts/a+: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # indent the current line by $1 spaces. If no argument is 3 | # given then the indentation defaults to 2. 4 | 5 | INDENT=" " 6 | if [[ -n "$1" ]]; then 7 | INDENT="$(printf %$1s)" 8 | fi 9 | 10 | sed "s/^/$INDENT/" -------------------------------------------------------------------------------- /scripts/a-: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # dedent the current line by $1 spaces. If no argument is 3 | # given then the indentation defaults to 2. 4 | 5 | INDENT=" " 6 | if [[ -n "$1" ]]; then 7 | INDENT="$(printf %$1s)" 8 | fi 9 | 10 | sed "s/^$INDENT//" -------------------------------------------------------------------------------- /scripts/acme-command-line.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # Use pick to provide a quick command line a-la vim's `:` command mode 3 | # 4 | # Supported actions: 5 | # `e` -- attempt to open in a new acme window 6 | # `?` -- button 3 (search) in $winid tag 7 | # `!` -- button 2 (execute) in $winid tag 8 | # `` -- treat input as an argument to the built in `Edit` command 9 | 10 | 11 | # We expect to be called via a user configured global keybinding so we wont have 12 | # a $winid env var to read for targetting the correct window so ping the snooper 13 | # to find out what the active window is. (If the snooper returns -1 for the id 14 | # of the window then we have not been able to determine focus yet so exit) 15 | WINID="$(echo "active / ." | nc localhost 2009)" 16 | (( WINID > 0 )) || exit 1 17 | 18 | 19 | # == helper functions == 20 | # see acme(4) for details on the control files and event structure 21 | # NOTE: character offsets are zero indexed 22 | 23 | function windowDirectory() { 24 | dirname "$(9p read "acme/$WINID/tag" | head -1 | cut -d' ' -f1)" 25 | } 26 | 27 | function writeClickEvent() { 28 | local eventType=$1 offset=$2 end=$3 29 | 30 | echo "M$eventType$offset $end" 31 | echo "M$eventType$offset $end" | 9p write "acme/$WINID/event" 32 | } 33 | 34 | function setTag() { 35 | local text=$1 36 | 37 | echo "cleartag" | 9p write "acme/$WINID/ctl" 38 | echo -n "$text" | 9p write "acme/$WINID/tag" 39 | echo "clean" | 9p write acme/"$WINID"/ctl 40 | } 41 | 42 | function spoofClickInTag() { 43 | local eventType=$1 rawText=$2 44 | 45 | text="$(echo "$rawText" | xargs)" 46 | textLen="${#text}" 47 | echo "clean" | 9p write acme/"$WINID"/ctl 48 | fullTag="$(9p read "acme/$WINID/tag")" 49 | original="$(echo "$fullTag" | cut -d'|' -f2 | xargs)" 50 | nChars="$(echo "$fullTag" | cut -d'|' -f1 | wc -c)" 51 | offset="$(( nChars + 1 ))" 52 | end=$(( offset + textLen + 1 )) 53 | 54 | setTag " $text" 55 | writeClickEvent "$eventType" "$offset" "$end" 56 | echo "show" | 9p write acme/"$WINID"/ctl 57 | setTag " $original" 58 | } 59 | 60 | 61 | # == actions == 62 | 63 | function openInAcme() { 64 | local fname 65 | fname="$(echo "$1" | xargs)" 66 | 67 | case $fname in 68 | \~/*) fname="$HOME/${fname:2}";; 69 | /*) fname=$fname;; 70 | *) fname="$(windowDirectory)/$fname";; 71 | esac 72 | 73 | echo "name $fname" | 9p write acme/new/ctl 74 | id="$(9p read acme/index | grep "$fname" | xargs | cut -d' ' -f1)" 75 | echo 'get' | 9p write "acme/$id/ctl" 76 | } 77 | 78 | function searchFromTag() { spoofClickInTag "l" "$1"; } 79 | function executeFromTag() { spoofClickInTag "x" "$1"; } 80 | function runEditCommand() { executeFromTag "Edit $1"; } 81 | 82 | # == main == 83 | 84 | # Open a new pick window for the user to provide their command, if pick exits 85 | # with no output then the user hit enter with nothing typed or we were closed. 86 | # Either way, there is nothing else to do. 87 | input="$(echo "" | pick -s -p ':')" 88 | [[ -n "$input" ]] || exit 0 89 | 90 | # If we got some input, try to parse and action it 91 | case $input in 92 | \?*) searchFromTag "${input:1}";; 93 | !*) executeFromTag "${input:1}";; 94 | e*) openInAcme "${input:1}";; 95 | *) runEditCommand "$input";; 96 | esac -------------------------------------------------------------------------------- /scripts/acme-fuzzy-window-search.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # Use pick to search through the current active window, jumping to the 3 | # selected line and highlighting it. 4 | 5 | winid="$(echo "active / ." | nc localhost 2009)" 6 | lnum=$(pick -n -N) 7 | echo -n "$lnum" | 9p write acme/"$winid"/addr 8 | echo "dot=addr" | 9p write acme/"$winid"/ctl 9 | echo "show" | 9p write acme/"$winid"/ctl -------------------------------------------------------------------------------- /scripts/afocused: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Ask snooper for the current focused window. To stay consistant 4 | # with the error case when snooper is running, we return -1 if we 5 | # were unable to determine the current window ID. 6 | id="$(echo "active / ." | nc localhost 2009)" 7 | 8 | if [ "$?" -ne 0 ]; then 9 | echo -1 10 | exit 1 11 | fi 12 | 13 | echo "$id" 14 | -------------------------------------------------------------------------------- /scripts/alinum: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Show the current line number in acme 3 | 4 | ID=$1 5 | tag=$(9p read acme/$ID/tag) 6 | custom=$(echo $tag | awk -F'|' '{ print $2 }') 7 | 8 | echo cleartag | 9p write acme/$ID/ctl 9 | echo -n " Edit =" | 9p write acme/$ID/tag 10 | echo Mx12 19 | 9p write acme/$ID/event 11 | echo cleartag | 9p write acme/$ID/ctl 12 | echo -n "$custom" | 9p write acme/$ID/tag 13 | -------------------------------------------------------------------------------- /scripts/c-astyle: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # use the astyle tool to format my c code 4 | astyle --style=kr --indent=tab < "$1" > out.c 5 | mv out.c "$1" 6 | -------------------------------------------------------------------------------- /scripts/fmtoff: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Turn autoformating off 4 | echo "fmt / off" | nc localhost 2009 >/dev/null 5 | -------------------------------------------------------------------------------- /scripts/fmton: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Turn autoformating on 4 | echo "fmt / on" | nc localhost 2009 >/dev/null 5 | -------------------------------------------------------------------------------- /scripts/gg: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # My replacement for the normal 'grep -n' script. I always use ripgrep 3 | # anyway and it is noticebly faster in returning results 4 | 5 | rg --color=never --no-heading -n "$*" 6 | exit 0 # ripgrep has an exit code of 1 if there were no results which 7 | # ends up in our output if we don't explicitly get rid of it. -------------------------------------------------------------------------------- /scripts/json-format: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | formatted=$(jq -M < "$1") 3 | echo "$formatted" > "$1" 4 | -------------------------------------------------------------------------------- /scripts/spell-check: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # Spell check with suggestions for the currently focused acme-window 3 | 4 | WINID="$(echo "active / ." | nc localhost 2009)" 5 | (( WINID > 0 )) || exit 1 6 | 7 | fname="$(basename "$(9p read "acme/$WINID/tag" | cut -d' ' -f1)")" 8 | body="$(9p read "acme/$WINID/body")" 9 | errors="$(echo "$body" | aspell -a 2>&1 | grep -E '^&' | tr -d ':' | while read -r line ; do 10 | echo "$line" | cut -d' ' -f2,5,6,7 | tr -d ',' 11 | done)" 12 | 13 | echo "$errors" | sort -u | while read -r line ; do 14 | mistake="$(echo "$line" | cut -d' ' -f1)" 15 | suggestions="$(echo "$line" | cut -d' ' -f2,3,4)" 16 | echo "$body" | grep -n "$mistake" | cut -d':' -f1 | while read -r lnum ; do 17 | echo -e "$fname:$lnum\t$mistake -- $suggestions" 18 | done 19 | done | sort | column -t -------------------------------------------------------------------------------- /scripts/start-acme: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Start the acme text editor and some related helper utilities 3 | 4 | case "$(uname)" in 5 | Linux) 6 | # font="/mnt/font/TerminessTTFNerdFontComplete-Medium/10a/font" 7 | font="/mnt/font/ProFontForPowerline/10a/font" 8 | ;; 9 | Darwin) 10 | # font="/mnt/font/TerminessTTFNerdFontComplete-Medium/12a/font" 11 | font="/mnt/font/ProFontForPowerline/14a/font" 12 | ;; 13 | esac 14 | 15 | export PATH=$PATH:$PLAN9/bin 16 | export MANPATH=$MANPATH:$PLAN9/man 17 | export SHELL=rc 18 | 19 | pgrep plumber || plumber & 20 | 21 | if [ -f "$PLUMBFILE" ]; then 22 | cat "$PLUMBFILE" | 9p write plumb/rules 23 | else 24 | cat $PLAN9/plumb/basic | 9p write plumb/rules 25 | fi 26 | 27 | acme -f $font & 28 | sleep 1 # wait for acme to come up 29 | snoop-acme & 30 | -------------------------------------------------------------------------------- /snoop-acme/README.md: -------------------------------------------------------------------------------- 1 | snoop-acme - automate your acme experience 2 | ========================================== 3 | 4 | I love tinkering with stuff, which shouldn't be a surprise given that I'm 5 | writing a bunch of tooling around acme. I'm also pretty forgetful however so I 6 | like to be able to automate as much as I can on both my work and home machines. 7 | The ability to manipulate editor state via a virtual file system is _wonderful_ 8 | and a true joy for someone who likes to poke at stuff. It does mean that I need 9 | to remember all of the tooling that I'm writing though which is less good... 10 | 11 | Previously I have always used Vim (I still use it as my daily driver at work to 12 | be honest) but with the workflow of living in a terminal, having a LOT of 13 | aliases, shell functions and utility scripts dotted around the place and making 14 | use of programs such as [fzf][0] and [wd][1] to help me navigate things. I'd 15 | like to be able to pull my current tooling into acme itself and get a proper 16 | workflow sorted out that lets me make the edits I need to make, manage external 17 | systems and APIs and keep track of what I am doing. 18 | 19 | Enter acme-corp and the snooper. 20 | 21 | The snopper is a local webserver currently. I know, I know: it would be a lot 22 | more in the spirit of things to have it mount its own 9fs file system to do all 23 | of this but I _do_ know how to write webservers, I don't (currently) know how to 24 | get 9fs working in a nice way that doesn't require `9p` to work so there's not a 25 | huge difference either way. The server comes with several utility scripts that 26 | are essentially canned requests that set state to modify some tooling I've 27 | written: 28 | - Enable / disable format on save for all windows. 29 | - Allow for canned "you X-clicked on this text in the tag" events to be 30 | injected, so that they can be bound to keys using something like shkd or my 31 | compiled in dwm config. (Indent the current paragraph, wrap lines etc...) 32 | - I'd _really_ like to get a kind of vim-style `ex` mode with a pop up command 33 | line via a key binding. My laptops don't have decent three button mouse 34 | emulation which means modifiers and frantic swiping all over the place to get 35 | to the tag to make a simple `,x/#/ c/;;/` style comment replacement edit. When 36 | I have access to my _real_ mouse this isn't a problem but I want to get a "run 37 | this Edit command" pop up working ASAP: a lot of my edits in vim are done 38 | using `:%s/.../.../g` so if I can get a replacement for that which I'm happy 39 | with I'll be a very happy Glenda. 40 | 41 | [0]: https://github.com/junegunn/fzf 42 | [1]: https://github.com/mfaerevaag/wd 43 | -------------------------------------------------------------------------------- /snoop-acme/afmt.go: -------------------------------------------------------------------------------- 1 | package snoop 2 | 3 | // afmt watches acme for know file extensions on files being written inside 4 | // acme. Each time a known file is written, it runs the appropriate Tool and 5 | // reloads the file in acme. 6 | // 7 | // TODO: Rewrite this to modify the _window_ body rather than the underlying 8 | // files. Would this also require a check that we had been idempotent? 9 | 10 | import ( 11 | "fmt" 12 | "io/ioutil" 13 | "os/exec" 14 | "path" 15 | "strings" 16 | 17 | "9fans.net/go/acme" 18 | ) 19 | 20 | // A Tool is a program that can rewrite source files or report on errors that 21 | // were encountered in the code. 22 | type Tool struct { 23 | cmd string 24 | args []string 25 | outputFixer func(string) string 26 | appendFilePath bool 27 | appendDirPath bool 28 | ignoreOutput bool 29 | } 30 | 31 | // TODO: copy the window body to a temp file, format it there and then reload it 32 | // in the window body. Probably best to set up the temp file in 33 | // FileType.Reformat and then pass that in here? >> Look at how acmego 34 | // does this 35 | func (t *Tool) reformat(e *acme.LogEvent) string { 36 | args := t.args 37 | 38 | if _, err := ioutil.ReadFile(e.Name); err != nil { 39 | return err.Error() 40 | } 41 | 42 | if t.appendFilePath { 43 | args = append(t.args, e.Name) 44 | } else if t.appendDirPath { 45 | args = append(t.args, path.Dir(e.Name)) 46 | } 47 | 48 | cmd := exec.Command(t.cmd, args...) 49 | cmd.Dir = path.Dir(e.Name) 50 | b, _ := cmd.CombinedOutput() 51 | if t.ignoreOutput || len(b) == 0 { 52 | return "" 53 | } 54 | 55 | if t.outputFixer != nil { 56 | return t.outputFixer(string(b)) 57 | } 58 | 59 | return string(b) 60 | } 61 | 62 | // A FileType defines a set of Tools and an associated file type to run them on. 63 | // If the files support a unix shebang then we try to parse that as well if the 64 | // extension is missing. 65 | type FileType struct { 66 | extensions []string 67 | shebangProgs []string 68 | Tools []Tool 69 | } 70 | 71 | // Matches checks to see if this is a file we need to reformat 72 | func (f *FileType) Matches(e *acme.LogEvent) bool { 73 | fileExtension := path.Ext(e.Name) 74 | if len(fileExtension) > 0 { 75 | fileExtension = fileExtension[1:] 76 | for _, ext := range f.extensions { 77 | if fileExtension == ext { 78 | return true 79 | } 80 | } 81 | } 82 | 83 | s, err := getFirstLine(e.ID) 84 | if err != nil { 85 | return false 86 | } 87 | 88 | for _, prog := range f.shebangProgs { 89 | if strings.HasSuffix(s, prog) { 90 | return true 91 | } 92 | } 93 | 94 | return false 95 | } 96 | 97 | // Reformat applies all known formatters to the underlying file 98 | func (f *FileType) Reformat(e *acme.LogEvent) string { 99 | var output string 100 | var w *acme.Win 101 | var err error 102 | 103 | if w, err = acme.Open(e.ID, nil); err != nil { 104 | fmt.Println(err) 105 | return err.Error() 106 | } 107 | defer w.CloseFiles() 108 | 109 | for _, t := range f.Tools { 110 | output += t.reformat(e) 111 | } 112 | 113 | w.Ctl("get") 114 | w.Ctl("clean") 115 | return output 116 | } 117 | 118 | func getFirstLine(winid int) (string, error) { 119 | w, err := acme.Open(winid, nil) 120 | if err != nil { 121 | fmt.Println(err) 122 | return "", err 123 | } 124 | defer w.CloseFiles() 125 | 126 | w.Addr("#1-+") 127 | b := make([]byte, 256) 128 | n, _ := w.Read("xdata", b) 129 | return string(b[:n-1]), nil 130 | } 131 | -------------------------------------------------------------------------------- /snoop-acme/constants.go: -------------------------------------------------------------------------------- 1 | package snoop 2 | 3 | const ( 4 | tcpPort = 2009 5 | defaultSnoopTag = "Clear fmton fmtoff dirtree" 6 | prompt = ">> " 7 | ) 8 | -------------------------------------------------------------------------------- /snoop-acme/ftype.go: -------------------------------------------------------------------------------- 1 | package snoop 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | var formatableTypes = []FileType{ 9 | golang, python, shell, rust, c, javascript, json, 10 | } 11 | 12 | var golang = FileType{ 13 | extensions: []string{"go"}, 14 | Tools: []Tool{ 15 | Tool{cmd: "goimports", args: []string{"-w"}, appendFilePath: true}, 16 | Tool{cmd: "golint", appendDirPath: true}, 17 | Tool{cmd: "go", args: []string{"vet"}, appendDirPath: true}, 18 | }, 19 | } 20 | 21 | var python = FileType{ 22 | extensions: []string{"py", "pyw"}, 23 | shebangProgs: []string{"python", "python3"}, 24 | Tools: []Tool{ 25 | Tool{ 26 | cmd: "isort", 27 | args: []string{"-m", "5"}, 28 | ignoreOutput: true, 29 | appendFilePath: true, 30 | }, 31 | Tool{cmd: "black", args: []string{"-q", "--line-length", "100"}, appendFilePath: true}, 32 | // Black is pep8 compliant but flake8 is not... 33 | Tool{cmd: "flake8", args: []string{"--ignore=E203,E501,W503"}, appendFilePath: true}, 34 | }, 35 | } 36 | 37 | var rust = FileType{ 38 | extensions: []string{"rs"}, 39 | Tools: []Tool{ 40 | Tool{cmd: "cargo", args: []string{"fmt", "--"}, appendFilePath: true}, 41 | // Tool{cmd: "cargo", args: []string{"check"}}, 42 | // Tool{cmd: "cargo", args: []string{"clippy"}}, 43 | }, 44 | } 45 | 46 | var shell = FileType{ 47 | extensions: []string{"sh", "bash", "zsh"}, 48 | shebangProgs: []string{"bash", "sh", "zsh"}, 49 | Tools: []Tool{ 50 | // Remove trailing whitespace and whitespace only lines 51 | //Tool{cmd: "sed", args: []string{"-i", "s/[[:blank:]]*$//g"}, appendFilePath: true}, 52 | Tool{cmd: "shellcheck", args: []string{"-f", "gcc"}, appendFilePath: true}, 53 | }, 54 | } 55 | 56 | var javascript = FileType{ 57 | extensions: []string{"js"}, 58 | shebangProgs: []string{"node"}, 59 | Tools: []Tool{ 60 | Tool{cmd: "js-beautify", args: []string{"-r"}, appendFilePath: true}, 61 | Tool{ 62 | cmd: "jshint", 63 | appendFilePath: true, 64 | outputFixer: func(s string) string { 65 | // Convert to button3 friendly output 66 | s = strings.Replace(s, " line ", "", -1) 67 | // Remove the column number as it just adds line noise 68 | re := regexp.MustCompile(", col .*,") 69 | return re.ReplaceAllString(s, " ->") 70 | }, 71 | }, 72 | }, 73 | } 74 | 75 | var json = FileType{ 76 | extensions: []string{"json"}, 77 | Tools: []Tool{Tool{cmd: "json-format", appendFilePath: true}}, 78 | } 79 | 80 | var c = FileType{ 81 | extensions: []string{"c", "h"}, 82 | Tools: []Tool{ 83 | Tool{cmd: "c-astyle", appendFilePath: true}, 84 | Tool{ 85 | cmd: "splint", 86 | args: []string{ 87 | "+charintliteral", "+charint", "-exportlocal", "-compdef", 88 | "-usedef", "-retvalint", "+relaxtypes", 89 | }, 90 | appendFilePath: true, 91 | }, 92 | }, 93 | } 94 | -------------------------------------------------------------------------------- /snoop-acme/snoop-acme/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | 8 | "github.com/sminez/acme-corp/snoop-acme" 9 | ) 10 | 11 | func main() { 12 | var progEndSignals = []os.Signal{ 13 | syscall.SIGINT, 14 | syscall.SIGTERM, 15 | syscall.SIGHUP, 16 | } 17 | 18 | chSignals := make(chan os.Signal, 1) 19 | signal.Notify(chSignals, progEndSignals...) 20 | 21 | a := snoop.NewAcmeSnooper(false) 22 | a.Snoop(chSignals) 23 | } 24 | -------------------------------------------------------------------------------- /snoop-acme/snooper.go: -------------------------------------------------------------------------------- 1 | package snoop 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "9fans.net/go/acme" 9 | ) 10 | 11 | // An AcmeSnooper snoops on acme events and listens for custom action requests over 12 | // TCP. This allows for richer reuse of existing acme wrappers from acme.go 13 | type AcmeSnooper struct { 14 | win *acme.Win 15 | listener *Listener 16 | chLogEvents chan acme.LogEvent 17 | active int 18 | formatOn bool 19 | debug bool 20 | } 21 | 22 | // NewAcmeSnooper inits an acme snooper and grabs the /+snoop window so that we 23 | // can send messages back to acme in a consistent way. 24 | func NewAcmeSnooper(debug bool) *AcmeSnooper { 25 | win, err := acme.New() 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | win.Name("+snoop") 30 | win.Ctl("clean") 31 | win.Write("tag", []byte(defaultSnoopTag)) 32 | 33 | return &AcmeSnooper{ 34 | win: win, 35 | listener: NewListener(tcpPort), 36 | chLogEvents: make(chan acme.LogEvent), 37 | active: -1, 38 | formatOn: false, 39 | debug: debug, 40 | } 41 | } 42 | 43 | func (a *AcmeSnooper) logf(s string, args ...interface{}) { 44 | a.win.Write("body", []byte(prompt+fmt.Sprintf(s, args...))) 45 | a.win.Ctl("clean") 46 | } 47 | 48 | func (a *AcmeSnooper) errorf(s string, args ...interface{}) { 49 | a.win.Write("errors", []byte(fmt.Sprintf(s, args...))) 50 | } 51 | 52 | func (a *AcmeSnooper) tailLog() { 53 | l, _ := acme.Log() 54 | for { 55 | e, _ := l.Read() 56 | a.chLogEvents <- e 57 | } 58 | } 59 | 60 | func (a *AcmeSnooper) fmtHandler(s string) (string, error) { 61 | switch s { 62 | case "on": 63 | a.formatOn = true 64 | a.logf("format on save: enabled\n") 65 | return "on", nil 66 | 67 | case "off": 68 | a.formatOn = false 69 | a.logf("format on save: disabled\n") 70 | return "off", nil 71 | 72 | default: 73 | return "", fmt.Errorf("'%s' is not a valid format directive", s) 74 | } 75 | } 76 | 77 | func (a *AcmeSnooper) activeHandler(s string) (string, error) { 78 | return fmt.Sprintf("%d", a.active), nil 79 | } 80 | 81 | // Snoop kicks off our local server and starts listening in on acme events. 82 | func (a *AcmeSnooper) Snoop(chSignals chan os.Signal) { 83 | a.listener.Register("active", a.activeHandler) 84 | a.listener.Register("fmt", a.fmtHandler) 85 | 86 | go a.listener.HandleIncomingConnections() 87 | go a.tailLog() 88 | 89 | a.win.Write("body", []byte("-- acme corp --\n")) 90 | a.logf("snooper now running...\n") 91 | 92 | for { 93 | select { 94 | case e := <-a.chLogEvents: 95 | switch e.Op { 96 | case "": 97 | os.Exit(0) // acme was closed 98 | 99 | case "focus": 100 | a.active = e.ID 101 | 102 | case "put": 103 | if a.formatOn && len(e.Name) > 0 { 104 | for _, ft := range formatableTypes { 105 | if ft.Matches(&e) { 106 | s := ft.Reformat(&e) 107 | if len(s) > 0 { 108 | a.errorf(s) 109 | } 110 | break 111 | } 112 | } 113 | } 114 | 115 | default: 116 | // log.Printf("%s: %v\n", e.Op, e) 117 | } 118 | 119 | case <-chSignals: 120 | os.Exit(0) 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /snoop-acme/tcp.go: -------------------------------------------------------------------------------- 1 | package snoop 2 | 3 | // This file implements a simple TCP based protocol allowing clients to submit 4 | // messages of the form ' / ' to the snooper for processing. 5 | // Messages at this level are all arbitrary strings with parsing and processing 6 | // being done by the MessageHandlers themselves. Invalid messages will have an 7 | // error message returned to them but the format of that error is not 8 | // guaranteed by the server. 9 | 10 | import ( 11 | "bufio" 12 | "fmt" 13 | "net" 14 | "strings" 15 | ) 16 | 17 | // A MessageHandler is a function that knows how to parse a given message type 18 | type MessageHandler func(s string) (string, error) 19 | 20 | // A Message is a simple RPC message format to allow for very simple scripts 21 | // that pass simple string messages to the snooper for it to process. Ideally 22 | // this should be exposed as a 9fs file system in the same way as acme itself 23 | // but for now this will have to do. 24 | type Message struct { 25 | route string 26 | content string 27 | } 28 | 29 | // NewMessage constructs a new Message from a string that we received over the 30 | // network. At this stage we are not guaranteed that this message is valid, 31 | // only that we were able to split it into a handler and content. 32 | func NewMessage(s string) (*Message, error) { 33 | sections := strings.SplitN(s, "/", 2) 34 | if len(sections) != 2 { 35 | return nil, fmt.Errorf("Invalid message '%s'", s) 36 | } 37 | return &Message{ 38 | route: strings.TrimSpace(sections[0]), 39 | content: strings.TrimSpace(sections[1]), 40 | }, nil 41 | } 42 | 43 | // A Listener runs the event loop that owns our TCP socket and routes incoming 44 | // messages to their relevant handlers. 45 | type Listener struct { 46 | handlers map[string]MessageHandler 47 | port int 48 | } 49 | 50 | // NewListener initialises a new Listener without any handlers 51 | func NewListener(port int) *Listener { 52 | return &Listener{ 53 | handlers: make(map[string]MessageHandler), 54 | port: port, 55 | } 56 | } 57 | 58 | // HandleIncomingConnections binds to a tcp socket and serves handler responses 59 | // for incoming connections. Runs in a goroutine. 60 | func (l *Listener) HandleIncomingConnections() { 61 | s, _ := net.Listen("tcp", fmt.Sprintf("localhost:%d", l.port)) 62 | for { 63 | // silently dropping failed incoming connections 64 | conn, _ := s.Accept() 65 | go l.handleConnection(conn) 66 | } 67 | } 68 | 69 | // Register registers a new message handler with a given route 70 | func (l *Listener) Register(route string, handler MessageHandler) { 71 | l.handlers[route] = handler 72 | } 73 | 74 | // Runs in a goroutine per incoming connection 75 | func (l *Listener) handleConnection(conn net.Conn) { 76 | s, _ := bufio.NewReader(conn).ReadString('\n') 77 | defer conn.Close() 78 | 79 | msg, err := NewMessage(s) 80 | if err != nil { 81 | conn.Write([]byte(err.Error())) 82 | return 83 | } 84 | 85 | handler, ok := l.handlers[msg.route] 86 | if !ok { 87 | conn.Write([]byte(fmt.Sprintf("'%s' is not a known handler", msg.route))) 88 | return 89 | } 90 | 91 | resp, err := handler(msg.content) 92 | if err != nil { 93 | conn.Write([]byte(err.Error())) 94 | return 95 | } 96 | conn.Write([]byte(resp)) 97 | } 98 | --------------------------------------------------------------------------------