The Problem
Your shell needs to respond to keyboard shortcuts: Super+D to show the desktop, Super+Space to open the app launcher, Super+Q to quit an application. Without a centralized system, you'll have key handlers scattered across every component, conflicting with each other.
The Naive Approach
Put Keys.onPressed on every component:
Item {
Keys.onPressed: function(event) {
if (event.key === Qt.Key_D && event.modifiers & Qt.MetaModifier) {
showDesktop()
}
}
}Now the app launcher also has a Keys.onPressed for Super+Space. And the notification center has one for Super+N. And the control center has one for Super+S. They all fight for keyboard focus.
Think of a keyboard shortcut system like a switchboard operator. Every shortcut is a call that comes in. The operator (your shortcut manager) looks at who's calling (the key combination) and routes the call to the right person (the component action). No one picks up the phone directly — they wait for the operator to connect them.
The Idea
Create a ShortcutManager singleton that registers all global shortcuts in one place. Components register their actions with the manager. The manager handles key dispatch, conflict resolution, and user customization.
Let's Build It
// ShortcutManager.qml
pragma Singleton
import Quickshell
import QtQml
QtObject {
property var shortcuts: ({})
function register(id, key, modifiers, callback) {
shortcuts[id] = {
key: key,
modifiers: modifiers,
callback: callback
}
}
function unregister(id) {
delete shortcuts[id]
}
function handleEvent(event) {
for (var id in shortcuts) {
var s = shortcuts[id]
if (event.key === s.key && event.modifiers === s.modifiers) {
s.callback()
event.accepted = true
return true
}
}
return false
}
}In your main shell file, add a global key handler:
// shell.qml
ShellRoot {
Item {
anchors.fill: parent
focus: true
Keys.onPressed: function(event) {
ShortcutManager.handleEvent(event)
}
Component.onCompleted: {
ShortcutManager.register("launcher", Qt.Key_Space,
Qt.MetaModifier, function() { appLauncher.toggle() })
ShortcutManager.register("notifications", Qt.Key_N,
Qt.MetaModifier, function() { notificationCenter.toggle() })
ShortcutManager.register("control-center", Qt.Key_S,
Qt.MetaModifier | Qt.ShiftModifier, function() { controlCenter.toggle() })
ShortcutManager.register("quit", Qt.Key_Q,
Qt.MetaModifier | Qt.ControlModifier, function() { quitApp() })
}
}
}The ShortcutManager is a singleton that maps shortcut IDs to key/modifier combinations. Components call register() at startup. The main Keys.onPressed delegates to handleEvent(). This centralizes shortcut logic and makes it easy to expose for user customization.
Let's Improve It
Add user-configurable shortcuts from a config file:
// ShortcutManager.qml
function loadUserShortcuts() {
var configPath = Quickshell.configPath("shortcuts.json")
var file = Qt.open(configPath, "r")
if (file.valid) {
var data = JSON.parse(file.readAll())
for (var id in data) {
if (shortcuts[id]) {
shortcuts[id].key = data[id].key
shortcuts[id].modifiers = data[id].modifiers
}
}
}
}Support multi-key chords (e.g., Super+K, then R for "record screen"):
property bool chordMode: false
property var chordBuffer: []
function handleEvent(event) {
if (chordMode) {
chordBuffer.push(event.key)
checkChord()
return true
}
// ... normal handling
}Not releasing grab when popup closes. If a PopupWindow grabs keyboard input and doesn't release it on close, your global shortcuts stop working. Always call releaseKeyboard() or ensure visible: false properly releases the grab.
Conflicting shortcuts between shell and compositor. Super+d is a common "show desktop" shortcut in both the compositor and the shell. Either coordinate with the compositor (let it handle it) or use different key combinations. Check your compositor's config (Hyprland, Sway, etc.) for overlaps.
Hardcoding keycodes. Qt.Key_Space works cross-platform; 16777217 (raw keycode from xev) does not. Always use the Qt.Key_* constants for portability.
Under the Hood
Wayland keyboard handling differs from X11. In Wayland, the compositor controls keyboard focus — a client can't listen for keys it doesn't have focus. Shell components (layer surfaces) can request keyboard interactivity at different levels: none, exclusive, or on-demand. Your shell must be in the overlay layer with exclusive keyboard interactivity to reliably capture global shortcuts.
Exercises
- Centralize all global shortcuts in a ShortcutManager singleton
- Use Keys.onPressed on a single focused item, not scattered across components
- Support user-customizable shortcuts from a config file
- Be aware of Wayland keyboard focus model and compositor conflicts