KMonad
Table of Contents
- 1. Configuration
- 1.1. Global Variables
- 1.2. Configuration
- 1.3. Keyboard Backlight Aliases
- 1.4. Default Layer
- 1.5. Base Leader Key Layer
- 1.6. Window Focusing Layer
- 1.7. Home Row Chord Layer
- 1.8. Numeric Desktop Switching Layer
- 1.9. Alphabetic Window Tiling Layer
- 1.10. Backslash Layer
- 1.11. Home Row Movement Layer
- 1.12. Program Paths
- 1.13. Jump List
- 1.14. Window Switcher
- 1.15. Discord
- 1.16. Emacs
- 1.17. Firefox
- 1.18. Brightness
- 1.19. TODO Terminal
- 1.20. Run
- 1.21. TODO Prompt
- 1.22. Agenda
- 1.23. Open Preset
- 1.24. Org Capture
- 1.25. Scroll
- 1.26. Volume
- 1.27. Yank
- 1.28. Switch Focus
- 1.29. Vim
- 1.30. Simple Datetime Overlay
- 1.31. Buffer Procedures
- 1.32. TODO Local Leader
- 2. KMonad Documentation
1. Configuration
To allow for easier configuration, I am using “variables” for parts of the config that are subject to change between different setups. These “variables” are implemented using Org mode’s noweb syntax, which allows for code blocks to insert the contents of other code blocks.
Because the default reference syntax: <<BLOCK_NAME>>
, is valid syntax in some languages, I changed the syntax to |>BLOCK_NAME<|
.
When the document is tangled, these |>BLOCK_NAME<|
references are replaced with the contents of the corresponding named variable block in 1.1 or elsewhere.
Variable names are all uppercase, and their definitions will mirror the structure shown in 1.1.
1.1. Global Variables
KEYBOARD
/dev/input/by-id/usb-Logitech_USB_Receiver-if02-event-kbd
STARSHIP_RUNNER_CONFIG
I want the runner to have a different configuration than my normal shell, so this points to the other config
~/.config/starship_runner.toml
1.1.1. Program Paths
ST
~/.local/bin/st
ROFI
/usr/bin/rofi
EMACS
/usr/bin/emacs
EMACSCLIENT
/usr/bin/emacsclient
DISCORD
/usr/bin/Discord
BROTAB
~/.local/bin/brotab
1.2. Configuration
[configuration] input = "|>KEYBOARD<|" output-name = "Logitech KMonad Output" output-pre-command = "/usr/bin/sleep 0.75 && /usr/bin/setxkbmap -option compose:rctrl" cmp-seq = 'rctrl' cmp-seq-delay = 5 fallthrough = true allow-cmd = true # indicate which layer you want to be in when KMonad launches starting-layer = qwerty-homerow-mods
1.2.1. Constants
[aliases] tap-hold-delay = 200 tap-hold-delay-min = 160 hmod-delay = $tap-hold-delay kwin_shortcut_cmd = "qdbus org.kde.kglobalaccel /component/kwin invokeShortcut"
1.3. Keyboard Backlight Aliases
Let’s define buttons for increasing/decreasing the keyboard backlight. These will be used in other layers so that I can easily see when I’m in the base layer and when I’m in a custom layer, like Volume. This will be done by turning on the backlight when in the layers, and turning it back off when leaving the layers.
[aliases] kbd-set-backlight-command = "dbus-send --system --print-reply --dest=\"org.freedesktop.UPower\" /org/freedesktop/UPower/KbdBacklight org.freedesktop.UPower.KbdBacklight.SetBrightness" kbd-set-backlight-on-command = "$kbd-set-backlight-command int32:1" kbd-set-backlight-off-command = "$kbd-set-backlight-command int32:0" kbd-set-backlight-on = (cmd-button "$kbd-set-backlight-command int32:1") kbd-set-backlight-off = (cmd-button "$kbd-set-backlight-command int32:0") kbd-toggle-backlight = (cmd-button "$kbd-set-backlight-on-command" "$kbd-set-backlight-off-command")
1.4. Default Layer
Optional: as many layers as you please
We had already defined `num` as referring to a `(layer-toggle numbers)`. We will get into layer-manipulation soon, but first, let’s just create a second layer that overlays a numpad under our right-hand.
To easily specify layers it is highly recommended to create an empty `deflayer` statement as a comment at the top of your config, so you can simply copy-paste this template. There are also various empty layer templates available in the ’./keymap/template’ directory.
Enable the “leader” layer for the next keypress. If we release @leader_key before the next key, we treat the keypress as a tap, even if for a short period of time both keys were down. If we release @leader_key after the next key, we treat it as holding.
Also, if we hold the key for more than 250 milliseconds, treat it like we are holding the key. When we are trying to use the super key in a tap melody, we have the key down for a very short time, so having the hold timeout on 250ms lets us use it for chords more conveniently
[base] [[private]] leader-key = (tap-hold-next-release 250 (around-next (layer-toggle leader)) lmet) [[keys]] lmet = leader-key grave = (tap-hold $tap-hold-delay-min grave @simple-datetime-overlay) # (simple-datetime-overlay) lalt = (tap-hold-next-release $tap-hold-delay XX (around-next (layer-toggle leader-no-block)))
[qwerty] # we inherit from source before base so that we can add the `qwerty` on top of # other layers and overwrite other mappings parent = { source, base } [[private]] enable-homerow-mods = (layer-switch qwerty-homerow-mods) [[keys]] ScrollLock = enable-homerow-mods caps = 'lctrl'
[qwerty-homerow-mods] parent = base [[private]] disable-homerow-mods = (layer-switch qwerty)
We want this key to act as escape when tapped, and lctrl when held. However, while holding the key, we want to disable home row modifiers. We do this by adding the stock qwerty layer on top of the stack and holding lctl while holding the key. To do this,
lctrl-or-escape = (tap-hold-next-release 125 esc (around (layer-toggle qwerty) lctl)) lshift-or-caps-lock = (tap-hold-next-release 125 caps (around (layer-toggle qwerty) lshift)) rshift-or-caps-lock = (tap-hold-next-release 125 caps (around (layer-toggle qwerty) rshift))
This is a GACS
home-row-mods configuration detailed on this page.
k
is bound to lctl
rather than rctl
because rctl
is the compose key on my system.
a_homerow_chords = (tap-hold $hmod-delay a (layer-toggle home-row-chord)) q_homerow_movement = (tap-hold $hmod-delay q (layer-toggle home-row-movement)) backslash_layer = (tap-hold-next-release $hmod-delay \ (layer-toggle backslash)) s_lalt = (tap-hold-next-release $hmod-delay s lalt) d_lctrl = (tap-hold-next-release $hmod-delay d lctl) f_lshift = (tap-hold-next-release $hmod-delay f lshift) k_lctrl = (tap-hold-next-release $hmod-delay k lctl) j_rshift = (tap-hold-next-release $hmod-delay j rshift) l_ralt = (tap-hold-next-release $hmod-delay l ralt)
[[keys]] ScrollLock = disable-homerow-mods caps = lctrl-or-escape a = a_homerow_chords q = q_homerow_movement backslash = backslash_layer s = s_lalt d = d_lctrl f = f_lshift k = k_lctrl j = j_rshift l = l_ralt
We want to disable the homerow mods whenever we explicitly hit a modifier key.
lshift = (around (layer-toggle qwerty) @lshift-or-caps-lock) rshift = (around (layer-toggle qwerty) @rshift-or-caps-lock) lctrl = (around (layer-toggle qwerty) lctrl)
1.5. Base Leader Key Layer
[leader] parent = block [[keys]] q = window-switcher:activate # (window-switcher) d = discord # (discord) e = emacs # (emacs) f = firefox_composite # (firefox) b = brightness:enter # (brightness) v = volume:enter # (volume) t = 't' # (terminal) r = run:entrypoint # (run) p = 'p' # (prompt) a = agenda # (agenda) o = open-preset # (open-preset) c = org-capture # (org-capture) s = scroll:enter # (scroll) y = yank # (yank) tab = switch_focus_composite # (switch-focus) f1 = vim # (vim) backslash = 'backslash' # (local-leader) lmet = 'lmet' [leader-no-block] parent = { source, leader }
1.6. Window Focusing Layer
[window-focusing] [[public]] focus-left = (around lmet (around lalt Left)) focus-down = (around lmet (around lalt Down)) focus-up = (around lmet (around lalt Up)) focus-right = (around lmet (around lalt Right)) [[keys]] h = focus-left j = focus-down k = focus-up l = focus-right
1.7. Home Row Chord Layer
[home-row-chord] parent = { block, numeric-desktop-switching, window-focusing } [[private]] show_desktop_grid = (cmd-button "$kwin_shortcut_cmd \"ShowDesktopGrid\"") show_current_desktop_windows = (cmd-button "$kwin_shortcut_cmd \"Expose\"") [[keys]] i = jump-list:next # (jump-list) o = jump-list:prev p = jump-list:add backslash = (tap-hold-next-release $tap-hold-delay-min @show_current_desktop_windows @show_desktop_grid)
1.8. Numeric Desktop Switching Layer
This is a layer where the numeric keys are mapped to buttons that switch to that numbered desktop.
SWAP_MONITOR_WINDOWS_SCRIPT
~/.config/kmonad/windows/swap_monitor_windows.sh
MONITOR_MOVE_RELATIVE_SCRIPT
~/.config/kmonad/windows/monitor_move_relative.sh
[numeric-desktop-switching] [[private]] SWAP_MONITOR_WINDOWS_SCRIPT = "|>SWAP_MONITOR_WINDOWS_SCRIPT<|" MONITOR_MOVE_RELATIVE_SCRIPT = "|>MONITOR_MOVE_RELATIVE_SCRIPT<|" window_to_next_screen = (cmd-button "$kwin_shortcut_cmd \"Window to Next Screen\"") [[keys]]
Let’s generate this repetitive code with Python.
return '\n'.join( map(lambda n: f'{n} = (tap-hold $tap-hold-delay-min (cmd-button "$kwin_shortcut_cmd \\"Switch to Desktop {n}\\"") (cmd-button "$kwin_shortcut_cmd \\"Window to Desktop {n}\\""))', range(1, 10)))
1 = (tap-hold $tap-hold-delay-min (cmd-button "$kwin_shortcut_cmd \"Switch to Desktop 1\"") (cmd-button "$kwin_shortcut_cmd \"Window to Desktop 1\"")) 2 = (tap-hold $tap-hold-delay-min (cmd-button "$kwin_shortcut_cmd \"Switch to Desktop 2\"") (cmd-button "$kwin_shortcut_cmd \"Window to Desktop 2\"")) 3 = (tap-hold $tap-hold-delay-min (cmd-button "$kwin_shortcut_cmd \"Switch to Desktop 3\"") (cmd-button "$kwin_shortcut_cmd \"Window to Desktop 3\"")) 4 = (tap-hold $tap-hold-delay-min (cmd-button "$kwin_shortcut_cmd \"Switch to Desktop 4\"") (cmd-button "$kwin_shortcut_cmd \"Window to Desktop 4\"")) 5 = (tap-hold $tap-hold-delay-min (cmd-button "$kwin_shortcut_cmd \"Switch to Desktop 5\"") (cmd-button "$kwin_shortcut_cmd \"Window to Desktop 5\"")) 6 = (tap-hold $tap-hold-delay-min (cmd-button "$kwin_shortcut_cmd \"Switch to Desktop 6\"") (cmd-button "$kwin_shortcut_cmd \"Window to Desktop 6\"")) 7 = (tap-hold $tap-hold-delay-min (cmd-button "$kwin_shortcut_cmd \"Switch to Desktop 7\"") (cmd-button "$kwin_shortcut_cmd \"Window to Desktop 7\"")) 8 = (tap-hold $tap-hold-delay-min (cmd-button "$kwin_shortcut_cmd \"Switch to Desktop 8\"") (cmd-button "$kwin_shortcut_cmd \"Window to Desktop 8\"")) 9 = (tap-hold $tap-hold-delay-min (cmd-button "$kwin_shortcut_cmd \"Switch to Desktop 9\"") (cmd-button "$kwin_shortcut_cmd \"Window to Desktop 9\""))
# go to previous desktop semicolon = (tap-hold $tap-hold-delay-min (cmd-button "$kwin_shortcut_cmd \"Switch to Previous Desktop\"") (cmd-button "$MONITOR_MOVE_RELATIVE_SCRIPT -1")) # go to next desktop apostrophe = (tap-hold $tap-hold-delay-min (cmd-button "$kwin_shortcut_cmd \"Switch to Next Desktop\"") (cmd-button "$MONITOR_MOVE_RELATIVE_SCRIPT 1")) Minus = (cmd-button "$SWAP_MONITOR_WINDOWS_SCRIPT") # NOTE: this approach only works with 2 monitors Equal = (tap-hold $tap-hold-delay-min @window_to_next_screen #(@window_to_next_screen @switch_focus_screen))
1.8.1. Swap Monitors Script
#!/bin/dash # first, get the current window ID WINDOW_ID=$(xdotool getactivewindow) runShortcut() { qdbus org.kde.kglobalaccel /component/kwin invokeShortcut "$1" } # next, switch focus to the other monitor runShortcut "Switch to Next Screen" sleep 0.05 # now, move the window to the previous monitor runShortcut "Window to Next Screen" sleep 0.05 # finally, focus the original window and move it to the other monitor xdotool windowfocus "$WINDOW_ID" sleep 0.05 runShortcut "Window to Next Screen"
1.8.2. Monitor Relative Move Script
#!/bin/bash # pass in the relative change in desktop numbers; i.e 1, -1 relative_change=$1 runShortcut() { qdbus org.kde.kglobalaccel /component/kwin invokeShortcut "$1" } # first, let's get the current desktop wmctrl_output=$(wmctrl -d) desktop_regex="^([0-9]+)" active_desktop_regex="([0-9]+) \\*" # 1. find the current desktop if [[ $wmctrl_output =~ $active_desktop_regex ]] then # this is zero-based active_desktop="${BASH_REMATCH[1]}" last_desktop_line=$(echo "$wmctrl_output" | tail -n 1) # let's take that line and extract the number from it if [[ $last_desktop_line =~ $desktop_regex ]] then # this is zero-based highest_desktop="${BASH_REMATCH[1]}" num_desktops=$((highest_desktop + 1)) new_desktop=$(((active_desktop + relative_change) % num_desktops)) if [[ $new_desktop -lt 0 ]] then new_desktop=$((new_desktop + num_desktops)) fi # take it from zero-based to one-based new_desktop=$((new_desktop + 1)) # finally, move the window runShortcut "Window to Desktop $new_desktop" fi fi
1.9. Alphabetic Window Tiling Layer
[alphabetic-window-tiling] [[keys]] u = (cmd-button "$kwin_shortcut_cmd 'Window Quick Tile Top Left'") i = (cmd-button "$kwin_shortcut_cmd 'Window Quick Tile Top'") o = (cmd-button "$kwin_shortcut_cmd 'Window Quick Tile Top Right'") j = (cmd-button "$kwin_shortcut_cmd 'Window Quick Tile Left'") k = (cmd-button "$kwin_shortcut_cmd 'Window Maximize'") l = (cmd-button "$kwin_shortcut_cmd 'Window Quick Tile Right'") m = (cmd-button "$kwin_shortcut_cmd 'Window Quick Tile Bottom Left'") comma = (cmd-button "$kwin_shortcut_cmd 'Window Quick Tile Bottom'") dot = (cmd-button "$kwin_shortcut_cmd 'Window Quick Tile Bottom Right'")
1.10. Backslash Layer
This used to be the “Desktop/Window” layer, but after adding leader key functionality, the backslash key now constitutes its own dedicated layer name.
[backslash] parent = { numeric-desktop-switching, alphabetic-window-tiling, leader } [[keys]] RightBrace = window-focusing:focus-right LeftBrace = window-focusing:focus-left
1.11. Home Row Movement Layer
[home-row-movement] [[keys]] h = 'Left' j = 'Down' k = 'Up' l = 'Right' b = 'PageUp' f = 'PageDown' e = (around lctl Right) w = (around lctl Left) semicolon = 'Home' apostrophe = 'End'
1.12. Program Paths
[aliases] ROFI = "|>ROFI<|" DISCORD = "|>DISCORD<|" EMACS = "|>EMACS<|" EMACSCLIENT = "|>EMACSCLIENT<|"
1.13. Jump List
The idea of the Jump List is to emulate Vim behavior and allow for switching between different positions. In a windowing system context, each entry in the Jump List is an active window (+ Desktop). Vim has a ton of operations which modify the Jump List, but for our purposes, we will have two main modes of modification:
- 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
/home/sridaran/.config/kmonad/jump-list/jump_list.pl
We move the command to the global scope so we can use it within other layers.
[aliases] jump-list-mark = "|>JUMP_LIST_SCRIPT<| add-shift"
[jump-list] [[private]] script = "|>JUMP_LIST_SCRIPT<|" [[public]] # adds current window as entry to the jump list add = (cmd-button "$script add-replace") # jumps to the next entry in the jump list next = (cmd-button "$script next") # jumps to the previous entry in the jump list prev = (cmd-button "$script prev")
1.13.1. Underlying Implementation
To make the underlying implementation work, we need to maintain a stack of jump list entries, where each entry contains a Window ID as well as a desktop number + monitor so that if the window is closed, we can still go back to it. Actually, it may make sense to simply skip an entry if the window has been closed.
- When using
prev
from a monitor which is different from the active jump list entry (this means that you manually moved away), we can either go back to the active entry OR ignore the current monitor and simply go back an entry- Similarly, when using
next
from a monitor which is different from the active jump list entry, we can either go back to the active entry OR ignore the current monitor and simply go back an entry - I think we will have to see what is more useful in practice, but I think to begin with it’ll be good to have it go back to the active entry in the
prev
scenario- If it gets in the way, we’ll get rid of it
- 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($_); } }
1.14. Window Switcher
Opens the Window Switcher
[window-switcher] [[private]] rofi-args = "-noplugins -lines 5 --normal-window -sort -sorting-method fzf -monitor -4 -matching fuzzy -modi window -show window" # note that in Posix SH, & acts as a separator between commands, and a semicolon # after it is invalid syntax. mark-and-jump-command = "$jump-list-mark & sleep 0.15; wmctrl -ia {window}" jump-to-window = (cmd-button "$ROFI $rofi-args -window-command \"/bin/dash -c '$mark-and-jump-command'\" -kb-accept-entry '' -kb-accept-alt 'Return'") pull-window = (cmd-button "$ROFI $rofi-args -window-command 'wmctrl -iR {window}' -kb-accept-entry '' -kb-accept-alt 'Return'") [[public]] activate = (tap-hold $tap-hold-delay-min @jump-to-window @pull-window)
I made this into a layer purely for organizational purposes.
When tapped, activate
will yield a Rofi window switcher which will switch to the selected window.
When held, activate
will yield a Rofi window switcher which will move the selected window to the current desktop before selecting it.
I compiled rofi
from source and put it in ~/.local/bin
because the RPM version was too slow for my taste.
Some of the flags are also there for optimization reasons: -modi
, -noplugins
and --normal-window
.
I noticed that the startup animation was faster with --normal-window
, and the other 2 flags stop rofi
from doing unnecessary work.
-matching fuzzy
makes it use fuzzy matching instead of only matching the raw string.
-sort
and -sorting-method fzf
make the selections a lot more intelligent.
-monitor -4
makes it open rofi
on the monitor of the currently focused window.
1.15. Discord
This command uses wmctrl
to switch to a currently-existing Discord window, and if it fails opens a new instance of Discord.
[aliases] DISCORD_SCRIPT = "~/.config/kmonad/discord/discord.sh" discord = (cmd-button "$DISCORD_SCRIPT")
1.15.1. TODO Switch back to the previous window when invoked a second time
1.16. Emacs
Opens Emacs: emacsclient
on tap, emacs
process on hold.
[aliases] emacs = (tap-hold $tap-hold-delay-min (cmd-button "$EMACSCLIENT --create-frame") (cmd-button "$EMACS"))
1.17. Firefox
Opens a new Firefox window
FIREFOX_TAB_SWITCHER_SCRIPT
~/.config/kmonad/firefox/firefox_tab_switcher.sh
[aliases] FIREFOX_TAB_SWITCHER_SCRIPT = "|>FIREFOX_TAB_SWITCHER_SCRIPT<|" open_firefox = (cmd-button "firefox") select_firefox_tab = (cmd-button "$FIREFOX_TAB_SWITCHER_SCRIPT") firefox_composite = (tap-hold 135 @open_firefox @select_firefox_tab)
1.17.1. Firefox Tab Switcher
#!/bin/sh IFS=" " tabs=$(|>BROTAB<| list) echo "$tabs" > /tmp/kmonadtabs selected=$(echo "$tabs" | awk -F '\t' '{print $2}' | rofi -noplugins -dmenu -i -lines 8 --normal-window -matching fuzzy -format 'd') selection=$(echo "$tabs" | tail -n "+$selected" | head -n 1) activation="$(echo "$selection" | awk -F '\t' '{print $1}')" |>BROTAB<| activate "$activation" title="$(echo "$selection" | awk -F '\t' '{print $2}')" wmctrl -a "$title"
1.18. Brightness
CHANGE_BRIGHTNESS_SCRIPT
~/.config/kmonad/brightness/change_brightness.sh
- CHANGE_BACKLIGHT_SCRIPT
~/.config/kmonad/brightness/change_backlight.sh
QUEUE_DIGIT_SCRIPT
~/.config/kmonad/brightness/queue_digit.sh
DIGIT_QUEUE_FILE
/tmp/kmonad_digit_queue
LAST_BRIGHTNESS_CHANGE_FILE
/tmp/kmonad_last_brightness_change
[brightness] [[private]] QUEUE_DIGIT_SCRIPT = "|>QUEUE_DIGIT_SCRIPT<|" CHANGE_BRIGHTNESS_SCRIPT = "|>CHANGE_BRIGHTNESS_SCRIPT<|" CHANGE_BACKLIGHT_SCRIPT = "|>CHANGE_BACKLIGHT_SCRIPT<|" exit_internal = (layer-rem brightness) # the definition for `exit` is generated by our preprocessing script [[public]] enter = (tap-hold-next-release $tap-hold-delay #((layer-add brightness) @kbd-set-backlight-on) (layer-toggle brightness)) up = (tap-hold-next-release $tap-hold-delay-min (cmd-button "$CHANGE_BRIGHTNESS_SCRIPT +") (cmd-button "$CHANGE_BACKLIGHT_SCRIPT +")) down = (tap-hold-next-release $tap-hold-delay-min (cmd-button "$CHANGE_BRIGHTNESS_SCRIPT -") (cmd-button "$CHANGE_BACKLIGHT_SCRIPT -")) toggle_nightlight = (cmd-button "$CHANGE_BRIGHTNESS_SCRIPT '*'") [[keys]]
This is repetitive, so let’s abstract it away by generating the code with Python.
return '\n'.join(map(lambda n: f'{n} = (cmd-button "$QUEUE_DIGIT_SCRIPT {n}")', range(0, 10)))
0 = (cmd-button "$QUEUE_DIGIT_SCRIPT 0") 1 = (cmd-button "$QUEUE_DIGIT_SCRIPT 1") 2 = (cmd-button "$QUEUE_DIGIT_SCRIPT 2") 3 = (cmd-button "$QUEUE_DIGIT_SCRIPT 3") 4 = (cmd-button "$QUEUE_DIGIT_SCRIPT 4") 5 = (cmd-button "$QUEUE_DIGIT_SCRIPT 5") 6 = (cmd-button "$QUEUE_DIGIT_SCRIPT 6") 7 = (cmd-button "$QUEUE_DIGIT_SCRIPT 7") 8 = (cmd-button "$QUEUE_DIGIT_SCRIPT 8") 9 = (cmd-button "$QUEUE_DIGIT_SCRIPT 9")
k = up j = down h = toggle_nightlight # q displays brightness on each monitor tab = switch_focus_composite lmet = exit
1.18.1. Queue Digit Script
This script takes a digit and appends it to the queue of currently waiting digits. The change brightness script consumes the queue as a single integer.
Using dash
shell for speed
#!/bin/dash FILE="|>DIGIT_QUEUE_FILE<|"
Verify that the argument is a number by using case
and globbing.
See https://stackoverflow.com/questions/806906/how-do-i-test-if-a-variable-is-a-number-in-bash/806923(this) StackOverflow post.
DIGIT=$1 case $DIGIT in '' | *[!0-9]*) echo "Need to pass in a number!" >/dev/stderr; exit 1;; *) ;; esac
Next, read the current file contents, prepend it to DIGIT
, and then write it back.
# read file if [ -e "$FILE" ]; then CURRENT_INT=$(cat "$FILE") fi NEW_INT="$CURRENT_INT$DIGIT" # also print it to stdout; helpful for debugging echo "$NEW_INT" | tee "$FILE"
1.18.2. Change Brightness Script
#!/bin/dash DIGIT_FILE="|>DIGIT_QUEUE_FILE<|" LAST_BRIGHTNESS_CHANGE_FILE="|>LAST_BRIGHTNESS_CHANGE_FILE<|" DIRECTION=$1
Depending on DIRECTION
, set SIGN
to the sign. There’s a special case for .
; with .
, SIGN
becomes zero and triggers special behavior further on.
case $DIRECTION in '+') SIGN=1 ;; '-') SIGN=-1 ;; '.') ;; '*') ;; *) echo "Invalid direction" >/dev/stderr exit 1 ;; esac
We preset CHANGE
so that any code path which never sets CHANGE
will use the value of 7
.
CHANGE=7
In the normal case, check if there are queued digits, and if there aren’t then default to 7
.
After reading the saved digits, clear the file’s contents.
if [ "$DIRECTION" != '.' ] && [ "$DIRECTION" != '*' ]; then QUEUED_DIGITS=$(cat "$DIGIT_FILE" 2>/dev/null) if [ -n "$QUEUED_DIGITS" ]; then if [ "$QUEUED_DIGITS" -ge 100 ]; then QUEUED_DIGITS=100 fi echo "" >"$DIGIT_FILE" CHANGE=$QUEUED_DIGITS fi
To get the final value for CHANGE
, multiply SIGN
by its current value.
Then, write the new value to LAST_BRIGHTNESS_CHANGE_FILE
.
CHANGE=$(echo "$SIGN * $CHANGE" | bc) echo "$CHANGE" >"$LAST_BRIGHTNESS_CHANGE_FILE"
If DIRECTION
is .
, then read CHANGE
directly from LAST_BRIGHTNESS_CHANGE_FILE
. If it doesn’t exist, then fail.
else if [ "$DIRECTION" = "." ]; then if [ -e "$LAST_BRIGHTNESS_CHANGE_FILE" ]; then CHANGE=$(cat "$LAST_BRIGHTNESS_CHANGE_FILE") else echo "Last brightness change file does not yet exist!" >/dev/stderr exit 1 fi
Otherwise, it is *
, which means that we want to toggle the nightlight.
In this case, we call a different script for toggling the nightlight on the actively focused monitor.
We exit the script afterwards so that we don’t end up calling the standard changeBrightness
script next.
else /home/sridaran/Development/Scripts/DE/toggleNightlight.sh exit 0 fi fi
Finally, pass CHANGE
to our main changeBrightness
script (not shown), which changes the brightness on the actively focused monitor.
/home/sridaran/Development/Scripts/DE/changeBrightness.sh "$CHANGE" -n
1.18.3. Change Backlight Script
#!/bin/dash case $1 in "+") DIR=+ ;; "-") DIR=- ;; *) echo "Please specify + or - as direction!" > /dev/stderr exit 1 ;; esac current_brightness=$(qdbus org.kde.Solid.PowerManagement /org/kde/Solid/PowerManagement/Actions/BrightnessControl org.kde.Solid.PowerManagement.Actions.BrightnessControl.brightness) # source: https://userbase.kde.org/KDE_Connect/Tutorials/Useful_commands#Brightness_settings new_brightness=$(expr $current_brightness $DIR 375) # for some reason, the setBrightness interface allows you to go out of bounds, # so we clamp it manually. if [ $new_brightness -lt 1 ]; then new_brightness=1 elif [ $new_brightness -gt 7500 ]; then new_brightness=7500 fi qdbus org.kde.Solid.PowerManagement /org/kde/Solid/PowerManagement/Actions/BrightnessControl org.kde.Solid.PowerManagement.Actions.BrightnessControl.setBrightness $new_brightness
1.19. TODO Terminal
1.20. Run
[run] [[private]] runner_script = "~/.config/kmonad/runner/runner.sh" toggle = (cmd-button "$runner_script toggle") dwim = (cmd-button "$runner_script dwim") [[public]] entrypoint = (tap-hold-next-release $tap-hold-delay @dwim (layer-toggle run)) [[keys]] t = toggle
1.20.1. Runner Script
Using the bash
shell so we can use arithmetic.
#!/bin/bash
Let’s first process our command-line argument to determine what to do.
If we want to focus
, then let’s attempt to activate a window containing the st-runner
class, and if that fails, then proceed to the rest of the code, which will create a new runner instance!
If we want to dwim
(do-what-I-mean), then IF the runner is currently focused, close the window, and otherwise, focus
it.
This is usually what we want, since it doesn’t make sense to try to re-focus the window if it’s already focused!
On the other hand, if we want to toggle
, then let’s first attempt to close a window containing the st-runner
class, and if that fails, then we proceed to the code for making a new instance!
processArg() { case $1 in focus) # when focusing, spawn new process if not open, and focus existing process if open if wmctrl -x -a st-runner; then # successfully focused! exit 0 fi ;; toggle) # when toggling, spawn new process if not open, and KILL existing process if open if wmctrl -x -c st-runner; then # successfully closed! exit 0 fi ;; dwim) focused_window=$(xdotool getwindowfocus) # if focused window is the runner, exit code will be zero if xprop -notype -id "$focused_window" WM_CLASS | grep -q "st-runner"; then # close window wmctrl -ic "$focused_window" exit 0 else # https://stackoverflow.com/questions/4827690/how-to-change-a-command-line-argument-in-bash processArg "focus" fi ;; *) echo "Invalid argument: please pass in focus|toggle|dwim" >&2 exit 1 esac } processArg "$1" # If control reaches past this point without exiting, that means we have to make # a new instance of the runner.
Setting environment variables for the fish
process to inherit.
SKIP_FISH_GREETING
- This is a custom variable that determines whether a message should display on startup. I set it to 1 because I do not want it to output for the runner.
STARSHIP_CONFIG
- Starship is the shell prompt I am using.
export SKIP_FISH_GREETING=1 export STARSHIP_CONFIG=|>STARSHIP_RUNNER_CONFIG<|
Sets the working directory back to home
cd
Now, let’s determine the geometry for our terminal window. First, we want the terminal to be placed on the active directory.
FOCUSED_WINDOW=$(xdotool getwindowfocus) eval $(xdotool getwindowgeometry --shell "$FOCUSED_WINDOW") eval $(monitor-utils --shell --at-point $X $Y --geometry)
This will give us the geometry of the monitor we have focused. Now, we want to have 10% padding on the top, and have the terminal be centered in the middle of the screen. From experimentation, 116 terminal columns translates to about 1660 pixels.
TERMINAL_WIDTH=1660 y_padding=$((HEIGHT / 10)) x_margin=$(((WIDTH - TERMINAL_WIDTH) / 2)) terminal_x_offset=$((x_margin + X_OFFSET)) terminal_y_offset=$((y_padding + Y_OFFSET))
|>ST<|
st
is the terminal emulator-c "..."
- This sets the X11 classnames for the window.
My KDE config contains window rules that rounds the corners of windows with the
rounded
class and gives transparency and several other properties to thest-runner
class -g ...
This sets the initial window dimensions for the terminal window.
The format we are using is
<width>x<height>+<xoffset>+<yoffset>
. I believe everything is in terms of characters, so the width represents 100 characters, and the height represents 8 lines of space. The offset, however, appears to be in pixels.See this link for more details
Using screen
to maintain a single shell session through each runner
invocation.
-DR runner
- Attaches to a session called
runner
, creating it if necessary. Some ofscreen
’s flag combinations seem a little arbitrary. -s /bin/fish
- Tells
screen
to start new sessions with thefish
shell.
|>ST<| -m 15 -c "rounded st-runner" -g "116x8+${terminal_x_offset}+${terminal_y_offset}" screen -c ~/.config/kmonad/runner/screenrc -DR runner
After st
closes, we scroll down our runner so we no longer see the commands/output from earlier.
We do this by telling screen to send Control+L
keystrokes to the runner
session’s first pane.
Since it’s the same shell, we will still be in the same working directory and have the same history as before.
screen -S runner -X stuff ""
term screen-256color
- This line fixes the colors in the
screen
window. Before, I was getting a lot of text that wasn’t being highlighted.
term screen-256color msgwait 0 shell /home/sridaran/.config/kmonad/runner/run_fish.sh
#!/bin/dash exec fish --init-command="source $HOME/.config/kmonad/runner/config.fish"
1.20.2. Autoresizing on commands
I want my terminal to go full-height for certain commands, like fzf
.
To do this, I will wrap the commands using functions which will emit a control code to the terminal emulator, telling it to resize.
function __fullsize_terminal -d "Emits a control code which causes the runner terminal to resize" # tell terminal to resize; # source: https://unix.stackexchange.com/questions/575337/using-terminal-escape-sequences-within-gnu-screen echo -e '\eP\005\e\\' > /dev/tty end function __wrap_fullsize -d "Given a command, wraps it into a function with the same name, which will resize the terminal before running the command" set WRAPPED_COMMAND $argv[1] function $argv[1] -V WRAPPED_COMMAND -d "Runs $1 after resizing the terminal" # get path of the wrapped command set command_path (which $WRAPPED_COMMAND) __fullsize_terminal # run command with the args $command_path $argv end end __wrap_fullsize fzf
1.20.3. TODO Cleanup exit command
Right now, we are doing killall st
, which only does what we want because we do not use st
for anything else.
We should aim for a more robust solution.
1.20.4. DONE Reuse the runner
terminal and shell between invocations
1.21. TODO Prompt
1.22. Agenda
[aliases] agenda = (cmd-button "$EMACSCLIENT -ce '(org-agenda nil \"o\")'")
1.22.1. TODO Open a floating, semi-transparent window
Instead of a fullscreen, opaque window.
1.22.2. DONE Maybe switch to org-agenda
1.23. Open Preset
OPEN_PRESET_SCRIPT
~/Development/Scripts/DE/presets/rofi_menu.sh
[aliases] OPEN_PRESET_SCRIPT = "~/Development/Scripts/DE/presets/rofi_menu.sh" open-preset = (cmd-button "$OPEN_PRESET_SCRIPT")
1.24. Org Capture
[aliases] org-capture = (cmd-button "~/.local/bin/org-capture")
1.25. Scroll
We set the scroll buttons to invoke the scroll.sh
script once on press and once on release.
On release, the script will kill the instance created on press
SCROLL_SCRIPT
~/.config/kmonad/scroll/scroll.sh
SCROLL_SPEED_SCRIPT
~/.config/kmonad/scroll/scroll_speed.sh
[scroll] [[private]] scroll_script = "|>SCROLL_SCRIPT<|" speed_script = "|>SCROLL_SPEED_SCRIPT<|" left = (cmd-button "$scroll_script h -" "$scroll_script h 0") up = (cmd-button "$scroll_script v -" "$scroll_script v 0") down = (cmd-button "$scroll_script v +" "$scroll_script v 0") right = (cmd-button "$scroll_script h +" "$scroll_script h 0") speed-up = (cmd-button "$speed_script 50" "$speed_script 0") speed-down = (cmd-button "$speed_script 200" "$speed_script 0") exit_internal = (layer-rem scroll) [[public]] enter = (tap-hold-next-release $tap-hold-delay #((layer-add scroll) @kbd-set-backlight-on) (layer-toggle scroll)) [[keys]] h = left l = right k = up j = down caps = speed-down lctrl = speed-down lshift = speed-up lmet = exit
1.25.1. Scroll Script
These are the files storing the scroll variables.
SCROLL_SPEED_FILE
/tmp/kmonad_scroll_script_speed
SCROLL_SPEED_FILE_OLD
/tmp/kmonad_scroll_script_speed_old
SCROLL_HORIZONTAL
/tmp/kmonad_scroll_script_horizontal
SCROLL_VERTICAL
/tmp/kmonad_scroll_script_vertical
Again using dash for speed
#!/bin/dash
DIRECTION
- Either
h
for “horizontal” 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
1.25.2. Scroll Speed Script
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<|)
1.25.3. Original Approach
This was my original idea, but I am now implementing scrolling through a shell script
We are using keys F13-F16 to represent scrolling. We need to do this because KMonad does not support sending mouse events. Because these keys are not used for anything else (they aren’t actually on the keyboard), we can safely remap them to buttons using
xmodmap
, which does support mouse buttons.
This was in my ~/.Xmodmap
keycode 191 = Left keycode 192 = Pointer_Button5 keycode 193 = Pointer_Button4 keycode 194 = Right
First of all, xmodmap
did not let me bind 191
and 194
to Pointer_Button6
and Pointer_Button7
(pushing the scroll wheel left/right), saying that it did not recognize either keysym.
As a workaround, I tried setting these to the arrow keys.
The horizontal arrow keys worked, but the up/down scrolling did not.
In most applications, the up/down arrow keys do selection in addition to scrolling, so binding j
and k
to arrow keys was not an acceptable solution
Scrolling works by repeatedly “clicking” the scroll buttons. Whenever you scroll a scrollbar on your mouse, the speed at which you scroll determines how fast the scrolling occurs on your screen.
I believe the reason the vertical scrolling was not working is because KMonad was repeating the keypresses too quickly. Because of this, it probably did not register as scrolling and was simply ignored.
1.26. Volume
VOLUME_SCRIPT
~/.config/kmonad/volume/volume.sh
VOLUME_TOGGLE_OSD_SCRIPT
~/.config/kmonad/volume/volume_popup_toggle.sh
VOLUME_SCRIPT_OSD_FILE
Stores whether to show/hide volume
osd
popups/tmp/kmonad_volume_script_display_osd
[volume] [[private]] volume_script = "|>VOLUME_SCRIPT<|" toggle_osd_script = "|>VOLUME_TOGGLE_OSD_SCRIPT<|" up = (cmd-button "$volume_script +" "$volume_script 0") down = (cmd-button "$volume_script -" "$volume_script 0") toggle-osd = (cmd-button "$toggle_osd_script") mute = (cmd-button "qdbus org.kde.kglobalaccel /component/kmix invokeShortcut mute") play-pause = 'PlayPause' exit_internal = (layer-rem volume) [[public]] enter = (tap-hold-next-release $tap-hold-delay #((layer-add volume) @kbd-set-backlight-on) (layer-toggle volume)) [[keys]] k = up j = down m = mute q = toggle-osd p = play-pause lmet = exit
1.26.1. Volume Script
VOLUME_SCRIPT_PID_FILE
/tmp/kmonad_volume_script
VOLUME_HELPER_SCRIPT
~/.config/kmonad/volume/change_volume.py
Similar to the 1.25.1, this script will modulate a parameter at a given rate, writing its own PID into a file so that it can be killed when a key is released
VOLUME_CHANGE_DIRECTION
- Either
+
to increase volume,-
to decrease it 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 1.26.1.1.
In this code, the python2
process inherits the PID of the shell since we are using exec
exec python2 -ES |>VOLUME_HELPER_SCRIPT<| $CURRENT_VOLUME $VOLUME_CHANGE_DIRECTION $DISPLAY_OSD fi
- Volume Helper Script
The reason I wrote this section in
Luapython2
is because it requires a loop to run with a subsecond delay. If this were written as part of the shell script, we would be calling out to/bin/sleep
tens of times per second, and the interval could become visibly inconsistent.volume
- An integer representing the starting volume percentage
increment
+
to increase volume,-
to decrease it 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 1.26.2, invert the value ofdisplay_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 bystring.format
, thinkingstring
was a module, but in realityformat
is a method defined on thestring
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 usingsubprocess
inpython2
, since the stock version ofsubprocess
that ships with it has several issues.
1.26.2. Volume OSD Toggle Script
This script switches the contents of $DISPLAY_OSD_FILE
between 0 and 1, setting the value to 0 if the file does not exist.
sed
- Stream editor
-i "$DISPLAY_OSD_FILE"
- Modifies the file in-place, so we don’t need to open the file once for reading and again for writing.
'y/01/10'
From the
sed
man page for 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
1.26.3. DONE Volume layer
The volume layer would remap hjkl to control the volume.
1.26.4. TODO Volume Next/Prev
Rotate to next/previous output with h/l
1.27. Yank
- Yank Script
~/.config/kmonad/yank/yank_active_window.sh
Copies the actively focused window title to the clipboard.
[aliases] yank_script = "|>YANK_SCRIPT<|" yank = (cmd-button "$yank_script")
1.27.1. Yank Script
This script copies the title to the clipboard, and also emits a notification to the screen.
#!/bin/dash window_title=$(xdotool getactivewindow getwindowname) # copy to clipboard echo "$window_title" | xclip -selection c -r # send notification qdbus org.kde.plasmashell /org/kde/osdService org.kde.osdService.showText "document-duplicate" "$window_title"
1.28. Switch Focus
SWITCH_MOUSE_SCREEN_SCRIPT
/home/sridaran/Development/Scripts/DE/mouseToNextDesktop.sh
[aliases] SWITCH_MOUSE_SCREEN_SCRIPT = "|>SWITCH_MOUSE_SCREEN_SCRIPT<|" switch_mouse_screen = (cmd-button "$SWITCH_MOUSE_SCREEN_SCRIPT") switch_focus_screen = (cmd-button "qdbus org.kde.kglobalaccel /component/kwin invokeShortcut \"Switch to Next Screen\"") switch_focus_composite = (tap-hold $tap-hold-delay-min @switch_focus_screen @switch_mouse_screen)
1.29. Vim
NVIM
/home/sridaran/Packages/neovim/nvim0-6-0.appimage
NVIM_SCRIPT
/home/sridaran/.config/kmonad/vim/run_neovim.sh
[aliases] NVIM_SCRIPT = "|>NVIM_SCRIPT<|" vim = (cmd-button "kitty fish -C \"$NVIM_SCRIPT\"")
1.29.1. Run Neovim Script
#!/bin/dash ELAPSED_TIME=$(/bin/time -f '%E' |>NEOVIM<|) zenity --text "Ran for $ELAPSED_TIME" --notification
1.30. Simple Datetime Overlay
- Simple Datetime Overlay Path
/home/sridaran/.local/bin/simple-datetime-overlay
This is a simple button that spawns my program and then kills all instances of it.
[aliases] SIMPLE_DATETIME_OVERLAY = "|>SDO_SCRIPT_PATH<|" simple-datetime-overlay = (cmd-button "/bin/dash -c '$SIMPLE_DATETIME_OVERLAY'" "sleep 0.15; kill \$(pgrep -f simple-datetime-overlay)")
Here are my issues with simple-datetime-overlay
:
- 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.
1.30.1. Invoke Simple Datetime Overlay Script
- SDO Script Path
/home/sridaran/.config/kmonad/simple-datetime-overlay/simple-datetime-overlay.sh
- CPUFreq Active Profile Path
/home/sridaran/.cache/set-cpufreq-profile/active-profile
This script checks what my current cpu profile is, and if it is on max performance, then it displays the datetime overlay on all monitors. Otherwise, it displays it only on the active monitor.
On medium performance mode and below, use the tock
command-line program to render a clock in the st
terminal.
#!/bin/dash CURRENT_CPUFREQ_PROFILE=$(cat "|>CPUFREQ_ACTIVE_PROFILE<|") PROGRAM=|>SIMPLE_DATETIME_OVERLAY<| PARAMS="--only-monitor 0" case "$CURRENT_CPUFREQ_PROFILE" in "Max Performance") PARAMS="" ;; "High Performance") PARAMS="-a" ;; *) PROGRAM=st PARAMS="-c simple-datetime-overlay-tock -g 95x15 -t 'simple-datetime-overlay' -- /home/sridaran/.cargo/bin/tock --seconds --center --format '%A, %B %d, %Y'" ;; esac # source: https://superuser.com/questions/1529226/get-bash-to-respect-quotes-when-word-splitting-subshell-output echo $PARAMS | xargs $PROGRAM
1.31. Buffer Procedures
1.31.1. Tangle and Compile
(progn (org-babel-tangle) (let* ((error-bufname "KMonadX Compilation Output") (display-buffer-alist '((".*" display-buffer-at-bottom)))) (progn (get-buffer-create error-bufname) (with-current-buffer error-bufname (erase-buffer)) (call-process "fish" nil (get-buffer error-bufname) nil "-c ./compile.sh") (if (not (eq (buffer-size (get-buffer error-bufname)) 0)) (progn (display-buffer (get-buffer error-bufname) nil) (switch-to-buffer-other-window error-bufname) (ansi-color-apply-on-region (point-min) (point-max))) (progn (message "%s" "Compilation completed successfully!") (when (y-or-n-p "Restart KMonad?") (srithon/spawn-process "systemctl" "--user" "restart" "kmonad")))))))
1.32. TODO Local Leader
2. KMonad Documentation
Documentation was copied from tutorial.kbd and adapted for Org mode
2.1. Basic Syntax
KMonad’s configuration language is styled on various lisps, like scheme or Common Lisp. In a lisp, every statement is entered between ’(’ and ’)’s. If you are more used to Fortan style languages (python, ruby, C, Java, etc.), the change is quite straightforward: the function name moves into the parentheses, and you don’t use commas to separate arguments. I.e.
This: my_function(a, 3, “Alakazam”) Becomes: (my_function a 3 “Alakazam”)
The reason for this is because Lisp-style languages are very easy to parse and write syntax-highlighters for.
We also provide standard Lisp syntax for comments:
- block comments between: #| and its reverse
line comments following: ;;
Unlike standard lisp, a single ; does not denote a command, but instead the keycode for semicolon.
Also, as you might have noticed, whitespace is possible anywhere.
To check for syntax errors while editing, invoke kmonad with the -d option.
2.2. defcfg block
Necessary: the `defcfg` block
There are a few bits of information that are required to be present in a KMonad configuration file. One of these is the existence of exactly 1 `defcfg` statement. This statement is used to customize various configuration settings. Many of these settings have default values, but a minimal definition must include at least an ’input’ field and an ’output’ field. These describe how KMonad captures its inputs and how it emits its outputs.
First, let’s go over the optional, non-OS specific settings. Currently there is only 2:
fallthrough: `true` or `false`, defaults to `false`
KMonad catches input events and tries to match them to various handlers. If it cannot match an event to any handler (for example, if it isn’t included in the `defsrc` block, or if it is, but the current keymap does not map any buttons to it), then the event gets quietly ignored. If `fallthrough` is set to `true`, any unhandled events simply get reemitted.
allow-cmd: `true` or `false`, defaults to `false`
If this is set to `false`, any action that runs a shell-command will simply log to `stdout` without ever running (log-level info). Don’t ever enable this on a configuration that you do not trust, because:
(cmd-button “rm -rf ~/*”)
is a thing. For more information on the `cmd-button’ function, see the section on Command buttons below.
There are also some optional OS specific settings that we support:
`cmp-seq’: KEY, defaults to `RightAlt’ (Linux X11 specific)
This sets your compose key for Unicode input. For more information, as well as a workaround to also make this work on windows, see the section on Compose-key sequences below.
`cmp-seq-delay’: NUMBER (in milliseconds)
This sets a delay between each pressed key in a compose-key sequence. Some environments may have troubles recognizing the key sequence if it’s pressed too rapidly; if you experience any problems in this direction, you can try setting this value to `5’ or `10’ and see if that helps.
Secondly, let’s go over how to specify the `input` and `output` fields of a `defcfg` block. This differs between OS’es, and so do the capabilities of these interfaces.
- Linux
In Linux we deal with input by performing an ioctl-grab on a specific device-file. This allows us to hook KMonad on the input of exactly 1 keyboard, and allows you to run multiple instances of KMonad for different keyboards. We make an input using: (device-file “/dev/input/by-id/my-keyboard-kbd”)
NOTE: Any valid path to a device-file will work, but it is recommended to use the ’by-id’ directory, since these names will not change if you replug the device.
We deal with output by creating a ’uinput’ device. This requires that the ’uinput’ kernel module is loaded. The easiest way to ensure this is by calling ’sudo modprobe uinput’. We create a uinput device using: (uinput-sink “name” “optional post-init command”)
- Windows
In Windows we do not get such fine-grained control. We use a low-level keyboard hook to intercept all non-injected keyboard events. There is currently an open issue to improve the C-bindings used to capture windows keyevents, and if you have a better way to approach this issue, help is deeply appreciated. You specify a windows input using: (low-level-hook)
Similarly, the output in Windows lacks the fine-grained control. We use the SendEvent API to emit key events directly to Windows. Since these are ’artificial’ events we won’t end up catching them again by the `low-level-hook`. It is very likely that KMonad does not play well with other programs that capture keyboard input like AHK. You specify windows output using: (send-event-sink)
- MacOS
For Mac questions I suggest filing an issue and tagging @thoelze1, he wrote the MacOS API. However, input using: (iokit-name “optional product string”)
By default this should grab all keyboards, however if a product string is provided, KMonad will only capture those devices that match the provided product string. If you would like to provide a product string, you can run `make; ./list-keyboards’ in c_src/mac to list the product strings of all connected keyboards.
You initialize output on MacOS using: (kext)
2.3. defsrc block
Necessary: the `defsrc` block
It is difficult to explain the `defsrc` block without immediately going into `deflayer` blocks as well. Essentially, KMonad maps input-events to various internal actions, many of which generate output events. The `defsrc` block explains the layout on which we specify our `deflayer`s down the line.
It is important to realize that the `defsrc` block doesn’t necessarily have to coincide with your actual input keyboard. You can specify a full 100% `defsrc` block, but only use a 40% keyboard. This will mean that every `deflayer` you specify will also have to match your 100% `defsrc`, and that your actual keyboard would be physically unable to trigger about 60% of your keymap, but it would be perfectly valid syntax.
The dual of this (and more useful) is that it is also perfectly valid to only specify that part of your keyboard in `defsrc` that you want to remap. If you use a 100% keyboard, but don’t want to remap the numpad at all you can simply leave the numpad out of your `defsrc`, and it should work just fine. In that particular case you probably want to set `fallthrough` to `true` in your `defcfg` block though.
In the future we would like to provide support for multiple, named `defsrc` blocks, so that it becomes easier to specify various layers for just the numpad, for example, but at the moment any more or less than 1 `defsrc` block will result in an error.
The layouting in the `defsrc` block is completely free, whitespace simply gets ignored. We strive to provide a name for every keycode that is no longer than 4 characters, so we find that laying out your keymap in columns of 5 works out quite nicely (although wider columns will allow for more informative aliases, see below).
Most keycodes should be obvious. If you are unsure, check ’./src/KMonad/Keyboard/Keycode.hs’. Every Keycode has a name corresponding to its Keycode name, but all lower-case and with the ’Key’ prefix removed. There are also various aliases for Keycodes starting around line 350. If you are trying to bind a key and there is not a 4-letter alias, please file an issue, or better yet, a pull-request, and it will be added promptly.
Also, you can consult ’./keymap/template/’ for various input templates to use directly or to look up keycodes by position. Here we use the input-template for ’us_ansi_60.kbd’
2.4. defalias
Optional : `defalias` statements
KMonad will let you specify some very specific, crazy buttons. These definitions can get pretty long, though, and would make `deflayer` blocks nearly impossible to read. Therefore we provide the ability to alias names to these buttons, to keep the actual `deflayer` statements orderly.
A `defalias` can contain any number of aliases, and it can refer backwards or forwards to layers without issue. The only sequencing that needs to be kept in mind is that a `defalias` cannot refer forward to another `defalias` that is not yet defined.
Here we define a few aliases, but we will define more later. Notice that we try to only use 3 letter names for aliases. If that is not enough to be clear, consider widening all columns to 6 or 7 characters (or be content with a messy config).
2.5. deflayer
Necessary: at least 1 `deflayer` block
As explained in the `defsrc` section, a `deflayer` will define a button for each corresponding entry in the `defsrc` definition. A `deflayer` statement consists of the `deflayer` keyword, followed by the name used to identify this layer, followed by N ’statements-that-evaluate-to-a-button’, where N is exactly how many entries are defined in the `defsrc` statement.
It is also important to mention that the ’keymap’ in KMonad is modelled as a stack of layers (just like in QMK). When an event is registered we look in the top-most layer for a handler. If we don’t find one we try the next layer, and then the next.
Exactly what ’evaluates-to-a-button’ will be expanded on in more detail below. There are very many different specialist buttons in KMonad that we will touch upon. However, for now, these 4 are a good place to begin:
- Any keycode evaluates to a button that, on press, emits the press of that keycode, and on release, emits the release of that keycode. Just a ’normal’ button. The exception is ’\’, which gets used as an escape character. Use ’\\’ instead. Other characters that need to be escaped to match the literal character are ’(’, ’)’, and ’_’.
- An @-prefixed name evaluates to an alias lookup. We named two buttons in the `defalias` block above, we could now refer to these buttons using `@num` and `@kil`. This is also why we only use alias-names no longer than 3 characters in this tutorial. Also, note that we are already referencing some aliases that have not yet been defined, this is not an issue.
- The ’_’ character evaluates to transparent. I.e. no handler for that key-event in this layer, causing this event to be handed down the layer stack to perhaps be handled by the next layer.
The ’XX’ character evaluates to blocked. I.e. no action bound to that key-event in this layer, but do actually catch event, preventing any underlying layer from handling it.
Finally, it is important to note that the first `deflayer` statement in a KMonad config will be the layer that is active when KMonad starts up.
2.5.1. Examples
(deflayer numbers _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ XX / 7 8 9 - _ _ _ _ _ _ _ _ XX * 4 5 6 + _ _ _ _ \( \) . XX 0 1 2 3 _ _ _ _ _ _ _ _ _ _ ) (deflayer my-numbers _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 1 2 3 4 5 6 7 8 9 0 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ )
2.6. Buttons with Modifiers
Optional: modded buttons
Let’s start by exploring the various special buttons that are supported by KMonad by looking at ’modded’ buttons, that is to say, buttons that activate some kind of ’mod’, then perform some button, and finally release that ’mod’ again.
We have already seen an example of this style of button, our `kil` button is one such button. Let’s look at it in more detail: C-A-del
This looks like a simple declarative statement, but it’s helpful to realize that is simply syntactic sugar around 2 function calls. This statement is equivalent to: (around ctl (around alt del))
This highlights a core design principle in KMonad: we try to provide very simple buttons, and then we provide rules and functions for combining them into new buttons. Although note: still very much a work in progress.
So, looking at this statement: (around foo bar)
Here, `around` is a function that takes two buttons and creates a new button. This new button will, on a press, first press foo, then press bar, and on a release first release bar, and then foo. Once created, this new button can be passed to anything in KMonad that expects a button.
We have already seen other examples of modded buttons, \(, \), *, and +. There are no Keycodes for these buttons in KMonad, but they are buttons. They simply evaluate to `(around lsft x)`. All shifted numbers have their corresponding characters, the same is true for all capitals, and < > : ~ “ | { } + and ?.
To wrap up ’modded-buttons’, let’s look back at C-A-del. We have 8 variants: C- : (around lctl X) A- : (around lalt X) M- : (around lmet X) S- : (around lsft X)
Then RC-, RA-, RM-, and RS- behave exactly the same, except using the right-modifier.
These can be combined however you please: C-A-M-S-x ;; Perfectly valid C-% ;; Perfectly valid: same as C-S-5 C-RC-RA-A-M-S-RS-m ;; Sure, but why would you?
Also, note that although we provide special syntax for certain modifiers, these buttons are in no way ’special’ in KMonad. There is no concept of ’modifier’. (around a (around b c)) ;; Perfectly valid
2.6.1. Examples
cpy C-c pst C-v cut C-x ;; Something silly md1 (around a (around b c)) ;; abc md2 (around a (around lsft b)) ;; aB md3 C-A-M-S-l md4 (around % b) ;; BEWARE: %B, not %b, do you see why?
Now we define the ’tst’ button as opening and closing a bunch of layers at the same time. If you understand why this works, you’re starting to grok KMonad.
Explanation: we define a bunch of testing-layers with buttons to illustrate the various options in KMonad. Each of these layers makes sure to have its buttons not overlap with the buttons from the other layers, and specifies all its other buttons as transparent. When we use the nested `around` statement, whenever we push the button linked to ’@tst’ (check `qwerty` layer, we bind it to `rctl`), any button we press when holding `rctl` will be pressed in the context of those 4 layers overlayed on the stack. When we release `rctl`, all these layers will be popped again.
(defalias tst (around (layer-toggle macro-test) (around (layer-toggle layer-test) (around (layer-toggle around-next-test) (around (layer-toggle command-test) (layer-toggle modded-test)))))) (deflayer modded-test _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ @md4 _ _ _ _ _ _ _ _ _ _ _ _ _ @md1 @md2 @md3 _ _ _ _ _ _ _ _ _ _ @cut @cpy @pst _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ )
2.7. Sticky Keys
Optional: sticky keys
KMonad also support so called “sticky keys”. These are keys that will behave as if they were pressed after just tapping them. This behaviour wears off after the next button is pressed, which makes them ideal for things like a quick control or shift. For example, tapping a sticky and then pressing `abc’ will result in `Abc’.
You can create these keys with the `sticky-key’ keyword:
(defalias slc (sticky-key 500 lctl))
The number after `sticky-key’ is the timeout you want, in milliseconds. If a key is tapped and that time has passed, it won’t act like it’s pressed down when we receive the next keypress.
It is also possible to combine sticky keys. For example, to get a sticky shift+control you can do
(defalias ssc (around (sticky-key 500 lsft) (sticky-key 500 lctl)))
2.7.1. Examples
Let’s make both shift keys sticky
(defalias sl (sticky-key 300 lsft) sr (sticky-key 300 rsft))
2.8. Tap Macros
Optional: tap-macros
Let’s look at a button we haven’t seen yet, tap-macros.
`tap-macro` is a function that takes an arbitrary number of buttons and returns a new button. When this new button is pressed it rapidly taps all its stored buttons in quick succesion except for its last button, which it only presses. This last button gets released when the `tap-macro` gets released.
There are two ways to define a `tap-macro`, using the `tap-macro` function directly, or through the #() syntactic sugar. Both evaluate to exactly the same button.
(tap-macro K M o n a d) #(K M o n a d)
If you are going to use a `tap-macro` to perform a sequence of actions inside some program you probably want to include short pauses between inputs to give the program time to register all the key-presses. Therefore we also provide the ’pause’ function, which simply pauses processing for a certain amount of milliseconds. Pauses can be created like this:
(pause 20) P20
You an also pause between each key stroke by specifying the `:delay’ keyword, as well as a time in ms, at the end of a `tap-macro’:
(tap-macro K M o n a d :delay 5) #(K M o n a d :delay 5)
The above would be equivalent to e.g.
(tap-macro K P5 M P5 o P5 n P5 a P5 d)
WARNING: DO NOT STORE YOUR PASSWORDS IN PLAIN TEXT OR IN YOUR KEYBOARD
I know it might be tempting to store your password as a macro, but there are 2 huge risks:
- You accidentally leak your config and expose your password
Anyone who knows about the button can get clear-text representation of your password with any text editor, shell, or text-input field.
Support for triggering shell commands directly from KMonad is described in the command buttons section below.
This concludes this public service announcement.
2.8.1. Examples
(defalias mc1 #(K M o n a d) mc2 #(C-c P50 A-tab P50 C-v) ;; Careful, this might do something mc3 #(P200 h P150 4 P100 > < P50 > < P20 0 r z 1 ! 1 ! !) mc4 (tap-macro a (pause 50) @md2 (pause 50) c) mc5 #(@mc3 spc @mc3 spc @mc3) ) (deflayer macro-test _ @mc1 @mc2 @mc3 @mc4 @mc5 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ )
2.9. Layer Manipulation
Optional: layer manipulation
You have already seen the basics of layer-manipulation. The `layer-toggle` button. This button adds a layer to the top of KMonad’s layer stack when pressed, and removes it again when released. There are a number of other ways to manipulate the layer stack, some safer than others. Let’s go through all of them from safest to least safe:
`layer-toggle` works as described before, 2 things to note:
- If you are confused or worried about pressing a key, changing layers, and then releasing a key and this causing issues: don’t be. KMonad handles presses and releases in very different ways. Presses get passed directly to the stacked keymap as previously described. When a KMonad button has its press-action triggered, it then registers a callback that will catch its own release before we ever touch the keymap. This guarantees that the button triggered by the press of X will be the button whose release is triggered by the release of X (the release of X might trigger other things as well, but that is besides the point.)
If `layer-toggle` can only ever add and then necessarily remove 1 layer from the stack, then it will never cause a permanent change, and is perfectly safe.
`layer-delay`, once pressed, temporarily switches to some layer for some milliseconds. Just like `layer-toggle` this will never permanently mess-up the layer stack. This button was initially implemented to provide some ’leader-key’ style behavior. Although I think in the future better solutions will be available. For now this will temporarily add a layer to the top of the stack: (layer-delay 500 my-layer)
`layer-next`, once pressed, primes KMonad to handle the next press from some arbitrary layer. This aims to fill the same usecase as `layer-delay`: the beginnings of ’leader-key’ style behavior. I think this whole button will get deleted soon, because the more general `around-next` now exists (see below) and this is nothing more than: (around-next (layer-toggle layer-name) some-button) Until then though, use `layer-next` like this: (layer-next layer-name)
`layer-switch`: change the base-layer of KMonad. As described at the top of this document, the first `deflayer` statement is the layer that is active when KMonad starts. Since `layer-toggle` can only ever add on and remove from the top of that, it can never change the base-layer. The following button will unregister the bottom-most layer of the keymap, and replace it with another layer. (layer-switch my-layer)
This is where things start getting potentially dangerous (i.e. get KMonad into an unusuable state until a restart has occured). It is perfectly possible to switch into a layer that you can never get out of. Or worse, you could theoretically have a layer full of only `XX`s and switch into that, rendering your keyboard unuseable until you somehow manage to kill KMonad (without using your keyboard).
However, when handled well, `layer-switch` is very useful, letting you switch between ’modes’ for your keyboard. I have a tiny keyboard with a weird keymap, but I switch into a simple ’qwerty’ keymap shifted 1 button to the right for gaming. Just make sure that any ’mode’ you switch into has a button that allows you to switch back out of the ’mode’ (or content yourself restarting KMonad somehow).
`layer-add` and `layer-rem`. This is where you can very quickly cause yourself a big headache. Originally I didn’t expose these operations, but someone wanted to use them, and I am not one to deny someone else a chainsaw. As the names might give away: (layer-add name) ;; Add a layer to the top of the stack (layer-rem name) ;; Remove a layer by name (noop if no such layer)
To use `layer-add` and `layer-rem` well, you should take a moment to think about how to create a layout that will prevent you from getting into situations where you enter a key-configuration you cannot get out of again. These two operations together, however, are very useful for activating a permanent overlay for a while. This technique is illustrated in the tap-hold overlay a bit further down.
2.9.1. Examples
(defalias yah (layer-toggle asking-for-trouble) ;; Completely safe nah (layer-add asking-for-trouble) ;; Completely unsafe ld1 (layer-delay 500 numbers) ;; One way to get a leader-key ld2 (layer-next numbers) ;; Another way to get a leader key ;; NOTE, this is safe because both `qwerty` and `colemak` contain the `@tst` ;; button which will get us to the `layer-test` layer, which itself contains ;; both `@qwe` and `@col`. qwe (layer-switch qwerty) ;; Set qwerty as the base layer col (layer-switch colemak) ;; Set colemak as the base layer ) (deflayer layer-test @qwe _ _ _ _ _ _ _ _ _ _ @add _ @nah @col _ _ _ _ _ _ _ _ _ _ _ _ @yah _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ @ld1 @ld2 _ _ _ _ _ _ _ _ _ ) ;; Exactly like qwerty, but with the letters switched around (deflayer colemak grv 1 2 3 4 5 6 7 8 9 0 - = bspc tab q w f p g j l u y ; [ ] \ @xcp a r s t d h n e i o ' ret @sl z x c v b k m , . / @sr lctl @num lalt spc ralt rmet @sym @tst ) (defalias lol #(: - D)) ;; Contrived example (deflayer asking-for-trouble @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol @lol ) ;; One way to safely use layer-add and layer-rem: the button bound to layer-add ;; is the same button bound to layer-rem in the layer that `add` adds to the ;; stack. I.e., it becomes impossible to add or remove multiple copies of a ;; layer. (defalias add (layer-add multi-overlay) ;; multi-overlay is defined in the next rem (layer-rem multi-overlay) ;; section below this )
2.10. Multi-use Buttons
Optional: Multi-use buttons
Perhaps one of the most useful features of KMonad, where a lot of work has gone into, but also an area with many buttons that are ever so slightly different. The naming and structuring of these buttons might change sometime soon, but for now, this is what there is.
For the next section being able to talk about examples is going to be handy, so consider the following scenario and mini-language that will be the same between scenarios.
- We have some button `foo` that will be different between scenarios
- `foo` is bound to ’Esc’ on the input keyboard
- the letters a s d f are bound to themselves
- Px signifies the press of button x on the keyboard
- Rx signifies the release of said button
- Tx signifies the sequential and near instantaneous press and release of x
100 signifies 100ms pass
So for example: Tesc Ta: tap of ’Esc’ (triggering `foo`), tap of ’a’ triggering `a` Pesc 100 Ta Tb Resc: press of ’Esc’, 100ms pause, tap of ’a’, tap of ’b’, release of ’Esc’
The `tap-next` button takes 2 buttons, one for tapping, one for holding, and combines them into a single button. When pressed, if the next event is its own release, we tap the ’tapping’ button. In all other cases we first press the ’holding’ button then we handle the event. Then when the `tap-next` gets released, we release the ’holding’ button.
So, using our mini-language, we set foo to: (tap-next x lsft) Then: Tesc -> x Tesc Ta -> xa Pesc Ta Resc -> A Pesc Ta Tr Resc -> AR
The `tap-hold` button is very similar to `tap-next` (a theme, trust me). The difference lies in how the decision is made whether to tap or hold. A `tap-hold` waits for a particular timeout, if the `tap-hold` is released anywhere before that moment we execute a tap immediately. If the timeout occurs and the `tap-hold` is still held, we switch to holding mode.
The additional feature of a `tap-hold` is that it pauses event-processing until it makes its decision and then rolls back processing when the decision has been made.
So, again with the mini-language, we set foo to: (tap-hold 200 x lsft) ;; Like tap-next, but with a 200ms timeout Then: Tesc -> x Tesc Ta -> xa Pesc 300 a -> A (the moment you press a) Pesc a 300 -> A (after 200 ms) Pesc a 100 Resc -> xa (both happening immediately on Resc)
The `tap-hold-next` button is a combination of the previous 2. Essentially, think of it as a `tap-next` button, but it also switches to held after a period of time. This is useful, because if you have a (tap-next ret ctl) for example, and you press it thinking you want to press C-v, but then you change your mind, you now cannot release the button without triggering a ’ret’, that you then have to backspace. With the `tap-hold-next` button, you simply outwait the delay, and you’re good. I see no benefit of `tap-next` over `tap-hold-next` with a decent timeout value.
The `tap-next-release` is like `tap-next`, except it decides whether to tap or hold based on the next release of a key that was not pressed before us. This also performs rollback like `tap-hold`.So, using the minilanguage and foo as: (tap-next-release x lsft) Then: Tesc Ta -> xa Pa Pesc Ra Resc -> ax (because ’a’ was already pressed when we started, so foo decides it is tapping) Pesc Ta Resc -> A (because a was pressed and released after we started, so foo decides it is holding)
These increasingly stranger buttons are, I think, coming from the stubborn drive of some of my more eccentric (and I mean that in the most positive way) users to make typing with modifiers on the home-row more comfortable. Especially layouts that encourage a lot of rolling motions are nicer to use with the `release` style buttons.
The `tap-hold-next-release` (notice a trend?) is just like `tap-next-release`, but it comes with an additional timeout that, just like `tap-hold-next` will jump into holding-mode after a timeout.
I honestly think that `tap-hold-next-release`, although it seems the most complicated, probably is the most comfortable to use. But I’ve put all of them in a testing layer down below, so give them a go and see what is nice.
2.10.1. Examples
(defalias xtn (tap-next x lsft) ;; Shift that does 'x' on tap xth (tap-hold 400 x lsft) ;; Long delay for easier testing thn (tap-hold-next 400 x lsft) tnr (tap-next-release x lsft) tnh (tap-hold-next-release 2000 x lsft) ;; Used it the colemak layer xcp (tap-hold-next 400 esc ctl) ) ;; Some of the buttons used here are defined in the next section (deflayer multi-overlay @mt _ _ _ _ _ _ _ _ _ _ _ @rem _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ @thn _ _ _ _ _ _ _ _ _ _ _ _ @xtn _ _ _ _ _ _ _ _ _ _ @xth @tnr _ _ _ _ _ _ @tnh )
2.11. Multi-tap
Optional: Multi-tap
Besides the tap-hold style buttons there is another multi-use button (with. only 1 variant, at the moment). The `multi-tap`.
A `multi-tap` codes for different buttons depending on how often it is tapped. It is defined by a series of delays and buttons, followed by a last button without delay. As long as you tap the `multi-tap` within the delay specified, it will jump to the next button. Once the delay is exceeded the selected button is pressed. If the last button in the list is reached, it is immediately pressed.
Note that you can actually hold the button, so in the below example, going: tap-tap-hold (wait 300ms) will get you a pressed c, until you release again.
2.11.1. Examples
(defalias mt (multi-tap 300 a 300 b 300 c 300 d e))
2.12. Around-next
Optional: Around-next
The `around-next` function creates a button that primes KMonad to perform the next button-press inside some context. This could be the context of ’having Shift pressed’ or ’being inside some layer’ or, less usefully, ’having d pressed’. It is a more general and powerful version of `layer-next`.
I think expansion of this button-style is probably the future of leader-key, hydra-style functionality support in KMonad.
2.12.1. Examples
(defalias ns (around-next sft) ;; Shift the next press nnm (around-next @num) ;; Perform next press in numbers layer nd (around-next d) ;; Silly, but possible ) (deflayer around-next-test _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ @nd _ _ _ _ _ _ _ _ _ _ _ _ @nnm _ _ _ _ _ _ _ _ _ _ _ @ns _ _ _ _ _ _ _ )
2.13. Compose Key Sequences
Optional: Compose-key sequences
Compose-key sequences are series of button-presses that your operating system will interpret as the insertion of a special character, like accented characters, or various special-languages. In that sense, they are just syntactic sugar for keyboard macros.
To get this to work on Linux you will need to set your compose-key with a tool like `setxkbmap’, as well as tell kmonad that information. See the `defcfg’ block at the top of this file for a working example. Note that you need to wait ever so slightly for the keyboard to register with linux before the command gets executed, that’s why the `sleep 1`. Also, note that all the `/run/current-system’ stuff is because the author uses NixOS. Just find a shell-command that will:
- Sleep a moment
Set the compose-key to your desired key
Please be aware that what `setxkbmap’ calls the `menu’ key is not actually the `menu’ key! If you want to use the often suggested
setxkbmap -option compose:menu
you will have to set your compose key within kmonad to `compose’ and not `menu’.
After this, this should work out of the box under Linux. Windows does not recognize the same compose-key sequences, but WinCompose will make most of the sequences line up with KMonad: http://wincompose.info/ This has not in any way been tested on Mac.
In addition to hard-coded symbols, we also provide ’uncompleted’ macros. Since a compose-key sequence is literally just a series of keystrokes, we can omit the last one, and enter the sequence for ’add an umlaut’ and let the user then press some letter to add this umlaut to. These are created using the `+“` syntax.
2.13.1. Examples
(defalias sym (layer-toggle symbols) ) (deflayer symbols _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ä é © _ _ _ _ _ _ _ _ _ _ _ +' +~ +` +^ _ _ _ _ _ _ _ _ _ +" _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _)
2.14. Command Buttons
Optional: Command buttons
Currently we also provide the ability to launch arbitrary shell-commands from inside kmonad. These commands are simply handed off to the command-shell without any further checking or waiting.
NOTE: currently only tested on Linux, but should work on any platform, as long as the command is valid for that platform.
The `cmd-button’ function takes two arguments, the second one of which is optional. These represent the commands to be executed on pressing and releasing the button respectively.
BEWARE: never run anyone’s configuration without looking at it. You wouldn’t want to push:
(cmd-button “rm -rf ~/*”) ;; Delete all this user’s data
(defalias dat (cmd-button "date >> /tmp/kmonad_example.txt") ;; Append date to tmpfile pth (cmd-button "echo $PATH > /tmp/kmonad_path.txt") ;; Write out PATH ;; `dat' on press and `pth' on release bth (cmd-button "date >> /tmp/kmonad_example.txt" "echo $PATH > /tmp/kmonad_path.txt") ) (deflayer command-test _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ @dat @pth _ _ _ _ _ _ _ _ _ )