about summary refs log tree commit diff homepage
path: root/blog/threa.md
diff options
context:
space:
mode:
authorNguyễn Gia Phong <mcsinyx@disroot.org>2021-07-03 17:34:28 +0700
committerNguyễn Gia Phong <mcsinyx@disroot.org>2021-07-03 17:34:28 +0700
commit74ecc540a26b2d4daa8bc180d5c30bfc42833a28 (patch)
tree489a5a470511273ac8418c8e0871905f2b0cbaa5 /blog/threa.md
parent8f6d0be7e7e0f306f7c6f03edae28d77c6bc3813 (diff)
downloadsite-74ecc540a26b2d4daa8bc180d5c30bfc42833a28.tar.gz
Blog about threa
Diffstat (limited to 'blog/threa.md')
-rw-r--r--blog/threa.md393
1 files changed, 393 insertions, 0 deletions
diff --git a/blog/threa.md b/blog/threa.md
new file mode 100644
index 0000000..d1063e6
--- /dev/null
+++ b/blog/threa.md
@@ -0,0 +1,393 @@
++++
+rss = "Raku's concision demonstrated in form of a tutorial"
+date = Date(2021, 7, 3)
++++
+@def tags = ["fun", "raku", "clipboard"]
+
+# Writing a Clipboard Manager
+
+!!! note "A word of protest:"
+
+    This was intended to be presented in [The Raku Conference][],
+    however the organizers insisted on using [Zoom][] and [Skype][],
+    which are privacy invasive platforms running on proprietary software
+    and shadily managed.
+
+\toc
+
+## Motivation
+
+Clipboard management is very important to my workflow.  To me,
+a clipboard manager is useful in two ways:
+
+1. It extends my (rather poor) temporary mundane memory by caching
+   a few dozens of most recent selections.
+2. It synchronizes clipboard and primary selections.
+   Since some programs only support one kind of selection,
+   this is particularly useful.
+
+For the first point, I have to be able to choose from the history
+by pressing a few keystrokes.  Having to touch the mouse during
+writing sessions is unacceptable.  The menu dropping down from the systray
+is also undesirable because I have a multi-monitor setup.  This narrows down
+to only one plausible option: [Diodon][], which I having been using on Debian
+for at least two years.  However, as I was migrating to NixOS earlier
+last month, [I was unable to package it for Nix][126190].
+
+Naturally, I went looking for [alternatives][], most of which I had
+tried before and did not satisfy my requirements.  [clipmenu][] got
+my attention however: it was made to work with dmenu(-compliant launchers),
+which I had a rather nice experience with in [Sxmo][] on my [PinePhone][].
+However, I use [awesome][] on my workstation and its widget toolkit covers
+my launcher and menu need perfectly.  I don't need [dmenu][] and do not
+wish to spend time configuring and theming it.  Plus, the architecture
+of dmenu scripts and awesome widgets vastly differs: while awesome
+executes the input programs, dmenu is called from the scripts.
+
+## Inspirations and Design
+
+As even the most plausible candidate is not a suitable replacement,
+I would need to write my own clipboard manager.  clipmenu is not really
+a good base though because it's written in shell script, something I ain't
+fluent in.[^1]  Its idea is brilliant however:
+
+> 1. `clipmenud` uses `clipnotify` to wait for new clipboard events.
+> 2. If `clipmenud` detects changes to the clipboard contents,
+>    it writes them out to the cache directory
+>    and an index using a hash as the filename.
+
+I later translated [clipnotify][] to [Zig][][^2] and called it [clipbuzz][].[^3]
+From clipbuzz's usage,
+
+```sh
+while clipbuzz
+do # something with xclip or xsel
+done
+```
+
+and this is exactly how yet another clipboard manager was written,
+but before we get there, let's talk about this article's sponsor!
+
+I'm kidding d-; though we cannot jump into the implementation just yet:
+we only resolved the first point out of two.  How about the data structure?
+Hashing sounds like overengineering in this case: nobody needs more than
+a few dozen entries[^4] and hashes are not very memorable.  Printable characters
+can serve much better as indices.
+
+What?  What happens when we run out of them?  We reuse/recycle them![^5]
+They would also fit within one single line, heck, we just store all of them
+in order inside a file and rotate each time there's a new selection.
+Picking would just be moving a char to the beginning.  The entire
+cache directory can just look something like this:
+
+```console
+$ ls $XDG_CACHE_HOME/$project
+order
+R
+A
+K
+U
+```
+
+Wait, is that a sign?  We must use [Raku][] to implement `$project` then…
+Speaking of `$project`, I planned to use it with awesome and [vicious][]
+so let's call it something brutal, like a *cutting board*, which is *thớt*
+in Vietnamese, an Internet slang for *thread*.  Cool, now we have
+the daemon name, and conventionally the client shall be *threac*,
+or *threa client*.
+
+## Daemon Implementation
+
+### Reading Inputs
+
+Raku was chosen[^6] for the ease of text manipulation and seamless interfacing
+with external programs.  I learned it quite a while ago and has always been
+waiting for a chance to do something more practical with it, other than
+competitive programming which isn't a good fit due to Rakudo's poor performance.
+In Raku, the snippet from clipbuzz's README becomes:
+
+```sh
+while run 'clipbuzz' {
+    # do something with xclip or xsel
+}
+```
+
+Out of all languages I know, this is by far the simplest way to [run][]
+an external program.  Most would require one to import something
+or do something with the call's return value, and don't even get me start
+on POSIX *fork* and *exec* model.
+
+OK, now what are we gonna do with `xclip`?  One obvious thing would be
+to read the current selection.  Raku got you covered, fam:
+
+```sh
+my $selection = qx/xclip -out/;
+```
+
+Remember when I said Raku can seamlessly interact with external programs?
+[qx][] is how you capture their standard output, it is really that simple.
+But wait, which selection is that?  No worries, `xclip` supports both
+primary and clipboard:
+
+```sh
+my $primary = qx/xclip -out -selection primary/;
+my $clipbroad = qx/xclip -out -selection clipboard/;
+```
+
+### Cache Directory Setup
+
+This is when we write those selection down for later use, right?
+Well, we need to figure out where to save them first.  According to
+[XDG Base Directory Specification][XDG], `$XDG_CACHE_HOME` shall
+falls back to `$HOME/.cache`:
+
+```sh
+my $XDG_CACHE_HOME = %*ENV<XDG_CACHE_HOME> // path $*HOME / '.cache':;
+```
+
+For convenience purposes, I defined the `/` operator as an alias
+for path concatination:
+
+```sh
+multi sub infix:</>($parent, $child) { add $parent: $child }
+```
+
+With `$XDG_CACHE_HOME` defined, we can prepare the base directory as follows:
+
+```sh
+my $base = $XDG_CACHE_HOME.IO / 'threa';
+mkdir $base: unless $base.e;
+die "thread: $base: File exists" when $base.f;
+```
+
+As [a wise man once said][roles],
+
+> As it often happens, writing an article ends up with a bug found in Rakudo.
+
+In this case, there's a [bug in mkdir][1507] that makes it happily returns
+even if the target path is a file.  I'm trying to fix it at the moment
+but [a test][4408] is still failing.
+
+Anyway, back to our clipboard manager.  Here we are using
+[flow controllers][control] such as `unless` and `when` in the form
+of *statement modifiers*, which can sometimes be easier on eyes
+keeping the code flat.  Existence checks like `e` (exists) and `f` (file)
+are also really handy.  Next, we check on the `order`:
+
+```sh
+constant $ALNUM = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
+
+sub valid($path) {
+    return False unless $path.f;
+    so /^\w\w+$/ && .chars == .comb.unique given trim slurp $path
+}
+
+my $order = $base / 'order';
+spurt $order, $ALNUM unless valid $order;
+```
+
+Instead of printable, we only allow alphanumerics and fallback to
+the uppercase ones (mainly because my screen can only fit as much vertically),
+unless `$XDG_CACHE_HOME/threa/order` is a file, contains at least
+two unique alphanumerics (exclusively).  Reading and writing files in Raku
+is incredibly trivial, just `slurp` and `spurt` the path.  Since we are not
+interested in whitespaces, they are `trim`'ed from `order`.  Notice that
+Raku allows subroutines to be called without any parentheses—I love Lisp,
+but opening parenthesis *after* the function name always confuses me,
+especially when nested.
+
+As you might have guessed, `given` is another statement modifier
+setting the [topic variable][] that is particularly useful
+in [pointfree programming][], where regular expressions (e.g. `/^\w\w+$/`)
+are matched against directly and methods are called without specifying
+the object.  Raku is also a weakly-typed language: `.comb.unique` (a list
+of unique characters) is coerced into an integer when compared to one
+(number of `.chars`).
+
+### Comparing and Saving Selections
+
+What do we do with the order then?  First we can determine the latest
+selection and compare it to the ones we got from `xclip` earlier
+to see which one is really new.  We'll also need to rotate the order,
+i.e. write the new selection to the `$last` file and move it in front
+of the others that we `$keep` as-is:
+
+```sh
+my ($first, $keep, $last) = do
+    given trim slurp $order { .comb.first, .chop, .substr: *-1 }
+my ($other, $content) = do given try slurp $base / $first or '' {
+    when * ne $primary { 'clipboard', $primary }
+    when * ne $clipboard { 'primary', $clipboard }
+}
+```
+
+On the first few run, probably the cache files don't exist just yet,
+so we fall them back to empty ones using `try ... or ...`.  We need
+to know the `$other` selection (outdated one) to later synchronize them both.
+In case of reselection, neither is updated and we simply skip this iteration:
+
+```sh
+next unless $other;
+```
+
+Otherwise, let's go ahead, write down the `$content`, rotate `$order`
+and synchronize with the `$other` selection:
+
+```sh
+my $path = $base / $last;
+spurt $path, $content;
+spurt $order, $last ~ $keep;
+run <xclip -in -selection>, $other, $path
+```
+
+That's it, now put the daemon in `$PATH` and run it in `~/.xinitrc`
+or something IDK.  If you're worried that some selection might be
+too big to read that you'll the next event, asynchronize the `qx` calls
+by prefixing them with `start`, and `await` the results later on.
+It is *that* easy.
+
+### Command-Line Interface
+
+Hol up, what if I want to store the cache elsewhere or use another set
+of characters?  *"Then you can go right ahead and have an intercourse
+with yourself, you ungrateful little piece of [redacted]."*  I would have said
+this were I to implement this in other languages, but luckily I got Raku,
+and Raku got `sub MAIN`:
+
+```sh
+sub MAIN(
+  :$children where /^\w\w+$/ = $ALNUM, #= alphanumerics
+  :$parent = $XDG_CACHE_HOME           #= cache path
+) {
+    my $snowflakes = $children.comb.unique.join;
+    my $base = $parent.IO / 'threa';
+    my $order = $base / 'order';
+
+    while run 'clipbuzz' {
+        ...
+        spurt $order, $snowflakes unless valid $order;
+        ...
+    }
+}
+```
+
+No matter how cool you think this is, it is cooler, I mean, look:
+
+```console
+$ thread --help
+Usage:
+  thread [--children[=Str where { ... }]] [--parent=<Str>]
+  
+    --children[=Str where { ... }]    alphanumerics
+    --parent=<Str>                    cache path
+```
+
+## Client Backend Implementation
+
+Following the Unix™ philosophy, `threac` will do only one thing and do it well:
+it shall take the chosen selection and *schedule* it to move to the beginning:
+
+```sh
+my $XDG_CACHE_HOME = %*ENV<XDG_CACHE_HOME> // path add $*HOME: '.cache':;
+
+sub MAIN(
+  Str  $choice where /^\w?$/,    #= alphanumeric
+  Str :$parent = $XDG_CACHE_HOME #= cache path
+) {
+    my $base = $parent.IO.add: 'threa';
+    my $order = add $base: 'order';
+    spurt $order, S/$choice(.*)/$0$choice/ with $order.slurp;
+    my $path = $base.add: $choice;
+    run 'xclip', $path
+}
+```
+
+The highlight here is the non-destructive substitution `S///`,
+which allow regex substitution in a pointfree and pure manner.
+Though, instead of moving `$choice` to top of the deque, we place it
+at the bottom and use `xclip` to trigger the daemon to do it
+and synchronize between selections.
+
+## Client Frontend Implementation
+
+Note that `threac` does not give any output: selection history (by default) 
+are stored in a standard and convenient location to be read by any frontend
+of choice.  For awesome I made a menu whose each entry is wired to `threac`
+and `xdotool` (to simulate primary paste with `S-Insert`) and bind
+the whole thing to a keyboard shortcut.
+
+```
+local base = os.getenv("HOME") .. "/.cache/threa/"
+local command = "threac %s && xdotool key shift+Insert"
+local f = io.open(base .. "order")
+local order = f:read("*a")
+f:close()
+
+local items = {}
+for c in order:gmatch(".") do
+  local f = io.open(base .. c)
+  table.insert(items, {f:read("*a"):gsub("\n", " "), function ()
+    awful.spawn.with_shell(command:format(c))
+  end})
+  f:close()
+end
+awful.menu{items = items, theme = {width = 911}}:show()
+```
+
+## Conclusion
+
+Through writing a clipboard manager, which is released as [threa][]
+under [GNU GPLv3+][] on [SourceHut][], we have discovered a few features
+of Raku that make it a great *scripting* language:
+
+* Out-of-box CLI support:
+  - Running programs and capturing output
+  - Environment variables
+  - File system operations
+  - Builtin argument parser
+* Concision:
+  - Statement modifiers
+  - Topic variable
+  - First-class regex
+* Trivial asynchronization
+
+As a generic programming language, Raku has other classes of characteristics
+that makes it useful in other larger projects such as grammars (i.e. regex
+on steroids) and OOP for human beings.  It is a truly versatile language
+and I really hope my words can convince someone new to try it out!
+
+[^1]: I ain't proud of this, okay?
+[^2]: I'm obsessed with exotic languages.
+[^3]: The *z*'s are for Zig, how original, I know.
+[^4]: [*citation needed*]
+[^5]: Wow much environment!
+[^6]: By some supernatural being of course!
+
+[The Raku Conference]: https://conf.raku.org
+[Zoom]: https://stallman.org/zoom.html
+[Skype]: https://stallman.org/skype.html
+[Diodon]: https://launchpad.net/diodon
+[126190]: https://github.com/NixOS/nixpkgs/pull/126190
+[alternatives]: https://search.nixos.org/packages?query=clip
+[clipmenu]: https://github.com/cdown/clipmenu
+[Sxmo]: https://sxmo.org
+[PinePhone]: https://www.pine64.org/pinephone
+[awesome]: https://awesomewm.org
+[dmenu]: https://tools.suckless.org/dmenu
+[clipnotify]: https://github.com/cdown/clipnotify
+[Zig]: https://ziglang.org
+[clipbuzz]: https://git.sr.ht/~cnx/clipbuzz
+[vicious]: https://vicious.rtfd.io
+[Raku]: https://raku.org
+[run]: https://docs.raku.org/routine/run
+[qx]: https://docs.raku.org/syntax/qx
+[XDG]: https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html
+[roles]: https://vrurg.github.io/2021/06/16/article-on-roles
+[1507]: https://github.com/MoarVM/MoarVM/pull/1507
+[4408]: https://github.com/rakudo/rakudo/pull/4408
+[control]: https://docs.raku.org/language/control
+[topic variable]: https://docs.raku.org/syntax/$_
+[pointfree programming]: https://raku-advent.blog/2020/12/22/draft-whats-the-point-of-point-free-programming
+[threa]: https://sr.ht/~cnx/threa
+[GNU GPLv3+]: https://www.gnu.org/licenses/gpl-3.0
+[SourceHut]: https://sourcehut.org