sh is ugly for sure, but it is a good constraint – and rail – for the creativity and composability (because it would be too ugly and slow to implement that and that in pure sh :D, we rely on external and language independant applications).
This is an odd reasoning, but a legitimate one. So, apparently, these are the arguments in favor of using a POSIX shell for scripting:
- It’s portable to any POSIX OS.
- Being a glue language, it encourages you to rely on external tools to do all sorts of work, thus enforcing composability.
- Being such a horrible language, no one wants to use it except for the bare minimum. Again, enforcing composability. In fact, the claim that Kakoune does not have a scripting language of choice is not very accurate, since it does have one (the POSIX shell), although not built-in; it’s just that it is such a pain to use in complex programs development that developers prefer to use something else.
So, let’s start to thing about alternative ways to enforce the same discipline without to much burden for plugin developers.
At the moment, I can see two possible alternatives I want to discuss with you.
Restrict the environment
Suppose Kakoune embeds a scripting language. It, as the host environment, can restrict what a script is able to see. I’m going to use Lua as an example, but I thing the same arguments hold for other scripting languages.
One very nice property of Lua is that it’s designed from the beginning not to be an interpreted language, but a scripting language. It means its design reflects the fact that it is conceived to be embedded in a host environment, being the Lua interpreter just an example of such a host. This is one of the reasons for its minimalism (the entire Lua C library has just a hundred kilobytes). As such, a Lua script can access the outside world only through the host, and the host has total control of what is exposed to the script.
For example, by default, its C API doesn’t expose any Lua module to the script. The host has to explicitly tell what it want the plugin to see.
So, one way to have Lua as a scripting language while still enforcing a Unix discipline is to load only the minimum set of modules necessary for the plugin to communicate with the POSIX environment (like the io
and os
modules). Or even providing a custom (more POSIX-like) module for accessing the OS, since the desire to be extremely portable and minimalist makes Lua do not provide many tools for interacting with the operating system.
Another interesting aspect is that the require
statement used to import modules is not really a statement, but a function. Being a function: it can be configured to just load modules from a specific PATH Kakoune has control of; it can be replaced by a custom one; or we can even remove the require
function altogether, making the plugin unable to use any module not explicitly provided by the host.
This way, we gain:
- a better scripting language;
- a way faster one;
- a portable one, because it’s embedded in Kakoune itself;
- a programming environment that enforces a composability discipline as much as we want.
Again, I’m using Lua as an example because I know it better (and I know it enough to say that it also has many aspects I don’t like at all). I’m sure you can list other languages with similar characteristics, and I really don’t care if another one would be chosen (in the case Kakoune decides to embed a scripting language). I don’t have a beloved programming language and actually like to learn new ones (provided they are not as arcane as bash scripting =D).
Provide a client-server model
A very interesting scripting model is the one adopted by bspwm. It has two programs: the actual window manager (bspwm
) and a configuration program (bspc
). The later communicates with the former using a socket, and it is used to send to bspwm
whatever messages necessary to control it.
It has some subcommands, among which bspc query
, to inspect bspwm
’s state, bspc config
, to set configuration options, and bspc subscribe
, to listen to events.
A configuration file is just an executable file bspwm
executes at initialisation. Here is mine:
#! /usr/bin/fish
sxhkd &
picom --experimental-backends &
bspc monitor eDP1 -d 1 2 3 4
bspc monitor HDMI1 -d 5 6 7 8
bspc config initial_polarity first_child
bspc config border_width 1
bspc config presel_feedback_color "#0b2f4f"
bspc config focused_border_color "#eff0f1"
bspc config normal_border_color "#2f353a"
bspc config window_gap 6
bspc config gapless_monocle true
bspc config click_to_focus true
bspc config pointer_follows_monitor true
bspc config remove_disabled_monitors true
bspc config remove_unplugged_monitor true
bspc rule -a plasmashell state=floating border=off layer=normal
bspc rule -a krunner border=off state=floating layer=above
My bspwmrc
is very basic and declarative, but it can have arbitrary logic in it if needed.
One nice property is that bspwm
is idempotent regarding the configuration. So, to reload the config file, one just needs to execute it again.
This models allows bspwm
to adhere more closely to the Unix philosophy. For instance, it doesn’t process keyboard shortcuts, relying on external programs (such as sxhkd
) to do so. So, a sample sxhkdrc
looks like this:
# Alternate between the tiled and monocle layout
super + m
bspc desktop -l next
# Swap the current node and the biggest node
super + g
bspc node -s biggest.local
# Cancel the preselection for the focused desktop
super + ctrl + shift + space
bspc query -N -d | xargs -I id -n 1 bspc node id -p cancel
Note how every command is just a simple shell command line invocation. Also, note how clever this model is: since the configuration file is just an executable, bspwm
doesn’t need to parse it; it doesn’t even need to read it, just execute it. From bspwm
perspective, its content is irrelevant. As such, I can write my config file in whatever language I want, even compiled ones. It’s written in fish
, but could also be in Lua:
#! /usr/bin/env lua
-- A bspwmrc written in lua
os.execute("sxhkd &")
-- ...
os.execute("bspc config initial_polarity first_child")
os.execute("bspc config border_width 1")
-- ...
Or, perhaps, Ruby:
#! /usr/bin/env ruby
# A bspwmrc written in ruby
`sxhkd &`
# ...
`bspc config initial_polarity first_child`
`bspc config border_width 1`
# ...
This is truly a language agnostic plugin model. Kakoune, instead, is tied to POSIX shell scripting more than we usually admit.
It also has the benefit to abstract away both the handling of communication with the server and the protocol to do so. If it uses json
, ZeroMQ
, raw strings, etc., for the user it doesn’t matter, and frees the bspwm
developers to change to another implementation if needed without compromising the API.
I don’t know if this is a model that can be used by Kakoune, but if it could, it would be a big improvement.