KMonad

Table of Contents

1. Configuration

To allow for easier configuration, I am using “variables” for parts of the config that are subject to change between different setups. These “variables” are implemented using Org mode’s noweb syntax, which allows for code blocks to insert the contents of other code blocks.

Because the default reference syntax: <<BLOCK_NAME>>, is valid syntax in some languages, I changed the syntax to |>BLOCK_NAME<|. When the document is tangled, these |>BLOCK_NAME<| references are replaced with the contents of the corresponding named variable block in 1.1 or elsewhere. Variable names are all uppercase, and their definitions will mirror the structure shown in 1.1.

1.1. Global Variables

KEYBOARD
/dev/input/by-id/usb-Logitech_USB_Receiver-if02-event-kbd
STARSHIP_RUNNER_CONFIG

I want the runner to have a different configuration than my normal shell, so this points to the other config

~/.config/starship_runner.toml

1.1.1. Program Paths

ST
~/.local/bin/st
ROFI
/usr/bin/rofi
EMACS
/usr/bin/emacs
EMACSCLIENT
/usr/bin/emacsclient
DISCORD
/usr/bin/Discord
BROTAB
~/.local/bin/brotab

1.2. Configuration

[configuration]
input = "|>KEYBOARD<|"

output-name = "Logitech KMonad Output"
output-pre-command = "/usr/bin/sleep 0.75 && /usr/bin/setxkbmap -option compose:rctrl"

cmp-seq = 'rctrl'
cmp-seq-delay = 5

fallthrough = true
allow-cmd = true

# indicate which layer you want to be in when KMonad launches
starting-layer = qwerty-homerow-mods

1.2.1. Constants

[aliases]
tap-hold-delay = 200
tap-hold-delay-min = 160

hmod-delay = $tap-hold-delay

kwin_shortcut_cmd = "qdbus org.kde.kglobalaccel /component/kwin invokeShortcut"

1.3. Keyboard Backlight Aliases

Let’s define buttons for increasing/decreasing the keyboard backlight. These will be used in other layers so that I can easily see when I’m in the base layer and when I’m in a custom layer, like Volume. This will be done by turning on the backlight when in the layers, and turning it back off when leaving the layers.

[aliases]
kbd-set-backlight-command = "dbus-send --system --print-reply --dest=\"org.freedesktop.UPower\" /org/freedesktop/UPower/KbdBacklight org.freedesktop.UPower.KbdBacklight.SetBrightness"
kbd-set-backlight-on-command = "$kbd-set-backlight-command int32:1"
kbd-set-backlight-off-command = "$kbd-set-backlight-command int32:0"

kbd-set-backlight-on = (cmd-button "$kbd-set-backlight-command int32:1")
kbd-set-backlight-off = (cmd-button "$kbd-set-backlight-command int32:0")
kbd-toggle-backlight = (cmd-button "$kbd-set-backlight-on-command" "$kbd-set-backlight-off-command")

1.4. Default Layer

Optional: as many layers as you please

We had already defined `num` as referring to a `(layer-toggle numbers)`. We will get into layer-manipulation soon, but first, let’s just create a second layer that overlays a numpad under our right-hand.

To easily specify layers it is highly recommended to create an empty `deflayer` statement as a comment at the top of your config, so you can simply copy-paste this template. There are also various empty layer templates available in the ’./keymap/template’ directory.

Enable the “leader” layer for the next keypress. If we release @leader_key before the next key, we treat the keypress as a tap, even if for a short period of time both keys were down. If we release @leader_key after the next key, we treat it as holding.

Also, if we hold the key for more than 250 milliseconds, treat it like we are holding the key. When we are trying to use the super key in a tap melody, we have the key down for a very short time, so having the hold timeout on 250ms lets us use it for chords more conveniently

[base]
[[private]]
leader-key = (tap-hold-next-release 250 (around-next (layer-toggle leader)) lmet)
[[keys]]
lmet = leader-key
grave = (tap-hold $tap-hold-delay-min grave @simple-datetime-overlay) # (simple-datetime-overlay)
lalt = (tap-hold-next-release $tap-hold-delay XX (around-next (layer-toggle leader-no-block)))
[qwerty]
# we inherit from source before base so that we can add the `qwerty` on top of
# other layers and overwrite other mappings
parent = { source, base }
[[private]]
enable-homerow-mods = (layer-switch qwerty-homerow-mods)
[[keys]]
ScrollLock = enable-homerow-mods
caps = 'lctrl'
[qwerty-homerow-mods]
parent = base
[[private]]
disable-homerow-mods = (layer-switch qwerty)

We want this key to act as escape when tapped, and lctrl when held. However, while holding the key, we want to disable home row modifiers. We do this by adding the stock qwerty layer on top of the stack and holding lctl while holding the key. To do this,

lctrl-or-escape = (tap-hold-next-release 125 esc (around (layer-toggle qwerty) lctl))
lshift-or-caps-lock = (tap-hold-next-release 125 caps (around (layer-toggle qwerty) lshift))
rshift-or-caps-lock = (tap-hold-next-release 125 caps (around (layer-toggle qwerty) rshift))

This is a GACS home-row-mods configuration detailed on this page. k is bound to lctl rather than rctl because rctl is the compose key on my system.

a_homerow_chords = (tap-hold $hmod-delay a (layer-toggle home-row-chord))
q_homerow_movement = (tap-hold $hmod-delay q (layer-toggle home-row-movement))
backslash_layer = (tap-hold-next-release $hmod-delay \ (layer-toggle backslash))
s_lalt = (tap-hold-next-release $hmod-delay s lalt)
d_lctrl = (tap-hold-next-release $hmod-delay d lctl)
f_lshift = (tap-hold-next-release $hmod-delay f lshift)

k_lctrl = (tap-hold-next-release $hmod-delay k lctl)
j_rshift = (tap-hold-next-release $hmod-delay j rshift)
l_ralt = (tap-hold-next-release $hmod-delay l ralt)
[[keys]]
ScrollLock = disable-homerow-mods
caps = lctrl-or-escape

a = a_homerow_chords
q = q_homerow_movement
backslash = backslash_layer
s = s_lalt
d = d_lctrl
f = f_lshift
k = k_lctrl
j = j_rshift
l = l_ralt

We want to disable the homerow mods whenever we explicitly hit a modifier key.

lshift = (around (layer-toggle qwerty) @lshift-or-caps-lock)
rshift = (around (layer-toggle qwerty) @rshift-or-caps-lock)
lctrl = (around (layer-toggle qwerty) lctrl)

1.5. Base Leader Key Layer

[leader]
parent = block
[[keys]]
q = window-switcher:activate # (window-switcher)
d = discord # (discord)
e = emacs # (emacs)
f = firefox_composite # (firefox)
b = brightness:enter # (brightness)
v = volume:enter # (volume)
t = 't' # (terminal)
r = run:entrypoint # (run)
p = 'p' # (prompt)
a = agenda # (agenda)
o = open-preset # (open-preset)
c = org-capture # (org-capture)
s = scroll:enter # (scroll)
y = yank # (yank)
tab = switch_focus_composite # (switch-focus)
f1 = vim # (vim)
backslash = 'backslash' # (local-leader)
lmet = 'lmet'

[leader-no-block]
parent = { source, leader }

1.6. Window Focusing Layer

[window-focusing]
[[public]]
focus-left = (around lmet (around lalt Left))
focus-down = (around lmet (around lalt Down))
focus-up = (around lmet (around lalt Up))
focus-right = (around lmet (around lalt Right))
[[keys]]
h = focus-left
j = focus-down
k = focus-up
l = focus-right

1.7. Home Row Chord Layer

[home-row-chord]
parent = { block, numeric-desktop-switching, window-focusing }
[[private]]
show_desktop_grid = (cmd-button "$kwin_shortcut_cmd \"ShowDesktopGrid\"")
show_current_desktop_windows = (cmd-button "$kwin_shortcut_cmd \"Expose\"")
[[keys]]
i = jump-list:next # (jump-list)
o = jump-list:prev
p = jump-list:add

backslash = (tap-hold-next-release $tap-hold-delay-min @show_current_desktop_windows @show_desktop_grid)

1.8. Numeric Desktop Switching Layer

This is a layer where the numeric keys are mapped to buttons that switch to that numbered desktop.

SWAP_MONITOR_WINDOWS_SCRIPT
~/.config/kmonad/windows/swap_monitor_windows.sh
MONITOR_MOVE_RELATIVE_SCRIPT
~/.config/kmonad/windows/monitor_move_relative.sh
[numeric-desktop-switching]
[[private]]
SWAP_MONITOR_WINDOWS_SCRIPT = "|>SWAP_MONITOR_WINDOWS_SCRIPT<|"
MONITOR_MOVE_RELATIVE_SCRIPT = "|>MONITOR_MOVE_RELATIVE_SCRIPT<|"

window_to_next_screen = (cmd-button "$kwin_shortcut_cmd \"Window to Next Screen\"")
[[keys]]

Let’s generate this repetitive code with Python.

return '\n'.join(
    map(lambda n: f'{n} = (tap-hold $tap-hold-delay-min (cmd-button "$kwin_shortcut_cmd \\"Switch to Desktop {n}\\"") (cmd-button "$kwin_shortcut_cmd \\"Window to Desktop {n}\\""))',
        range(1, 10)))
1 = (tap-hold $tap-hold-delay-min (cmd-button "$kwin_shortcut_cmd \"Switch to Desktop 1\"") (cmd-button "$kwin_shortcut_cmd \"Window to Desktop 1\""))
2 = (tap-hold $tap-hold-delay-min (cmd-button "$kwin_shortcut_cmd \"Switch to Desktop 2\"") (cmd-button "$kwin_shortcut_cmd \"Window to Desktop 2\""))
3 = (tap-hold $tap-hold-delay-min (cmd-button "$kwin_shortcut_cmd \"Switch to Desktop 3\"") (cmd-button "$kwin_shortcut_cmd \"Window to Desktop 3\""))
4 = (tap-hold $tap-hold-delay-min (cmd-button "$kwin_shortcut_cmd \"Switch to Desktop 4\"") (cmd-button "$kwin_shortcut_cmd \"Window to Desktop 4\""))
5 = (tap-hold $tap-hold-delay-min (cmd-button "$kwin_shortcut_cmd \"Switch to Desktop 5\"") (cmd-button "$kwin_shortcut_cmd \"Window to Desktop 5\""))
6 = (tap-hold $tap-hold-delay-min (cmd-button "$kwin_shortcut_cmd \"Switch to Desktop 6\"") (cmd-button "$kwin_shortcut_cmd \"Window to Desktop 6\""))
7 = (tap-hold $tap-hold-delay-min (cmd-button "$kwin_shortcut_cmd \"Switch to Desktop 7\"") (cmd-button "$kwin_shortcut_cmd \"Window to Desktop 7\""))
8 = (tap-hold $tap-hold-delay-min (cmd-button "$kwin_shortcut_cmd \"Switch to Desktop 8\"") (cmd-button "$kwin_shortcut_cmd \"Window to Desktop 8\""))
9 = (tap-hold $tap-hold-delay-min (cmd-button "$kwin_shortcut_cmd \"Switch to Desktop 9\"") (cmd-button "$kwin_shortcut_cmd \"Window to Desktop 9\""))
# go to previous desktop
semicolon = (tap-hold $tap-hold-delay-min (cmd-button "$kwin_shortcut_cmd \"Switch to Previous Desktop\"") (cmd-button "$MONITOR_MOVE_RELATIVE_SCRIPT -1"))
# go to next desktop
apostrophe = (tap-hold $tap-hold-delay-min (cmd-button "$kwin_shortcut_cmd \"Switch to Next Desktop\"") (cmd-button "$MONITOR_MOVE_RELATIVE_SCRIPT 1"))

Minus = (cmd-button "$SWAP_MONITOR_WINDOWS_SCRIPT")
# NOTE: this approach only works with 2 monitors
Equal = (tap-hold $tap-hold-delay-min @window_to_next_screen #(@window_to_next_screen @switch_focus_screen))

1.8.1. Swap Monitors Script

#!/bin/dash

# first, get the current window ID
WINDOW_ID=$(xdotool getactivewindow)

runShortcut() {
    qdbus org.kde.kglobalaccel /component/kwin invokeShortcut "$1"
}

# next, switch focus to the other monitor
runShortcut "Switch to Next Screen"

sleep 0.05

# now, move the window to the previous monitor
runShortcut "Window to Next Screen"

sleep 0.05

# finally, focus the original window and move it to the other monitor
xdotool windowfocus "$WINDOW_ID"

sleep 0.05

runShortcut "Window to Next Screen"

1.8.2. Monitor Relative Move Script

#!/bin/bash

# pass in the relative change in desktop numbers; i.e 1, -1
relative_change=$1

runShortcut() {
    qdbus org.kde.kglobalaccel /component/kwin invokeShortcut "$1"
}

# first, let's get the current desktop
wmctrl_output=$(wmctrl -d)

desktop_regex="^([0-9]+)"
active_desktop_regex="([0-9]+)  \\*"

# 1. find the current desktop
if [[ $wmctrl_output =~ $active_desktop_regex ]]
then
    # this is zero-based
    active_desktop="${BASH_REMATCH[1]}"

    last_desktop_line=$(echo "$wmctrl_output" | tail -n 1)

    # let's take that line and extract the number from it
    if [[ $last_desktop_line =~ $desktop_regex ]]
    then
        # this is zero-based
        highest_desktop="${BASH_REMATCH[1]}"
        num_desktops=$((highest_desktop + 1))

        new_desktop=$(((active_desktop + relative_change) % num_desktops))
        if [[ $new_desktop -lt 0 ]]
        then
            new_desktop=$((new_desktop + num_desktops))
        fi

        # take it from zero-based to one-based
        new_desktop=$((new_desktop + 1))

        # finally, move the window
        runShortcut "Window to Desktop $new_desktop"
    fi
fi

1.9. Alphabetic Window Tiling Layer

[alphabetic-window-tiling]
[[keys]]
u = (cmd-button "$kwin_shortcut_cmd 'Window Quick Tile Top Left'")
i = (cmd-button "$kwin_shortcut_cmd 'Window Quick Tile Top'")
o = (cmd-button "$kwin_shortcut_cmd 'Window Quick Tile Top Right'")

j = (cmd-button "$kwin_shortcut_cmd 'Window Quick Tile Left'")
k = (cmd-button "$kwin_shortcut_cmd 'Window Maximize'")
l = (cmd-button "$kwin_shortcut_cmd 'Window Quick Tile Right'")

m = (cmd-button "$kwin_shortcut_cmd 'Window Quick Tile Bottom Left'")
comma = (cmd-button "$kwin_shortcut_cmd 'Window Quick Tile Bottom'")
dot = (cmd-button "$kwin_shortcut_cmd 'Window Quick Tile Bottom Right'")

1.10. Backslash Layer

This used to be the “Desktop/Window” layer, but after adding leader key functionality, the backslash key now constitutes its own dedicated layer name.

[backslash]
parent = { numeric-desktop-switching, alphabetic-window-tiling, leader }
[[keys]]
RightBrace = window-focusing:focus-right
LeftBrace = window-focusing:focus-left

1.11. Home Row Movement Layer

[home-row-movement]
[[keys]]
h = 'Left'
j = 'Down'
k = 'Up'
l = 'Right'

b = 'PageUp'
f = 'PageDown'

e = (around lctl Right)
w = (around lctl Left)

semicolon = 'Home'
apostrophe = 'End'

1.12. Program Paths

[aliases]
ROFI = "|>ROFI<|"
DISCORD = "|>DISCORD<|"
EMACS = "|>EMACS<|"
EMACSCLIENT = "|>EMACSCLIENT<|"

1.13. Jump List

The idea of the Jump List is to emulate Vim behavior and allow for switching between different positions. In a windowing system context, each entry in the Jump List is an active window (+ Desktop). Vim has a ton of operations which modify the Jump List, but for our purposes, we will have two main modes of modification:

  1. Window Switcher
    • using the window switcher will automatically add entries to the Jump List
  2. Manual Marks
    • to fill in for the Vim operations, we will provide an easy way to mark an entry in the Jump List through a direct keybinding
  3. /home/sridaran/.config/kmonad/jump-list/jump_list.pl
    

We move the command to the global scope so we can use it within other layers.

[aliases]
jump-list-mark = "|>JUMP_LIST_SCRIPT<| add-shift"
[jump-list]
[[private]]
script = "|>JUMP_LIST_SCRIPT<|"
[[public]]
# adds current window as entry to the jump list
add = (cmd-button "$script add-replace")

# jumps to the next entry in the jump list
next = (cmd-button "$script next")
# jumps to the previous entry in the jump list
prev = (cmd-button "$script prev")

1.13.1. Underlying Implementation

To make the underlying implementation work, we need to maintain a stack of jump list entries, where each entry contains a Window ID as well as a desktop number + monitor so that if the window is closed, we can still go back to it. Actually, it may make sense to simply skip an entry if the window has been closed.

  • When using prev from a monitor which is different from the active jump list entry (this means that you manually moved away), we can either go back to the active entry OR ignore the current monitor and simply go back an entry
    • Similarly, when using next from a monitor which is different from the active jump list entry, we can either go back to the active entry OR ignore the current monitor and simply go back an entry
    • I think we will have to see what is more useful in practice, but I think to begin with it’ll be good to have it go back to the active entry in the prev scenario
      • If it gets in the way, we’ll get rid of it
  • add should get the current focused window id and desktop, and add it to the jump list
    • If the current entry is not the latest entry, then delete anything that came after it (this appears to be Vim’s behavior, too)
  • prev should shift the pointer back
  • next should move the pointer forward, and do nothing if it isn’t relevant
#!/bin/env perl
use v5.28;
use strict;
use warnings;

# first, let's get the current window configuration: current window id and desktop
my $focusedWindow = `xdotool getwindowfocus`;
chomp $focusedWindow;

sub getCurrentDesktop {
    my $cmd = "wmctrl -d |";
    open FH, $cmd;

    my $currentDesktop;
    while (<FH>) {
        if ($_ =~ m/^(?<n>\d)  \*/) {
            # MISTAKE: `chomp(lval)` yields the number of removed characters from the end;
            # also, chomp wasn't even necessary here.
            $currentDesktop = $+{n};
            last;
        }
    }

    close FH;
    return $currentDesktop;
}

my $currentDesktop = getCurrentDesktop();

# now, let's define the path for the jump list file.
my $jumpListPath = "/tmp/kmonad_jump_list";
my $fileExists = open(FH, '<', $jumpListPath);

my $activeEntryNum = -1;
my @currentJumpList = ();
if ($fileExists) {
    $activeEntryNum = <FH>;
    # source: https://stackoverflow.com/questions/1877330/how-can-i-read-the-lines-of-a-file-into-an-array-in-perl

    while (<FH>) {
        chomp;
        my @items = split / /;
        # MISTAKE: `push @list @other` actually pushes the ELEMENTS of @other onto @list.
        # to remedy this, we push a scalar reference to the array
        push @currentJumpList, \@items;
    }
}

close FH;

# we use a queue for our commands so that we can run them after updating the file
my @commandQueue = ();

sub addJumpPredicate {
    # check if it's the KDE desktop background window
    my $classOutput = `xprop -notype -id $focusedWindow WM_CLASS`;
    die if not $classOutput =~ m/WM_CLASS = "(?<CLASS>[^"]+)",/;

    return ($+{CLASS} !~ m/plasmashell/);
}

# this function takes in no args.
# it will mutate the @currentJumpList array, setting the current window
# configuration as the latest entry.
sub addJumpEntry {
    my ($keepLaterEntries) = @_;

    if (addJumpPredicate) {
        $activeEntryNum += 1;

        my $newEntry = [$currentDesktop, $focusedWindow];
        if ($keepLaterEntries) {
            # push back everything after
            splice @currentJumpList, $activeEntryNum, 0, $newEntry;
        } else {
            $currentJumpList[$activeEntryNum] = $newEntry;

            # remove all elements after
            splice @currentJumpList, $activeEntryNum + 1;
        }
    }
}

sub updateJumpListBasedOnFocus {
    my ($keepLaterEntriesIfAdding) = @_;

    my $doAddEntry = 0;
    if (@currentJumpList) {
        # if we have moved away from the active jump list entry, then add this to the top of the jump list
        my ($activeEntryDesktop, $activeEntryWindow) = @{ $currentJumpList[$activeEntryNum] };

        my $sameWindow = $activeEntryWindow eq $focusedWindow;
        my $sameDesktop = $activeEntryDesktop eq $currentDesktop;
        if ($sameWindow && not $sameDesktop) {
            # if we are focused on the current window, then overwrite the entry's desktop
            $currentJumpList[$activeEntryNum][0] = $currentDesktop;
        } elsif ($sameDesktop) {
            # then, just update the window instead of making a whole new entry
            $currentJumpList[$activeEntryNum][1] = $focusedWindow;
        } else {
            $doAddEntry = 1;
        }
    } else {
        $doAddEntry = 1;
    }

    if ($doAddEntry) {
        # (different window, different desktop)
        addJumpEntry $keepLaterEntriesIfAdding;

        # if offset is negative, then we need to skip past the entry we just created;
        # ACTUALLY, our current desired behavior is that if we go backwards and we aren't on the active entry, it will go to the active entry
        # if ($offset < 0) {
        #     $offset -= 1;
        # }
    }
}

# this function takes in an offset, and modifies the count accordingly
sub shiftStackPointer {
    my ($offset) = @_;

    updateJumpListBasedOnFocus 1;

    $activeEntryNum += $offset;

    # MISTAKE: when given only one argument, it will treat the argument as the /pattern/, and use $_ as the expression to split.
    my ($desktop, $window) = @{ ($currentJumpList[$activeEntryNum]) };

    # now, let's switch the window
    my $exitCode = system("wmctrl -ia $window");

    if ($exitCode eq 0) {
        # move cursor to center of window
        push @commandQueue, "xdotool mousemove --window $window --sync --polar 0 0";

        # trackmouse flash
        my $toggleTrackMouse = "qdbus org.kde.kglobalaccel /component/kwin org.kde.kglobalaccel.Component.invokeShortcut TrackMouse";
        push @commandQueue, $toggleTrackMouse;
        push @commandQueue, "sleep 0.50";
        push @commandQueue, $toggleTrackMouse;
    } else {
        # TODO: focus correct monitor
        system("wmctrl -s $desktop");
    }
}

sub prevEntry {
    updateJumpListBasedOnFocus 1;

    if ($activeEntryNum > 0) {
        shiftStackPointer -1;
    } else {
        print STDERR "No previous entry!";
        exit 1;
    }
}

sub nextEntry {
    if ($activeEntryNum < $#currentJumpList) {
        shiftStackPointer 1;
    } else {
        print STDERR "No next entry!";
        exit 1;
    }
}

# now, let's do a dispatch list
for ($ARGV[0]) {
    /add-replace/ && do { updateJumpListBasedOnFocus 0; last };
    /add-shift/ && do { updateJumpListBasedOnFocus 1; last };
    /next/ && do { nextEntry; last };
    /prev/ && do { prevEntry; last };

    print STDERR "Please specify 'add-replace', 'add-shift', 'next', or 'prev' as the first argument; received $ARGV[0]";
    exit 1;
}

# if empty, then we did not add any entries
if (@currentJumpList) {
    # now, let's yield the new jump list
    open FH, ">", $jumpListPath;
    print FH $activeEntryNum;
    print FH "\n";
    print FH join("\n", map { join(" ", @{$_}) } @currentJumpList);
    close FH;

    for (@commandQueue) {
        system($_);
    }
}

1.14. Window Switcher

Opens the Window Switcher

[window-switcher]
[[private]]
rofi-args = "-noplugins -lines 5 --normal-window -sort -sorting-method fzf -monitor -4 -matching fuzzy -modi window -show window"

# note that in Posix SH, & acts as a separator between commands, and a semicolon
# after it is invalid syntax.
mark-and-jump-command = "$jump-list-mark & sleep 0.15; wmctrl -ia {window}"
jump-to-window = (cmd-button "$ROFI $rofi-args -window-command \"/bin/dash -c '$mark-and-jump-command'\" -kb-accept-entry '' -kb-accept-alt 'Return'")
pull-window = (cmd-button "$ROFI $rofi-args -window-command 'wmctrl -iR {window}' -kb-accept-entry '' -kb-accept-alt 'Return'")
[[public]]
activate = (tap-hold $tap-hold-delay-min @jump-to-window @pull-window)

I made this into a layer purely for organizational purposes. When tapped, activate will yield a Rofi window switcher which will switch to the selected window. When held, activate will yield a Rofi window switcher which will move the selected window to the current desktop before selecting it.

I compiled rofi from source and put it in ~/.local/bin because the RPM version was too slow for my taste. Some of the flags are also there for optimization reasons: -modi, -noplugins and --normal-window. I noticed that the startup animation was faster with --normal-window, and the other 2 flags stop rofi from doing unnecessary work. -matching fuzzy makes it use fuzzy matching instead of only matching the raw string. -sort and -sorting-method fzf make the selections a lot more intelligent. -monitor -4 makes it open rofi on the monitor of the currently focused window.

1.15. Discord

This command uses wmctrl to switch to a currently-existing Discord window, and if it fails opens a new instance of Discord.

[aliases]
DISCORD_SCRIPT = "~/.config/kmonad/discord/discord.sh"
discord = (cmd-button "$DISCORD_SCRIPT")

1.15.1. TODO Switch back to the previous window when invoked a second time

1.16. Emacs

Opens Emacs: emacsclient on tap, emacs process on hold.

[aliases]
emacs = (tap-hold $tap-hold-delay-min (cmd-button "$EMACSCLIENT --create-frame") (cmd-button "$EMACS"))

1.17. Firefox

Opens a new Firefox window

FIREFOX_TAB_SWITCHER_SCRIPT
~/.config/kmonad/firefox/firefox_tab_switcher.sh
[aliases]
FIREFOX_TAB_SWITCHER_SCRIPT = "|>FIREFOX_TAB_SWITCHER_SCRIPT<|"

open_firefox = (cmd-button "firefox")
select_firefox_tab = (cmd-button "$FIREFOX_TAB_SWITCHER_SCRIPT")

firefox_composite = (tap-hold 135 @open_firefox @select_firefox_tab)

1.17.1. Firefox Tab Switcher

#!/bin/sh

IFS="
"

tabs=$(|>BROTAB<| list)

echo "$tabs" > /tmp/kmonadtabs

selected=$(echo "$tabs" | awk -F '\t' '{print $2}' | rofi -noplugins -dmenu -i -lines 8 --normal-window -matching fuzzy -format 'd')

selection=$(echo "$tabs" | tail -n "+$selected" | head -n 1)

activation="$(echo "$selection" | awk -F '\t' '{print $1}')"
|>BROTAB<| activate "$activation"

title="$(echo "$selection" | awk -F '\t' '{print $2}')"
wmctrl -a "$title"

1.18. Brightness

CHANGE_BRIGHTNESS_SCRIPT
~/.config/kmonad/brightness/change_brightness.sh
CHANGE_BACKLIGHT_SCRIPT
~/.config/kmonad/brightness/change_backlight.sh
QUEUE_DIGIT_SCRIPT
~/.config/kmonad/brightness/queue_digit.sh
DIGIT_QUEUE_FILE
/tmp/kmonad_digit_queue
LAST_BRIGHTNESS_CHANGE_FILE
/tmp/kmonad_last_brightness_change
[brightness]
[[private]]
QUEUE_DIGIT_SCRIPT = "|>QUEUE_DIGIT_SCRIPT<|"
CHANGE_BRIGHTNESS_SCRIPT = "|>CHANGE_BRIGHTNESS_SCRIPT<|"
CHANGE_BACKLIGHT_SCRIPT = "|>CHANGE_BACKLIGHT_SCRIPT<|"

exit_internal = (layer-rem brightness)
# the definition for `exit` is generated by our preprocessing script
[[public]]
enter = (tap-hold-next-release $tap-hold-delay #((layer-add brightness) @kbd-set-backlight-on) (layer-toggle brightness))

up = (tap-hold-next-release $tap-hold-delay-min (cmd-button "$CHANGE_BRIGHTNESS_SCRIPT +") (cmd-button "$CHANGE_BACKLIGHT_SCRIPT +"))
down = (tap-hold-next-release $tap-hold-delay-min (cmd-button "$CHANGE_BRIGHTNESS_SCRIPT -") (cmd-button "$CHANGE_BACKLIGHT_SCRIPT -"))

toggle_nightlight = (cmd-button "$CHANGE_BRIGHTNESS_SCRIPT '*'")
[[keys]]

This is repetitive, so let’s abstract it away by generating the code with Python.

return '\n'.join(map(lambda n: f'{n} = (cmd-button "$QUEUE_DIGIT_SCRIPT {n}")', range(0, 10)))
0 = (cmd-button "$QUEUE_DIGIT_SCRIPT 0")
1 = (cmd-button "$QUEUE_DIGIT_SCRIPT 1")
2 = (cmd-button "$QUEUE_DIGIT_SCRIPT 2")
3 = (cmd-button "$QUEUE_DIGIT_SCRIPT 3")
4 = (cmd-button "$QUEUE_DIGIT_SCRIPT 4")
5 = (cmd-button "$QUEUE_DIGIT_SCRIPT 5")
6 = (cmd-button "$QUEUE_DIGIT_SCRIPT 6")
7 = (cmd-button "$QUEUE_DIGIT_SCRIPT 7")
8 = (cmd-button "$QUEUE_DIGIT_SCRIPT 8")
9 = (cmd-button "$QUEUE_DIGIT_SCRIPT 9")
k = up
j = down

h = toggle_nightlight

# q displays brightness on each monitor

tab = switch_focus_composite

lmet = exit

1.18.1. Queue Digit Script

This script takes a digit and appends it to the queue of currently waiting digits. The change brightness script consumes the queue as a single integer.

Using dash shell for speed

#!/bin/dash

FILE="|>DIGIT_QUEUE_FILE<|"

Verify that the argument is a number by using case and globbing. See https://stackoverflow.com/questions/806906/how-do-i-test-if-a-variable-is-a-number-in-bash/806923(this) StackOverflow post.

DIGIT=$1

case $DIGIT in
'' | *[!0-9]*) echo "Need to pass in a number!" >/dev/stderr; exit 1;;
*) ;;
esac

Next, read the current file contents, prepend it to DIGIT, and then write it back.

# read file
if [ -e "$FILE" ]; then
    CURRENT_INT=$(cat "$FILE")
fi

NEW_INT="$CURRENT_INT$DIGIT"

# also print it to stdout; helpful for debugging
echo "$NEW_INT" | tee "$FILE"

1.18.2. Change Brightness Script

#!/bin/dash

DIGIT_FILE="|>DIGIT_QUEUE_FILE<|"
LAST_BRIGHTNESS_CHANGE_FILE="|>LAST_BRIGHTNESS_CHANGE_FILE<|"

DIRECTION=$1

Depending on DIRECTION, set SIGN to the sign. There’s a special case for .; with ., SIGN becomes zero and triggers special behavior further on.

case $DIRECTION in
'+') SIGN=1 ;;
'-') SIGN=-1 ;;
'.') ;;
'*') ;;
*)
    echo "Invalid direction" >/dev/stderr
    exit 1
    ;;
esac

We preset CHANGE so that any code path which never sets CHANGE will use the value of 7.

CHANGE=7

In the normal case, check if there are queued digits, and if there aren’t then default to 7. After reading the saved digits, clear the file’s contents.

if [ "$DIRECTION" != '.' ] && [ "$DIRECTION" != '*' ]; then
    QUEUED_DIGITS=$(cat "$DIGIT_FILE" 2>/dev/null)

    if [ -n "$QUEUED_DIGITS" ]; then
        if [ "$QUEUED_DIGITS" -ge 100 ]; then
            QUEUED_DIGITS=100
        fi

        echo "" >"$DIGIT_FILE"
        CHANGE=$QUEUED_DIGITS
    fi

To get the final value for CHANGE, multiply SIGN by its current value. Then, write the new value to LAST_BRIGHTNESS_CHANGE_FILE.

    CHANGE=$(echo "$SIGN * $CHANGE" | bc)
    echo "$CHANGE" >"$LAST_BRIGHTNESS_CHANGE_FILE"

If DIRECTION is ., then read CHANGE directly from LAST_BRIGHTNESS_CHANGE_FILE. If it doesn’t exist, then fail.

else
    if [ "$DIRECTION" = "." ]; then
        if [ -e "$LAST_BRIGHTNESS_CHANGE_FILE" ]; then
            CHANGE=$(cat "$LAST_BRIGHTNESS_CHANGE_FILE")
        else
            echo "Last brightness change file does not yet exist!" >/dev/stderr
            exit 1
        fi

Otherwise, it is *, which means that we want to toggle the nightlight. In this case, we call a different script for toggling the nightlight on the actively focused monitor. We exit the script afterwards so that we don’t end up calling the standard changeBrightness script next.

    else
        /home/sridaran/Development/Scripts/DE/toggleNightlight.sh
        exit 0
    fi
fi

Finally, pass CHANGE to our main changeBrightness script (not shown), which changes the brightness on the actively focused monitor.

/home/sridaran/Development/Scripts/DE/changeBrightness.sh "$CHANGE" -n

1.18.3. Change Backlight Script

#!/bin/dash

case $1 in
    "+") DIR=+
         ;;
    "-") DIR=-
         ;;
    *)
        echo "Please specify + or - as direction!" > /dev/stderr
        exit 1
        ;;
esac

current_brightness=$(qdbus org.kde.Solid.PowerManagement /org/kde/Solid/PowerManagement/Actions/BrightnessControl org.kde.Solid.PowerManagement.Actions.BrightnessControl.brightness)
# source: https://userbase.kde.org/KDE_Connect/Tutorials/Useful_commands#Brightness_settings
new_brightness=$(expr $current_brightness $DIR 375)

# for some reason, the setBrightness interface allows you to go out of bounds,
# so we clamp it manually.
if [ $new_brightness -lt 1 ]; then
    new_brightness=1
elif [ $new_brightness -gt 7500 ]; then
    new_brightness=7500
fi

qdbus org.kde.Solid.PowerManagement /org/kde/Solid/PowerManagement/Actions/BrightnessControl org.kde.Solid.PowerManagement.Actions.BrightnessControl.setBrightness $new_brightness

1.19. TODO Terminal

1.20. Run

[run]
[[private]]
runner_script = "~/.config/kmonad/runner/runner.sh"
toggle = (cmd-button "$runner_script toggle")
dwim = (cmd-button "$runner_script dwim")
[[public]]
entrypoint = (tap-hold-next-release $tap-hold-delay @dwim (layer-toggle run))
[[keys]]
t = toggle

1.20.1. Runner Script

Using the bash shell so we can use arithmetic.

#!/bin/bash

Let’s first process our command-line argument to determine what to do. If we want to focus, then let’s attempt to activate a window containing the st-runner class, and if that fails, then proceed to the rest of the code, which will create a new runner instance! If we want to dwim (do-what-I-mean), then IF the runner is currently focused, close the window, and otherwise, focus it. This is usually what we want, since it doesn’t make sense to try to re-focus the window if it’s already focused!

On the other hand, if we want to toggle, then let’s first attempt to close a window containing the st-runner class, and if that fails, then we proceed to the code for making a new instance!

processArg() {
    case $1 in
        focus)
            # when focusing, spawn new process if not open, and focus existing process if open
            if wmctrl -x -a st-runner; then
                # successfully focused!
                exit 0
            fi
            ;;
        toggle)
            # when toggling, spawn new process if not open, and KILL existing process if open
            if wmctrl -x -c st-runner; then
                # successfully closed!
                exit 0
            fi
            ;;
        dwim)
            focused_window=$(xdotool getwindowfocus)
            # if focused window is the runner, exit code will be zero
            if xprop -notype -id "$focused_window" WM_CLASS | grep -q "st-runner"; then
                # close window
                wmctrl -ic "$focused_window"
                exit 0
            else
                # https://stackoverflow.com/questions/4827690/how-to-change-a-command-line-argument-in-bash
                processArg "focus"
            fi
            ;;
        *)
            echo "Invalid argument: please pass in focus|toggle|dwim" >&2
            exit 1
    esac
}

processArg "$1"

# If control reaches past this point without exiting, that means we have to make
# a new instance of the runner.

Setting environment variables for the fish process to inherit.

SKIP_FISH_GREETING
This is a custom variable that determines whether a message should display on startup. I set it to 1 because I do not want it to output for the runner.
STARSHIP_CONFIG
Starship is the shell prompt I am using.
export SKIP_FISH_GREETING=1
export STARSHIP_CONFIG=|>STARSHIP_RUNNER_CONFIG<|

Sets the working directory back to home

cd

Now, let’s determine the geometry for our terminal window. First, we want the terminal to be placed on the active directory.

FOCUSED_WINDOW=$(xdotool getwindowfocus)
eval $(xdotool getwindowgeometry --shell "$FOCUSED_WINDOW")
eval $(monitor-utils --shell --at-point $X $Y --geometry)

This will give us the geometry of the monitor we have focused. Now, we want to have 10% padding on the top, and have the terminal be centered in the middle of the screen. From experimentation, 116 terminal columns translates to about 1660 pixels.

TERMINAL_WIDTH=1660

y_padding=$((HEIGHT / 10))
x_margin=$(((WIDTH - TERMINAL_WIDTH) / 2))

terminal_x_offset=$((x_margin + X_OFFSET))
terminal_y_offset=$((y_padding + Y_OFFSET))
|>ST<|
st is the terminal emulator
-c "..."
This sets the X11 classnames for the window. My KDE config contains window rules that rounds the corners of windows with the rounded class and gives transparency and several other properties to the st-runner class
-g ...

This sets the initial window dimensions for the terminal window.

The format we are using is <width>x<height>+<xoffset>+<yoffset>. I believe everything is in terms of characters, so the width represents 100 characters, and the height represents 8 lines of space. The offset, however, appears to be in pixels.

See this link for more details

Using screen to maintain a single shell session through each runner invocation.

-DR runner
Attaches to a session called runner, creating it if necessary. Some of screen’s flag combinations seem a little arbitrary.
-s /bin/fish
Tells screen to start new sessions with the fish shell.
|>ST<| -m 15 -c "rounded st-runner" -g "116x8+${terminal_x_offset}+${terminal_y_offset}" screen -c ~/.config/kmonad/runner/screenrc -DR runner

After st closes, we scroll down our runner so we no longer see the commands/output from earlier. We do this by telling screen to send Control+L keystrokes to the runner session’s first pane. Since it’s the same shell, we will still be in the same working directory and have the same history as before.

screen -S runner -X stuff ""
term screen-256color
This line fixes the colors in the screen window. Before, I was getting a lot of text that wasn’t being highlighted.
term screen-256color
msgwait 0
shell /home/sridaran/.config/kmonad/runner/run_fish.sh
#!/bin/dash
exec fish --init-command="source $HOME/.config/kmonad/runner/config.fish"

1.20.2. Autoresizing on commands

I want my terminal to go full-height for certain commands, like fzf. To do this, I will wrap the commands using functions which will emit a control code to the terminal emulator, telling it to resize.

function __fullsize_terminal -d "Emits a control code which causes the runner terminal to resize"
    # tell terminal to resize;
    # source: https://unix.stackexchange.com/questions/575337/using-terminal-escape-sequences-within-gnu-screen
    echo -e '\eP\005\e\\' > /dev/tty
end

function __wrap_fullsize -d "Given a command, wraps it into a function with the same name, which will resize the terminal before running the command"
    set WRAPPED_COMMAND $argv[1]
    function $argv[1] -V WRAPPED_COMMAND -d "Runs $1 after resizing the terminal"
        # get path of the wrapped command
        set command_path (which $WRAPPED_COMMAND)

        __fullsize_terminal

        # run command with the args
        $command_path $argv
    end
end

__wrap_fullsize fzf

1.20.3. TODO Cleanup exit command

Right now, we are doing killall st, which only does what we want because we do not use st for anything else. We should aim for a more robust solution.

1.20.4. DONE Reuse the runner terminal and shell between invocations

1.21. TODO Prompt

1.22. Agenda

[aliases]
agenda = (cmd-button "$EMACSCLIENT -ce '(org-agenda nil \"o\")'")

1.22.1. TODO Open a floating, semi-transparent window

Instead of a fullscreen, opaque window.

1.22.2. DONE Maybe switch to org-agenda

1.23. Open Preset

OPEN_PRESET_SCRIPT
~/Development/Scripts/DE/presets/rofi_menu.sh
[aliases]
OPEN_PRESET_SCRIPT = "~/Development/Scripts/DE/presets/rofi_menu.sh"
open-preset = (cmd-button "$OPEN_PRESET_SCRIPT")

1.24. Org Capture

[aliases]
org-capture = (cmd-button "~/.local/bin/org-capture")

1.25. Scroll

We set the scroll buttons to invoke the scroll.sh script once on press and once on release. On release, the script will kill the instance created on press

SCROLL_SCRIPT
~/.config/kmonad/scroll/scroll.sh
SCROLL_SPEED_SCRIPT
~/.config/kmonad/scroll/scroll_speed.sh
[scroll]
[[private]]
scroll_script = "|>SCROLL_SCRIPT<|"
speed_script = "|>SCROLL_SPEED_SCRIPT<|"

left  = (cmd-button "$scroll_script h -"
                    "$scroll_script h 0")
up    = (cmd-button "$scroll_script v -"
                    "$scroll_script v 0")
down  = (cmd-button "$scroll_script v +"
                    "$scroll_script v 0")
right = (cmd-button "$scroll_script h +"
                    "$scroll_script h 0")

speed-up   = (cmd-button "$speed_script 50"
                         "$speed_script 0")
speed-down = (cmd-button "$speed_script 200"
                         "$speed_script 0")

exit_internal = (layer-rem scroll)
[[public]]
enter = (tap-hold-next-release $tap-hold-delay #((layer-add scroll) @kbd-set-backlight-on) (layer-toggle scroll))
[[keys]]
h = left
l = right
k = up
j = down

caps = speed-down
lctrl = speed-down
lshift = speed-up

lmet = exit

1.25.1. Scroll Script

These are the files storing the scroll variables.

SCROLL_SPEED_FILE
/tmp/kmonad_scroll_script_speed
SCROLL_SPEED_FILE_OLD
/tmp/kmonad_scroll_script_speed_old
SCROLL_HORIZONTAL
/tmp/kmonad_scroll_script_horizontal
SCROLL_VERTICAL
/tmp/kmonad_scroll_script_vertical

Again using dash for speed

#!/bin/dash
DIRECTION
Either h for “horizontal” or v for “vertical”.
MAGNITUDE
Either + for the positive direction, - for the negative direction or 0 to stop
DIRECTION="$1"
MAGNITUDE="$2"

Check if a process is already running for the current direction, and kill it if necessary. We have separate PID files for horizontal and vertical scrolling because we want to be able to scroll in both directions simultaneously.

if [ $DIRECTION = "h" ]
then
    DIRECTION_PID_FILE=|>SCROLL_HORIZONTAL<|

    if [ $MAGNITUDE = "-" ]
    then
        # if negative, then use scroll left button
        TARGET_BUTTON=6
    else
        # if positive, then use scroll right button
        TARGET_BUTTON=7
    fi
else
    DIRECTION_PID_FILE=|>SCROLL_VERTICAL<|

    if [ $MAGNITUDE = "-" ]
    then
        # if negative, then use scroll up button
        TARGET_BUTTON=4
    else
        # if positive, then use scroll down button
        TARGET_BUTTON=5
    fi
fi

if [ -e $DIRECTION_PID_FILE ]
then
    kill $(head -n1 $DIRECTION_PID_FILE)
    rm $DIRECTION_PID_FILE

This condition is an else if because if we are holding h and then press l, we want the two to cancel out rather than having the l override the h. In this code, if the direction pid file exists, we kill the process, creating a new one only if we did not kill an existing one.

elif ! [ $MAGNITUDE = "0" ]
then

We want this section of code in a loop, so that if the speed changes we can react to it and restart xdotool with the new speed.

    while true
    do

Get the current delay from SCROLL_SPEED_FILE, creating it if necessary

        if ! [ -e |>SCROLL_SPEED_FILE<| ]
        then
            DELAY=150
            echo $DELAY > |>SCROLL_SPEED_FILE<|
        else
            DELAY=$(cat |>SCROLL_SPEED_FILE<|)
        fi

To emulate scrolling, we use xdotool to repeatedly send scroll button presses at a fixed interval: $DELAY milliseconds. The 10000 number effectively represents “infinity”, as it means that the process will only exit after 10000 * $DELAY milliseconds

        xdotool click --repeat 10000 --delay $DELAY $TARGET_BUTTON &

$$ is the PID of the shell process

        echo "$$" > "$DIRECTION_PID_FILE"

Send incoming SIGTERM’s to the xdotool process so that it can be killed (source)

        trap "kill $!" TERM

If we receive a USR1 signal, restart the loop so the speed can be updated

        trap "kill $!; wait $!; continue" USR1

Wait for the xdotool process to complete

        wait $!

If we get to the end of the “loop” without USR1 signal firing, we can safely exit

        break
    done
fi

1.25.2. Scroll Speed Script

NEW_DELAY
The new delay in milliseconds that we need xdotool to use. If it is equal to 0, then reset the delay to the old delay
#!/bin/dash

NEW_DELAY=$1

Save the current speed to another file

if [ $NEW_DELAY -ne 0 ]
then
    cat |>SCROLL_SPEED_FILE<| > |>SCROLL_SPEED_FILE_OLD<|

    # write new speed to the file
    echo $NEW_DELAY > |>SCROLL_SPEED_FILE<|
else
    cat |>SCROLL_SPEED_FILE_OLD<| > |>SCROLL_SPEED_FILE<|
fi

Send USR1 signals to both the vertical and horizontal processes, so that they will refresh their speed

kill -s USR1 $(cat |>SCROLL_VERTICAL<|)
kill -s USR1 $(cat |>SCROLL_HORIZONTAL<|)

1.25.3. Original Approach

This was my original idea, but I am now implementing scrolling through a shell script

We are using keys F13-F16 to represent scrolling. We need to do this because KMonad does not support sending mouse events. Because these keys are not used for anything else (they aren’t actually on the keyboard), we can safely remap them to buttons using xmodmap, which does support mouse buttons.

This was in my ~/.Xmodmap

keycode 191 = Left
keycode 192 = Pointer_Button5
keycode 193 = Pointer_Button4
keycode 194 = Right

First of all, xmodmap did not let me bind 191 and 194 to Pointer_Button6 and Pointer_Button7 (pushing the scroll wheel left/right), saying that it did not recognize either keysym. As a workaround, I tried setting these to the arrow keys.

The horizontal arrow keys worked, but the up/down scrolling did not. In most applications, the up/down arrow keys do selection in addition to scrolling, so binding j and k to arrow keys was not an acceptable solution

Scrolling works by repeatedly “clicking” the scroll buttons. Whenever you scroll a scrollbar on your mouse, the speed at which you scroll determines how fast the scrolling occurs on your screen.

I believe the reason the vertical scrolling was not working is because KMonad was repeating the keypresses too quickly. Because of this, it probably did not register as scrolling and was simply ignored.

1.26. Volume

VOLUME_SCRIPT
~/.config/kmonad/volume/volume.sh
VOLUME_TOGGLE_OSD_SCRIPT
~/.config/kmonad/volume/volume_popup_toggle.sh
VOLUME_SCRIPT_OSD_FILE

Stores whether to show/hide volume osd popups

/tmp/kmonad_volume_script_display_osd
[volume]
[[private]]
volume_script = "|>VOLUME_SCRIPT<|"
toggle_osd_script = "|>VOLUME_TOGGLE_OSD_SCRIPT<|"

up   = (cmd-button "$volume_script +"
                   "$volume_script 0")
down = (cmd-button "$volume_script -"
                   "$volume_script 0")

toggle-osd = (cmd-button "$toggle_osd_script")
mute = (cmd-button "qdbus org.kde.kglobalaccel /component/kmix invokeShortcut mute")

play-pause = 'PlayPause'

exit_internal = (layer-rem volume)
[[public]]
enter = (tap-hold-next-release $tap-hold-delay #((layer-add volume) @kbd-set-backlight-on) (layer-toggle volume))
[[keys]]
k = up
j = down

m = mute
q = toggle-osd

p = play-pause

lmet = exit

1.26.1. Volume Script

VOLUME_SCRIPT_PID_FILE
/tmp/kmonad_volume_script
VOLUME_HELPER_SCRIPT
~/.config/kmonad/volume/change_volume.py

Similar to the 1.25.1, this script will modulate a parameter at a given rate, writing its own PID into a file so that it can be killed when a key is released

VOLUME_CHANGE_DIRECTION
Either + to increase volume, - to decrease it or 0 to stop.

Like all of the other scripts, this one is POSIX-compliant

#!/bin/dash

VOLUME_CHANGE_DIRECTION="$1"

Kill the instance that is currently modifying the volume (if it exists). kill will throw an error if the process is no longer alive, but that will not crash the script

DIRECTION_PID_FILE=|>VOLUME_SCRIPT_PID_FILE<|

# Kill existing process if necessary
if [ -e $DIRECTION_PID_FILE ]; then
    kill "$(cat $DIRECTION_PID_FILE)"
    rm $DIRECTION_PID_FILE
fi

Only run the code if the direction is non-zero

if ! [ "$VOLUME_CHANGE_DIRECTION" = "0" ]; then

Reads whether or not to display osd popups from the disk

    DISPLAY_OSD_FILE=|>VOLUME_SCRIPT_OSD_FILE<|

    # I'm not exactly sure what a control is
    if [ -e $DISPLAY_OSD_FILE ]; then
        DISPLAY_OSD=$(cat $DISPLAY_OSD_FILE)
    else
        DISPLAY_OSD=1
        echo $DISPLAY_OSD > $DISPLAY_OSD_FILE &
    fi

I had to go to the dark side and use text parsing to get the volume because when I revisited Arch Linux, I saw that the DBus interface for getting the audio control and manipulating the volume no longer existed.

I found the following command on StackOverflow

    # Use amixer to get the current volume
    CURRENT_VOLUME=$(amixer get Master | grep % | awk '{print $5}' | sed -e 's/\[//' -e 's/%\]//' | head -n 1)

Explicitly unmute the output. The & spawns it in the background so that we don’t add extra delay before the actual volume modulation

    pactl set-sink-mute @DEFAULT_SINK@ false &

Write the shell’s pid to disk so the next invocation can kill it

    echo "$$" > "$DIRECTION_PID_FILE"
-E
Prevents unnecessary environment variables from being loaded (optimization).
-S
Prevents unnecessary modules from being loaded (optimization)

The reasoning behind this section being written in Python can be found under 1.26.1.1​. In this code, the python2 process inherits the PID of the shell since we are using exec

    exec python2 -ES |>VOLUME_HELPER_SCRIPT<| $CURRENT_VOLUME $VOLUME_CHANGE_DIRECTION $DISPLAY_OSD
fi
  1. Volume Helper Script

    The reason I wrote this section in Luapython2 is because it requires a loop to run with a subsecond delay. If this were written as part of the shell script, we would be calling out to /bin/sleep tens of times per second, and the interval could become visibly inconsistent.

    volume
    An integer representing the starting volume percentage
    increment
    + to increase volume, - to decrease it or 0 to toggle mute.
    display_osd
    1 to display the osd popups when the volume changes, 0 to suppress them
    from time import sleep
    from os import system
    from sys import argv
    
    volume = int(argv[1])
    increment = 1 if argv[2] == '+' else -1
    display_osd = True if argv[3] == '1' else False
    

    When we receive a USR1 signal from the 1.26.2, invert the value of display_osd. This is equivalent to reading the new value of the file; we know that the script would have inverted the value from what it was originally, so we can simply invert our variable to mirror it.

    import signal
    
    def usr1_handler(signum, frame):
        global display_osd
        display_osd = not display_osd
    
    signal.signal(signal.SIGUSR1, usr1_handler)
    

    f-strings were only introduced in python3.6, so this code uses string.format. I was originally confused by string.format, thinking string was a module, but in reality format is a method defined on the string class.

    while True:
        # Clamp the range of the loop between 0 and 100
        # Without these checks, there would be nothing stopping it from going out of bounds
        if volume > 100 and increment > 0 or volume < 0 and increment < 0:
            break
    
        volume += increment
    
        system('pactl set-sink-volume @DEFAULT_SINK@ {}%'.format(volume))
    
        if display_osd:
            system('qdbus org.kde.plasmashell /org/kde/osdService org.kde.osdService.volumeChanged {}'.format(volume))
    
        # 30 ms delay
        sleep(0.030)
    

    This code could be further optimized by spawning the system commands with subprocess.Popen, saving the handles to a list and polling/filtering them on each iteration of the loop. The subprocess32 package is recommended when using subprocess in python2, since the stock version of subprocess that ships with it has several issues.

1.26.2. Volume OSD Toggle Script

This script switches the contents of $DISPLAY_OSD_FILE between 0 and 1, setting the value to 0 if the file does not exist.

sed
Stream editor
-i "$DISPLAY_OSD_FILE"
Modifies the file in-place, so we don’t need to open the file once for reading and again for writing.
'y/01/10'

From the sed man page for the y command:

Transliterate the characters in the pattern space which appear in source to the corresponding character in dest.

This effectively maps 0 to 1 and 1 to 0.

#!/bin/dash

DISPLAY_OSD_FILE=|>VOLUME_SCRIPT_OSD_FILE<|

if ! [ -e $DISPLAY_OSD_FILE ]; then
    echo "0" > "$DISPLAY_OSD_FILE"
else
    sed -i 'y/01/10/' "$DISPLAY_OSD_FILE"
fi

if [ -e |>VOLUME_SCRIPT_PID_FILE<| ]; then
    kill -s USR1 $(cat |>VOLUME_SCRIPT_PID_FILE<|)
fi

This is an alternate implementation of the swap using tr. See this StackOverflow post on why we can’t redirect the output of tr back into the file using >.

tr '01' '10' < $DISPLAY_OSD_FILE | sponge $DISPLAY_OSD_FILE

1.26.3. DONE Volume layer

The volume layer would remap hjkl to control the volume.

1.26.4. TODO Volume Next/Prev

Rotate to next/previous output with h/l

1.27. Yank

Yank Script
~/.config/kmonad/yank/yank_active_window.sh

Copies the actively focused window title to the clipboard.

[aliases]
yank_script = "|>YANK_SCRIPT<|"
yank = (cmd-button "$yank_script")

1.27.1. Yank Script

This script copies the title to the clipboard, and also emits a notification to the screen.

#!/bin/dash
window_title=$(xdotool getactivewindow getwindowname)

# copy to clipboard
echo "$window_title" | xclip -selection c -r
# send notification
qdbus org.kde.plasmashell /org/kde/osdService org.kde.osdService.showText "document-duplicate" "$window_title"

1.28. Switch Focus

SWITCH_MOUSE_SCREEN_SCRIPT
/home/sridaran/Development/Scripts/DE/mouseToNextDesktop.sh
[aliases]
SWITCH_MOUSE_SCREEN_SCRIPT = "|>SWITCH_MOUSE_SCREEN_SCRIPT<|"

switch_mouse_screen = (cmd-button "$SWITCH_MOUSE_SCREEN_SCRIPT")
switch_focus_screen = (cmd-button "qdbus org.kde.kglobalaccel /component/kwin invokeShortcut \"Switch to Next Screen\"")

switch_focus_composite = (tap-hold $tap-hold-delay-min @switch_focus_screen @switch_mouse_screen)

1.29. Vim

NVIM
/home/sridaran/Packages/neovim/nvim0-6-0.appimage
NVIM_SCRIPT
/home/sridaran/.config/kmonad/vim/run_neovim.sh
[aliases]
NVIM_SCRIPT = "|>NVIM_SCRIPT<|"
vim = (cmd-button "kitty fish -C \"$NVIM_SCRIPT\"")

1.29.1. Run Neovim Script

#!/bin/dash

ELAPSED_TIME=$(/bin/time -f '%E' |>NEOVIM<|)
zenity --text "Ran for $ELAPSED_TIME" --notification

1.30. Simple Datetime Overlay

Simple Datetime Overlay Path
/home/sridaran/.local/bin/simple-datetime-overlay

This is a simple button that spawns my program and then kills all instances of it.

[aliases]
SIMPLE_DATETIME_OVERLAY = "|>SDO_SCRIPT_PATH<|"
simple-datetime-overlay = (cmd-button "/bin/dash -c '$SIMPLE_DATETIME_OVERLAY'" "sleep 0.15; kill \$(pgrep -f simple-datetime-overlay)")

Here are my issues with simple-datetime-overlay:

  1. Setting it to show up on all monitors is ideal, but it feels too slow unless I have my CPU profile on high or max
  2. Setting it to show up on the active monitor is nice, but sometimes I don’t know which monitor is active so I don’t know where to look
  3. Setting it to show up on monitor 0 makes it consistently fast, but I don’t want to have to turn my head to look at it

Ideally, show up on all monitors when we are on max performance, active monitor otherwise.

1.30.1. Invoke Simple Datetime Overlay Script

SDO Script Path
/home/sridaran/.config/kmonad/simple-datetime-overlay/simple-datetime-overlay.sh
CPUFreq Active Profile Path
/home/sridaran/.cache/set-cpufreq-profile/active-profile

This script checks what my current cpu profile is, and if it is on max performance, then it displays the datetime overlay on all monitors. Otherwise, it displays it only on the active monitor.

On medium performance mode and below, use the tock command-line program to render a clock in the st terminal.

#!/bin/dash

CURRENT_CPUFREQ_PROFILE=$(cat "|>CPUFREQ_ACTIVE_PROFILE<|")

PROGRAM=|>SIMPLE_DATETIME_OVERLAY<|
PARAMS="--only-monitor 0"

case "$CURRENT_CPUFREQ_PROFILE" in
    "Max Performance")
        PARAMS=""
        ;;
    "High Performance")
        PARAMS="-a"
        ;;
    *)
        PROGRAM=st

        PARAMS="-c simple-datetime-overlay-tock -g 95x15 -t 'simple-datetime-overlay' -- /home/sridaran/.cargo/bin/tock --seconds --center --format '%A, %B %d, %Y'"
        ;;
esac

# source: https://superuser.com/questions/1529226/get-bash-to-respect-quotes-when-word-splitting-subshell-output
echo $PARAMS | xargs $PROGRAM

1.31. Buffer Procedures

1.31.1. Tangle and Compile

(progn
  (org-babel-tangle)
  (let* ((error-bufname "KMonadX Compilation Output")
         (display-buffer-alist '((".*" display-buffer-at-bottom))))
    (progn
      (get-buffer-create error-bufname)
      (with-current-buffer error-bufname
        (erase-buffer))

      (call-process "fish" nil (get-buffer error-bufname) nil "-c ./compile.sh")
      (if (not (eq (buffer-size (get-buffer error-bufname)) 0))
          (progn
            (display-buffer (get-buffer error-bufname) nil)
            (switch-to-buffer-other-window error-bufname)
            (ansi-color-apply-on-region (point-min) (point-max)))
        (progn
          (message "%s" "Compilation completed successfully!")
          (when (y-or-n-p "Restart KMonad?")
            (srithon/spawn-process "systemctl" "--user" "restart" "kmonad")))))))

1.32. TODO Local Leader

2. KMonad Documentation

Documentation was copied from tutorial.kbd and adapted for Org mode

2.1. Basic Syntax

KMonad’s configuration language is styled on various lisps, like scheme or Common Lisp. In a lisp, every statement is entered between ’(’ and ’)’s. If you are more used to Fortan style languages (python, ruby, C, Java, etc.), the change is quite straightforward: the function name moves into the parentheses, and you don’t use commas to separate arguments. I.e.

This: my_function(a, 3, “Alakazam”) Becomes: (my_function a 3 “Alakazam”)

The reason for this is because Lisp-style languages are very easy to parse and write syntax-highlighters for.

We also provide standard Lisp syntax for comments:

  • block comments between: #| and its reverse
  • line comments following: ;;

    Unlike standard lisp, a single ; does not denote a command, but instead the keycode for semicolon.

    Also, as you might have noticed, whitespace is possible anywhere.

    To check for syntax errors while editing, invoke kmonad with the -d option.

2.2. defcfg block

Necessary: the `defcfg` block

There are a few bits of information that are required to be present in a KMonad configuration file. One of these is the existence of exactly 1 `defcfg` statement. This statement is used to customize various configuration settings. Many of these settings have default values, but a minimal definition must include at least an ’input’ field and an ’output’ field. These describe how KMonad captures its inputs and how it emits its outputs.

First, let’s go over the optional, non-OS specific settings. Currently there is only 2:

  • fallthrough: `true` or `false`, defaults to `false`

    KMonad catches input events and tries to match them to various handlers. If it cannot match an event to any handler (for example, if it isn’t included in the `defsrc` block, or if it is, but the current keymap does not map any buttons to it), then the event gets quietly ignored. If `fallthrough` is set to `true`, any unhandled events simply get reemitted.

  • allow-cmd: `true` or `false`, defaults to `false`

    If this is set to `false`, any action that runs a shell-command will simply log to `stdout` without ever running (log-level info). Don’t ever enable this on a configuration that you do not trust, because:

    (cmd-button “rm -rf ~/*”)

    is a thing. For more information on the `cmd-button’ function, see the section on Command buttons below.

    There are also some optional OS specific settings that we support:

    • `cmp-seq’: KEY, defaults to `RightAlt’ (Linux X11 specific)

      This sets your compose key for Unicode input. For more information, as well as a workaround to also make this work on windows, see the section on Compose-key sequences below.

    • `cmp-seq-delay’: NUMBER (in milliseconds)

      This sets a delay between each pressed key in a compose-key sequence. Some environments may have troubles recognizing the key sequence if it’s pressed too rapidly; if you experience any problems in this direction, you can try setting this value to `5’ or `10’ and see if that helps.

    Secondly, let’s go over how to specify the `input` and `output` fields of a `defcfg` block. This differs between OS’es, and so do the capabilities of these interfaces.

  1. Linux

    In Linux we deal with input by performing an ioctl-grab on a specific device-file. This allows us to hook KMonad on the input of exactly 1 keyboard, and allows you to run multiple instances of KMonad for different keyboards. We make an input using: (device-file “/dev/input/by-id/my-keyboard-kbd”)

    NOTE: Any valid path to a device-file will work, but it is recommended to use the ’by-id’ directory, since these names will not change if you replug the device.

    We deal with output by creating a ’uinput’ device. This requires that the ’uinput’ kernel module is loaded. The easiest way to ensure this is by calling ’sudo modprobe uinput’. We create a uinput device using: (uinput-sink “name” “optional post-init command”)

  2. Windows

    In Windows we do not get such fine-grained control. We use a low-level keyboard hook to intercept all non-injected keyboard events. There is currently an open issue to improve the C-bindings used to capture windows keyevents, and if you have a better way to approach this issue, help is deeply appreciated. You specify a windows input using: (low-level-hook)

    Similarly, the output in Windows lacks the fine-grained control. We use the SendEvent API to emit key events directly to Windows. Since these are ’artificial’ events we won’t end up catching them again by the `low-level-hook`. It is very likely that KMonad does not play well with other programs that capture keyboard input like AHK. You specify windows output using: (send-event-sink)

  3. MacOS

    For Mac questions I suggest filing an issue and tagging @thoelze1, he wrote the MacOS API. However, input using: (iokit-name “optional product string”)

    By default this should grab all keyboards, however if a product string is provided, KMonad will only capture those devices that match the provided product string. If you would like to provide a product string, you can run `make; ./list-keyboards’ in c_src/mac to list the product strings of all connected keyboards.

    You initialize output on MacOS using: (kext)

2.3. defsrc block

Necessary: the `defsrc` block

It is difficult to explain the `defsrc` block without immediately going into `deflayer` blocks as well. Essentially, KMonad maps input-events to various internal actions, many of which generate output events. The `defsrc` block explains the layout on which we specify our `deflayer`s down the line.

It is important to realize that the `defsrc` block doesn’t necessarily have to coincide with your actual input keyboard. You can specify a full 100% `defsrc` block, but only use a 40% keyboard. This will mean that every `deflayer` you specify will also have to match your 100% `defsrc`, and that your actual keyboard would be physically unable to trigger about 60% of your keymap, but it would be perfectly valid syntax.

The dual of this (and more useful) is that it is also perfectly valid to only specify that part of your keyboard in `defsrc` that you want to remap. If you use a 100% keyboard, but don’t want to remap the numpad at all you can simply leave the numpad out of your `defsrc`, and it should work just fine. In that particular case you probably want to set `fallthrough` to `true` in your `defcfg` block though.

In the future we would like to provide support for multiple, named `defsrc` blocks, so that it becomes easier to specify various layers for just the numpad, for example, but at the moment any more or less than 1 `defsrc` block will result in an error.

The layouting in the `defsrc` block is completely free, whitespace simply gets ignored. We strive to provide a name for every keycode that is no longer than 4 characters, so we find that laying out your keymap in columns of 5 works out quite nicely (although wider columns will allow for more informative aliases, see below).

Most keycodes should be obvious. If you are unsure, check ’./src/KMonad/Keyboard/Keycode.hs’. Every Keycode has a name corresponding to its Keycode name, but all lower-case and with the ’Key’ prefix removed. There are also various aliases for Keycodes starting around line 350. If you are trying to bind a key and there is not a 4-letter alias, please file an issue, or better yet, a pull-request, and it will be added promptly.

Also, you can consult ’./keymap/template/’ for various input templates to use directly or to look up keycodes by position. Here we use the input-template for ’us_ansi_60.kbd’

2.4. defalias

Optional : `defalias` statements

KMonad will let you specify some very specific, crazy buttons. These definitions can get pretty long, though, and would make `deflayer` blocks nearly impossible to read. Therefore we provide the ability to alias names to these buttons, to keep the actual `deflayer` statements orderly.

A `defalias` can contain any number of aliases, and it can refer backwards or forwards to layers without issue. The only sequencing that needs to be kept in mind is that a `defalias` cannot refer forward to another `defalias` that is not yet defined.

Here we define a few aliases, but we will define more later. Notice that we try to only use 3 letter names for aliases. If that is not enough to be clear, consider widening all columns to 6 or 7 characters (or be content with a messy config).

2.5. deflayer

Necessary: at least 1 `deflayer` block

As explained in the `defsrc` section, a `deflayer` will define a button for each corresponding entry in the `defsrc` definition. A `deflayer` statement consists of the `deflayer` keyword, followed by the name used to identify this layer, followed by N ’statements-that-evaluate-to-a-button’, where N is exactly how many entries are defined in the `defsrc` statement.

It is also important to mention that the ’keymap’ in KMonad is modelled as a stack of layers (just like in QMK). When an event is registered we look in the top-most layer for a handler. If we don’t find one we try the next layer, and then the next.

Exactly what ’evaluates-to-a-button’ will be expanded on in more detail below. There are very many different specialist buttons in KMonad that we will touch upon. However, for now, these 4 are a good place to begin:

  1. Any keycode evaluates to a button that, on press, emits the press of that keycode, and on release, emits the release of that keycode. Just a ’normal’ button. The exception is ’\’, which gets used as an escape character. Use ’\\’ instead. Other characters that need to be escaped to match the literal character are ’(’, ’)’, and ’_’.
  2. An @-prefixed name evaluates to an alias lookup. We named two buttons in the `defalias` block above, we could now refer to these buttons using `@num` and `@kil`. This is also why we only use alias-names no longer than 3 characters in this tutorial. Also, note that we are already referencing some aliases that have not yet been defined, this is not an issue.
  3. The ’_’ character evaluates to transparent. I.e. no handler for that key-event in this layer, causing this event to be handed down the layer stack to perhaps be handled by the next layer.
  4. The ’XX’ character evaluates to blocked. I.e. no action bound to that key-event in this layer, but do actually catch event, preventing any underlying layer from handling it.

    Finally, it is important to note that the first `deflayer` statement in a KMonad config will be the layer that is active when KMonad starts up.

2.5.1. Examples

(deflayer numbers
  _    _    _    _    _    _    _    _    _    _    _    _    _    _
  _    _    _    _    _    XX   /    7    8    9    -    _    _    _
  _    _    _    _    _    XX   *    4    5    6    +    _    _
  _    _    \(   \)   .    XX   0    1    2    3    _    _
  _    _    _              _              _    _    _    _
  )

(deflayer my-numbers
  _    _    _    _    _    _    _    _    _    _    _    _    _    _
  _    1    2    3    4    5    6    7    8    9    0    _    _    _
  _    _    _    _    _    _    _    _    _    _    _    _    _
  _    _    _    _    _    _    _    _    _    _    _    _                  _
  _    _    _              _              _    _    _                    _   _
  _
  )

2.6. Buttons with Modifiers

Optional: modded buttons

Let’s start by exploring the various special buttons that are supported by KMonad by looking at ’modded’ buttons, that is to say, buttons that activate some kind of ’mod’, then perform some button, and finally release that ’mod’ again.

We have already seen an example of this style of button, our `kil` button is one such button. Let’s look at it in more detail: C-A-del

This looks like a simple declarative statement, but it’s helpful to realize that is simply syntactic sugar around 2 function calls. This statement is equivalent to: (around ctl (around alt del))

This highlights a core design principle in KMonad: we try to provide very simple buttons, and then we provide rules and functions for combining them into new buttons. Although note: still very much a work in progress.

So, looking at this statement: (around foo bar)

Here, `around` is a function that takes two buttons and creates a new button. This new button will, on a press, first press foo, then press bar, and on a release first release bar, and then foo. Once created, this new button can be passed to anything in KMonad that expects a button.

We have already seen other examples of modded buttons, \(, \), *, and +. There are no Keycodes for these buttons in KMonad, but they are buttons. They simply evaluate to `(around lsft x)`. All shifted numbers have their corresponding characters, the same is true for all capitals, and < > : ~ “ | { }  + and ?.

To wrap up ’modded-buttons’, let’s look back at C-A-del. We have 8 variants: C- : (around lctl X) A- : (around lalt X) M- : (around lmet X) S- : (around lsft X)

Then RC-, RA-, RM-, and RS- behave exactly the same, except using the right-modifier.

These can be combined however you please: C-A-M-S-x ;; Perfectly valid C-% ;; Perfectly valid: same as C-S-5 C-RC-RA-A-M-S-RS-m ;; Sure, but why would you?

Also, note that although we provide special syntax for certain modifiers, these buttons are in no way ’special’ in KMonad. There is no concept of ’modifier’. (around a (around b c)) ;; Perfectly valid

2.6.1. Examples

cpy C-c
pst C-v
cut C-x

;; Something silly
md1 (around a (around b c))    ;; abc
md2 (around a (around lsft b)) ;; aB
md3 C-A-M-S-l
md4 (around % b)               ;; BEWARE: %B, not %b, do you see why?

Now we define the ’tst’ button as opening and closing a bunch of layers at the same time. If you understand why this works, you’re starting to grok KMonad.

Explanation: we define a bunch of testing-layers with buttons to illustrate the various options in KMonad. Each of these layers makes sure to have its buttons not overlap with the buttons from the other layers, and specifies all its other buttons as transparent. When we use the nested `around` statement, whenever we push the button linked to ’@tst’ (check `qwerty` layer, we bind it to `rctl`), any button we press when holding `rctl` will be pressed in the context of those 4 layers overlayed on the stack. When we release `rctl`, all these layers will be popped again.

(defalias tst (around (layer-toggle macro-test)
                      (around (layer-toggle layer-test)
                              (around (layer-toggle around-next-test)
                                      (around (layer-toggle command-test)
                                              (layer-toggle modded-test))))))

(deflayer modded-test
  _    _    _    _    _    _    _    _    _    _    _    _    _    _
  _    _    @md4 _    _    _    _    _    _    _    _    _    _    _
  _    _    @md1 @md2 @md3 _    _    _    _    _    _    _    _
  _    _    @cut @cpy @pst _    _    _    _    _    _    _
  _    _    _              _              _    _    _    _
  )

2.7. Sticky Keys

Optional: sticky keys

KMonad also support so called “sticky keys”. These are keys that will behave as if they were pressed after just tapping them. This behaviour wears off after the next button is pressed, which makes them ideal for things like a quick control or shift. For example, tapping a sticky and then pressing `abc’ will result in `Abc’.

You can create these keys with the `sticky-key’ keyword:

(defalias slc (sticky-key 500 lctl))

The number after `sticky-key’ is the timeout you want, in milliseconds. If a key is tapped and that time has passed, it won’t act like it’s pressed down when we receive the next keypress.

It is also possible to combine sticky keys. For example, to get a sticky shift+control you can do

(defalias ssc (around (sticky-key 500 lsft) (sticky-key 500 lctl)))

2.7.1. Examples

Let’s make both shift keys sticky

(defalias
  sl (sticky-key 300 lsft)
  sr (sticky-key 300 rsft))

2.8. Tap Macros

Optional: tap-macros

Let’s look at a button we haven’t seen yet, tap-macros.

`tap-macro` is a function that takes an arbitrary number of buttons and returns a new button. When this new button is pressed it rapidly taps all its stored buttons in quick succesion except for its last button, which it only presses. This last button gets released when the `tap-macro` gets released.

There are two ways to define a `tap-macro`, using the `tap-macro` function directly, or through the #() syntactic sugar. Both evaluate to exactly the same button.

(tap-macro K M o n a d) #(K M o n a d)

If you are going to use a `tap-macro` to perform a sequence of actions inside some program you probably want to include short pauses between inputs to give the program time to register all the key-presses. Therefore we also provide the ’pause’ function, which simply pauses processing for a certain amount of milliseconds. Pauses can be created like this:

(pause 20) P20

You an also pause between each key stroke by specifying the `:delay’ keyword, as well as a time in ms, at the end of a `tap-macro’:

(tap-macro K M o n a d :delay 5) #(K M o n a d :delay 5)

The above would be equivalent to e.g.

(tap-macro K P5 M P5 o P5 n P5 a P5 d)

WARNING: DO NOT STORE YOUR PASSWORDS IN PLAIN TEXT OR IN YOUR KEYBOARD

I know it might be tempting to store your password as a macro, but there are 2 huge risks:

  1. You accidentally leak your config and expose your password
  2. Anyone who knows about the button can get clear-text representation of your password with any text editor, shell, or text-input field.

    Support for triggering shell commands directly from KMonad is described in the command buttons section below.

    This concludes this public service announcement.

2.8.1. Examples

(defalias
  mc1 #(K M o n a d)
  mc2 #(C-c P50 A-tab P50 C-v) ;; Careful, this might do something
  mc3 #(P200 h P150 4 P100 > < P50 > < P20 0 r z 1 ! 1 ! !)
  mc4 (tap-macro a (pause 50) @md2 (pause 50) c)
  mc5 #(@mc3 spc @mc3 spc @mc3)
  )

(deflayer macro-test
  _    @mc1 @mc2 @mc3 @mc4 @mc5 _    _    _    _    _    _    _    _
  _    _    _    _    _    _    _    _    _    _    _    _    _    _
  _    _    _    _    _    _    _    _    _    _    _    _    _
  _    _    _    _    _    _    _    _    _    _    _    _
  _    _    _              _              _    _    _    _
  )

2.9. Layer Manipulation

Optional: layer manipulation

You have already seen the basics of layer-manipulation. The `layer-toggle` button. This button adds a layer to the top of KMonad’s layer stack when pressed, and removes it again when released. There are a number of other ways to manipulate the layer stack, some safer than others. Let’s go through all of them from safest to least safe:

`layer-toggle` works as described before, 2 things to note:

  1. If you are confused or worried about pressing a key, changing layers, and then releasing a key and this causing issues: don’t be. KMonad handles presses and releases in very different ways. Presses get passed directly to the stacked keymap as previously described. When a KMonad button has its press-action triggered, it then registers a callback that will catch its own release before we ever touch the keymap. This guarantees that the button triggered by the press of X will be the button whose release is triggered by the release of X (the release of X might trigger other things as well, but that is besides the point.)
  2. If `layer-toggle` can only ever add and then necessarily remove 1 layer from the stack, then it will never cause a permanent change, and is perfectly safe.

    `layer-delay`, once pressed, temporarily switches to some layer for some milliseconds. Just like `layer-toggle` this will never permanently mess-up the layer stack. This button was initially implemented to provide some ’leader-key’ style behavior. Although I think in the future better solutions will be available. For now this will temporarily add a layer to the top of the stack: (layer-delay 500 my-layer)

    `layer-next`, once pressed, primes KMonad to handle the next press from some arbitrary layer. This aims to fill the same usecase as `layer-delay`: the beginnings of ’leader-key’ style behavior. I think this whole button will get deleted soon, because the more general `around-next` now exists (see below) and this is nothing more than: (around-next (layer-toggle layer-name) some-button) Until then though, use `layer-next` like this: (layer-next layer-name)

    `layer-switch`: change the base-layer of KMonad. As described at the top of this document, the first `deflayer` statement is the layer that is active when KMonad starts. Since `layer-toggle` can only ever add on and remove from the top of that, it can never change the base-layer. The following button will unregister the bottom-most layer of the keymap, and replace it with another layer. (layer-switch my-layer)

    This is where things start getting potentially dangerous (i.e. get KMonad into an unusuable state until a restart has occured). It is perfectly possible to switch into a layer that you can never get out of. Or worse, you could theoretically have a layer full of only `XX`s and switch into that, rendering your keyboard unuseable until you somehow manage to kill KMonad (without using your keyboard).

    However, when handled well, `layer-switch` is very useful, letting you switch between ’modes’ for your keyboard. I have a tiny keyboard with a weird keymap, but I switch into a simple ’qwerty’ keymap shifted 1 button to the right for gaming. Just make sure that any ’mode’ you switch into has a button that allows you to switch back out of the ’mode’ (or content yourself restarting KMonad somehow).

    `layer-add` and `layer-rem`. This is where you can very quickly cause yourself a big headache. Originally I didn’t expose these operations, but someone wanted to use them, and I am not one to deny someone else a chainsaw. As the names might give away: (layer-add name) ;; Add a layer to the top of the stack (layer-rem name) ;; Remove a layer by name (noop if no such layer)

    To use `layer-add` and `layer-rem` well, you should take a moment to think about how to create a layout that will prevent you from getting into situations where you enter a key-configuration you cannot get out of again. These two operations together, however, are very useful for activating a permanent overlay for a while. This technique is illustrated in the tap-hold overlay a bit further down.

2.9.1. Examples

(defalias
  yah (layer-toggle asking-for-trouble) ;; Completely safe
  nah (layer-add asking-for-trouble)    ;; Completely unsafe

  ld1 (layer-delay 500 numbers) ;; One way to get a leader-key
  ld2 (layer-next numbers)      ;; Another way to get a leader key

  ;; NOTE, this is safe because both `qwerty` and `colemak` contain the `@tst`
  ;; button which will get us to the `layer-test` layer, which itself contains
  ;; both `@qwe` and `@col`.
  qwe (layer-switch qwerty) ;; Set qwerty as the base layer
  col (layer-switch colemak) ;; Set colemak as the base layer
  )

(deflayer layer-test
  @qwe _    _    _    _    _    _    _    _    _    _    @add _    @nah
  @col _    _    _    _    _    _    _    _    _    _    _    _    @yah
  _    _    _    _    _    _    _    _    _    _    _    _    _
  _    _    _    _    _    _    _    _    _    @ld1 @ld2 _
  _    _    _              _              _    _    _    _
  )

;; Exactly like qwerty, but with the letters switched around
(deflayer colemak
  grv  1    2    3    4    5    6    7    8    9    0    -    =    bspc
  tab  q    w    f    p    g    j    l    u    y    ;    [    ]    \
  @xcp a    r    s    t    d    h    n    e    i    o    '    ret
  @sl  z    x    c    v    b    k    m    ,    .    /    @sr
  lctl @num lalt           spc            ralt rmet @sym @tst
  )

(defalias lol #(: - D))

;; Contrived example
(deflayer asking-for-trouble
  @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol
  @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol
  @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol
  @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol
  @lol @lol @lol           @lol           @lol @lol @lol @lol
  )

;; One way to safely use layer-add and layer-rem: the button bound to layer-add
;; is the same button bound to layer-rem in the layer that `add` adds to the
;; stack. I.e., it becomes impossible to add or remove multiple copies of a
;; layer.
(defalias
  add (layer-add multi-overlay) ;; multi-overlay is defined in the next
  rem (layer-rem multi-overlay) ;; section below this
  )

2.10. Multi-use Buttons

Optional: Multi-use buttons

Perhaps one of the most useful features of KMonad, where a lot of work has gone into, but also an area with many buttons that are ever so slightly different. The naming and structuring of these buttons might change sometime soon, but for now, this is what there is.

For the next section being able to talk about examples is going to be handy, so consider the following scenario and mini-language that will be the same between scenarios.

  • We have some button `foo` that will be different between scenarios
  • `foo` is bound to ’Esc’ on the input keyboard
  • the letters a s d f are bound to themselves
  • Px signifies the press of button x on the keyboard
  • Rx signifies the release of said button
  • Tx signifies the sequential and near instantaneous press and release of x
  • 100 signifies 100ms pass

    So for example: Tesc Ta: tap of ’Esc’ (triggering `foo`), tap of ’a’ triggering `a` Pesc 100 Ta Tb Resc: press of ’Esc’, 100ms pause, tap of ’a’, tap of ’b’, release of ’Esc’

    The `tap-next` button takes 2 buttons, one for tapping, one for holding, and combines them into a single button. When pressed, if the next event is its own release, we tap the ’tapping’ button. In all other cases we first press the ’holding’ button then we handle the event. Then when the `tap-next` gets released, we release the ’holding’ button.

    So, using our mini-language, we set foo to: (tap-next x lsft) Then: Tesc -> x Tesc Ta -> xa Pesc Ta Resc -> A Pesc Ta Tr Resc -> AR

    The `tap-hold` button is very similar to `tap-next` (a theme, trust me). The difference lies in how the decision is made whether to tap or hold. A `tap-hold` waits for a particular timeout, if the `tap-hold` is released anywhere before that moment we execute a tap immediately. If the timeout occurs and the `tap-hold` is still held, we switch to holding mode.

    The additional feature of a `tap-hold` is that it pauses event-processing until it makes its decision and then rolls back processing when the decision has been made.

    So, again with the mini-language, we set foo to: (tap-hold 200 x lsft) ;; Like tap-next, but with a 200ms timeout Then: Tesc -> x Tesc Ta -> xa Pesc 300 a -> A (the moment you press a) Pesc a 300 -> A (after 200 ms) Pesc a 100 Resc -> xa (both happening immediately on Resc)

    The `tap-hold-next` button is a combination of the previous 2. Essentially, think of it as a `tap-next` button, but it also switches to held after a period of time. This is useful, because if you have a (tap-next ret ctl) for example, and you press it thinking you want to press C-v, but then you change your mind, you now cannot release the button without triggering a ’ret’, that you then have to backspace. With the `tap-hold-next` button, you simply outwait the delay, and you’re good. I see no benefit of `tap-next` over `tap-hold-next` with a decent timeout value.

    The `tap-next-release` is like `tap-next`, except it decides whether to tap or hold based on the next release of a key that was not pressed before us. This also performs rollback like `tap-hold`.So, using the minilanguage and foo as: (tap-next-release x lsft) Then: Tesc Ta -> xa Pa Pesc Ra Resc -> ax (because ’a’ was already pressed when we started, so foo decides it is tapping) Pesc Ta Resc -> A (because a was pressed and released after we started, so foo decides it is holding)

    These increasingly stranger buttons are, I think, coming from the stubborn drive of some of my more eccentric (and I mean that in the most positive way) users to make typing with modifiers on the home-row more comfortable. Especially layouts that encourage a lot of rolling motions are nicer to use with the `release` style buttons.

    The `tap-hold-next-release` (notice a trend?) is just like `tap-next-release`, but it comes with an additional timeout that, just like `tap-hold-next` will jump into holding-mode after a timeout.

    I honestly think that `tap-hold-next-release`, although it seems the most complicated, probably is the most comfortable to use. But I’ve put all of them in a testing layer down below, so give them a go and see what is nice.

2.10.1. Examples

(defalias
  xtn (tap-next x lsft)         ;; Shift that does 'x' on tap
  xth (tap-hold 400 x lsft)     ;; Long delay for easier testing
  thn (tap-hold-next 400 x lsft)
  tnr (tap-next-release x lsft)
  tnh (tap-hold-next-release 2000 x lsft)

  ;; Used it the colemak layer
  xcp (tap-hold-next 400 esc ctl)
  )

;; Some of the buttons used here are defined in the next section
(deflayer multi-overlay
  @mt  _    _    _    _    _    _    _    _    _    _    _    @rem _
  _    _    _    _    _    _    _    _    _    _    _    _    _    _
  @thn _    _    _    _    _    _    _    _    _    _    _    _
  @xtn _    _    _    _    _    _    _    _    _    _    @xth
  @tnr _    _              _              _    _    _    @tnh
  )

2.11. Multi-tap

Optional: Multi-tap

Besides the tap-hold style buttons there is another multi-use button (with. only 1 variant, at the moment). The `multi-tap`.

A `multi-tap` codes for different buttons depending on how often it is tapped. It is defined by a series of delays and buttons, followed by a last button without delay. As long as you tap the `multi-tap` within the delay specified, it will jump to the next button. Once the delay is exceeded the selected button is pressed. If the last button in the list is reached, it is immediately pressed.

Note that you can actually hold the button, so in the below example, going: tap-tap-hold (wait 300ms) will get you a pressed c, until you release again.

2.11.1. Examples

(defalias
  mt  (multi-tap 300 a 300 b 300 c 300 d e))

2.12. Around-next

Optional: Around-next

The `around-next` function creates a button that primes KMonad to perform the next button-press inside some context. This could be the context of ’having Shift pressed’ or ’being inside some layer’ or, less usefully, ’having d pressed’. It is a more general and powerful version of `layer-next`.

I think expansion of this button-style is probably the future of leader-key, hydra-style functionality support in KMonad.

2.12.1. Examples

(defalias
  ns  (around-next sft)  ;; Shift the next press
  nnm (around-next @num) ;; Perform next press in numbers layer
  nd  (around-next d)    ;; Silly, but possible

  )

(deflayer around-next-test
  _    _    _    _    _    _    _    _    _    _    _    _    _    _
  _    _    _    _    _    _    _    _    _    _    _    _    _    _
  @nd  _    _    _    _    _    _    _    _    _    _    _    _
  @nnm _    _    _    _    _    _    _    _    _    _    _
  @ns  _    _              _              _    _    _    _
  )

2.13. Compose Key Sequences

Optional: Compose-key sequences

Compose-key sequences are series of button-presses that your operating system will interpret as the insertion of a special character, like accented characters, or various special-languages. In that sense, they are just syntactic sugar for keyboard macros.

To get this to work on Linux you will need to set your compose-key with a tool like `setxkbmap’, as well as tell kmonad that information. See the `defcfg’ block at the top of this file for a working example. Note that you need to wait ever so slightly for the keyboard to register with linux before the command gets executed, that’s why the `sleep 1`. Also, note that all the `/run/current-system’ stuff is because the author uses NixOS. Just find a shell-command that will:

  1. Sleep a moment
  2. Set the compose-key to your desired key

    Please be aware that what `setxkbmap’ calls the `menu’ key is not actually the `menu’ key! If you want to use the often suggested

    setxkbmap -option compose:menu

    you will have to set your compose key within kmonad to `compose’ and not `menu’.

    After this, this should work out of the box under Linux. Windows does not recognize the same compose-key sequences, but WinCompose will make most of the sequences line up with KMonad: http://wincompose.info/ This has not in any way been tested on Mac.

    In addition to hard-coded symbols, we also provide ’uncompleted’ macros. Since a compose-key sequence is literally just a series of keystrokes, we can omit the last one, and enter the sequence for ’add an umlaut’ and let the user then press some letter to add this umlaut to. These are created using the `+“` syntax.

2.13.1. Examples

(defalias
  sym (layer-toggle symbols)
  )

(deflayer symbols
  _    _    _    _    _    _    _    _    _    _    _    _    _    _
  _    ä    é    ©    _    _    _    _    _    _    _    _    _    _
  _    +'   +~   +`   +^   _    _    _    _    _    _    _    _
  _    +"   _    _    _    _    _    _    _    _    _    _
  _    _    _              _              _    _    _    _)

2.14. Command Buttons

Optional: Command buttons

Currently we also provide the ability to launch arbitrary shell-commands from inside kmonad. These commands are simply handed off to the command-shell without any further checking or waiting.

NOTE: currently only tested on Linux, but should work on any platform, as long as the command is valid for that platform.

The `cmd-button’ function takes two arguments, the second one of which is optional. These represent the commands to be executed on pressing and releasing the button respectively.

BEWARE: never run anyone’s configuration without looking at it. You wouldn’t want to push:

(cmd-button “rm -rf ~/*”) ;; Delete all this user’s data

(defalias
  dat (cmd-button "date >> /tmp/kmonad_example.txt")   ;; Append date to tmpfile
  pth (cmd-button "echo $PATH > /tmp/kmonad_path.txt") ;; Write out PATH
  ;; `dat' on press and `pth' on release
  bth (cmd-button "date >> /tmp/kmonad_example.txt"
                   "echo $PATH > /tmp/kmonad_path.txt")
  )

(deflayer command-test
  _    _    _    _    _    _    _    _    _    _    _    _    _    _
  _    _    _    _    _    _    _    _    _    _    _    _    _    _
  _    _    _    _    _    _    _    _    _    _    _    _    _
  _    _    _    _    _    _    _    _    _    @dat @pth _
  _    _    _              _              _    _    _    _
  )

Author: Sridaran Thoniyil

Created: 2023-11-14 Tue 17:09