Plan 9 in Linux: Mouse menus
Let’s begin with an introduction to Plan 9 (from Bell Labs): it is an old UNIX-like operating system from the 90s, where1 everything is pretty much either text, image or some sort of rectangular box. There are no fancy graphical menus of any kind, no title bars, no file managers and nothing utilizes icons, but the window manager is still stacked. Everything is done with text, actions in a window can be done by “clicking” any text inside that corresponds to an available action. For example, in the main text editor “acme”, you open a file by just pressing2 on a string “New”, which for convenience is always available up top:
How you manage windows and differentiate between selecting some text and “executing” it is done entirely via a three-button mouse. Each button corresponds to some sort of action, and different application can configure how every button works, which is why I’ll only work with the “default” options in the desktop and terminal windows. Every action is started by pressing (and potentially holding) a mouse button, and then releasing it (pressing other buttons before release cancels the action).
The left mouse button (button 1), works as expected: you use it to select text.
Pressing and holding down the right mouse button (button 3), shows all possible window options:
For example, if you release it while hovering “New”, you cursor will change, and after holding it down again, dragging out will result in a grey rectangle representing the dimensions (and position) of a new terminal window (releasing the button creates that new window).
“Opening” an application is always done via a command in the terminal, where the program will “take over the window”.
Finally, the middle mouse button (button 2), shows text-control options, which can be executed on selected text:
You’ll recognise cut
, paste
and snarf
3, the latter being just another word for copy.
Since every modern window manager supports those in some form, I’ll concentrate on the others.
plumb
is the most interesting: it sends the selection to a “plumber” application, which practically4 tries to open or execute it.
For example, a selected URL will open in the browser, but a selected image file will open in an image viewer.
send
enters the selected text as a command in the current shell.
Usefulness in Linux
#Implementing these ideas in Linux sound like a fun sunday project, but that doesn’t mean they will be handy.
Arguably, when it comes to managing windows, all Linux window managers do it more efficiently than rio. There isn’t much to gain with implementing the right-click menu, but I’ll still dip into it, for completeness sake.
Text actions (plumber and “send”) are the exact opposite. A very simple use case would be opening files in the current folder, it could be faster by selecting it and doing a keyboard combination (to send it to plumber) than typing out a command.
Alternatively, what if you’re looking through some sort of guide or manpage in the terminal, and you see a command listed, that you want to run? No worries, select it and send it!
If you’re reading text in another language and you don’t understand some word, you can open a new browser window with a translation page just by selecting it and clicking the appropriate option.
Another idea would be: say you’re going through some sort of text document, and you want to email some part of the document to someone. Yet again, select that, press the appropriate menu item or combination, and a proper email window will open with the text inside.
While you can do all of those stuff normally, if you do them often enough, the time save could stack up. And everything depends on your configuration, you can make the plumber do as much or as little as you would like, and send could run commands in different shells or terminals depending on context. Most importantly, this works in any application, any time, any where, as long as you can select the text.
Going about it
# Another “constraint” is that I use openbox, an X11 window manager, so my solution will be oriented around tools that work with it. The result should be simple enough that adapting it to your window manager shouldn’t take too much effort.Looking around, you would first find a video by Luke Smith, where he creates a small plumber in bash (YouTube link, Odysee link) and an expansion of it by Ren Tatsumoto (surprisingly enough from around 3 months before I started). These posts served as a good base and inspiration.
The overall life cycle is split into four parts:
- Get selection
- Open menu
- Execute action
#1
#A tool that allows that (mentioned in Ren Tatsumoto’s blog post) is sxhkd. Sparing you the details, unfortunately sxhkd cannot pass the mouse press to the application below, if you want something to also happen on key-down (Issue 198, and the workaround in didn’t work). This means that the original idea isn’t really feasible, but I couldn’t really find any other good tool that does what I wanted.
Ctrl + Button3 is used for opening links, Alt + Button3 is reserved for resizing the current window and binding with the Super (Windows) key causes issues with opening of my application menu.After a lot of research, I found out there is actually a fourth modifier key, called Hyper
, which X11 still supports.
It’s inception come from 1978 with the Space-cadet keyboard, but thanks to the popularity of the Model M, modern keyboards don’t include it.
All that was left was to assign Hyper
to some key that I don’t use, and what better than Caps Lock!
There are many ways to do it, but I prefer using setxkbmap:
setxkbmap -option "caps:hyper"
This line is needs to be ran at start of openbox, so it’s placed inside ~/.config/openbox/autostart
.
# Hyper_L + Middle mouse button
mod4 + button2
$HOME/.a/plumb.sh 1
# Hyper_L + Right mouse button
mod4 + button3
$HOME/.a/plumb.sh
For details about these options, refer to the manpage.
Feel free to change this behaviour.#2
#Getting the current selection turns out to be easy, X11’s selections (clipboard) is split into a primary, secondary and a clipboard, where the primary is the current selection you’ve done with your mouse, the secondary isn’t used, and the clipboard is what you know and love. uninformativ.de has a great blog post about X11’s “clipboard”, I recommend it, even though it dives more into C than one would like.
In the very beginning, we’ll just store the selection in a variable and work from there:selection="$(xclip -o)"
#3
#With that out of the way, all we now need is an actual menu to appear. Luke Smith and Ren Tatsumoto both used dmenu, which is keyboard driven. As for a mouse-oriented standalone menu, that could be easily configured, all I could find was my current application menu: jgmenu.
Generally it is meant to serve as an application menu, but with --simple
you can directly echo and pipe to it all menu option and what they do.
Menu options are formatted as comma-separated (CSV) text, where each option is on a new line, the first item being the name, and the second, the executed command upon press:
Send,echo 1
Resize,echo 2
Move,echo 3
In this case I’m echo-ing a number, rather than doing an action, for readability and formatting sake. The number is parsed by a switch statement, so all actions are in a single place and can be more easily formatted than if they were inlined in the string.
Via --at-pointer
the menu can spawn right under the pointer.
Unfortunately it always spawns in such a way that the pointer is at the top-left corner, I couldn’t find a way to make it appear in a way that the first option is below the pointer.
Finally, since I use it as an application menu too, I specify a config file which makes it look more like Plan 9’s menu (though I’ve personally omitted the color scheme, since it doesn’t fit my normal theme):
menu_width = 1
arrow_width = 1
icon_size = 0
menu_radius = 0
item_radius = 0
# Plan 9 color scheme
menu_border = 5
menu_padding_top = 0
menu_padding_right = 0
menu_padding_bottom = 0
menu_padding_left = 0
color_menu_bg = #eaffea 100
color_menu_border = #88cc88 100
color_norm_fg = #000000 100
color_sel_bg = #448844 100
color_sel_fg = #000000 100
More info on these options can be found in the manpage.
Putting it all together, it looks something like this:
action=$(echo -ne "Send,echo 1\nResize,echo 2\nMove,echo 3\n" | jgmenu --at-pointer --simple --config-file="$HOME/.config/jgmenu/jgmenusimplerc")
case "$action" in
1) ...
;;
2 ...
;;
3) ...
;;
*)
...
;;
esac
An important aspect of our menu options is that they can change depending on context. For example, if we have a URL we might want the options “Open it in browser” and “Download”, which we wouldn’t want in case of a .docx file. Or we might want it “ship” it with a lot of possible menu options and let the user decide which ones they need.
This is very simple to do, and can be done in many ways.
My preferred way is to have a variable menu
to which different options are appended depending on a regular expression or “flag” (variable).
To work with regular expressions more easily, I’ve made a function which returns a status code depending on matching.
All in all, it would look something like this:
menu=""
windowOptions=0
function matches() {
echo "$selection" | grep "$1" >/dev/null 2>&1
}
[ windowOptions -eq 1 ] && menu+="Resize,echo 3\nMove,echo 4\nDelete,echo 5\n,....."
matches ".*\.cpp" && menu+="Compile and run,echo 10\n"
action=$(echo -ne "$menu" | jgmenu --at-pointer --simple --config-file="$HOME/.config/jgmenu/jgmenusimplerc")
case "$action" in
...
esac
#4
#Finally, we need to execute a selected option, aka put commands in each case of $action
.
Open files
#Opening a file is very simple, xdg-open
handles opening files by name, with the default program for that file extension.
This problem was solved somewhat by Luke Smith, he figured out a way to find (and cd
into) the current directory of the active window (pid1
should be pid
, but you’ll see later why I did that change):
pid=$(xprop -id "$(xprop -root | awk '/_NET_ACTIVE_WINDOW\(WINDOW\)/{print $NF}')" | grep -m 1 PID | cut -d " " -f 3)
pid1=$(pstree -lpA "$pid" | tail -n 1 | awk -F'---' '{print $NF}' | sed -re 's/[^0-9]//g')
cd "$(readlink /proc/"$pid1"/cwd)"
Still, one important situation where this doesn’t work is tabs, but those are handled differently from application to application. I figured out tabs only for terminal emulators, since that’s where text actions can be the most useful.
The whole situation turned out to be quite convoluted. We only get the active window, but there is no way to get the active/currently “seen” subprocess of a window. Additionally, pretty much all terminal emulators have different shell, not terminal, processes for the different tabs. Terminal plugins/extensions and special application were considered, but they would take too much effort and/or couldn’t work.
In the end, I settled on a hack: the shell would do the heavy lifting.
I use zsh, so I implemented it by creating a zsh widget: certain functionality that can be ran by the shell itself and (usually) doesn’t need to be executed like a normal program. Examples include key binds, completion, navigation through history, undo and redo. User-defined widgets are just zsh functions, so we create a function that stores the current directory, add it as a widget and bind it to our keyboard combination6:
plumb-store-cwd () {
pwd > /tmp/plumb-cwd
}
zle -N plumb-store-cwd
bindkey "^[[5;7~" plumb-store-cwd # Ctrl+Alt+Page_Up
wname="$(cat /proc/"$pid"/comm)"
if [ $wname == "TERMINAL" ]; then # Replace with your terminal emulator name
xdotool key Control+Alt+Page_Up # Execute the shell-compatible combination
sleep 0.1 # Rework with inotify?
cd "$(cat /tmp/plumb-cwd)"
fi
For the uninitiated, xdotool is a general X11 tool, which allows (amongst other things) to “execute” key presses via commands.
Send
#Sending the command to the current shell is by far the simplest action.
xdotool
supports “typing out” text, so I’ll give it the selection and let it do it’s thing:
xdotool type --delay 0 "$selection"
It could be optimized if there was a way to make the selection be “pasted” into the shell (without being added to the clipboard), since that would mostly eliminate waiting times. Sadly I couldn’t figure out how to do anything like that.
Window management
#As promised, I won’t dive into it too much, but the easiest way to implement window management actions is via another dirty hack: sending key presses that make the window manager do something. For example, normally resizing is done via Alt+RightClick, so I’ll just do this:
xdotool keydown Alt
xdotool mousedown 3
xdotool keyup Alt
After this is executed, moving the mouse will cause the screen to be resized, the same way if I did the original combination.
Pressing and releasing the mouse button will stop resizing (that’s why keyup Alt
is done, because otherwise you would need to also press Alt).
Though it work, it’s kind of clunky and the normal Alt+RightClick is arguably simpler and easier to work with.
-
When I say “Plan 9” in the context of graphical stuff, I usually refer to the default window system rio. Similarly to most UNIX-like OSs, it can be changed, but there doesn’t seem to be any others out there[citation needed]. ↩
-
acme actually changes how the three mouse buttons work. Paraphrasing the official page, button 2 “executes things” and button 3 gets or searches for things. ↩
-
It’s hard to deduce why exactly it was named like this, but the most probable explanation comes from unix.stackexchange: copying might’ve been seen as a two step action, obtain the contents you want to copy and paste them somewhere. So to copy something you need a duplicate of itself to be created, and either putting it in the “clipboard” isn’t considered duplicating, or an optimization makes “the clipboard” store some sort of reference to what you snarfed, but not the exact thing itself.
On an unrelated note, this might have played a part in the reasoning behind vim using the word “yank” for copy. ↩ -
Technically, a plumber just passes messages between applications, so you can configure it to not open a URL but do something else with it in a certain application. However, the aforementioned way is how it works most of the time. ↩