From 678d0563edab76bc419c361409e12ef15c218b7d Mon Sep 17 00:00:00 2001 From: Ben Buhse Date: Fri, 6 Mar 2026 09:21:07 -0600 Subject: [PATCH] Add exit_river keybinding Recently, river removed the hardcoded Ctrl+Alt Delete keybinding that exits river and replaces it with a new `exit_session` request. This adds support for that request via the new `exit_session` bind. We also added 3 hardcoded default keybinds to: exit river, reload the config, and open foot. This way, if the config fails to load or is missing, you should still be able to try reload. I guess you're still SOL if you have at least one keybind and it's not reload_config, but you do what you can do. --- README.md | 9 +- build.zig | 2 +- docs/CONFIGURATION.md | 11 +- examples/config.kdl | 2 + man/beansprout.1.scd | 16 +- man/beansprout.5.scd | 3 + protocol/river-window-management-v1.xml | 333 +++++++++++++++++------- src/Config.zig | 18 ++ src/XkbBindings.zig | 3 + src/config/keybind.zig | 1 + src/main.zig | 3 +- 11 files changed, 293 insertions(+), 108 deletions(-) diff --git a/README.md b/README.md index 6a82723..029e90f 100644 --- a/README.md +++ b/README.md @@ -62,8 +62,13 @@ Run `zig build -h` to see a list of all options. You can either add `beansprout` to the river `init` file or directly run `river -c beansprout`. -If you need to exit river, for example, if `beansprout` crashes, you can use river's hardcoded -`Ctrl+Alt+Delete` keybind. +If no keybinds are configured (e.g. missing or broken config), beansprout provides +fallback keybinds including `Ctrl+Alt+Delete` to exit the River session, `Super+Shift+R` +to reload config, and `Super+T` to spawn a terminal. + +If beansprout crashes, you can use River's hardcoded `Ctrl+Alt F1`–`F12` bindings to +switch to another TTY. From there, you can kill River or restart beansprout (you will +need to set `WAYLAND_DISPLAY` yourself, e.g. `WAYLAND_DISPLAY=wayland-1 beansprout`). ## Configuration diff --git a/build.zig b/build.zig index 2640ad8..0fd779c 100644 --- a/build.zig +++ b/build.zig @@ -50,7 +50,7 @@ pub fn build(b: *std.Build) !void { scanner.generate("river_input_manager_v1", 1); scanner.generate("river_libinput_config_v1", 1); scanner.generate("river_layer_shell_v1", 1); - scanner.generate("river_window_manager_v1", 3); + scanner.generate("river_window_manager_v1", 4); scanner.generate("river_xkb_bindings_v1", 2); scanner.generate("zwlr_layer_shell_v1", 3); diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 07f36e2..713b4b1 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -10,8 +10,14 @@ If `$XDG_CONFIG_HOME` is not set, this defaults to `~/.config/beansprout/config. An example config can be found at [examples/config.kdl](../examples/config.kdl). -If the config file is missing, beansprout falls back to its built-in defaults (which -is missing keybinds!). Similarly, if an individual node or block is invalid, it will +If the config file is missing or fails to load, beansprout falls back to its built-in +defaults. If no keybinds are configured, the following fallback keybinds are added: + +- `Ctrl+Alt Delete` — exit the River session +- `Super+Shift R` — reload config +- `Super T` — spawn `foot` + +Similarly, if an individual node or block is invalid, it will try to ignore the error and continue on. All configuration is applied top down, so later options will overwrite earlier ones. @@ -261,6 +267,7 @@ Full command reference: | `toggle_float` | | Float/unfloat the focused window | | `toggle_fullscreen` | | Toggle fullscreen on focused window | | `close_window` | | Close the focused window | +| `exit_river` | | Exit the river session | | `change_ratio` | float | Adjust primary/stack ratio on current output | | `increment_primary_count`| | Add a window to the primary side | | `decrement_primary_count`| | Remove a window from the primary side | diff --git a/examples/config.kdl b/examples/config.kdl index 2fa5222..2d4a182 100644 --- a/examples/config.kdl +++ b/examples/config.kdl @@ -83,6 +83,8 @@ keybinds { toggle_fullscreen Mod4 F // Close the currently-focused window close_window Mod4+Shift Q + // Exit the entire river session + exit_river Ctrl+Alt Delete // Move windows up or down the stack swap_next Mod4+Shift N swap_prev Mod4+Shift P diff --git a/man/beansprout.1.scd b/man/beansprout.1.scd index da4d1a9..f1e75bd 100644 --- a/man/beansprout.1.scd +++ b/man/beansprout.1.scd @@ -35,8 +35,14 @@ You can either add *beansprout* to the river init file or directly run: river -c beansprout -If you need to exit river, for example if *beansprout* crashes, you can use -river's hardcoded *Ctrl+Alt+Delete* keybind. +If no keybinds are configured (e.g. missing or broken config), *beansprout* +provides fallback keybinds: *Ctrl+Alt+Delete* to exit the River session, +*Super+Shift+R* to reload config, and *Super+T* to spawn a terminal. + +If *beansprout* crashes, you can use River's hardcoded *Ctrl+Alt F1*–*F12* +bindings to switch to another TTY. From there, you can kill River or restart +*beansprout* (you will need to set *WAYLAND_DISPLAY* yourself, e.g. +_WAYLAND_DISPLAY=wayland-1 beansprout_). # CONFIGURATION @@ -46,9 +52,9 @@ this defaults to _~/.config/beansprout/config.kdl_. See *beansprout*(5) for a full configuration reference. -If the config file is missing, *beansprout* falls back to built-in defaults -(which will be missing keybinds). If an individual node or block is invalid, -*beansprout* will try to ignore the error and continue. +If the config file is missing or fails to load, *beansprout* falls back to +built-in defaults. If an individual node or block is invalid, *beansprout* +will try to ignore the error and continue. # AUTHORS diff --git a/man/beansprout.5.scd b/man/beansprout.5.scd index 2c8813c..eb9a66c 100644 --- a/man/beansprout.5.scd +++ b/man/beansprout.5.scd @@ -269,6 +269,9 @@ can typically be found at _/usr/include/xkbcommon/xkbcommon-keysyms.h_. *close_window* Close the focused window. +*exit_river* + Exit the river session. + *change_ratio* _modifiers_ _keysym_ _float_ Adjust the primary/stack ratio on the current output. diff --git a/protocol/river-window-management-v1.xml b/protocol/river-window-management-v1.xml index db170d4..dd9e556 100644 --- a/protocol/river-window-management-v1.xml +++ b/protocol/river-window-management-v1.xml @@ -32,14 +32,9 @@ The key words "must", "must not", "required", "shall", "shall not", "should", "should not", "recommended", "may", and "optional" in this document are to be interpreted as described in IETF RFC 2119. - - Warning! The protocol described in this file is currently in the testing - phase. Backward compatible changes may be added together with the - corresponding interface version bump. Backward incompatible changes can only - be done by creating a new major version of the extension. - + This global interface should only be advertised to the window manager process. Only one window management client may be active at a time. The @@ -272,7 +267,7 @@ This event will be followed by a manage_start event after all other new state has been sent by the server. - + @@ -284,7 +279,7 @@ events as well as a manage_start event after all other new state has been sent by the server. - + @@ -294,7 +289,7 @@ This event will be followed by a manage_start event after all other new state has been sent by the server. - + @@ -305,20 +300,34 @@ Providing a wl_surface which already has a role or already has a buffer attached or committed is a protocol error. - - + + + + + + + End the current Wayland session and exit the compositor. + All Wayland clients running in the current session, including + the window manager, will be disconnected. + + Window managers should only make this request if the user explicitly + asks to exit the Wayland session, not for example on normal window + manager termination. + - + This represents a logical window. For example, a window may correspond to an xdg_toplevel or Xwayland window. A newly created window will not be displayed until the window manager - proposes window dimensions with the propose_dimensions request as part of - a manage sequence, the server replies with a dimensions event as part of - a render sequence, and that render sequence is finished. + makes a propose_dimensions or fullscreen request as part of a manage + sequence, the server replies with a dimensions event as part of a render + sequence, and that render sequence is finished. @@ -379,7 +388,7 @@ It is a protocol error to make this request more than once for a single window. - + @@ -398,10 +407,10 @@ This event will be followed by a manage_start event after all other new state has been sent by the server. - - - - + + + + @@ -417,12 +426,15 @@ This event is sent as part of a render sequence before the render_start event. - It may be sent due to a propose_dimensions request in a previous manage - sequence or because a window independently decides to change its - dimensions. + It may be sent due to a propose_dimensions or fullscreen request in a + previous manage sequence or because a window independently decides to + change its dimensions. + + The window will not be displayed until the first dimensions event is + received and the render sequence is finished. - - + + @@ -442,10 +454,9 @@ When a propose_dimensions request is made, the server must send a dimensions event in response as soon as possible. It may not be possible to send a dimensions event in the very next render sequence if, for - example, the window takes too long to respond to the first proposed + example, the window takes too long to respond to the proposed dimensions. In this case, the server will send the dimensions event in a - future render sequence. The window will not be displayed until the first - dimensions event is received and the render sequence is finished. + future render sequence. Note that the dimensions of a river_window_v1 refer to the dimensions of the window content and are unaffected by the presence of borders or @@ -454,8 +465,8 @@ This request modifies window management state and may only be made as part of a manage sequence, see the river_window_manager_v1 description. - - + + @@ -496,7 +507,8 @@ This event will be followed by a manage_start event after all other new state has been sent by the server. - + @@ -510,7 +522,7 @@ This event will be followed by a manage_start event after all other new state has been sent by the server. - + @@ -530,7 +542,7 @@ state has been sent by the server. + interface="river_window_v1" summary="parent window, if any"/> @@ -555,7 +567,7 @@ This event will be followed by a manage_start event after all other new state has been sent by the server. - + @@ -621,12 +633,12 @@ This request modifies rendering state and may only be made as part of a render sequence, see the river_window_manager_v1 description. - - - - - - + + + + + + @@ -645,7 +657,7 @@ This request modifies window management state and may only be made as part of a manage sequence, see the river_window_manager_v1 description. - + @@ -657,8 +669,10 @@ Providing a wl_surface which already has a role or already has a buffer attached or committed is a protocol error. - - + + @@ -670,8 +684,10 @@ Providing a wl_surface which already has a role or already has a buffer attached or committed is a protocol error. - - + + @@ -690,7 +706,8 @@ This event will be followed by a manage_start event after all other new state has been sent by the server. - + @@ -713,8 +730,10 @@ This event will be followed by a manage_start event after all other new state has been sent by the server. - - + + @@ -766,7 +785,8 @@ This request modifies window management state and may only be made as part of a manage sequence, see the river_window_manager_v1 description. - + @@ -853,7 +873,7 @@ state has been sent by the server. + interface="river_output_v1" summary="fullscreen output requested"/> @@ -913,6 +933,12 @@ shall not affect the current position and dimensions of a fullscreen window. + When a fullscreen request is made, the server must send a dimensions + event in response as soon as possible. It may not be possible to send a + dimensions event in the very next render sequence if, for example, the + window takes too long to respond. In this case, the server will send the + dimensions event in a future render sequence. + The compositor will clip window content, decoration surfaces, and borders to the given output's dimensions while the window is fullscreen. The effects of set_clip_box and set_content_clip_box are ignored while @@ -928,7 +954,8 @@ This request modifies window management state and may only be made as part of a manage sequence, see the river_window_manager_v1 description. - + @@ -981,10 +1008,10 @@ This request modifies rendering state and may only be made as part of a render sequence, see the river_window_manager_v1 description. - - - - + + + + @@ -1000,7 +1027,7 @@ This event is sent once when the river_window_v1 is created and never sent again. - + @@ -1025,14 +1052,70 @@ This request modifies rendering state and may only be made as part of a render sequence, see the river_window_manager_v1 description. - - - - + + + + + + + + + This event communicates the window's preferred presentation mode. + + This event will be followed by a render_start event after all other new + state has been sent by the server. + + + + + + + The identifier is a string that contains up to 32 printable ASCII bytes. + The identifier must not be an empty string. + + It is compositor policy how the identifier is generated, but the following + properties must be upheld: + + 1. The identifier must uniquely identify the window. Two windows must not + share the same identifier. + + 2. The identifier must not be reused. This avoids races around window + creation/destruction when identifiers are used in out-of-band IPC. + + If the compositor implements the ext-foreign-toplevel-list-v1 protocol, + the river_window_v1.identifier event must match the corresponding + ext_foreign_toplevel_handle_v1.identifier event. + + This event is sent once when the river_window_v1 is created and never + sent again. + + + + + + + Recommend that the window keep its dimensions within a given + maximum width/height. This recommendation is only a hint and the window + may ignore it. + + Setting the width and height to 0 indicates that there are no bounds + and is equivalent to having never made this request. + + Setting width or height to a negative value is a protocol error. + + The server should communicate this hint to an xdg_toplevel window with + the xdg_toplevel.configure_bounds event for example. + + This request modifies window management state and may only be made as + part of a manage sequence, see the river_window_manager_v1 description. + + + - + The rendering order of windows with decorations is follows: @@ -1068,8 +1151,8 @@ This request modifies rendering state and may only be made as part of a render sequence, see the river_window_manager_v1 description. - - + + @@ -1088,7 +1171,7 @@ - + The window manager might use a shell surface to display a status bar, background image, desktop notifications, launcher, desktop menu, or @@ -1116,7 +1199,7 @@ It is a protocol error to make this request more than once for a single shell surface. - + @@ -1135,7 +1218,7 @@ - + The render list is a list of nodes that determines the rendering order of the compositor. Nodes may correspond to windows or shell surfaces. The @@ -1169,8 +1252,8 @@ This request modifies rendering state and may only be made as part of a render sequence, see the river_window_manager_v1 description. - - + + @@ -1203,7 +1286,8 @@ This request modifies rendering state and may only be made as part of a render sequence, see the river_window_manager_v1 description. - + @@ -1216,11 +1300,12 @@ This request modifies rendering state and may only be made as part of a render sequence, see the river_window_manager_v1 description. - + - + An area in the compositor's logical coordinate space that should be treated as a single output for window management purposes. This area may @@ -1297,8 +1382,8 @@ cause the areas of multiple logical outputs to overlap when the corresponding manage_start event is received. - - + + @@ -1317,21 +1402,63 @@ cause the areas of multiple logical outputs to overlap when the corresponding manage_start event is received. - - + + + + + + + + + + + Output page-flips should be synchronized to the vertical blanking + period, eliminating tearing. This is the default presentation mode. + + + + + Output page-flips should not be synchronized to the vertical blanking + period, visual screen tearing may occur. + + + + + + + Set the preferred presentation mode of the output. The compositor should + always respect the preference of the window manager if possible. If this + request is never made, the preferred presentation mode is vsync. + + This request modifies rendering state and may only be made as part of a + render sequence, see the river_window_manager_v1 description. + + + - + This object represents a single user's collection of input devices. It allows the window manager to route keyboard input to windows, get - high-level information about pointer input, define keyboard and pointer - bindings, etc. + high-level information about pointer input, define pointer bindings, etc. - TODO: - - touch input - - tablet input + For keyboard bindings, see the river-xkb-bindings-v1 protocol. + + Since version 4: The cursor surface/shape set by the window manager on the + wl_pointer of this seat is used when no client has pointer focus, for + example during a pointer operation. Since the window manager is allowed to + set cursor surface/shape even when it does not have pointer focus, the + compositor must ignore the serial argument of wl_pointer.set_cursor and + wp_cursor_shape_device_v1.set_shape requests made by the window manager. + + The most recent cursor surface/shape set by the window manager is + remembered by the compositor and restored whenever no client has pointer + focus. If the window manager never sets a cursor surface/shape, the + "default" shape is used. @@ -1390,7 +1517,8 @@ This request modifies window management state and may only be made as part of a manage sequence, see the river_window_manager_v1 description. - + @@ -1401,7 +1529,8 @@ This request modifies window management state and may only be made as part of a manage sequence, see the river_window_manager_v1 description. - + @@ -1430,7 +1559,8 @@ This event will be followed by a manage_start event after all other new state has been sent by the server. - + @@ -1461,7 +1591,8 @@ This event will be followed by a manage_start event after all other new state has been sent by the server. - + @@ -1483,7 +1614,8 @@ This event will be followed by a manage_start event after all other new state has been sent by the server. - + @@ -1502,6 +1634,11 @@ This request is ignored if an operation is already in progress. + The compositor must ensure that no client has pointer focus from this + seat during the pointer operation. This means that the window manager + has control over the pointer's cursor surface/shape during the pointer + operation. See the river_seat_v1 description. + This request modifies window management state and may only be made as part of a manage sequence, see the river_window_manager_v1 description. @@ -1565,8 +1702,8 @@ - Define a pointer binding in terms of a pointer button, modifiers, and - other configurable properties. + Define a pointer binding in terms of a pointer button, keyboard + modifiers, and other configurable properties. The button argument is a Linux input event code defined in the linux/input-event-codes.h header file (e.g. BTN_RIGHT). @@ -1574,9 +1711,11 @@ The new pointer binding is not enabled until initial configuration is completed and the enable request is made during a manage sequence. - + - + @@ -1588,8 +1727,8 @@ Note: The window manager may also wish to set the XCURSOR_THEME and XCURSOR_SIZE environment variable for programs it starts. - - + + @@ -1604,8 +1743,8 @@ sequence unless there is no change in x/y position since the last time this event was sent. - - + + @@ -1619,12 +1758,12 @@ This request modifies window management state and may only be made as part of a manage sequence, see the river_window_manager_v1 description. - - + + - + This object allows the window manager to configure a pointer binding and receive events when the binding is triggered. diff --git a/src/Config.zig b/src/Config.zig index 6475028..9130a10 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -127,9 +127,27 @@ pub fn create() !*Config { }; } + if (config.keybinds.count() == 0) { + config.addFallbackKeybinds(); + } + return config; } +fn addFallbackKeybinds(config: *Config) void { + const Entry = struct { keybind.Key, XkbBindings.Command }; + const fallbacks = [_]Entry{ + .{ .{ .modifiers = .{ .ctrl = true, .mod1 = true }, .keysym = .Delete }, @unionInit(XkbBindings.Command, "exit_river", {}) }, + .{ .{ .modifiers = .{ .mod4 = true, .shift = true }, .keysym = .R }, @unionInit(XkbBindings.Command, "reload_config", {}) }, + .{ .{ .modifiers = .{ .mod4 = true }, .keysym = .T }, .{ .spawn = utils.gpa.dupe([]const u8, &.{ + utils.gpa.dupe(u8, "foot") catch @panic("Out of memory"), + }) catch @panic("Out of memory") } }, + }; + for (&fallbacks) |*entry| { + config.keybinds.put(utils.gpa, entry[0], entry[1]) catch @panic("Out of memory"); + } +} + pub fn destroy(config: *Config) void { for (config.keybinds.values()) |cmd| { cmd.deinit(); diff --git a/src/XkbBindings.zig b/src/XkbBindings.zig index 3f9552a..e644e50 100644 --- a/src/XkbBindings.zig +++ b/src/XkbBindings.zig @@ -22,6 +22,7 @@ pub const Command = union(enum) { reload_config, toggle_fullscreen, close_window, + exit_river, // Tag management set_output_tags: u32, set_window_tags: u32, @@ -70,6 +71,7 @@ pub const Command = union(enum) { .reload_config, .toggle_fullscreen, .close_window, + .exit_river, .set_output_tags, .set_window_tags, .toggle_output_tags, @@ -246,6 +248,7 @@ const XkbBinding = struct { window.river_window_v1.close(); } }, + .exit_river => context.wm.river_window_manager_v1.exitSession(), .set_output_tags => |tags| { const seat = first_seat orelse return; const output = seat.focused_output orelse return; diff --git a/src/config/keybind.zig b/src/config/keybind.zig index 316e71f..1f8372f 100644 --- a/src/config/keybind.zig +++ b/src/config/keybind.zig @@ -140,6 +140,7 @@ pub fn load(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8) !void { .reload_config, .toggle_fullscreen, .close_window, + .exit_river, .increment_primary_count, .decrement_primary_count, .swap_next, diff --git a/src/main.zig b/src/main.zig index aec0c10..1334af5 100644 --- a/src/main.zig +++ b/src/main.zig @@ -262,7 +262,8 @@ fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, globals: * fatal("Failed to bind to river_layer_shell_v1: {any}", .{@errorName(e)}); }; } else if (mem.orderZ(u8, ev.interface, river.WindowManagerV1.interface.name) == .eq) { - globals.river_window_manager_v1 = registry.bind(ev.name, river.WindowManagerV1, 3) catch |e| { + if (ev.version < 4) utils.versionNotSupported(river.WindowManagerV1, ev.version, 4); + globals.river_window_manager_v1 = registry.bind(ev.name, river.WindowManagerV1, 4) catch |e| { fatal("Failed to bind to river_window_manager_v1: {any}", .{@errorName(e)}); }; } else if (mem.orderZ(u8, ev.interface, river.XkbBindingsV1.interface.name) == .eq) {