summaryrefslogtreecommitdiffhomepage
path: root/packages/ext-keybindings/src/extension.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'packages/ext-keybindings/src/extension.cpp')
-rw-r--r--packages/ext-keybindings/src/extension.cpp75
1 files changed, 70 insertions, 5 deletions
diff --git a/packages/ext-keybindings/src/extension.cpp b/packages/ext-keybindings/src/extension.cpp
index b582a31..273fd47 100644
--- a/packages/ext-keybindings/src/extension.cpp
+++ b/packages/ext-keybindings/src/extension.cpp
@@ -147,6 +147,7 @@ class KeybindingsExt final : public kernel::Extension {
public:
explicit KeybindingsExt(std::optional<std::string> config_path)
: config_path_(std::move(config_path)),
+ effective_path_(discover_config_path(config_path_)),
matcher_(load_bindings(config_path_)) {}
auto manifest() const -> const kernel::Manifest& override { return manifest_; }
@@ -202,9 +203,64 @@ public:
// continues from wherever focus ACTUALLY is.
ring_.note_focused(e.toplevel);
});
+
+ // Config hot-reload: watch the EFFECTIVE config path so editing
+ // unbox.toml re-applies keybindings live, with no restart. We watch even
+ // if the file does not exist yet — the kernel fires on_change when a
+ // not-yet-existing file is CREATED, so a user who later writes the config
+ // gets it picked up. The FileWatch is a member (config_watch_) so the
+ // watch lives exactly as long as this extension; on_change is coalesced
+ // and error-isolated by the kernel. No path -> no watch (defaults stand).
+ if (effective_path_) {
+ config_watch_ = host.watch_file(*effective_path_, [this] { reload_config(); });
+ }
}
private:
+ // Re-read + re-parse the EFFECTIVE config file and SWAP the live binding
+ // table the key_filter link matches against. Runs on the event-loop thread
+ // from the FileWatch callback. ROBUST by construction: an unreadable file, a
+ // toml parse error, or a parse that yields zero usable bindings KEEPS the
+ // currently-active bindings and logs exactly ONE warning — a half-saved /
+ // broken-mid-edit file never drops the user's working keys, and nothing
+ // throws out of this callback (toml::parse_error is caught inside the pure
+ // config loader). On success the new table replaces matcher_ in place; the
+ // key_filter lambda reads matcher_ through `this`, so the swap takes effect
+ // immediately with NO re-subscribe.
+ void reload_config() {
+ if (!effective_path_) {
+ return; // defensive: only registered when a path exists
+ }
+
+ std::string text;
+ if (!read_file(*effective_path_, text)) {
+ // File vanished or became unreadable mid-edit (e.g. an editor's
+ // unlink+rename window). Keep the working keys; the watch stays
+ // armed for the next write.
+ wlr_log(WLR_ERROR,
+ "ext-keybindings: config '%s' not readable on reload; keeping current bindings",
+ effective_path_->c_str());
+ return;
+ }
+
+ // Pure swap-or-keep-old decision over the CURRENTLY-LIVE table.
+ config::ReloadDecision decision =
+ config::reload_bindings(matcher_.bindings(), text);
+ if (!decision.swapped) {
+ // Parse error or zero usable bindings: keep current, ONE warning.
+ wlr_log(WLR_ERROR,
+ "ext-keybindings: reload of '%s' failed (%zu issue(s)); keeping current bindings",
+ effective_path_->c_str(), decision.warnings.size());
+ return;
+ }
+
+ // SUCCESS: swap the live matcher (and thus its binding table) in place.
+ const std::size_t n = decision.bindings.size();
+ matcher_ = policy::Matcher(std::move(decision.bindings));
+ wlr_log(WLR_INFO, "ext-keybindings: config reloaded (%zu binding(s)) from '%s'",
+ n, effective_path_->c_str());
+ }
+
void run_action(const policy::Binding& b) {
switch (b.action) {
case policy::Action::spawn:
@@ -256,23 +312,32 @@ private:
};
std::optional<std::string> config_path_;
+ // The EFFECTIVE config path (explicit --config, else the discovered XDG /
+ // ~/.config path), resolved ONCE in the ctor. Drives both the initial load
+ // and the hot-reload watch + re-read. nullopt only when no HOME/XDG exists.
+ // Declared before matcher_ so it is initialized first (ctor init order).
+ std::optional<std::string> effective_path_;
// Decision cores (constructed before any wiring; matcher_ owns the parsed
- // bindings). Declared before the subscriptions so they outlive callbacks
- // that capture `this` (members tear down in reverse declaration order:
- // subscriptions drop first, then the cores).
+ // bindings). matcher_ is the LIVE binding table the key_filter link reads
+ // through `this`; reload_config() swaps it in place. Declared before the
+ // subscriptions so they outlive callbacks that capture `this` (members tear
+ // down in reverse declaration order: subscriptions drop first, then cores).
policy::Matcher matcher_;
policy::FocusRing<Toplevel*> ring_;
Host* host_ = nullptr;
ext_xdg_shell::Service* shell_ = nullptr; // borrow; fetched in activate()
- // RAII subscriptions — destruction unsubscribes (listener-lifetime). Last
- // members so they release FIRST at teardown, before the cores they touch.
+ // RAII subscriptions + the file watch — destruction unsubscribes / stops the
+ // watch (listener-lifetime). Last members so they release FIRST at teardown,
+ // before the cores their callbacks touch (matcher_, ring_). config_watch_'s
+ // on_change reads matcher_ + effective_path_, so it must die before them.
kernel::Subscription key_filter_;
kernel::Subscription mapped_;
kernel::Subscription unmapped_;
kernel::Subscription focused_;
+ kernel::FileWatch config_watch_;
};
} // namespace