summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-14 00:26:07 +0900
committerAdam Malczewski <[email protected]>2026-06-14 00:26:07 +0900
commit151fbb589f99aa25f40c2ddb2562c992ef5d3adf (patch)
treeb6016ed63a86fa1f131ac393a9c1e504269ee483
parentf7df36c8f656652e8245d781b35cb3252f17bad2 (diff)
downloadunbox-151fbb589f99aa25f40c2ddb2562c992ef5d3adf.tar.gz
unbox-151fbb589f99aa25f40c2ddb2562c992ef5d3adf.zip
ext-keybindings + ext-stage-dock: config-driven dock-toggle-visible (Super+d)
- **ext-stage-dock**: exports a `Service` interface with `toggle_visible()`. The extension inherits from it and registers via `provide_service` in `activate()`. The method slides the dock in/out (using the existing RCSS transition) regardless of slot count; showing an empty dock is valid. - **ext-keybindings**: new `Action::dock_toggle_visible` action, mapped from `"dock-toggle-visible"` in `unbox.toml [[keybind]]`, dispatched to the stage-dock Service. Default binding: `Super+d`. - **Manifest**: ext-keybindings now `depends_on {"xdg-shell", "stage-dock"}`. - **Build**: subdir order swapped so ext-stage-dock builds before ext-keybindings; `ext_stage_dock_dep` is a link-time dep of the ext-keybindings library and transitively exposed via `ext_keybindings_dep`. - **Tests**: glue tests install ext-stage-dock alongside ext-xdg-shell; policy test expects 6 default bindings. All 10/10 green on build + build-asan. Configure in unbox.toml: [[keybind]] keys = "Super+d" action = "dock-toggle-visible"
-rw-r--r--meson.build2
-rw-r--r--packages/ext-keybindings/meson.build12
-rw-r--r--packages/ext-keybindings/src/config.hpp1
-rw-r--r--packages/ext-keybindings/src/extension.cpp22
-rw-r--r--packages/ext-keybindings/src/policy.hpp15
-rw-r--r--packages/ext-keybindings/tests/test_glue.cpp14
-rw-r--r--packages/ext-keybindings/tests/test_policy.cpp8
-rw-r--r--packages/ext-stage-dock/include/unbox/ext-stage-dock/ext_stage_dock.hpp22
-rw-r--r--packages/ext-stage-dock/src/extension.cpp34
9 files changed, 105 insertions, 25 deletions
diff --git a/meson.build b/meson.build
index 6237ad7..17668f5 100644
--- a/meson.build
+++ b/meson.build
@@ -60,6 +60,6 @@ add_test_setup(
subdir('packages/kernel')
subdir('packages/ext-xdg-shell')
subdir('packages/ext-layer-shell')
-subdir('packages/ext-keybindings')
subdir('packages/ext-stage-dock')
+subdir('packages/ext-keybindings')
subdir('packages/host-bin')
diff --git a/packages/ext-keybindings/meson.build b/packages/ext-keybindings/meson.build
index 4629fed..4573810 100644
--- a/packages/ext-keybindings/meson.build
+++ b/packages/ext-keybindings/meson.build
@@ -16,23 +16,25 @@ ext_keybindings_inc = include_directories('include')
tomlplusplus_dep = dependency('tomlplusplus')
# Glue library. Needs the kernel ABI, ext-xdg-shell's public contract (the glue
-# includes its header to consume the Service + Toplevel), xkbcommon (the combo
+# includes its header to consume the Service + Toplevel), ext-stage-dock's public
+# contract (the dock Service for dock-toggle-visible), xkbcommon (the combo
# parser resolves keysym names), and toml++ (the config loader).
ext_keybindings_lib = static_library(
'unbox-ext-keybindings',
'src/extension.cpp',
'src/config.cpp',
include_directories: ext_keybindings_inc,
- dependencies: [kernel_dep, ext_xdg_shell_dep, xkbcommon_dep, tomlplusplus_dep],
+ dependencies: [kernel_dep, ext_xdg_shell_dep, ext_stage_dock_dep,
+ xkbcommon_dep, tomlplusplus_dep],
)
# What host-bin links against: the factory. kernel_dep rides through for the
-# Extension ABI the factory returns. ext-xdg-shell stays a build-time dep of OUR
-# lib only (consumers of the factory do not need it).
+# Extension ABI the factory returns. ext_stage_dock_dep rides so consumers
+# (including host-bin and the glue test) can resolve the dock Service type.
ext_keybindings_dep = declare_dependency(
link_with: ext_keybindings_lib,
include_directories: ext_keybindings_inc,
- dependencies: [kernel_dep],
+ dependencies: [kernel_dep, ext_stage_dock_dep],
)
# Tests, asymmetric: the pure decision cores doctest-hard (combo parser, toml
diff --git a/packages/ext-keybindings/src/config.hpp b/packages/ext-keybindings/src/config.hpp
index 967a997..072c357 100644
--- a/packages/ext-keybindings/src/config.hpp
+++ b/packages/ext-keybindings/src/config.hpp
@@ -34,6 +34,7 @@ struct LoadResult {
// USER-APPROVED schema:
// keys = "Mod+...+Key" | "Mod" (required, string)
// action = "spawn" | "focus-next" | "focus-prev" | "close-active" | "quit"
+// | "dock-toggle-visible"
// command = "..." (required for action="spawn")
// Each entry is validated independently: a malformed combo, unknown action,
// missing/empty keys, missing command for spawn, or wrong value types skip that
diff --git a/packages/ext-keybindings/src/extension.cpp b/packages/ext-keybindings/src/extension.cpp
index 273fd47..a133ff5 100644
--- a/packages/ext-keybindings/src/extension.cpp
+++ b/packages/ext-keybindings/src/extension.cpp
@@ -4,6 +4,7 @@
#include "focus_ring.hpp"
#include "policy.hpp"
+#include <unbox/ext-stage-dock/ext_stage_dock.hpp>
#include <unbox/ext-xdg-shell/ext_xdg_shell.hpp>
#include <unbox/kernel/host.hpp>
#include <unbox/kernel/wlr.hpp>
@@ -155,15 +156,22 @@ public:
void activate(Host& host) override {
host_ = &host;
- // The ONLY fatal: a missing ext-xdg-shell Service (our focus ring + the
- // window-targeting actions are meaningless without it; depends_on
- // guarantees it activated first, so absence is a broken core session).
+ // The ONLY fatals: missing ext-xdg-shell Service (actions that target
+ // windows — focus, close — are meaningless without it), and missing
+ // ext-stage-dock Service (dock-toggle-visible is a no-op without docks).
+ // depends_on guarantees both activate first, so absence is a broken core.
shell_ = host.service<ext_xdg_shell::Service>();
if (shell_ == nullptr) {
throw std::runtime_error(
"ext-keybindings: ext-xdg-shell Service unavailable (depends_on "
"\"xdg-shell\" not satisfied)");
}
+ dock_ = host.service<ext_stage_dock::Service>();
+ if (dock_ == nullptr) {
+ throw std::runtime_error(
+ "ext-keybindings: ext-stage-dock Service unavailable (depends_on "
+ "\"stage-dock\" not satisfied)");
+ }
// Input path: thread every key through the Matcher. Non-matching keys
// pass through untouched; a matched chord is consumed, a tap fires its
@@ -278,6 +286,9 @@ private:
case policy::Action::quit:
wl_display_terminate(host_->display());
return;
+ case policy::Action::dock_toggle_visible:
+ dock_->toggle_visible();
+ return;
}
}
@@ -308,7 +319,7 @@ private:
const kernel::Manifest manifest_{
.id = "keybindings",
.tier = kernel::Tier::core,
- .depends_on = {"xdg-shell"},
+ .depends_on = {"xdg-shell", "stage-dock"},
};
std::optional<std::string> config_path_;
@@ -327,7 +338,8 @@ private:
policy::FocusRing<Toplevel*> ring_;
Host* host_ = nullptr;
- ext_xdg_shell::Service* shell_ = nullptr; // borrow; fetched in activate()
+ ext_xdg_shell::Service* shell_ = nullptr; // borrow; fetched in activate()
+ ext_stage_dock::Service* dock_ = nullptr; // borrow; fetched in activate()
// RAII subscriptions + the file watch — destruction unsubscribes / stops the
// watch (listener-lifetime). Last members so they release FIRST at teardown,
diff --git a/packages/ext-keybindings/src/policy.hpp b/packages/ext-keybindings/src/policy.hpp
index fbb2b3e..c5f99d4 100644
--- a/packages/ext-keybindings/src/policy.hpp
+++ b/packages/ext-keybindings/src/policy.hpp
@@ -45,11 +45,12 @@ inline constexpr std::uint32_t keysym_super_r = 0xffec; // XKB_KEY_Super_R
// ---- Action vocabulary ------------------------------------------------------
enum class Action {
- spawn, // run `command` via `sh -c`
- focus_next, // rotate focus forward across mapped windows (wrapping)
- focus_prev, // rotate focus backward (wrapping)
- close_active, // close the focused toplevel (no-op if none)
- quit, // wl_display_terminate
+ spawn, // run `command` via `sh -c`
+ focus_next, // rotate focus forward across mapped windows (wrapping)
+ focus_prev, // rotate focus backward (wrapping)
+ close_active, // close the focused toplevel (no-op if none)
+ quit, // wl_display_terminate
+ dock_toggle_visible, // show/hide the stage dock sidebar
};
// Map an action token (lowercased) to the enum; nullopt = unknown action.
@@ -69,6 +70,9 @@ enum class Action {
if (s == "quit") {
return Action::quit;
}
+ if (s == "dock-toggle-visible") {
+ return Action::dock_toggle_visible;
+ }
return std::nullopt;
}
@@ -338,6 +342,7 @@ private:
add("Alt+Shift+Tab", Action::focus_prev, {});
add("Alt+F1", Action::focus_next, {});
add("Ctrl+Alt+BackSpace", Action::quit, {});
+ add("Super+d", Action::dock_toggle_visible, {});
return out;
}
diff --git a/packages/ext-keybindings/tests/test_glue.cpp b/packages/ext-keybindings/tests/test_glue.cpp
index e5b6fb8..2141961 100644
--- a/packages/ext-keybindings/tests/test_glue.cpp
+++ b/packages/ext-keybindings/tests/test_glue.cpp
@@ -2,6 +2,7 @@
#include <doctest/doctest.h>
#include <unbox/ext-keybindings/ext_keybindings.hpp>
+#include <unbox/ext-stage-dock/ext_stage_dock.hpp>
#include <unbox/ext-xdg-shell/ext_xdg_shell.hpp>
#include <unbox/kernel/server.hpp>
@@ -25,12 +26,13 @@ auto make_headless_server() -> std::unique_ptr<unbox::kernel::Server> {
} // namespace
-TEST_CASE("ext-keybindings installs and activates atop ext-xdg-shell") {
+TEST_CASE("ext-keybindings installs and activates atop ext-xdg-shell + ext-stage-dock") {
auto server = make_headless_server();
server->install(unbox::ext_xdg_shell::create());
+ server->install(unbox::ext_stage_dock::create());
server->install(unbox::ext_keybindings::create());
- // Topological activation runs xdg-shell first (keybindings depends_on it),
- // so keybindings finds the Service. A missing-Service throw would propagate.
+ // Topological activation runs xdg-shell first, then stage-dock, then
+ // keybindings (depends_on both). A missing-Service throw would propagate.
server->activate_extensions();
CHECK(!server->socket_name().empty());
}
@@ -38,18 +40,20 @@ TEST_CASE("ext-keybindings installs and activates atop ext-xdg-shell") {
TEST_CASE("ext-keybindings dispatches and shuts down cleanly") {
auto server = make_headless_server();
server->install(unbox::ext_xdg_shell::create());
+ server->install(unbox::ext_stage_dock::create());
server->install(unbox::ext_keybindings::create());
server->activate_extensions();
for (int i = 0; i < 5; ++i) {
CHECK(server->dispatch(10));
}
- // Destruction tears down the key_filter link + the three xdg-shell event
- // subscriptions in reverse declaration order with no leaked listeners.
+ // Destruction tears down the key_filter link + the xdg-shell event
+ // subscriptions + the stage-dock Service in reverse declaration order.
}
TEST_CASE("ext-keybindings degrades to defaults for a bad explicit config path") {
auto server = make_headless_server();
server->install(unbox::ext_xdg_shell::create());
+ server->install(unbox::ext_stage_dock::create());
// A non-existent --config path must NOT throw out of activate(); the
// extension logs and uses compiled defaults.
server->install(unbox::ext_keybindings::create(
diff --git a/packages/ext-keybindings/tests/test_policy.cpp b/packages/ext-keybindings/tests/test_policy.cpp
index 8b6b91b..d7fd32e 100644
--- a/packages/ext-keybindings/tests/test_policy.cpp
+++ b/packages/ext-keybindings/tests/test_policy.cpp
@@ -165,7 +165,7 @@ TEST_CASE("empty / keybind-less document yields zero bindings, not an error") {
TEST_CASE("compiled defaults match the documented out-of-the-box set") {
auto d = pol::default_bindings();
- REQUIRE(d.size() == 5);
+ REQUIRE(d.size() == 6);
CHECK(d[0].combo.is_tap);
CHECK(d[0].combo.modifiers == pol::mod_logo);
CHECK(d[0].action == Action::spawn);
@@ -178,6 +178,8 @@ TEST_CASE("compiled defaults match the documented out-of-the-box set") {
CHECK(d[3].action == Action::focus_next);
CHECK(d[4].combo == parse_combo("Ctrl+Alt+BackSpace").value());
CHECK(d[4].action == Action::quit);
+ CHECK(d[5].combo == parse_combo("Super+d").value());
+ CHECK(d[5].action == Action::dock_toggle_visible);
}
// ============================================================================
@@ -236,11 +238,11 @@ TEST_CASE("reload: a valid-but-empty doc keeps the old table (never drops workin
// mid-edit with the [[keybind]] block deleted, or every entry malformed)
// must NOT swap — the user's live keys survive.
std::vector<Binding> live = pol::default_bindings();
- REQUIRE(live.size() == 5);
+ REQUIRE(live.size() == 6);
auto empty = cfg::reload_bindings(live, "title = \"unrelated\"\n");
CHECK_FALSE(empty.swapped);
- REQUIRE(empty.bindings.size() == 5);
+ REQUIRE(empty.bindings.size() == 6);
CHECK(empty.bindings == live); // kept
// Same for a doc where every entry is individually skipped (all malformed).
diff --git a/packages/ext-stage-dock/include/unbox/ext-stage-dock/ext_stage_dock.hpp b/packages/ext-stage-dock/include/unbox/ext-stage-dock/ext_stage_dock.hpp
index f56ed8c..6cbdbdb 100644
--- a/packages/ext-stage-dock/include/unbox/ext-stage-dock/ext_stage_dock.hpp
+++ b/packages/ext-stage-dock/include/unbox/ext-stage-dock/ext_stage_dock.hpp
@@ -22,6 +22,28 @@
namespace unbox::ext_stage_dock {
+// ---- Exported Service (cross-extension typed coupling) -----------------------
+//
+// Other extensions (ext-keybindings) fetch this via Host::service<T>() to drive
+// dock policy (hiding/showing the dock, minimizing windows, etc.). Registered
+// by this extension in activate(). See <unbox/kernel/host.hpp> for the service
+// registry contract — single-responder, type-keyed, no strings.
+class Service {
+public:
+ virtual ~Service() = default;
+
+ // Toggle the dock's visibility: if the dock is currently shown (slid into
+ // the visible area), hide it with the slide-out transition; if hidden, show
+ // it with the slide-in. Works regardless of whether the dock has slots
+ // (minimized windows) — showing an empty dock is valid. The auto-reveal on
+ // minimize (refresh_slots) is independent: minimizing a window also shows
+ // the dock.
+ virtual void toggle_visible() = 0;
+
+protected:
+ Service() = default;
+};
+
// Construct the extension (ownership transfer to the caller; host-bin installs
// it via Server::install at c2). Construction is cheap and side-effect free per
// the Extension contract; ALL wiring — once it exists — happens in activate().
diff --git a/packages/ext-stage-dock/src/extension.cpp b/packages/ext-stage-dock/src/extension.cpp
index 08e3800..685f6e4 100644
--- a/packages/ext-stage-dock/src/extension.cpp
+++ b/packages/ext-stage-dock/src/extension.cpp
@@ -96,7 +96,8 @@ struct Slot {
// -2dp thumb overscan clipped by the rounded overflow; d1 slot-enter animation;
// transform-origin 0% 0%).
-class StageDockExtension final : public kernel::Extension, public TestProbe {
+class StageDockExtension final : public kernel::Extension, public TestProbe,
+ public ext_stage_dock::Service {
public:
auto manifest() const -> const kernel::Manifest& override { return manifest_; }
@@ -163,6 +164,11 @@ public:
// gracefully (model still tracked, hide/show still works, no visual).
create_dock_surface();
+ // Register the Service so ext-keybindings (or any extension) can drive
+ // dock policy — toggle_visible, and future minimize/restore — through
+ // the typed cross-extension coupling (no strings, link-time safety).
+ host.provide_service<ext_stage_dock::Service>(this);
+
activated_ = true;
}
@@ -243,6 +249,32 @@ private:
refresh_slots();
}
+ // ---- Service: toggle_visible -------------------------------------------
+ void toggle_visible() override { do_toggle_visible(); }
+
+ void do_toggle_visible() {
+ if (dock_surface_ == nullptr) {
+ return;
+ }
+ if (open_) {
+ // Close: slide out. Keep the surface compositing so the animation
+ // plays; on_dock_settled won't hide it (closing_ is false), so a
+ // subsequent toggle re-opens instantly without delay.
+ open_ = false;
+ closing_ = false;
+ dock_surface_->dirty("open");
+ } else {
+ // Open: make the surface visible (may already be from slots), then
+ // slide in. If the dock was fully hidden (surface invisible), the
+ // set_visible(true) begins compositing; the dirty("open")
+ // transitions the body to translateX(0).
+ open_ = true;
+ closing_ = false;
+ dock_surface_->set_visible(true);
+ dock_surface_->dirty("open");
+ }
+ }
+
// ---- helpers ------------------------------------------------------------
// The first mapped window that is neither `except` nor currently minimized