Which Key Source Files¶
wks is the scripting language used by wk(1) to define key chords. This page is the complete reference. If you’re new to wks, start with the getting started guide and the examples.
Grammar¶
key_chord -> ( chord | prefix | chord_array ) ;
chord -> trigger_key description keyword* ( command | meta_command ) ;
prefix -> trigger_key description keyword* '{' ( key_chord )+ '}' ;
chord_array -> ( implicit_array | explicit_array ) ;
implicit_array -> modifier* '...' description keyword* ( command | meta_command ) ;
explicit_array -> '[' ( trigger_key | chord_expression )+ ']' description keyword* ( command | meta_command ) ;
chord_expression -> '(' trigger_key description keyword* ( command | meta_command )? ')' ;
trigger_key -> modifier* ( normal_key | special_key | key_options ) ;
normal_key -> ( '\\' [\\\[\]{}#":^+()] | [^\s\[\]{}#":^+()] ) ;
special_key -> ( 'Left' | 'Right' | 'Up' | 'Down' | 'BS'
| 'TAB' | 'SPC' | 'RET' | 'DEL' | 'ESC'
| 'Home' | 'PgUp' | 'PgDown' | 'End' | 'Begin'
| 'VolDown' | 'VolMute' | 'VolUp' | 'Play' | 'Stop'
| 'Prev' | 'Next' | 'F'[1-35] ) \s+ ;
modifier -> ( 'C' | 'H' | 'M' | 'S' ) '-' ;
key_options -> '<' ( modifier* ( normal_key | special_key | '...' ) )+ '>' ;
description -> '"' ( '\\"' | [^"] | interpolation )* '"' ;
command -> '%' delimiter ( . | interpolation )* delimiter ;
delimiter -> ( open_delim | close_delim | ([^[{(])\1 ) ;
open_delim -> ( '{{' | '((' | '[[' ) ;
close_delim -> ( '}}' | '))' | ']]' ) ;
interpolation -> '%(' ( chord_metadata | arg_position | user_variable ) ')' ;
chord_metadata -> ( 'key'
| 'index'
| 'index+1'
| 'desc'
| 'desc^'
| 'desc^^'
| 'desc,'
| 'desc,,'
| 'wrap_cmd' );
arg_position -> '$' [0-9]+ ;
user_variable -> [^)]+ ;
keyword -> ( hook | flag ) ;
meta_command -> '@' ( 'goto' description ) ;
hook -> '^' ( 'before'
| 'after'
| 'sync-before'
| 'sync-after' ) command ;
flag -> '+' ( 'keep'
| 'close'
| 'inherit'
| 'ignore'
| 'unhook'
| 'deflag'
| 'no-before'
| 'no-after'
| 'write'
| 'execute'
| 'sync-command'
| 'unwrap'
| 'wrap' '"' ( '\\"' | [^"] | interpolation )* '"'
| 'title' ( '"' ( '\\"' | [^"] | interpolation )* '"' )?
| 'args' ( '"' ( '\\"' | [^"] | interpolation )* '"' )+ ) ;
preprocessor_macro -> ':' ( string_macro
| switch_macro
| integer_macro
| unsigned_macro
| number_macro ) ;
string_macro -> ( 'include'
| 'fg'
| 'fg-key'
| 'fg-delimiter'
| 'fg-prefix'
| 'fg-chord'
| 'fg-title'
| 'fg-goto'
| 'bg'
| 'bd'
| 'shell'
| 'font'
| 'title'
| 'title-font'
| 'delimiter'
| 'wrap-cmd'
| 'var' '"' ( '\\"' | [^"] | user_variable )* '"' ) '"' ( '\\"' | [^"] | user_variable )* '"' ;
switch_macro -> ( 'debug'
| 'unsorted'
| 'top'
| 'bottom' );
integer_macro -> ( 'menu-width'
| 'menu-gap'
| 'table-padding' ) '-'? [0-9]+ ;
unsigned_macro -> ( 'max-columns'
| 'border-width'
| 'width-padding'
| 'height-padding'
| 'delay'
| 'keep-delay' ) [0-9]+ ;
number_macro -> ( 'border-radius' ) '-'? [0-9]+ ( '.' [0-9]* )? ;
Comments begin with # and extend to the end of the line. The #
character is literal inside descriptions and commands; it
does not need to be escaped.
Trigger Keys¶
A trigger key is the keypress that activates a chord.
trigger_key -> modifier* ( normal_key | special_key | key_options ) ;
Normal Keys¶
Any printable, non-whitespace, UTF-8 character.
normal_key -> ( '\\' [\\\[\]{}#":^+()] | [^\s\[\]{}#":^+()] ) ;
The following characters have special meaning in wks and must be
escaped with a backslash (\) to use as trigger keys:
Character |
Meaning |
|---|---|
|
Chord array |
|
Prefix block |
|
Chord expression |
|
Comment |
|
Description |
|
Preprocessor macro |
|
Hook |
|
Flag |
Special Keys¶
Non-printable or whitespace keys use named forms. Whitespace is required after a special key.
Key |
wks form |
|---|---|
Left arrow |
|
Right arrow |
|
Up arrow |
|
Down arrow |
|
Backspace |
|
Tab |
|
Space |
|
Enter/Return |
|
Delete |
|
Esc |
|
Home |
|
Page up |
|
Page down |
|
End |
|
Begin |
|
Function keys |
|
Volume Down |
|
Mute |
|
Volume Up |
|
Play |
|
Stop |
|
Previous Track |
|
Next Track |
|
TAB "Cycle" %{{cycle-windows}}
C-RET "Confirm" %{{confirm}}
Modifiers¶
Zero or more modifiers can precede a key.
Modifier |
wks form |
|---|---|
Control |
|
Meta/Alt |
|
Hyper/Super |
|
Shift |
|
To match Control+c, write C-c. Modifiers chain: C-M-x matches
Control+Alt+x.
Note
The Shift modifier (S-) has a subtlety with normal keys. If pressing
a key with Shift produces a different character (e.g., a → A), use
the shifted character directly - A, not S-a. The S- modifier is
only needed for keys where Shift does not change the output (e.g.,
special keys like S-TAB).
Key Options¶
Key options let a chord bind to the first available key from a list.
key_options -> '<' ( modifier* ( normal_key | special_key | '...' ) )+ '>' ;
<s i g> "Signal" %{{signal}}
The parser tries s first; if already bound, it tries i, then g.
If all keys are taken, an error is raised. The ellipsis (...)
expands to the implicit array keys as a fallback:
<s i g ...> "Signal" %{{signal}}
Modifiers before < apply to all options. Modifiers inside apply to
individual keys:
M-<a b> # Both options get M-
<a M-b> # Only 'b' gets M-
Key options are most useful with :include to avoid
trigger key collisions across files. See the key options
example.
Descriptions¶
A description is a quoted string shown as the hint text in the menu.
description -> '"' ( '\\"' | [^"] | interpolation )* '"' ;
Descriptions can contain interpolations. Use \"
for a literal double quote. An empty description ("") is valid.
Commands¶
A command is the action executed when a chord completes.
command -> '%' delimiter ( . | interpolation )* delimiter ;
A command starts with % followed by a delimiter pair. Anything
between the delimiters is passed to the shell. If it runs in your
terminal, it works in a wks command.
Delimiters¶
delimiter -> ( open_delim | close_delim | ([^[{(])\1 ) ;
open_delim -> ( '{{' | '((' | '[[' ) ;
close_delim -> ( '}}' | '))' | ']]' ) ;
The three bracket-style delimiters have matching open/close pairs. Any other ASCII character repeated twice also works as a delimiter:
%{{echo "hello"}} # Standard
%((echo "hello")) # Parentheses
%[[echo "hello"]] # Brackets
%||echo "hello"|| # Arbitrary
This flexibility, inspired by sed(1), means your command content never conflicts with the delimiter - just pick one that doesn’t appear in the command. Different chords can use different delimiters.
Chords¶
A chord is a complete key binding - pressing its trigger key performs an action.
chord -> trigger_key description keyword* ( command | meta_command ) ;
b "Brave" %{{brave}}
C-s "Save" %{{save-file}}
Prefixes¶
A prefix groups related chords under a common trigger key. Pressing the prefix key opens a submenu rather than executing a command.
prefix -> trigger_key description keyword* '{' ( key_chord )+ '}' ;
Prefixes can be nested arbitrarily deep:
e "Emacs"
{
o "Open" %{{emacs}}
r "Roam"
{
h "Home" %{{emacs ~/Documents/roam/home.org}}
j "Journal" %{{emacs ~/Documents/roam/daily}}
}
}
Chord Arrays¶
Chord arrays generate multiple chords from a shared template, reducing repetition.
chord_array -> ( implicit_array | explicit_array ) ;
Implicit Arrays¶
An implicit array uses the predefined implicit array keys (set via
--implicit-keys or :implicit-array-keys) as trigger keys.
implicit_array -> modifier* '...' description keyword* command ;
... "Workspace %(index+1)" %{{switch-to-workspace %(index)}}
# Assuming implicit keys are "asdfghjkl;"
a "Workspace 1" %{{switch-to-workspace 0}}
s "Workspace 2" %{{switch-to-workspace 1}}
d "Workspace 3" %{{switch-to-workspace 2}}
# ... and so on for each implicit key
Explicit Arrays¶
An explicit array lists its trigger keys between brackets.
explicit_array -> '[' ( trigger_key | chord_expression )+ ']' description keyword* command ;
[asdfghjkl] "Workspace %(index+1)" %{{switch-to-workspace %(index)}}
a "Workspace 1" %{{switch-to-workspace 0}}
s "Workspace 2" %{{switch-to-workspace 1}}
d "Workspace 3" %{{switch-to-workspace 2}}
f "Workspace 4" %{{switch-to-workspace 3}}
g "Workspace 5" %{{switch-to-workspace 4}}
h "Workspace 6" %{{switch-to-workspace 5}}
j "Workspace 7" %{{switch-to-workspace 6}}
k "Workspace 8" %{{switch-to-workspace 7}}
l "Workspace 9" %{{switch-to-workspace 8}}
Chord Expressions¶
A chord expression customizes an individual entry within a chord array. Wrap it in parentheses inside the brackets. Only a trigger key and description are required - any missing command or keywords are filled in by the surrounding array.
chord_expression -> '(' trigger_key description keyword* command? ')' ;
[
(b "Brave")
(c "Mullvad Chrome" %{{mullvad-exclude chrome ~/startpage.html}})
x
] "XDG-OPEN" %{{%(desc,,) ~/startpage.html}}
b "Brave" %{{brave ~/startpage.html}}
c "Mullvad Chrome" %{{mullvad-exclude chrome ~/startpage.html}}
x "XDG-OPEN" %{{xdg-open ~/startpage.html}}
Here, b and x inherit the array’s command template, while c
provides its own. The x key has no expression, so it also inherits
the array’s description.
Interpolations¶
An interpolation inserts dynamic values into descriptions and commands.
interpolation -> '%(' ( chord_metadata | arg_position | user_variable ) ')' ;
There are three kinds:
Chord Metadata¶
Built-in identifiers that reference properties of the current chord. Valid in descriptions and commands.
Identifier |
Value |
|---|---|
|
The trigger key of the current chord |
|
0-based position in parse order within the current scope |
|
1-based position (same as above, offset by one) |
|
The description (not valid inside a description) |
|
Description with first character capitalized |
|
Description fully uppercased |
|
Description with first character lowercased |
|
Description fully lowercased |
|
The global wrap command value |
Note
Index values are resolved before sorting. %(index) reflects
parse order, not the final display order. Prefixes start a new scope
for their children.
User Variables¶
Variables defined with the :var macro. Valid in
descriptions, commands, and preprocessor macro arguments.
user_variable -> [^)]+ ;
:var "CMD" "hyprctl dispatch workspace"
[asdfghjkl] "Workspace %(index+1)" %{{%(CMD) %(index)}}
Argument Positions¶
Positional references to values defined with +args. Valid in
descriptions and commands where +args is defined.
arg_position -> '$' [0-9]+ ;
%($0) is the first argument, %($1) the second, and so on.
Undefined positions resolve to an empty string.
Arguments follow lexical scoping - descendants within a prefix can
access that prefix’s arguments. When a descendant defines its own
+args, its values shadow ancestor arguments at the same index, but
other indices remain accessible:
p "+Prefix" +args "outer0" "outer1"
{
a "Use both" +write %{{%($0) %($1)}}
n "+Nested" +args "inner0"
{
# %($0) = "inner0" (shadowed), %($1) = "outer1" (inherited)
b "Mixed" +write %{{%($0) %($1)}}
}
}
Hooks¶
Hooks attach additional commands to a chord or prefix.
hook -> '^' ( 'before' | 'after' | 'sync-before' | 'sync-after' ) command ;
- ^before / ^after
Run a command before or after the chord’s own command, asynchronously.
- ^sync-before / ^sync-after
Same, but block wk until the hook completes.
Warning
A blocking sync-* hook that never returns will freeze wk. You may
need to restart your system to regain keyboard control.
Hooks on a prefix apply to all direct child chords but not to
nested prefixes (see Inheritance). Use +unhook,
+no-before, or +no-after on individual chords to opt out:
e "Emacs" ^before %{{switch-to-workspace 1}}
{
o "Open" %{{emacs}}
r "Roam" %{{emacs ~/Documents/roam}}
s "Scratch" +unhook %{{emacs --eval "(scratch-buffer)"}}
}
e "Emacs"
{
o "Open" %{{switch-to-workspace 1; emacs}}
r "Roam" %{{switch-to-workspace 1; emacs ~/Documents/roam}}
s "Scratch" %{{emacs --eval "(scratch-buffer)"}}
}
Flags¶
Flags modify the behavior of a chord or prefix.
flag -> '+' ( 'keep' | 'close' | 'inherit' | 'ignore' | 'unhook'
| 'deflag' | 'no-before' | 'no-after' | 'write'
| 'execute' | 'sync-command' | 'unwrap'
| 'wrap' description | 'title' description?
| 'args' description+ ) ;
Output Control¶
- +write
Print the command text to stdout instead of executing it. Turns wk into a selection prompt, like dmenu.
- +execute
Execute the command normally. Overrides
+writeinherited from a parent prefix.- +sync-command
Execute the command synchronously (blocking). See the warning in Hooks about blocking commands.
Inheritance Control¶
- +inherit
Force a nested prefix to inherit hooks and flags from its parent. Has no effect on chords.
- +ignore
Ignore all inherited hooks and flags. Has no effect on prefixes.
- +unhook
Ignore all inherited hooks only.
- +deflag
Ignore all inherited flags only.
- +no-before
Ignore inherited
^beforeand^sync-beforehooks.- +no-after
Ignore inherited
^afterand^sync-afterhooks.
Command Wrapping¶
- +wrap
"wrapper" Wrap this chord’s command (or all commands in a prefix) with the given string. The command is executed as
/bin/sh -c "WRAPPER CMD". Supports interpolation.- +unwrap
Prevent wrapping, even if a global wrap or inherited
+wrapis set.
Wrap precedence, most specific to least:
+unwrapon chord (no wrap)+wrap "custom"on chord+wrapinherited from parent prefixGlobal
:wrap-cmd/--wrap-cmdNo wrapper
:wrap-cmd "uwsm-app --"
a "Apps"
{
b "Brave" %{{brave}}
}
f "Foot" +wrap "foot -e"
{
n "ncmpcpp" %{{ncmpcpp}}
}
a "Apps"
{
b "Brave" %{{uwsm-app -- brave}}
}
f "Foot"
{
n "ncmpcpp" %{{foot -e ncmpcpp}}
}
Title¶
- +title
"text" Set a title displayed above the menu for this chord or prefix. Supports interpolation. Overrides the global
--titlesetting. An empty string ("") clears any inherited or global title. Omitting the argument sets the title to the chord’s description.
Arguments¶
- +args
"arg0""arg1"… Define positional arguments accessible via
%($0),%($1), etc. See Argument Positions for scoping rules.
[
(a "Alice" +args "alice@example.com")
(b "Bob" +args "bob@example.com")
c
] "Email %($0)" +args "(default)" %{{mailto:%($0)}}
# a -> mailto:alice@example.com
# b -> mailto:bob@example.com
# c -> mailto:(default)
a "Alice" %{{mailto:alice@example.com}}
b "Bob" %{{mailto:bob@example.com}}
c "Email (default)" %{{mailto:(default)}}
Meta Commands¶
A meta command controls wk’s menu rather than running a shell command. Meta commands are mutually exclusive with hooks and regular commands.
meta_command -> '@' ( 'goto' description ) ;
@goto¶
Navigates to a different location in the key chord hierarchy without
closing and restarting wk. The argument uses the same syntax as
--press:
@goto ""- navigate to the root menu@goto "w"- navigate to the “w” prefix@goto "w m"- navigate through “w” then “m”
If the path leads to a chord (not a prefix), that chord’s command is executed.
w "Window" +keep
{
m "Move" +keep
{
... "Move to %(index+1)" %{{move-window %(index)}}
BS "Go back" @goto "w"
S-BS "Go home" @goto ""
}
r "Resize" +keep +inherit
{
... "Resize %(index+1)" %{{resize-window %(index)}}
}
}
Note
@goto cannot be combined with hooks or commands. Circular goto
chains (e.g., a @goto "b" and b @goto "a") are detected at
runtime and produce an error.
Preprocessor Macros¶
Preprocessor macros configure the menu appearance and behavior, or
include other wks files. They start with : and override the
equivalent wk(1) command-line flags.
preprocessor_macro -> ':' ( string_macro | switch_macro
| integer_macro | unsigned_macro
| number_macro ) ;
String Macros¶
String macros take a quoted string argument. All string macro arguments support user variable interpolation.
Macro |
Equivalent CLI flag |
Description |
|---|---|---|
|
|
All foreground text color |
|
|
Key text color |
|
|
Delimiter text color |
|
|
Prefix text color |
|
|
Chord text color |
|
|
Title text color |
|
|
Goto text color |
|
|
Background color |
|
|
Border color |
|
|
Shell for command execution |
|
|
Menu font (Pango format) |
|
|
Global title text |
|
|
Title font (Pango format) |
|
- |
Default command delimiter |
|
|
Global command wrapper |
:bg "#27212E"
:fg-key "#74DFC4"
:bd "#EB64B9"
:font "JetBrains Mono, 14"
Include Macro¶
The :include macro inserts the contents of another wks file,
similar to C’s #include. Relative paths resolve from the directory
of the file being processed.
b "Browser" { :include "browser.wks" }
e "Emacs" ^before %{{switch-to-workspace 1}} { :include "emacs.wks" }
Note
Self-includes and recursive includes produce an error. The same file
may be included multiple times. Unlike C’s #include, the :include
macro can appear anywhere - even mid-description (though you probably
shouldn’t).
See the modular configs example.
Var Macro¶
The :var macro defines a variable for use in descriptions, commands,
and other preprocessor arguments. It takes two string arguments: a
name and a value.
:var "CMD" "hyprctl dispatch workspace"
[asdfghjkl] "Workspace %(index+1)" %{{%(CMD) %(index)}}
Setting the value to an empty string ("") unsets the variable. The
variable name can contain any character except ) and cannot shadow
built-in chord metadata identifiers.
Both name and value support variable interpolation, enabling meta-variables (variables with computed names) and chained references:
# Meta-variable: name computed from another variable
:var "key" "GREETING"
:var "%(key)" "Hello, World!"
a "Say hello" %{{echo %(GREETING)}}
# Chained references
:var "home" "/home/user"
:var "config" "%(home)/.config/wk"
Variables are resolved at definition time in a single pass - define them before use.
A common pattern is combining :var with :include for
environment-specific configs:
~/hyprland.wks¶:var "WORKSPACE_CMD" "hyprctl dispatch workspace"
:include "main.wks"
~/dwm.wks¶:var "WORKSPACE_CMD" "xdotool set_desktop"
:include "main.wks"
~/main.wks¶w "Workspace"
{
[asdfghjkl] "%(index+1)" %{{%(WORKSPACE_CMD) %(index)}}
}
See the variables example.
Switch Macros¶
Switch macros are simple toggles with no argument.
Macro |
Equivalent CLI flag |
Description |
|---|---|---|
|
|
Print debug info |
|
|
Disable sorting |
|
|
Position menu at top |
|
|
Position menu at bottom |
|
|
Position menu at center |
Integer Macros¶
Take a positive or negative integer argument.
Macro |
Equivalent CLI flag |
|---|---|
|
|
|
|
|
|
Unsigned Macros¶
Take a positive integer argument.
Macro |
Equivalent CLI flag |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Number Macros¶
Take a decimal number argument.
Macro |
Equivalent CLI flag |
|---|---|
|
|
Inheritance¶
Hooks and flags on a prefix are inherited by its direct child chords,
but not by nested prefixes. Use +inherit to opt a nested prefix
in, or use the negation flags (+unhook, +deflag, +ignore,
+no-before, +no-after) to opt individual chords out.
a "Prefix" +write
{
w "Written" %{{I get written!}}
n "Nested"
{
r "Runs" %{{echo "I get run!"}}
}
}
# a w -> prints "I get written!" to stdout
# a n r -> executes echo "I get run!"
To make the nested prefix inherit:
a "Prefix" +write
{
w "Written" %{{I get written!}}
n "Nested" +inherit
{
r "Also written" %{{I also get written!}}
}
}
Sorting¶
Key chords are sorted alphabetically by default. Index interpolations
are resolved before sorting, so %(index) reflects parse order,
not display order.
[neio] "Switch %(index+1)" %{{xdotool set_desktop %(index)}}
b "Second" +write %{{%(index)}}
a "First" +write %{{%(index)}}
# Display order (sorted): a, b, e, i, n, o
# But: a's command is %{{5}}, b's is %{{4}}, n's is %{{0}}, etc.
Disable sorting with --unsorted or :unsorted.
Bug Reports¶
If you find a bug, please report it at https://github.com/3L0C/wk/issues.