2026年10月26日
hyprland emergency mode
Hyprland enters emergency mode when a Lua config error leaves no binds registered.
Emergency mode tripped: A lua config error resulted in no binds being registered. Emergency binds active: SUPER + Q -> any known terminal, SUPER + R -> hyprland-run, SUPER + M -> Exit
Since 0.55, hyprland.lua is loaded in place of hyprland.conf when present.
An uncaught error aborts the module that raised it and every statement after it is skipped, so an error raised before the bind registration loop yields zero binds.
The trigger condition is the bind count, not the error.
The three fallback binds are hardcoded and are the only remaining input path.
Implemented in src/config/lua/ConfigManager.cpp.
diagnosis
hyprctl configerrors returns empty.
It reports hyprlang parse errors only; Lua exceptions are not recorded there and are not written to hyprland.log.
Count the registered binds instead:
$ hyprctl binds | grep -c '^bind' 3
A count of 3 is the emergency set.
nil plugin namespaces
hl.plugin.<name> is nil unless that plugin is loaded.
Indexing it raises attempt to index a nil value.
local hc = hl.plugin.hyprchords
hc.chord("SUPER + SLASH", hl.plugin.hyprshortcuts.toggle, "System: Keybind cheatsheet")
Placed above the registration loop, this aborts keybinds.lua before any bind is registered.
Guard each access, and wrap the call so a plugin API mismatch degrades to a log line instead of aborting the module:
if hc and hs then
local ok, err = pcall(hc.chord, "SUPER + SLASH", hs.toggle, "System: Keybind cheatsheet")
if not ok then
print(("keybinds: cheatsheet chord failed (%s)"):format(tostring(err)))
end
end
hl.config({ plugin = { … } }) is subject to the same rule: configuring an unloaded plugin is an error.
Plugins are loaded after the first config pass.
hl.plugin.* is therefore nil on initial parse and populated only on the reload that follows.
offline config validation
The config loads under a standalone Lua interpreter with a stub hl global, which reports the error without restarting the compositor.
local any
any = setmetatable({}, {
__index = function() return any end,
__call = function() return { tag = "dispatch" } end,
})
hl = setmetatable({ plugin = {}, dsp = any },
{__index = function() return function() return any end end})
package.path = os.getenv("HOME") .. "/.config/hypr/?.lua;" .. package.path
for _, m in ipairs({"animations","autostart","decorations","environment","input",
"keybinds","monitor","plugins","variables","windowrules"}) do
local ok, err = pcall(require, "config/" .. m)
print(string.format("%-14s %s", m, ok and "OK" or ("FAILED: " .. tostring(err))))
end
Run once with plugin = {} and once with the plugin names populated.
Both must pass, since the first config pass always precedes plugin load.
the stub dispatch object must return a table with no index 1.
keybinds.lua classifies an entry as a group via rawget(e.action, 1) ~= nil, so a stub returning {"d"} makes every action appear to be a single-element group and reports errors that are not present in the config.
|
hyprpm
packaging
hyprpm is packaged separately from hyprland on arch and cachyos.
$ pacman -Ql hyprland | grep /usr/bin/ hyprland /usr/bin/Hyprland hyprland /usr/bin/hyprctl hyprland /usr/bin/hyprland hyprland /usr/bin/start-hyprland
plugins.lua invokes hyprpm reload -n through hl.exec_cmd, which does not propagate exit status.
A missing hyprpm produces no diagnostic.
state directory
State is kept under /var/cache, not ~/.local/share/hyprpm.
/var/cache/hyprpm/$USER/
├── headersRoot/
├── state.toml
└── <plugin>/
├── <plugin>.so
└── state.toml
The tree is root owned; hyprpm elevates on its own and should not be invoked under sudo.
The top-level state.toml records the compositor build the plugins were compiled against:
[state]
dont_warn_install = true
hash = 'a0136d8c04687bb36eb8a28eb9d1ff92aea99704_aq_0.12_hu_0.13_hg_0.5_hc_0.1_hlg_0.6'
The hash is the Hyprland commit followed by the aquamarine, hyprutils, hyprgraphics, hyprcursor and hyprlang versions.
Compare it against hyprctl version.
Plugin loading is gated on an exact match.
A plugin compiled against one Hyprland build is not loaded into another, and the only symptom is hyprctl plugin list reporting no plugins loaded.
a0136d8c is 0.55.4; the running compositor was 0.56.2.
The per-plugin state.toml records the enabled flag and the pinned repository commit:
[hyprchords]
enabled = true
failed = false
filename = 'hyprchords.so'
update semantics
✖ Hyprland's ABI changed and some repositories failed to update: no plugins will be loaded until every repository updates successfully.
A single repository that fails to build blocks every plugin, including repositories that built successfully and repositories with enabled = false.
Remove the failing repository and re-run the update:
$ hyprpm remove <repo> $ hyprpm update
source of truth
→ Cloning https://github.com/0x42697262/hyprshortcuts
hyprpm builds from the repository URL recorded in state.toml.
The local working tree is not consulted, so changes must be pushed before hyprpm update will build them.
building against the installed headers
hyprpm reports only failed to build.
To obtain compiler diagnostics, build the plugin directly against the headers it installed:
$ export PKG_CONFIG_PATH=/var/cache/hyprpm/$USER/headersRoot/share/pkgconfig:$PKG_CONFIG_PATH $ make all
hyprland.pc in that directory carries a Version: field, which identifies the header set in use.
hyprland 0.56 plugin api
0.56 relocates monitor and workspace ownership out of CCompositor into dedicated State:: trackers.
Two headers move, and the direct accessors are removed rather than renamed.
Headers:
src/helpers/Monitor.hpp-
now
src/output/Monitor.hpp src/managers/PointerManager.hpp-
now
src/pointer/PointerManager.hpp
Accessors:
g_pCompositor→m_monitors-
State::monitorState()→monitors() g_pCompositor→getMonitorFromCursor()-
State::monitorState()→query().vec(Pointer::mgr()→position()).run() g_pCompositor→getMonitorFromName(n)-
State::monitorState()→query().name(n).run() g_pCompositor→getWorkspacesCopy()-
State::workspaceState()→workspaceRefs() g_pPointerManager-
Pointer::mgr()
The monitor getters are replaced by a builder, CMonitorQuery, declared in src/state/MonitorQuery.hpp.
It chains
.id() .name() .description() .selector() .configString() .vec() .output() .inDirection() .relativeTo() .includeDisabled()
and .run() resolves to a PHLMONITOR.
Every method is && qualified, so the query is usable only as a temporary, i.e. chained in a single expression.
Workspaces take the equivalent CWorkspaceQuery.
UP<> requires #include <src/helpers/memory/Memory.hpp>.
getWorkspacesCopy() → workspaceRefs() is not a rename.
The former returns a copy, the latter returns live PHLWORKSPACEREF handles.
Capturing those in a lambda does not have the same semantics.
|