From a1bd35694344a9e517b43d945d048a1c41b55bf9 Mon Sep 17 00:00:00 2001 From: Ben Buhse Date: Mon, 16 Mar 2026 08:41:49 -0500 Subject: [PATCH] Implement river-xkb-config-v1 This commit adds support for the river-xkb-config-v1 protocol. There's a new keyboard_layout block in config that can take options from xkeyboard-config(7). --- build.zig | 2 + docs/CONFIGURATION.md | 37 +++- examples/config.kdl | 8 + man/beansprout.5.scd | 39 +++- protocol/river-xkb-config-v1.xml | 275 ++++++++++++++++++++++++++++ src/Bar.zig | 2 +- src/Config.zig | 17 ++ src/Context.zig | 9 + src/WindowManager.zig | 5 + src/XkbConfig.zig | 242 ++++++++++++++++++++++++ src/config/KeyboardLayoutConfig.zig | 75 ++++++++ src/main.zig | 9 +- 12 files changed, 714 insertions(+), 6 deletions(-) create mode 100644 protocol/river-xkb-config-v1.xml create mode 100644 src/XkbConfig.zig create mode 100644 src/config/KeyboardLayoutConfig.zig diff --git a/build.zig b/build.zig index 0fd779c..a341e84 100644 --- a/build.zig +++ b/build.zig @@ -41,6 +41,7 @@ pub fn build(b: *std.Build) !void { scanner.addCustomProtocol(b.path("protocol/river-layer-shell-v1.xml")); scanner.addCustomProtocol(b.path("protocol/river-window-management-v1.xml")); scanner.addCustomProtocol(b.path("protocol/river-xkb-bindings-v1.xml")); + scanner.addCustomProtocol(b.path("protocol/river-xkb-config-v1.xml")); scanner.addSystemProtocol("stable/xdg-shell/xdg-shell.xml"); // dep of wlr-layer-shell-unstable-v1 scanner.addCustomProtocol(b.path("protocol/wlr-layer-shell-unstable-v1.xml")); @@ -52,6 +53,7 @@ pub fn build(b: *std.Build) !void { scanner.generate("river_layer_shell_v1", 1); scanner.generate("river_window_manager_v1", 4); scanner.generate("river_xkb_bindings_v1", 2); + scanner.generate("river_xkb_config_v1", 1); scanner.generate("zwlr_layer_shell_v1", 3); const options = b.addOptions(); diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 713b4b1..45a8ec5 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -13,9 +13,9 @@ An example config can be found at [examples/config.kdl](../examples/config.kdl). 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` +- `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. @@ -201,6 +201,37 @@ tag_overlay { | `square_inactive_border_color` | color | `0x6c7086` | Inactive tag square border | | `square_inactive_occupied_color` | color | `0xcdd6f4` | Inactive tag occupied indicator | +## Keyboard Layout + +Keyboard layout settings are placed inside a `keyboard_layout` block. These +configure the XKB keymap applied to all keyboard devices via the +`river-xkb-config` protocol. All fields are optional and default to +system/xkbcommon defaults. + +```kdl +keyboard_layout { + layout "us" + variant "dvorak" + options "compose:rctrl" +} +``` + +| Setting | Type | Default | Description | +|-----------|--------|----------------|--------------------------------------------------| +| `rules` | string | system default | XKB rules file (almost always `evdev`) | +| `model` | string | system default | Keyboard model (e.g., `pc104`, `pc105`) | +| `layout` | string | system default | Keyboard layout (e.g., `us`, `de`, `fr`) | +| `variant` | string | none | Layout variant (e.g., `dvorak`, `colemak`, `intl`)| +| `options` | string | none | XKB options (e.g., `compose:rctrl`, `caps:escape`)| + +Multiple options can be separated by commas: `"compose:rctrl,caps:escape"`. + +If the `keyboard_layout` block is removed from the config and reloaded, beansprout +will revert to the system/xkbcommon default keymap. + +See `xkeyboard-config(7)` for a full list of available rules, models, layouts, +variants, and options. + ## Keybinds Keyboard bindings are placed inside a `keybinds` block. Each binding has the diff --git a/examples/config.kdl b/examples/config.kdl index 2d4a182..84d702f 100644 --- a/examples/config.kdl +++ b/examples/config.kdl @@ -123,6 +123,14 @@ pointer_binds { // tiled windows will automatically float if resized resize_window Mod4 BTN_RIGHT } + +// Keyboard layout configuration +// All fields are optional and default to system/xkbcommon defaults +keyboard_layout { + layout "us" + // variant "dvorak" + options "compose:rctrl" +} // Default input config for all devices input { accel_profile "flat" diff --git a/man/beansprout.5.scd b/man/beansprout.5.scd index eb9a66c..a02dbf6 100644 --- a/man/beansprout.5.scd +++ b/man/beansprout.5.scd @@ -216,6 +216,43 @@ default to 0. *top*, *right*, *bottom*, *left* +# KEYBOARD LAYOUT + +Keyboard layout settings are placed inside a *keyboard_layout* block. These +configure the XKB keymap applied to all keyboard devices via the +river-xkb-config protocol. All fields are optional and default to +system/xkbcommon defaults. + +``` +keyboard_layout { + layout "us" + variant "dvorak" + options "compose:rctrl" +} +``` + +*rules* _rules_ + XKB rules file. Almost always "evdev". (Default: system default) + +*model* _model_ + Keyboard model, e.g., "pc104", "pc105". (Default: system default) + +*layout* _layout_ + Keyboard layout, e.g., "us", "de", "fr". (Default: system default) + +*variant* _variant_ + Layout variant, e.g., "dvorak", "colemak", "intl". + +*options* _options_ + XKB options, e.g., "compose:rctrl", "caps:escape". Multiple options + can be separated by commas. + +If the *keyboard_layout* block is removed from the config and reloaded, +beansprout will revert to the system/xkbcommon default keymap. + +See *xkeyboard-config*(7) for a full list of available rules, models, layouts, +variants, and options. + # KEYBINDS Keyboard bindings are placed inside a *keybinds* block. Each binding has the @@ -383,4 +420,4 @@ All libinput configuration options supported by river are also supported by # SEE ALSO -*beansprout*(1), *river*(1), *strftime*(3) +*beansprout*(1), *river*(1), *strftime*(3), *xkeyboard-config*(7) diff --git a/protocol/river-xkb-config-v1.xml b/protocol/river-xkb-config-v1.xml new file mode 100644 index 0000000..eb57b91 --- /dev/null +++ b/protocol/river-xkb-config-v1.xml @@ -0,0 +1,275 @@ + + + + SPDX-FileCopyrightText: © 2026 Isaac Freund + SPDX-License-Identifier: MIT + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + IN THE SOFTWARE. + + + + This protocol allow a client to set the xkbcommon keymap of individual + keyboard input devices. It also allows switching between the layouts of a + keymap and toggling capslock/numlock state. + + 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. + + + + + Global interface for configuring xkb devices. + + This global should only be advertised if river_input_manager_v1 is + advertised as well. + + + + + + + + + + This request indicates that the client no longer wishes to receive + events on this object. + + The Wayland protocol is asynchronous, which means the server may send + further events until the stop request is processed. The client must wait + for a river_xkb_config_v1.finished event before destroying this object. + + + + + + This event indicates that the server will send no further events on this + object. The client should destroy the object. See + river_xkb_config_v1.destroy for more information. + + + + + + This request should be called after the finished event has been received + to complete destruction of the object. + + It is a protocol error to make this request before the finished event + has been received. + + If a client wishes to destroy this object it should send a + river_xkb_config_v1.stop request and wait for a + river_xkb_config_v1.finished event. Once the finished event is received + it is safe to destroy this object and any other objects created through + this interface. + + + + + + + + + + + The server must be able to mmap the fd with MAP_PRIVATE. + The server will fstat the fd to obtain the size of the keymap. + The client must not modify the contents of the fd after making this request. + The client should seal the fd with fcntl. + + + + + + + + + A new xkbcommon keyboard has been created. Not every + river_input_device_v1 is necessarily an xkbcommon keyboard as well. + + + + + + + + This object is the result of attempting to create an xkbcommon keymap. + + + + + This request indicates that the client will no longer use the keymap + object and that it may be safely destroyed. + + + + + + The keymap object was successfully created and may be used with the + river_xkb_keyboard_v1.set_keymap request. + + + + + + The compositor failed to create a keymap from the given parameters. + + It is a protocol error to use this keymap object with + river_xkb_keyboard_v1.set_keymap. + + + + + + + + This object represent a physical keyboard which has its configuration and + state managed by xkbcommon. + + + + + + + + + This request indicates that the client will no longer use the keyboard + object and that it may be safely destroyed. + + + + + + This event indicates that the xkb keyboard has been removed. + + The server will send no further events on this object and ignore any + request (other than river_xkb_keyboard_v1.destroy) made after this event + is sent. The client should destroy this object with the + river_xkb_keyboard_v1.destroy request to free up resources. + + + + + + The river_input_device_v1 corresponding to this xkb keyboard. This event + will always be the first event sent on the river_xkb_keyboard_v1 object, + and it will be sent exactly once. + + + + + + + Set the keymap for the keyboard. + + It is a protocol error to pass a keymap object for which the + river_xkb_keymap_v1.success event was not received. + + + + + + + Set the active layout for the keyboard's keymap. Has no effect if the + layout index is out of bounds for the current keymap. + + + + + + + Set the active layout for the keyboard's keymap. Has no effect if there + is no layout with the give name for the keyboard's keymap. + + + + + + + The currently active layout index and name. The name arg may be null if + the active layout does not have a name. + + This event is sent once when the river_xkb_keyboard_v1 is created and + again whenever the layout changes. + + + + + + + + Enable capslock for the keyboard. + + + + + + Disable capslock for the keyboard. + + + + + + Capslock is currently enabled for the keyboard. + + This event is sent once when the river_xkb_keyboard_v1 is created and + again whenever the capslock state changes. + + + + + + Capslock is currently disabled for the keyboard. + + This event is sent once when the river_xkb_keyboard_v1 is created and + again whenever the capslock state changes. + + + + + + Enable numlock for the keyboard. + + + + + + Disable numlock for the keyboard. + + + + + + Numlock is currently enabled for the keyboard. + + This event is sent once when the river_xkb_keyboard_v1 is created and + again whenever the numlock state changes. + + + + + + Numlock is currently disabled for the keyboard. + + This event is sent once when the river_xkb_keyboard_v1 is created and + again whenever the numlock state changes. + + + + diff --git a/src/Bar.zig b/src/Bar.zig index 6cfbfc0..1a0fdf1 100644 --- a/src/Bar.zig +++ b/src/Bar.zig @@ -282,7 +282,7 @@ fn draw(bar: *Bar) !void { } }.f; - // Measure center first — needed to constrain left and right widths + // Measure center first; needed to constrain left and right widths const center_codepoints = componentSlice(options.center, clock_codepoints, title_codepoints, wm_info_codepoints); const center_width = try bar.textWidth(center_codepoints); var center_x: i32 = @divFloor(buffer.width - center_width, 2); diff --git a/src/Config.zig b/src/Config.zig index 9130a10..90cce78 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -46,6 +46,9 @@ tag_overlay_config: ?TagOverlayConfig = null, /// Bar configuration. If null, no bar is created. bar_config: ?BarConfig = null, +/// Keyboard layout configuration +keyboard_layout: KeyboardLayoutConfig = .{}, + /// Tag bind entries parsed from config (tag_bind nodes in keybinds block) tag_binds: std.ArrayList(Keybind) = .{}, // We use a hash map so that duplicate keybinds can be easily de-duplicated @@ -77,6 +80,7 @@ const NodeName = enum { bar, borders, input, + keyboard_layout, keybinds, pointer_binds, tag_overlay, @@ -120,6 +124,11 @@ pub fn create() !*Config { if (config.bar_config) |bc| { if (bc.fonts) |fonts| utils.gpa.free(fonts); } + if (config.keyboard_layout.rules) |s| utils.gpa.free(s); + if (config.keyboard_layout.model) |s| utils.gpa.free(s); + if (config.keyboard_layout.layout) |s| utils.gpa.free(s); + if (config.keyboard_layout.variant) |s| utils.gpa.free(s); + if (config.keyboard_layout.options) |s| utils.gpa.free(s); if (config.wallpaper_image_path) |path| { utils.gpa.free(path); } @@ -167,6 +176,11 @@ pub fn destroy(config: *Config) void { if (config.bar_config) |bc| { if (bc.fonts) |fonts| utils.gpa.free(fonts); } + if (config.keyboard_layout.rules) |s| utils.gpa.free(s); + if (config.keyboard_layout.model) |s| utils.gpa.free(s); + if (config.keyboard_layout.layout) |s| utils.gpa.free(s); + if (config.keyboard_layout.variant) |s| utils.gpa.free(s); + if (config.keyboard_layout.options) |s| utils.gpa.free(s); if (config.wallpaper_image_path) |path| { utils.gpa.free(path); } @@ -305,6 +319,7 @@ fn load(config: *Config, reader: *Io.Reader) !void { }, inline .bar, .borders, + .keyboard_layout, .keybinds, .pointer_binds, .tag_overlay, @@ -320,6 +335,7 @@ fn load(config: *Config, reader: *Io.Reader) !void { switch (child_block) { .bar => try BarConfig.load(config, &parser, hostname), .borders => try border.load(config, &parser, hostname), + .keyboard_layout => try KeyboardLayoutConfig.load(config, &parser, hostname), .keybinds => try keybind.load(config, &parser, hostname), .pointer_binds => try pointer_bind.load(config, &parser, hostname), .input => { @@ -375,6 +391,7 @@ const XkbBindings = @import("XkbBindings.zig"); const border = @import("config/border.zig"); const helpers = @import("config/helpers.zig"); +const KeyboardLayoutConfig = @import("config/KeyboardLayoutConfig.zig"); const keybind = @import("config/keybind.zig"); const pointer_bind = @import("config/pointer_bind.zig"); const window_rule = @import("config/window_rule.zig"); diff --git a/src/Context.zig b/src/Context.zig index c7f59d2..ebaa6a2 100644 --- a/src/Context.zig +++ b/src/Context.zig @@ -21,6 +21,7 @@ river_layer_shell_v1: *river.LayerShellV1, im: *InputManager, wm: *WindowManager, xkb_bindings: *XkbBindings, +xkb_config: ?*XkbConfig, /// Pool of Buffers used for rendering wallpapers buffer_pool: BufferPool = .{}, @@ -52,6 +53,7 @@ pub const Options = struct { river_layer_shell_v1: *river.LayerShellV1, river_window_manager_v1: *river.WindowManagerV1, river_xkb_bindings_v1: *river.XkbBindingsV1, + river_xkb_config_v1: *river.XkbConfigV1, config: *Config, }; @@ -66,6 +68,8 @@ pub fn create(options: Options) !*Context { errdefer wm.destroy(); const xkb_bindings = try XkbBindings.create(context, options.river_xkb_bindings_v1); errdefer xkb_bindings.destroy(); + const xkb_config = try XkbConfig.create(context, options.river_xkb_config_v1); + errdefer xkb_config.destroy(); var env = try process.getEnvMap(utils.gpa); errdefer env.deinit(); @@ -85,6 +89,7 @@ pub fn create(options: Options) !*Context { .im = im, .wm = wm, .xkb_bindings = xkb_bindings, + .xkb_config = xkb_config, .config = options.config, }; @@ -96,6 +101,9 @@ pub fn destroy(context: *Context) void { context.im.destroy(); context.wm.destroy(); context.xkb_bindings.destroy(); + if (context.xkb_config) |xkb_config| { + xkb_config.destroy(); + } if (context.wallpaper_image) |wallpaper_image| { wallpaper_image.destroy(); @@ -269,5 +277,6 @@ const TagOverlay = @import("TagOverlay.zig"); const Wallpaper = @import("Wallpaper.zig"); const WindowManager = @import("WindowManager.zig"); const XkbBindings = @import("XkbBindings.zig"); +const XkbConfig = @import("XkbConfig.zig"); const log = std.log.scoped(.Context); diff --git a/src/WindowManager.zig b/src/WindowManager.zig index 67bc845..c6126f8 100644 --- a/src/WindowManager.zig +++ b/src/WindowManager.zig @@ -134,6 +134,11 @@ fn initialize(wm: *WindowManager) void { } binding.enable(); } + + // Apply keyboard layout from config + if (context.xkb_config) |xkb_config| { + xkb_config.applyKeyboardLayout(); + } } fn manage(wm: *WindowManager) void { diff --git a/src/XkbConfig.zig b/src/XkbConfig.zig new file mode 100644 index 0000000..0db374f --- /dev/null +++ b/src/XkbConfig.zig @@ -0,0 +1,242 @@ +// SPDX-FileCopyrightText: 2026 Ben Buhse +// +// SPDX-License-Identifier: GPL-3.0-only + +const XkbConfig = @This(); + +context: *Context, + +xkb_config_v1: *river.XkbConfigV1, + +/// The keymap object sent to the compositor, if any. +/// Only valid to use with set_keymap once xkb_keymap_confirmed is true. +xkb_keymap_v1: ?*river.XkbKeymapV1 = null, +/// True once xkb_keymap_v1 has received the success event. +xkb_keymap_confirmed: bool = false, + +/// Tracked keyboards so we can apply keymaps to all of them. +keyboards: wl.list.Head(Keyboard, .link), + +const Keyboard = struct { + xkb_keyboard_v1: *river.XkbKeyboardV1, + xkb_config: *XkbConfig, + link: wl.list.Link, + + fn create(xkb_keyboard_v1: *river.XkbKeyboardV1, xkb_config: *XkbConfig) !*Keyboard { + const keyboard = try utils.gpa.create(Keyboard); + keyboard.* = .{ + .xkb_keyboard_v1 = xkb_keyboard_v1, + .xkb_config = xkb_config, + .link = undefined, + }; + xkb_keyboard_v1.setListener(*Keyboard, keyboardListener, keyboard); + xkb_config.keyboards.append(keyboard); + + // Only apply if the keymap has been confirmed via the success event. + // Applying an unconfirmed keymap is a protocol error. + if (xkb_config.xkb_keymap_confirmed) { + xkb_keyboard_v1.setKeymap(xkb_config.xkb_keymap_v1.?); + } + + return keyboard; + } + + fn destroy(keyboard: *Keyboard) void { + keyboard.link.remove(); + keyboard.xkb_keyboard_v1.destroy(); + utils.gpa.destroy(keyboard); + } + + fn keyboardListener(_: *river.XkbKeyboardV1, event: river.XkbKeyboardV1.Event, keyboard: *Keyboard) void { + switch (event) { + .removed => keyboard.destroy(), + .input_device, .layout, .capslock_enabled, .capslock_disabled, .numlock_enabled, .numlock_disabled => {}, + } + } +}; + +pub fn create(context: *Context, xkb_config_v1: *river.XkbConfigV1) !*XkbConfig { + const xkb_config = try utils.gpa.create(XkbConfig); + errdefer utils.gpa.destroy(xkb_config); + + xkb_config.* = .{ + .context = context, + .xkb_config_v1 = xkb_config_v1, + .keyboards = undefined, // we will initialize this shortly + }; + xkb_config.keyboards.init(); + + xkb_config_v1.setListener(*XkbConfig, riverXkbConfigV1Listener, xkb_config); + + return xkb_config; +} + +pub fn destroy(xkb_config: *XkbConfig) void { + var it = xkb_config.keyboards.safeIterator(.forward); + while (it.next()) |keyboard| { + keyboard.destroy(); + } + if (xkb_config.xkb_keymap_v1) |keymap_v1| { + keymap_v1.destroy(); + } + xkb_config.xkb_config_v1.destroy(); + utils.gpa.destroy(xkb_config); +} + +fn riverXkbConfigV1Listener(xkb_config_v1: *river.XkbConfigV1, event: river.XkbConfigV1.Event, xkb_config: *XkbConfig) void { + assert(xkb_config.xkb_config_v1 == xkb_config_v1); + switch (event) { + .finished => { + xkb_config.context.xkb_config = null; + xkb_config.destroy(); + }, + .xkb_keyboard => |ev| { + _ = Keyboard.create(ev.id, xkb_config) catch |e| { + log.err("Failed to create keyboard: {}", .{e}); + return; + }; + }, + } +} + +/// Build a keymap from the config's keyboard_layout settings and send it +/// to the compositor. On success, apply it to all tracked keyboards. +pub fn applyKeyboardLayout(xkb_config: *XkbConfig) void { + const layout_config = xkb_config.context.config.keyboard_layout; + + // If nothing is configured and we haven't previously applied a custom keymap, + // there's nothing to do. If we did previously apply one, fall through to apply + // the system default, reverting the custom keymap. + if (layout_config.rules == null and + layout_config.model == null and + layout_config.layout == null and + layout_config.variant == null and + layout_config.options == null and + !xkb_config.xkb_keymap_confirmed) + { + return; + } + + // Convert ?[]const u8 to ?[*:0]const u8 for xkbcommon + const rules_z = dupeZOrNull(layout_config.rules); + defer if (rules_z) |s| utils.gpa.free(s); + const model_z = dupeZOrNull(layout_config.model); + defer if (model_z) |s| utils.gpa.free(s); + const layout_z = dupeZOrNull(layout_config.layout); + defer if (layout_z) |s| utils.gpa.free(s); + const variant_z = dupeZOrNull(layout_config.variant); + defer if (variant_z) |s| utils.gpa.free(s); + const options_z = dupeZOrNull(layout_config.options); + defer if (options_z) |s| utils.gpa.free(s); + + const names = xkbcommon.RuleNames{ + .rules = if (rules_z) |s| s.ptr else null, + .model = if (model_z) |s| s.ptr else null, + .layout = if (layout_z) |s| s.ptr else null, + .variant = if (variant_z) |s| s.ptr else null, + .options = if (options_z) |s| s.ptr else null, + }; + + // Create xkb context and keymap + const xkb_context = xkbcommon.Context.new(.no_flags) orelse { + log.err("Failed to create XKB context", .{}); + return; + }; + defer xkb_context.unref(); + + const keymap = xkbcommon.Keymap.newFromNames(xkb_context, &names, .no_flags) orelse { + log.err("Invalid keyboard layout configuration", .{}); + return; + }; + defer keymap.unref(); + + // Serialize keymap to string + const keymap_str = keymap.getAsString(.text_v1) orelse { + log.err("Failed to serialize keymap", .{}); + return; + }; + const keymap_len = mem.len(keymap_str); + // Include the null terminator in the buffer sent to the compositor + const keymap_buf = keymap_str[0 .. keymap_len + 1]; + defer utils.gpa.free(keymap_buf); + + // Create memfd and write keymap + const fd = switch (builtin.target.os.tag) { + .linux => posix.memfd_createZ("beansprout-xkb-keymap", os.linux.MFD.CLOEXEC | os.linux.MFD.ALLOW_SEALING), + .freebsd => posix.memfd_createZ("beansprout-xkb-keymap", std.c.MFD.CLOEXEC | std.c.MFD.ALLOW_SEALING), + else => @compileError("target OS not supported"), + } catch |err| { + log.err("Failed to create memfd: {}", .{err}); + return; + }; + defer posix.close(fd); + + _ = posix.write(fd, keymap_buf) catch |err| { + log.err("Failed to write keymap to fd: {}", .{err}); + return; + }; + + // Send to compositor + const keymap_v1 = xkb_config.xkb_config_v1.createKeymap(fd, .text_v1) catch |err| { + log.err("Failed to create keymap object: {}", .{err}); + return; + }; + + // Destroy old keymap if any + if (xkb_config.xkb_keymap_v1) |old| { + old.destroy(); + } + + xkb_config.xkb_keymap_confirmed = false; + keymap_v1.setListener(*XkbConfig, keymapListener, xkb_config); + xkb_config.xkb_keymap_v1 = keymap_v1; +} + +fn keymapListener(_: *river.XkbKeymapV1, event: river.XkbKeymapV1.Event, xkb_config: *XkbConfig) void { + switch (event) { + .success => { + log.info("Keyboard layout applied successfully", .{}); + xkb_config.xkb_keymap_confirmed = true; + // Apply to all tracked keyboards + var it = xkb_config.keyboards.iterator(.forward); + while (it.next()) |keyboard| { + keyboard.xkb_keyboard_v1.setKeymap(xkb_config.xkb_keymap_v1.?); + } + }, + .failure => |ev| { + log.err("Failed to apply keyboard layout: {s}", .{ev.error_msg}); + xkb_config.xkb_keymap_confirmed = false; + if (xkb_config.xkb_keymap_v1) |keymap| { + keymap.destroy(); + xkb_config.xkb_keymap_v1 = null; + } + }, + } +} + +fn dupeZOrNull(opt: ?[]const u8) ?[:0]u8 { + const slice = opt orelse return null; + return utils.gpa.dupeZ(u8, slice) catch { + log.err("Out of memory", .{}); + return null; + }; +} + +const builtin = @import("builtin"); +const std = @import("std"); +const mem = std.mem; +const os = std.os; +const posix = std.posix; +const assert = std.debug.assert; + +const wayland = @import("wayland"); +const wl = wayland.client.wl; +const river = wayland.client.river; + +const xkbcommon = @import("xkbcommon"); + +const utils = @import("utils.zig"); +const Context = @import("Context.zig"); +const Seat = @import("Seat.zig"); + +const log = std.log.scoped(.XkbConfig); diff --git a/src/config/KeyboardLayoutConfig.zig b/src/config/KeyboardLayoutConfig.zig new file mode 100644 index 0000000..12893a3 --- /dev/null +++ b/src/config/KeyboardLayoutConfig.zig @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2026 Ben Buhse +// +// SPDX-License-Identifier: GPL-3.0-only + +const KeyboardLayoutConfig = @This(); + +const NodeName = enum { + rules, + model, + layout, + variant, + options, +}; + +rules: ?[]const u8 = null, +model: ?[]const u8 = null, +layout: ?[]const u8 = null, +variant: ?[]const u8 = null, +options: ?[]const u8 = null, + +pub fn load(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8) !void { + while (try parser.next()) |event| { + switch (event) { + .node => |node| { + const node_name = std.meta.stringToEnum(NodeName, node.name); + if (node_name) |name| { + if (!helpers.hostMatches(node, parser, hostname)) { + log.debug("Skipping \"keyboard_layout.{s}\" (host mismatch)", .{@tagName(name)}); + continue; + } + const value = utils.stripQuotes(node.arg(parser, 0) orelse { + logWarnMissingNodeArg(name); + continue; + }); + const duped = try utils.gpa.dupe(u8, value); + const target = switch (name) { + .rules => &config.keyboard_layout.rules, + .model => &config.keyboard_layout.model, + .layout => &config.keyboard_layout.layout, + .variant => &config.keyboard_layout.variant, + .options => &config.keyboard_layout.options, + }; + if (target.*) |old| { + utils.gpa.free(old); + } + target.* = duped; + logDebugSettingNode(name, value); + } else { + helpers.logWarnInvalidNode(node.name); + } + }, + .child_block_begin => try helpers.skipChildBlock(parser), + .child_block_end => return, + } + } +} + +inline fn logDebugSettingNode(node_name: NodeName, node_value: []const u8) void { + log.debug("Setting keyboard_layout.{s} to {s}", .{ @tagName(node_name), node_value }); +} + +inline fn logWarnMissingNodeArg(node_name: NodeName) void { + log.warn("\"keyboard_layout.{s}\" missing argument. Ignoring", .{@tagName(node_name)}); +} + +const std = @import("std"); + +const kdl = @import("kdl"); + +const utils = @import("../utils.zig"); +const Config = @import("../Config.zig"); + +const helpers = @import("helpers.zig"); + +const log = std.log.scoped(.config_keyboard_layout); diff --git a/src/main.zig b/src/main.zig index 1334af5..e69d48d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -9,6 +9,7 @@ const Globals = struct { river_layer_shell_v1: ?*river.LayerShellV1 = null, river_window_manager_v1: ?*river.WindowManagerV1 = null, river_xkb_bindings_v1: ?*river.XkbBindingsV1 = null, + river_xkb_config_v1: ?*river.XkbConfigV1 = null, wl_compositor: ?*wl.Compositor = null, wl_shm: ?*wl.Shm = null, @@ -65,6 +66,7 @@ pub fn main() !void { const river_layer_shell_v1 = globals.river_layer_shell_v1 orelse utils.interfaceNotAdvertised(river.LayerShellV1); const river_window_manager_v1 = globals.river_window_manager_v1 orelse utils.interfaceNotAdvertised(river.WindowManagerV1); const river_xkb_bindings_v1 = globals.river_xkb_bindings_v1 orelse utils.interfaceNotAdvertised(river.XkbBindingsV1); + const river_xkb_config_v1 = globals.river_xkb_config_v1 orelse utils.interfaceNotAdvertised(river.XkbConfigV1); const config = try Config.create(); defer config.destroy(); @@ -78,6 +80,7 @@ pub fn main() !void { .river_layer_shell_v1 = river_layer_shell_v1, .river_window_manager_v1 = river_window_manager_v1, .river_xkb_bindings_v1 = river_xkb_bindings_v1, + .river_xkb_config_v1 = river_xkb_config_v1, .config = config, }); defer context.destroy(); @@ -243,7 +246,7 @@ fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, globals: * }; } else if (mem.orderZ(u8, ev.interface, wl.Output.interface.name) == .eq) { if (ev.version < 4) utils.versionNotSupported(wl.Output, ev.version, 4); - // We don't bind wl_output until the river_output send its .wl_output event + // We don't bind wl_output until the river_output sends its .wl_output event // This way, we don't miss the initial configuration events } else if (mem.orderZ(u8, ev.interface, wl.Shm.interface.name) == .eq) { globals.wl_shm = registry.bind(ev.name, wl.Shm, 1) catch |e| { @@ -270,6 +273,10 @@ fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, globals: * globals.river_xkb_bindings_v1 = registry.bind(ev.name, river.XkbBindingsV1, 2) catch |e| { fatal("Failed to bind to river_xkb_bindings_v1: {any}", .{@errorName(e)}); }; + } else if (mem.orderZ(u8, ev.interface, river.XkbConfigV1.interface.name) == .eq) { + globals.river_xkb_config_v1 = registry.bind(ev.name, river.XkbConfigV1, 1) catch |e| { + fatal("Failed to bind to river_xkb_config_v1: {any}", .{@errorName(e)}); + }; } }, .global_remove => |_| {