Skip to content

Instantly share code, notes, and snippets.

@Momijiichigo
Last active December 12, 2025 20:36
Show Gist options
  • Select an option

  • Save Momijiichigo/ee457aa10693df151ebf74c42d47ce68 to your computer and use it in GitHub Desktop.

Select an option

Save Momijiichigo/ee457aa10693df151ebf74c42d47ce68 to your computer and use it in GitHub Desktop.
Hyprland Intuitive Keybind Config

Hyprland Intuitive Keybind Config

Hyprland keybinding configuration syntax is not for humans.

I made a little script so you can write keybinding config with short and intuitive syntax.

The script will write the transpiled config into hyprland.conf

Syntax

Based on the SXHKD / SWHKD keybinding syntax, with useful extension (bind flags)

Example:

# program launcher
alt + { _, super} + space
  rofi -show {drun, run} -dpi 200 -show-icons

alt + bracket{left,right}
  hyprctl dispatch workspace {r-1, r+1}

alt + shift + bracket{left,right}
  hyprctl dispatch hy3:movetoworkspace {r-1, r+1}, follow

# focus or send to the given desktop
super + {_, shift} + {1-9,0}
  hyprctl dispatch {workspace, hy3:movetoworkspace} {1-9,10}

# resize windows
( repeat )
super + ctrl + {h,j,k,l}
  hyprctl dispatch resizeactive {-20 0, 0 20, 0 -20, 20 0}
Transpiled Result config (hyprland.conf)
# >>> HOTKEY CONFIG TRANSPILED SECTION >>>

bind = alt,  space, exec, rofi -show drun -dpi 200 -show-icons
bind = alt super, space, exec, rofi -show run -dpi 200 -show-icons
bind = alt, bracketleft, workspace,  r-1
bind = alt, bracketright, workspace,  r+1
bind = alt shift, bracketleft, hy3:movetoworkspace,  r-1, follow
bind = alt shift, bracketright, hy3:movetoworkspace,  r+1, follow
bind = super,  1, workspace,  1
bind = super,  2, workspace,  2
bind = super,  3, workspace,  3
bind = super,  4, workspace,  4
bind = super,  5, workspace,  5
bind = super,  6, workspace,  6
bind = super,  7, workspace,  7
bind = super,  8, workspace,  8
bind = super,  9, workspace,  9
bind = super,  0, workspace,  10
bind = super shift, 1, hy3:movetoworkspace,  1
bind = super shift, 2, hy3:movetoworkspace,  2
bind = super shift, 3, hy3:movetoworkspace,  3
bind = super shift, 4, hy3:movetoworkspace,  4
bind = super shift, 5, hy3:movetoworkspace,  5
bind = super shift, 6, hy3:movetoworkspace,  6
bind = super shift, 7, hy3:movetoworkspace,  7
bind = super shift, 8, hy3:movetoworkspace,  8
bind = super shift, 9, hy3:movetoworkspace,  9
bind = super shift, 0, hy3:movetoworkspace,  10
binde = super ctrl, h, resizeactive,  -20 0
binde = super ctrl, j, resizeactive,  0 20
binde = super ctrl, k, resizeactive,  0 -20
binde = super ctrl, l, resizeactive,  20 0

# <<< HOTKEY CONFIG TRANSPILED SECTION <<<

The transpiled binding config will be inserted between

# >>> HOTKEY CONFIG TRANSPILED SECTION >>>

and

# <<< HOTKEY CONFIG TRANSPILED SECTION <<<

in the hyprland.conf.

Flags

Specifying Flags

In addition to the normal SXHKD syntax, you can also specify flags supported by hyprland:

### Flags:
# l -> locked, will also work when an input inhibitor (e.g. a lockscreen) is active.
# r -> release, will trigger on release of a key.
# o -> long-press, will trigger on long press of a key.
# e -> repeat, will repeat when held.
# n -> non-consuming, key/mouse events will be passed to the active window in addition to triggering the dispatcher.
# m -> mouse, see below.
# t -> transparent, cannot be shadowed by other binds.
# i -> ignore mods, will ignore modifiers.
# s -> separate, will arbitrarily combine keys between each mod/key, see [Keysym combos](#keysym-combos) above.
# d -> has description, will allow you to write a description for your bind.
# p -> bypasses the app's requests to inhibit keybinds.
#### 

You can place ( flag1, flag2, flag3... ) right before the keybinding line.

You can either use full keywords (e.g. repeat) or abbreviated (e.g. e)

Setup

  1. Install bun.js
    • feature-rich, lightweight, fast
    • doesn't hurt to install it. Do it.
  2. add the following in the hyprland.conf:
  # >>> HOTKEY CONFIG TRANSPILED SECTION >>>

  # <<< HOTKEY CONFIG TRANSPILED SECTION <<<
  1. Copy and paste the hotkey_transpile.ts in the same directory as hyprland.conf
    • You might also want to copy-paste tsconfig.json if you'll edit the script.
  2. Create hotkeys.conf in the same directory
  3. Customize, and run ./hotkey_transpile.ts
#!/usr/bin/bun run
// Define flag mappings for abbreviations to full form
const flagMappings: Record<string, string> = {
'locked': 'l',
'release': 'r',
'long-press': 'o',
'repeat': 'e',
'non-consuming': 'n',
'mouse': 'm',
'transparent': 't',
'ignore-mods': 'i',
'separate': 's',
'description': 'd',
'bypasses': 'p',
};
// Paths
const inputPath = './hotkeys.conf';
const outputPath = './hyprland.conf';
// Read config files
let input = '';
let hyprlandConfig = '';
try {
input = await Bun.file(inputPath).text();
console.log(`Successfully read hotkeys file: ${inputPath}`);
} catch (error) {
throw new Error(`Error reading ${inputPath}: ${error}`);
}
try {
hyprlandConfig = await Bun.file(outputPath).text();
console.log(`Successfully read Hyprland config: ${outputPath}`);
} catch (error) {
throw new Error(`Error reading ${outputPath}: ${error}`);
}
const MODIFIER_KEYS = ['ctrl', 'shift', 'alt', 'super'];
function getExpandedLines(line: string): string[] {
const results: string[] = [line];
let hasExpansion = true;
while (hasExpansion) {
hasExpansion = false;
const newResults: string[] = [];
for (const currentLine of results) {
const openCurly = currentLine.indexOf('{');
const closeCurly = currentLine.indexOf('}');
if (openCurly === -1 || closeCurly === -1 || closeCurly < openCurly) {
newResults.push(currentLine);
continue;
}
hasExpansion = true;
const before = currentLine.slice(0, openCurly);
const after = currentLine.slice(closeCurly + 1);
const inner = currentLine.slice(openCurly + 1, closeCurly).split(',').map(s => s.trim());
for (const key of inner) {
if (key === '_') {
newResults.push(`${before}${after}`);
} else {
newResults.push(`${before}${key}${after}`);
}
}
}
results.length = 0;
results.push(...newResults);
}
return results;
}
/**
* Simple transpiler to convert swhkd-style hotkey config to Hyprland format
*/
function transpileHotkeys(input: string): string[] {
// replace `{<other keys>, 1-9, <other keys>}` with `{<other keys>, 1, 2, 3, <abbrev>, 8, 9, <other keys>}`
input = input.replace(/({[\s\w,]*)(\d-\d)([\s\w,]*)/g, (_, before, range, after) => {
const [start, end] = range.split('-').map(Number);
const rangeKeys = Array.from({length: end - start + 1}, (_, i) => (start + i).toString());
return `${before}${rangeKeys.join(', ')}${after}`;
});
const lines = input.split('\n');
type Binding = {
modKeys: string[],
keys: string[],
command: string,
flags: string[]
}
const bindings: Array<Binding> = [];
let flags: string[] = [];
let command = '';
let currentBindings: Binding[] = [];
for (const line of lines) {
if (!line || line.trim().startsWith('#')) continue; // skip empty lines and comments
if (line.startsWith('(')) {
// flags
flags = line.slice(1, line.indexOf(')'))
.split(',')
.map(flag => {
flag = flag.trim();
return flagMappings[flag] || flag;
});
} else if (!line.startsWith(' ')) {
getExpandedLines(line).map((expLine, i) => {
let modKeys: string[] = [];
let keys: string[] = [];
expLine.split('+').map(key => key.trim()).forEach(key => {
if (MODIFIER_KEYS.includes(key)) {
modKeys.push(key);
} else {
keys.push(key);
}
})
currentBindings[i] = {
modKeys,
keys,
command: "",
flags
}
})
} else {
// multi line command
if (line.endsWith('\\')) {
command += line.slice(0, -1).trim() + ' ';
continue;
} else {
command += line.trim();
getExpandedLines(command).forEach((expCommand, i) => {
if (currentBindings[i]) currentBindings[i].command = expCommand;
})
// reset
command = '';
flags = [];
bindings.push(...currentBindings);
currentBindings = [];
}
}
}
return bindings.map(binding => {
const modKeys = binding.modKeys.join(' ');
const keys = binding.keys.join(' ');
const flags = binding.flags.join('');
if (binding.command.startsWith('hyprctl dispatch')) {
const [, dispatcher, args] = binding.command.match(/hyprctl dispatch ([\w:]+)( .*)?/) || [];
return `bind${flags} = ${modKeys}, ${keys}, ${dispatcher}, ${args ?? ''}`;
} else {
return `bind${flags} = ${modKeys}, ${keys}, exec, ${binding.command}`;
}
});
}
// Generate the bindings
console.log('Transpiling hotkey configuration...');
const bindings = transpileHotkeys(input);
if (bindings.length === 0) {
throw new Error('Error: No bindings were generated. Check your hotkeys.conf file.');
}
console.log(`Generated ${bindings.length} bindings.`);
const transpiledContent = bindings.join('\n');
// Define the markers for the transpiled section
const startMarker = '# >>> HOTKEY CONFIG TRANSPILED SECTION >>>';
const endMarker = '# <<< HOTKEY CONFIG TRANSPILED SECTION <<<';
// Find the markers in the config
const startIndex = hyprlandConfig.indexOf(startMarker);
const endIndex = hyprlandConfig.indexOf(endMarker);
let updatedConfig: string;
if (startIndex === -1 || endIndex === -1) {
console.log('Markers not found in hyprland.conf. Adding them...');
// Find a good place to insert the transpiled section
const bindIdx = hyprlandConfig.indexOf('bind = ');
if (bindIdx !== -1) {
// Insert before the first bind
const insertPos = hyprlandConfig.lastIndexOf('\n', bindIdx);
if (insertPos !== -1) {
updatedConfig =
hyprlandConfig.substring(0, insertPos) +
`\n\n${startMarker}\n\n${transpiledContent}\n\n${endMarker}\n` +
hyprlandConfig.substring(insertPos);
} else {
updatedConfig =
`${startMarker}\n\n${transpiledContent}\n\n${endMarker}\n\n` +
hyprlandConfig;
}
} else {
// Append to the end
updatedConfig =
hyprlandConfig +
`\n\n${startMarker}\n\n${transpiledContent}\n\n${endMarker}\n`;
}
} else {
// Replace the content between markers
updatedConfig =
hyprlandConfig.substring(0, startIndex + startMarker.length) +
`\n\n${transpiledContent}\n\n` +
hyprlandConfig.substring(endIndex);
}
// Write the updated config
try {
await Bun.write(outputPath, updatedConfig);
console.log(`Successfully updated ${outputPath} with ${bindings.length} transpiled bindings.`);
} catch (error) {
throw new Error(`Error writing to ${outputPath}: ${error}`);
}
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment