README.md docs: document seamless SSH mode 32 revisions

wnl—Wait 'n' Listen #

wnl helps create a "Unix as IDE" workflow. You bind a frequently-run command with wnl, then trigger it from anywhere with wnlctl. Optionally configure a hotkey in your desktop environment, and you have keyboard shortcuts for ad-hoc shell commands.

asciicast

plaintext demo
# you frequently run this command,
# usually pressing up+enter to run it again and again
me@pc:~$ make test
running tests...
done
# preface that command with `wnl` to bind that command
me@pc:~$ wnl make test
# nothing happens until you trigger wnl with the `wnlctl` command,
# e.g. in another shell.
# it's useful to bind `wnlctl` to a global shortcut in your desktop environment
running tests...
done
[[ finished with exit code 0 at 13:07:25 ]]
# even after that, wnl keeps running, waiting to be triggered again
running tests...
done
[[ finished with exit code 0 at 13:08:17 ]]

For example:

  1. Bind a command (COMMAND) to SLOT_ID 1 in one shell with wnl (see below for explanation of slots)

    $ wnl 1 make test
    
  2. (repeatedly) Trigger COMMAND by calling wnlctl from another shell

    $ wnlctl 1
    

    Or bind wnlctl 1 to a keyboard shortcut within your desktop environment.

  3. (optional) Interrupt COMMAND with wnlctl

    $ SIGNAL=USR2 wnlctl 1
    
  4. When you're done with this command, un-bind it by exiting wnl with Ctrl-c

While COMMAND is running, repeated calls to wnlctl do nothing. When COMMAND is not running, wnl will sit and wait until wnlctl triggers it again.

Contents #

Installation #

Manual #

Run sudo make install to install system-wide.

Run make install USER_LOCAL=1 to install just to your home directory.

Uninstall with sudo make uninstall and make uninstall USER_LOCAL=1 respectively.

Alternatively, you can just put these two shell scripts—wnl and wnlctl—in your $PATH, e.g. in ~/.local/bin.

Dependencies #

  • flock
    • util-linux package on Arch
    • util-linux package on Debian
    • util-linux-core package on Fedora
    • util-linux package on openSUSE
  • tput
    • ncurses package on Arch
    • ncurses-bin package on Debian
    • ncurses package on Fedora
    • ncurses-utils package on openSUSE
  • pgrep and pkill
    • procps-ng package on Arch
    • procps package on Debian
    • procps-ng package on Fedora
    • procps package on openSUSE
  • socat
    • socat package on Arch
    • socat package on Debian
    • socat package on Fedora
    • socat package on openSUSE

Packages #

build result

Download the packages or add the openSUSE Build Service repository here. Current distros include:

  • Arch
  • Debian
  • Fedora
  • openSUSE
  • Ubuntu

It's recommended to follow the instructions for "Add repository and install manually." If instead you choose "Grab binary package directly," you'll need to install the package with your package manager, such as with pacman -U /path/to/wnl*.pkg.tar.zst for Arch.

Please report any issues you may encounter with the packages!

Usage #

Syntax #

wnl [SLOT_ID] COMMAND [COMMAND_ARGUMENTS...]
wnl [SLOT_ID] ssh [REMOTE_SLOT_ID] SSH_REMOTE_HOST [COMMAND [COMMAND_ARGUMENTS...]]
wnlctl [SLOT_ID]

Slots #

A "slot" (specified with SLOT_ID) represents a single instance of wnl. This allows for multiple, separate commands to be bound:

# running two instances in subshells, just to keep this example concise
$ (wnl 1 echo hi from slot 1! &); (wnl 2 echo hi from slot 2! &)
$ wnlctl 1; wnlctl 2
[[ running echo hi from slot 1! at 10:12:29 in slot 1 ]]
[[ running echo hi from slot 2! at 10:12:29 in slot 2 ]]
hi from slot 1!
hi from slot 2!
[[ finished echo hi from slot 1! with exit code 0 at 10:12:29 in slot 1 ]]
[[ finished echo hi from slot 2! with exit code 0 at 10:12:29 in slot 2 ]]

SSH Mode #

The ssh syntax shown above allows you to use wnlctl locally to trigger an instance of wnl running on a remote host. An interactive SSH session will be opened to the host specified in SSH_REMOTE_HOST. wnl must already be installed on the remote host.

If COMMAND is specified, wnl will immediately be started on the remote host, ready to be triggered by your local calls to wnlctl.

user@localhost:~$ wnl ssh remotehost.example.com make test
wnl starting with slot 1
wnl starting with slot 1 on remotehost

If COMMAND is not specified, wnl will not be started and you'll be given a normal, interactive SSH session. You will have to manually run wnl [REMOTE_SLOT_ID] COMMAND. Instructions to that effect will be printed by wnl before opening the SSH session.

# you'd rarely want to manually specify REMOTE_SLOT_ID (3 here),
# but it's an option
user@localhost:~$ wnl 2 ssh 3 remotehost.example.com
wnl starting with slot 2
enter in 'wnl 3 <yourcommand>'
user@remotehost:~$ wnl 3 echo hi on a remote host!
# you trigger slot 2 with wnlctl on your local machine
hi on a remote host!

Options #

SLOT_ID: Numeric identifier of the slot. wnl defaults to the first unused slot (counting up from 1). wnlctl defaults to slot 1.

REMOTE_SLOT_ID: In SSH mode, numeric identifier of the slot used on the remote host. By default, it is the same as SLOT_ID.

Environment #

SIGNAL: Used with wnlctl. The signal that is sent to wnl. Either USR1 to tell wnl to start command execution, or USR2 to tell wnl to terminate execution. Defaults to USR1.

DOUBLE_TAP_REQUIRED: Used with wnl. If true, two signals from wnlctl are required before triggering COMMAND. Choose true or false. Defaults to false.

RESTART_MODE: Used with wnl. If true, a trigger from wnlctl while COMMAND is already running will restart COMMAND. Choose true or false. Defaults to false.

Configuration #

User configuration file: ~/.config/wnl/wnlrc

The only interesting things to configure are hooks. Hooks are shell snippets that are executed at various points in wnl's lifecycle:

Name Description
HOOK_STARTUP Run once when wnl starts
HOOK_EXIT Run once when wnl exits (after you hit Ctrl-c)
HOOK_PRE Run just before each invocation of COMMAND
HOOK_POST Run just after each invocation of COMMAND. The variable EXIT_CODE contains the command's exit status.

Example wnlrc:

# Play a gentle tone whenever wnl is triggered
HOOK_PRE='pw-play /usr/share/sounds/ocean/stereo/service-logout.oga &'
# Play a an alert whenever the command run by wnl fails with a nonzero exit code
# $EXIT_CODE is set to the exit code from the now-finished command
HOOK_POST='test "$EXIT_CODE" -eq 0 || pw-play /usr/share/sounds/oxygen/stereo/message-connectivity-error.ogg &'
# ANSI color/formatting codes are available in $FMT_* variables
HOOK_EXIT='echo "$FMT_GREEN$FMT_BOLD"; cowsay thanks for using wnl; echo "$FMT_NORMAL"'

Roadmap #

  • Multiple instances ("slots")
    • Automatically use first available slot if slot ID not specified
  • Allow sending SIGINT to command executions
  • Richer controls from wnlctl
    • Switch from signals for IPC to text over unix stream sockets
    • wnlctl can instruct wnl to send arbitrary signals to command executions
    • Send text to command executions' stdin
  • Add RESTART_MODE to restart a still-running command, rather than doing nothing
  • Emit shell integration escape codes to enable skipping between command executions
  • Config file for things like emitting shell integration escape codes, enabling/configuring the banner emitted after a command executions finishes
  • Pre- and post-exec hooks
  • Shell completion
    • Bash
    • Fish
  • Manpage
  • Packages
    • Arch
    • Debian
    • RPM
  • Add DOUBLE_TAP_REQUIRED to require multiple, quick signals from wnlctl to prevent accidental/fat-fingered triggers
  • Add wnl ssh subcommand

The problem space #

In both IDEs and Unix-as-IDE, you need to execute various tasks besides editing source code:

  • run tests
  • build executables
  • push code

In a typical IDE, you have buttons for these functions. More critically, you have keyboard shortcuts. Those shortcuts allow you execute those other tasks with little friction—just a brief flick of your fingers, without any need to even unfocus from your editor.

In Unix-as-IDE, what's typical is to go to a shell (either by Ctrl-Z to background your text editor, or by switching to a window, terminal pane, etc.). Then you run your command in the shell. Since it's probably the same command you had run in that shell before, you press Up+Enter to make it easy.

That typical Unix-as-IDE approach has the virtues of being both simple and easily composable— it's easy to understand, and it's trivial to modify your command lines as your needs, languages, and toolchains change.

However, it lacks the extremely rapid feedback loops and reduced mental overhead that you get with IDEs' keyboard shortcuts.

Example scenarios #

Application deployment #

You are writing an application and want to frequently deploy from your workstation to a test environment. Deployment isn't sufficiently fast/cheap/safe, so it doesn't make sense to trigger a deploy whenever you save in your editor. Normally, you work in your text editor, and then periodically switch to a shell and deploy with make deploy.

To set up wnl, you bind wnlctl to Ctrl-F1, and SIGNAL=USR2 wnlctl to Ctrl-Super-F1 in your desktop environment. Then, when you start to work on your application, you run wnl make deploy in a shell. At any point, you can hit Ctrl-F1 to run your deployment, and Ctrl-Super-F1 to abort the deployment.

Applying configuration management #

You're writing some config management code like Ansible or Terraform/OpenTofu. Frequently, you run terraform apply -auto-approve. Sometimes, you also run a diagnostic like curl https://dev.example.com/api/healthz -ik.

With wnl, you'd use your desktop environment to set up shortcuts for a couple different slots:

Shortcut Command Description
Ctrl-F1 wnlctl 1 Runs the commmand in slot 1 if it's not already running
Ctrl-Super-F1 SIGNAL=USR2 wnlctl 1 Stops the command in slot 1 if it's running
Ctrl-F2 wnlctl 2 Runs the commmand in slot 2 if it's not already running
Ctrl-Super-F2 SIGNAL=USR2 wnlctl 2 Stops the command in slot 2 if it's running

You then run wnl 1 terraform apply -auto-approve in one shell, and wnl 2 curl https://… in another. You can then use the configured keyboard shortcuts to trigger terraform and curl without ever leaving your editor or breaking your focus.

Other Unix-as-IDE tools #

  • entr: run arbitrary commands when files change This will do much of what wnl does, but automatically, based on file changes. Good if your tasks are suitable for automatic execution.