Skip to content

Latest commit

 

History

History
2115 lines (1787 loc) · 71 KB

logitech_keyboard.org

File metadata and controls

2115 lines (1787 loc) · 71 KB

Logitech Keyboard 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 Global Variables or elsewhere. Variable names are all uppercase, and their definitions will mirror the structure shown in Global Variables.

Global Variables

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
    

Program Paths

ST
~/.local/bin/st
    
ROFI_PRIMARY
~/.config/rofi/curated-themes/primary.sh
    

This is a script which wraps rofi and passes flags related to theming and default behavior. It’s convenient to have since it lets us omit a lot of arguments when invoking it.

EMACS
/usr/bin/emacs
    
EMACSCLIENT
/usr/bin/emacsclient
    
DISCORD
/usr/bin/Discord
    
BROTAB
~/.local/bin/brotab
    

Configuration

[configuration]
#[PP:ONLY(logitech_keyboard)]
input = "/dev/input/by-id/usb-Logitech_USB_Receiver-if02-event-kbd"
#[PP:ONLY(system_keyboard)]
input = "/dev/input/by-path/platform-i8042-serio-0-event-kbd"
#[PP:ONLY(keychron_c3_pro_keyboard)]
input = "/dev/input/by-id/usb-Keychron_Keychron_C3_Pro-if02-event-kbd"
#[PP:ONLY(keychron_v1_max_keyboard)]
input = "/dev/keychron-v1-max"

#[PP:ONLY(logitech_keyboard)]
output-name = "Logitech KMonad Output"
#[PP:ONLY(system_keyboard)]
output-name = "System Keyboard KMonad Output"
#[PP:ONLY(keychron_c3_pro_keyboard)]
output-name = "Keychron C3 Pro KMonad Output"
#[PP:ONLY(keychron_v1_max_keyboard)]
output-name = "Keychron V1 Max 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

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"

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")

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)) (around lmet (layer-toggle leader-held)))
[[keys]]
lmet = leader-key
grave = (tap-hold $tap-hold-delay-min grave @simple-datetime-overlay) # (ref: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-next-release $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

a = a_homerow_chords
backslash = backslash_layer

caps = lctrl-or-escape
q = q_homerow_movement

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)

Let’s also bind our volume up/down keys to those from our volume layer! This way we don’t have to go into the layer in order to change our brightness.

VolumeUp = volume:up
VolumeDown = volume:down

Base Leader Key Layer

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

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

Leader-Ctrl

This is a sub-layer under the leader layer which is activated by the ctrl keys: lctl, rctl and capslock.

[leader-ctrl]
[[keys]]
c = org-capture-goto-last
v = paste-clipboard

Leader-Held

This is a special layer that gets activated when lmet is held for the tap-hold duration. While this layer is active, we can assume that lmet is actively being held down.

[leader-held]
[[keys]]

With KWin (KDE’s window manager), holding Super and then dragging with the left/right mouse has the effect of moving or resizing the window. I thought it would be convenient to be able to do this dragging without actually clicking the physical mouse button, so I added mouse bindings to the leader layer. However, because our leader key is also our Super key, having the mouse buttons bound to leader['1'] means that we cannot normally hold Super while pressing the leader['1'] key; we could try lmet, 1, lmet- to hold lmet after pressing the mouse down, but holding Super after starting to drag with the mouse has no effect in KWin.

To work around this, I added the mouse buttons in the leader-held layer; when leader-held is active, lmet is actively being held! With this addition, when we use the 1 or 2 keys, we are able to move/resize our windows!

1 = mouse:left
2 = mouse:right

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

Home Row Chord Layer

[home-row-chord]
parent = { block, leader, 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 # (ref: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)

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.

def generate_binding(n: int) -> str:
    switch_button = f'(cmd-button "$kwin_shortcut_cmd \\"Switch to Desktop {n}\\"")'
    move_window_button = f'(cmd-button "$kwin_shortcut_cmd \\"Window to Desktop {n}\\"")'
    return f'{n} = (tap-hold $tap-hold-delay-min {switch_button} {move_window_button})'

return '\n'.join(map(generate_binding, range(1, 10)))

# 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))

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"

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

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'")

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 = { alphabetic-window-tiling, leader, numeric-desktop-switching }
[[keys]]
RightBrace = window-focusing:focus-right
LeftBrace = window-focusing:focus-left

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'

Program Paths

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

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
JUMP_LIST_SCRIPT
/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")

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($_);
    }
}
Opens the Window Switcher
[window-switcher]
[[private]]
rofi-args = "-lines 5 -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_PRIMARY $rofi-args -window-command \"/bin/dash -c '$mark-and-jump-command'\" -kb-accept-entry '' -kb-accept-alt 'Return'")
pull-window = (cmd-button "$ROFI_PRIMARY $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. -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.

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")

Switch back to the previous window when invoked a second time

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"))

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)

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"
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)))

k = up
j = down

h = toggle_nightlight

# q displays brightness on each monitor

tab = switch_focus_composite

lmet = exit

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"

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

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
[run]
[[private]]
runner_script = "~/.config/kmonad/runner/runner.pl"
toggle = (cmd-button "$toggle_cmd runner")
dwim = (cmd-button "$dwim_cmd runner")
[[public]]
toggle_cmd = "$runner_script toggle"
dwim_cmd = "$runner_script dwim"

entrypoint = (tap-hold-next-release $tap-hold-delay @dwim (layer-toggle run))
[[keys]]
t = toggle
enter = #('Enter' @toggle)

Runner Script

Arguments:

  • $1: action; could be focus|toggle|dwim
  • $2: type; could be runner|terminal

Let’s first process our command-line argument to determine what to do. First, let’s assert that the type is either runner or terminal.

my ($action, $runType, $maybeSession) = @ARGV;

if (!($runType =~ /terminal/ || $runType =~ /runner/)) {
    print "Invalid second argument: should be runner|terminal\n";
    exit(1);
}

Our target X11 classname is now st-<type>.

my $targetClassname = "st-${runType}";

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!

sub processAction {
    my ($actionType) = @_;

    my %dispatchTable = (
        focus => sub {
            # when focusing, spawn new process if not open, and focus existing process if open
            # successfully focused!
            if (system("wmctrl -x -a $targetClassname") eq 0) {
                exit(0);
            }
        },

        toggle => sub {
            # when toggling, spawn new process if not open, and KILL existing process if open
            if (system("wmctrl -x -c $targetClassname") eq 0) {
                # successfully closed!
                exit(0);
            }
        },

        dwim => sub {
            my $focusedWindow = `xdotool getwindowfocus`;
            # if focused window is the runner, exit code will be zero
            my $focusedWindowClass = `xprop -notype -id "$focusedWindow" WM_CLASS`;

            if ($focusedWindowClass =~ m/$targetClassname/) {
                # close window
                if (system("wmctrl -ic '$focusedWindow'") eq 0) {
                    exit(0);
                }
            }
            else {
                # https://stackoverflow.com/questions/4827690/how-to-change-a-command-line-argument-in-bash
                processAction("focus");
            }
        }
    );

    my $proc = $dispatchTable{$actionType};
    if (defined $proc) {
        $proc->();
    } else {
        print STDERR "Invalid first argument: please pass in focus|toggle|dwim\n";
        exit(1);
    }
}

processAction($action);

# 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.
$ENV{"SKIP_FISH_GREETING"} = 1;
$ENV{"STARSHIP_CONFIG"} = "~/.config/starship_runner.toml";

Sets the working directory back to home

chdir("~");

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

my $windowGeometryOutput = `xdotool getwindowfocus getwindowgeometry --shell`;
chomp($windowGeometryOutput);
my %windowGeometry = map { split /=/, $_, 2 } split /\n/, $windowGeometryOutput;
my ($X, $Y) = @windowGeometry{("X", "Y")};

my $monitorUtilsOutput = `monitor-utils --shell --at-point $X $Y --geometry`;
chomp($monitorUtilsOutput);
my %monitorUtils = map { split /=/, $_, 2 } split /\n/, $monitorUtilsOutput;
my ($X_OFFSET, $Y_OFFSET, $WIDTH, $HEIGHT) = @monitorUtils{("X_OFFSET", "Y_OFFSET", "WIDTH", "HEIGHT")};

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. Also, 23 terminal rows translates to about 823 pixels.

Through experimentation, I found that I want the following terminal sizes on each of my monitors:

  • For my 1920x1080 monitor, I want my terminal to be 1460x755
  • For a hypothetical 2765x1580 monitor, I found that I would want my terminal to be 1865x1250
    • I’m currently ignoring this measurement to make it easier to fit the equations
  • For my 3840x2160 monitor, I want my terminal to be 2170x1580

Let’s use numpy.linalg.lstsq (Least-Squares approximation) with slightly tuned numbers to come up with equations for arbitrary display sizes!

import numpy as np
monitor_measure_coefficients = np.array([[w, 1] for w in monitor_measure_coefficients])
expected_measures = np.array(expected_measures)
res = np.linalg.lstsq(monitor_measure_coefficients, expected_measures, rcond=None)

weights = res[0]
squared_error = res[1]

print(f"Coefficients: {weights}")
print(f"Squared error: {squared_error}")

for expected_measure, monitor_coefficients in zip(expected_measures, monitor_measure_coefficients):
    monitor_measure = monitor_coefficients[0]
    print(f"Expected, actual measure for {monitor_measure}: {(expected_measure, np.matmul(monitor_coefficients, weights))}")

First, let’s come up with our equation for computing the terminal width.

Monitor widths:

  • 1920
  • 3840

Expected terminal widths:

  • 1460
  • 2170
Coefficients: [3.69791667e-01 7.50000000e+02]
Squared error: []
Expected, actual measure for 1920: (1460, 1459.999999999999)
Expected, actual measure for 3840: (2170, 2169.9999999999995)

Monitor heights:

  • 1080
  • 2160

Expected terminal heights:

  • 755
  • 1580

Next, let’s come up with our equation for computing the terminal height!

Coefficients: [  0.76388889 -70.        ]
Squared error: []
Expected, actual measure for 1080: (755, 755.0000000000007)
Expected, actual measure for 2160: (1580, 1580.0000000000011)
my ($terminalRows, $terminalCols, $terminalWidth, $yPadding);
if ($runType =~ m/runner/) {
    $terminalRows = 8;
    $terminalCols = 116;
    $terminalWidth = 1660;
    $yPadding = ($HEIGHT / 10);
} else {
    $terminalWidth = int(0.369792 * $WIDTH + 750 + 0.5);

    my $terminalHeight = int(0.76389 * $HEIGHT - 70 + 0.5);
    # measured: 36 pixels per line
    # we need to specify scale=3 because the input is all integers, so bc will default to integer arithmetic.
    $terminalRows = int($terminalHeight / 36 + 0.5);

    # measured: 14.26 pixels per column
    $terminalCols = int($terminalWidth / 14.26 + 0.5);
    $yPadding = int(($HEIGHT - $terminalHeight) / 2 + 0.5);
}

my $xMargin = ($WIDTH - $terminalWidth) / 2;

my $terminalXOffset = $xMargin + $X_OFFSET;
my $terminalYOffset = $yPadding + $Y_OFFSET;

Now, let’s determine the arguments to start our terminal with, depending on the type.

runner
For the runner, we use screen to maintain a single shell session through each runner invocation, since my auto-resizing patch breaks with tmux.

st arguments:

-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

screen arguments:

-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.

We also specify -m 15 so that it will resize to a max height of 15 rows.

terminal
For the terminal, we don’t need the auto-resizing functionality, so we are free to use tmux, which is superior in every other way.
tmux new -As kmonad-terminal
Attaches to a session called kmonad-terminal, creating it if necessary.
my @st_args = ();
my @st_command = ();
if ($runType =~ /runner/) {
    @st_args = ("-m", "15", "-c", "rounded $targetClassname");
    # https://stackoverflow.com/questions/48920868/merge-arrays-to-make-a-new-array-in-perl
    @st_command = (qw(screen -c ~/.config/kmonad/runner/screenrc -DR), ($maybeSession ? $maybeSession : "runner"));
} else {
    @st_args = ("-c", $targetClassname);
    @st_command = (qw(tmux new -As), ($maybeSession ? $maybeSession : "default"));
}
|>ST<|
st is the terminal emulator
-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

my @all_args = ();
push @all_args, @st_args;
push @all_args, ("-g", "${terminalCols}x${terminalRows}+${terminalXOffset}+${terminalYOffset}");
push @all_args, @st_command;

system("$ENV{'HOME'}/.local/bin/st", @all_args);

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.

if ($runType =~ /runner/) {
    `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.
altscreen on
Opens vim and other similar programs on the alternate screen, so that their text doesn’t remain after closing.
term screen-256color
msgwait 0
shell /home/sridaran/.config/kmonad/runner/run_fish.sh
altscreen on
#!/bin/dash
exec fish --init-command="source $HOME/.config/kmonad/runner/config.fish"

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

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.

Reuse the runner terminal and shell between invocations

Similar to our Runner setup, it would be nice to have a full-size terminal scratchpad for other use-cases. We can use the same st build for both, and simply disable the auto-resizing for this one!

[terminal]
[[private]]
toggle = (cmd-button "$run:toggle_cmd terminal")
toggle_scratch = (cmd-button "$run:toggle_cmd terminal scratch")
dwim = (cmd-button "$run:dwim_cmd terminal")
[[public]]
entrypoint = (tap-hold-next-release $tap-hold-delay @dwim (layer-toggle terminal))
[[keys]]
# using r instead of t because t is the key for this layer
r = toggle
s = toggle_scratch
enter = #('Enter' @toggle)
[aliases]
agenda = (cmd-button "$EMACSCLIENT -ce '(org-agenda nil \"o\")'")

Open a floating, semi-transparent window

Instead of a fullscreen, opaque window.

Maybe switch to org-agenda

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")
[aliases]
org-capture = (cmd-button "~/.local/bin/org-capture")

Jump to Last

When provided with a prefix argument, org-capture jumps to the location where it would otherwise insert a note. This is something I often want to do, i.e to amend the last TODO I filed.

[aliases]
org-capture-goto-last = (cmd-button "emacsclient -a '' -e \"(let ((+org-capture-fn #'org-capture-goto-target)) (+org-capture/open-frame)))\"")

The implementation isn’t ideal, but it’s good enough for now. Mainly, I want to customize the displayed buffer so that I can exit out with C-c C-c (like in regular capture buffers) rather than using q f to close the frame.

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

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

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<|)

This layer provides buttons for holding mouse buttons.

[mouse]
[[private]]
mousedown_cmd = "xdotool mousedown"
mouseup_cmd = "xdotool mouseup"
[[public]]
left = (cmd-button "$mousedown_cmd 1" "$mouseup_cmd 1")
right = (cmd-button "$mousedown_cmd 3" "$mouseup_cmd 3")
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<|"

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

mute = (tap-hold $tap-hold-delay-min @mute_output @mute_microphone)

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))

up   = (cmd-button "$volume_script +"
                   "$volume_script 0")
down = (cmd-button "$volume_script -"
                   "$volume_script 0")
[[keys]]
k = up
j = down

m = mute
q = toggle-osd

p = play-pause

lmet = exit

Volume Script

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

Similar to the Scroll Script, 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 Volume Helper Script​. 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

Volume Helper Script

The reason I wrote this section in +Lua+​~python2~ 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 Volume OSD Toggle Script, 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.

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

Volume layer

The volume layer would remap hjkl to control the volume.

Volume Next/Prev

Rotate to next/previous output with h/l

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")

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"

Paste

Paste Script
~/.config/kmonad/paste/paste_clipboard.sh
    
[aliases]
paste_script = "|>PASTE_SCRIPT<|"
paste-clipboard = (cmd-button "$paste_script")

Paste Script

#!/bin/dash

xdotool type -- "$(xsel --clipboard)"
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)
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\"")

Run Neovim Script

#!/bin/dash

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

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

Buffer Procedures

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.target")))))))