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.
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
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]
#[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
[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"
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")
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
[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 }
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
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]
[[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]
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)
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))
#!/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"
#!/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]
[[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'")
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]
[[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'
[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:
- Window Switcher
- using the window switcher will automatically add entries to the Jump List
- 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")
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
- Similarly, when using
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 backnext
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($_);
}
}
[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")
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)
#!/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
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"
#!/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
#!/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)
Arguments:
$1
: action; could befocus|toggle|dwim
$2
: type; could berunner|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 usescreen
to maintain a single shell session through eachrunner
invocation, since my auto-resizing patch breaks withtmux
.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 thest-runner
class
screen
arguments:-DR runner
-
Attaches to a session called
runner
, creating it if necessary. Some ofscreen
’s flag combinations seem a little arbitrary. -s /bin/fish
-
Tells
screen
to start new sessions with thefish
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 usetmux
, 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"
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
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.
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\")'")
Instead of a fullscreen, opaque window.
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")
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
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” orv
for “vertical”. MAGNITUDE
-
Either
+
for the positive direction,-
for the negative direction or0
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
NEW_DELAY
-
The new delay in milliseconds that we need
xdotool
to use. If it is equal to0
, 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_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 or0
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
/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 or0
to toggle mute. display_osd
-
1
to display theosd
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.
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 they
command:Transliterate the characters in the pattern space which appear in source to the corresponding character in dest.
This effectively maps
0
to1
and1
to0
.
#!/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
The volume layer would remap hjkl to control the volume.
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")
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 Script
-
~/.config/kmonad/paste/paste_clipboard.sh
[aliases]
paste_script = "|>PASTE_SCRIPT<|"
paste-clipboard = (cmd-button "$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\"")
#!/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
:
- 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
- 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
- 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.
- 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
(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")))))))