diff --git a/modules/lib/maintainers.nix b/modules/lib/maintainers.nix index 0a9d8af4f..82f564d95 100644 --- a/modules/lib/maintainers.nix +++ b/modules/lib/maintainers.nix @@ -756,4 +756,10 @@ github = "mipmip"; githubId = 658612; }; + LesVu = { + name = "John Ferse"; + email = "lesvu@ingressland.com"; + github = "LesVu"; + githubId = 66196443; + }; } diff --git a/modules/modules.nix b/modules/modules.nix index befe59e63..13b719ce1 100644 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -446,6 +446,7 @@ let ./services/window-managers/i3-sway/i3.nix ./services/window-managers/i3-sway/sway.nix ./services/window-managers/i3-sway/swaynag.nix + ./services/window-managers/labwc/labwc.nix ./services/window-managers/river.nix ./services/window-managers/spectrwm.nix ./services/window-managers/wayfire.nix diff --git a/modules/services/window-managers/labwc/function.nix b/modules/services/window-managers/labwc/function.nix new file mode 100644 index 000000000..e15057959 --- /dev/null +++ b/modules/services/window-managers/labwc/function.nix @@ -0,0 +1,146 @@ +{ lib, ... }: + +let + # Escape XML special characters (e.g., <, >, &, etc.) + escape = lib.escapeXML; + + # Indent each non-empty line of the given text by `level` using two spaces per level. + indent = + level: text: + let + indentation = lib.concatStrings (lib.genList (_: " ") level); # Two spaces per level + lines = lib.splitString "\n" text; # Split text into lines + indentedLines = map (line: if line == "" then "" else "${indentation}${line}") lines; + in + lib.concatStringsSep "\n" indentedLines; + + # Generate a or or XML entry based on a menu item definition + generateMenu = + item: + if item ? separator then + let + labelAttr = if item.separator ? label then " label=\"${escape item.separator.label}\"" else ""; + in + "" + + else if item ? menuId then + let + idAttr = " id=\"${escape item.menuId}\""; + labelAttr = if item ? label then " label=\"${escape item.label}\"" else ""; + children = if item ? items then lib.concatMapStringsSep "\n" generateMenu item.items else ""; + in + "\n${indent 1 children}\n" + + else + let + labelAttr = " label=\"${escape item.label}\""; + action = item.action; + nameAttr = " name=\"${escape action.name}\""; + toAttr = if action ? to then " to=\"${escape action.to}\"" else ""; + commandAttr = if action ? command then " command=\"${escape action.command}\"" else ""; + in + "\n \n"; + + # Get keys in a preferred order + orderedKeys = + name: keys: + let + # Define key orderings for known structures + tagOrder = { + font = [ "@place" ]; + keyboard = [ "default" ]; + mouse = [ "default" ]; + action = [ "@name" ]; + mousebind = [ "@button" ]; + }; + preferred = lib.attrByPath [ name ] [ ] tagOrder; + cmp = + a: b: + let + ia = lib.lists.findFirstIndex (x: x == a) (-1) preferred; + ib = lib.lists.findFirstIndex (x: x == b) (-1) preferred; + in + if ia == -1 && ib == -1 then + builtins.lessThan a b + else if ia == -1 then + false + else if ib == -1 then + true + else + builtins.lessThan ia ib; + in + builtins.sort cmp keys; + + generateRc = + name: value: + # If the value is an attribute set (i.e., a record / dictionary) + if builtins.isAttrs value then + let + # keys = builtins.attrNames value; + keys = orderedKeys name (builtins.attrNames value); + + attrKeys = builtins.filter (k: lib.hasPrefix "@" k) keys; + childKeys = builtins.filter (k: !(lib.hasPrefix "@" k)) keys; + + # Generate string of XML attributes from keys like "@id" → id="value" + attrs = lib.concatStrings ( + map ( + k: + let + attrName = builtins.substring 1 999 k; # Remove "@" prefix + attrValue = value.${k}; + in + " ${attrName}=\"${escape (builtins.toString attrValue)}\"" + ) attrKeys + ); + + # Recursively convert children to XML, with increased indentation + children = lib.concatStringsSep "\n" (map (k: generateRc k value.${k}) childKeys); + in + + if children == "" then + # Only attributes — use self-closing tag with attributes + "<${name}${attrs} />" + + else + # Attributes and/or children — use full open/close tag + "<${name}${attrs}>\n${indent 1 children}\n" + + # If the value is a boolean `true`, render as self-closing tag + else if builtins.isBool value && value then + "<${name} />" + + # If the value is a list, emit the same tag name for each item + else if builtins.isList value then + # Reuse the same tag name for each list item + lib.concatStringsSep "\n" (map (v: generateRc name v) value) + + # All other primitive values: wrap in start/end tag + else + "<${name}>${escape (builtins.toString value)}"; + + generateXML = name: config: extraConfig: '' + + + <${name}> + ${indent 1 ( + lib.concatStringsSep "\n" ( + ( + if name == "openbox_menu" then + map generateMenu + else if name == "labwc_config" then + lib.mapAttrsToList generateRc + else + builtins.throw "error ${name} is neither openbox_menu nor labwc_config" + ) + config + ) + )} + ${indent 1 extraConfig} + + ''; + +in +{ + generateXML = generateXML; +} diff --git a/modules/services/window-managers/labwc/labwc.nix b/modules/services/window-managers/labwc/labwc.nix new file mode 100644 index 000000000..443715c7e --- /dev/null +++ b/modules/services/window-managers/labwc/labwc.nix @@ -0,0 +1,272 @@ +{ + lib, + pkgs, + config, + ... +}: +let + function = import ./function.nix { + inherit lib; + }; + + xmlFormat = pkgs.formats.xml { }; + + cfg = config.wayland.windowManager.labwc; + + variables = builtins.concatStringsSep " " cfg.systemd.variables; + extraCommands = builtins.concatStringsSep " " (map (f: "&& ${f}") cfg.systemd.extraCommands); + systemdActivation = "${pkgs.dbus}/bin/dbus-update-activation-environment --systemd ${variables} ${extraCommands}"; +in +{ + meta.maintainers = [ lib.hm.maintainers.LesVu ]; + + options.wayland.windowManager.labwc = { + enable = lib.mkEnableOption "Labwc, a wayland window-stacking compositor"; + + package = lib.mkPackageOption pkgs "labwc" { + nullable = true; + extraDescription = '' + Set to `null` to use Nixos labwc package. + ''; + }; + + xwayland.enable = lib.mkEnableOption "XWayland" // { + default = true; + }; + + rc = lib.mkOption { + type = lib.types.submodule { + freeformType = xmlFormat.type; + }; + default = { }; + description = '' + Config to configure labwc options. + Use "@attributes" for attributes. + See for configuration. + ''; + example = lib.literalExpression '' + { + theme = { + name = "nord"; + cornerRadius = 8; + font = { + "@name" = "FiraCode"; + "@size" = "11"; + }; + }; + keyboard = { + default = true; + keybind = [ + # + { + "@key" = "W-Return"; + action = { + "@name" = "Execute"; + "@command" = "foot"; + }; + } + # + { + "@key" = "W-Esc"; + action = { + "@name" = "Execute"; + "@command" = "loot"; + }; + } + ]; + }; + } + ''; + }; + + extraConfig = lib.mkOption { + type = lib.types.lines; + default = ""; + example = '' + + + + + + + + ''; + description = "Extra lines appended to {file}`$XDG_CONFIG_HOME/labwc/rc.xml`."; + }; + + menu = lib.mkOption { + type = lib.types.listOf xmlFormat.type; + default = [ ]; + description = "Config to configure labwc menu"; + example = lib.literalExpression '' + [ + { + menuId = "client-menu"; + label = "Client Menu"; + icon = ""; + items = [ + { + label = "Maximize"; + icon = ""; + action = { + name = "ToggleMaximize"; + }; + } + { + label = "Fullscreen"; + action = { + name = "ToggleFullscreen"; + }; + } + { + label = "Always on Top"; + action = { + name = "ToggleAlwaysOnTop"; + }; + } + { + label = "Alacritty"; + action = { + name = "Execute"; + command = "alacritty"; + }; + } + { + label = "Move Left"; + action = { + name = "SendToDesktop"; + to = "left"; + }; + } + { + separator = { }; + } + { + label = "Workspace"; + menuId = "workspace"; + icon = ""; + items = [ + { + label = "Move Left"; + action = { + name = "SendToDesktop"; + to = "left"; + }; + } + ]; + } + { + separator = true; + } + ]; + } + ]; + ''; + }; + + autostart = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + Command to autostart when labwc start. + ''; + example = [ + "wayvnc &" + "waybar &" + "swaybg -c '#113344' >/dev/null 2>&1 &" + ]; + }; + + environment = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = '' + Environment variable to add when labwc start. + ''; + example = [ + "XDG_CURRENT_DESKTOP=labwc:wlroots" + "XKB_DEFAULT_LAYOUT=us" + ]; + }; + + systemd = { + enable = lib.mkEnableOption null // { + default = true; + description = '' + Whether to enable {file}`labwc-session.target` on + labwc startup. This links to {file}`graphical-session.target`. + Some important environment variables will be imported to systemd + and D-Bus user environment before reaching the target, including + - `DISPLAY` + - `WAYLAND_DISPLAY` + - `XDG_CURRENT_DESKTOP` + ''; + }; + + variables = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ + "DISPLAY" + "WAYLAND_DISPLAY" + "XDG_CURRENT_DESKTOP" + ]; + example = [ "-all" ]; + description = '' + Environment variables to be imported in the systemd & D-Bus user + environment. + ''; + }; + + extraCommands = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ + "systemctl --user stop labwc-session.target" + "systemctl --user start labwc-session.target" + ]; + description = "Extra commands to be run after D-Bus activation."; + }; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + (lib.hm.assertions.assertPlatform "wayland.windowManager.labwc" pkgs lib.platforms.linux) + ]; + + home.packages = lib.mkIf (cfg.package != null) ( + [ cfg.package ] ++ lib.optional cfg.xwayland.enable pkgs.xwayland + ); + + xdg.configFile."labwc/rc.xml".text = function.generateXML "labwc_config" cfg.rc cfg.extraConfig; + + xdg.configFile."labwc/menu.xml".text = function.generateXML "openbox_menu" cfg.menu ""; + + xdg.configFile."labwc/autostart".source = pkgs.writeShellScript "autostart" ( + '' + ### This file was generated with Nix. Don't modify this file directly. + + ### AUTOSTART SERVICE ### + ${lib.concatStringsSep "\n" cfg.autostart} + + '' + + (lib.optionalString cfg.systemd.enable '' + ### SYSTEMD INTEGRATION ### + ${systemdActivation} + '') + ); + + xdg.configFile."labwc/environment".text = lib.concatStringsSep "\n" ( + cfg.environment ++ (lib.optionals (!cfg.xwayland.enable) [ "WLR_XWAYLAND=" ]) + ); + + systemd.user.targets.labwc-session = lib.mkIf cfg.systemd.enable { + Unit = { + Description = "labwc compositor session"; + Documentation = [ "man:systemd.special(7)" ]; + BindsTo = [ "graphical-session.target" ]; + Wants = [ "graphical-session-pre.target" ]; + After = [ "graphical-session-pre.target" ]; + }; + }; + }; +} diff --git a/tests/default.nix b/tests/default.nix index 0e7f42a2c..68483df5b 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -629,6 +629,7 @@ import nmtSrc { ./modules/services/window-managers/herbstluftwm ./modules/services/window-managers/hyprland ./modules/services/window-managers/i3 + ./modules/services/window-managers/labwc ./modules/services/window-managers/river ./modules/services/window-managers/spectrwm ./modules/services/window-managers/sway diff --git a/tests/modules/services/window-managers/labwc/autostart b/tests/modules/services/window-managers/labwc/autostart new file mode 100644 index 000000000..b26c9c153 --- /dev/null +++ b/tests/modules/services/window-managers/labwc/autostart @@ -0,0 +1,11 @@ +#!/nix/store/58br4vk3q5akf4g8lx0pqzfhn47k3j8d-bash-5.2p37/bin/bash +### This file was generated with Nix. Don't modify this file directly. + +### AUTOSTART SERVICE ### +wayvnc & +waybar & +swaybg -c '#113344' >/dev/null 2>&1 & + +### SYSTEMD INTEGRATION ### +@dbus@/bin/dbus-update-activation-environment --systemd DISPLAY WAYLAND_DISPLAY XDG_CURRENT_DESKTOP && systemctl --user stop labwc-session.target && systemctl --user start labwc-session.target + diff --git a/tests/modules/services/window-managers/labwc/default.nix b/tests/modules/services/window-managers/labwc/default.nix new file mode 100644 index 000000000..41aa025d7 --- /dev/null +++ b/tests/modules/services/window-managers/labwc/default.nix @@ -0,0 +1,6 @@ +{ + labwc-rc-configuration = ./labwc-rc.nix; + labwc-menu-configuration = ./labwc-menu.nix; + labwc-autostart-configuration = ./labwc-autostart.nix; + labwc-environment-configuration = ./labwc-environment.nix; +} diff --git a/tests/modules/services/window-managers/labwc/environment b/tests/modules/services/window-managers/labwc/environment new file mode 100644 index 000000000..96db6e545 --- /dev/null +++ b/tests/modules/services/window-managers/labwc/environment @@ -0,0 +1,3 @@ +XDG_CURRENT_DESKTOP=labwc:wlroots +XKB_DEFAULT_LAYOUT=us +WLR_XWAYLAND= \ No newline at end of file diff --git a/tests/modules/services/window-managers/labwc/labwc-autostart.nix b/tests/modules/services/window-managers/labwc/labwc-autostart.nix new file mode 100644 index 000000000..c0fe66c91 --- /dev/null +++ b/tests/modules/services/window-managers/labwc/labwc-autostart.nix @@ -0,0 +1,18 @@ +{ + wayland.windowManager.labwc = { + enable = true; + package = null; + autostart = [ + "wayvnc &" + "waybar &" + "swaybg -c '#113344' >/dev/null 2>&1 &" + ]; + }; + + nmt.script = '' + labwcAutostart=home-files/.config/labwc/autostart + + assertFileExists "$labwcAutostart" + assertFileContent "$labwcAutostart" "${./autostart}" + ''; +} diff --git a/tests/modules/services/window-managers/labwc/labwc-environment.nix b/tests/modules/services/window-managers/labwc/labwc-environment.nix new file mode 100644 index 000000000..81f9b3a48 --- /dev/null +++ b/tests/modules/services/window-managers/labwc/labwc-environment.nix @@ -0,0 +1,18 @@ +{ + wayland.windowManager.labwc = { + enable = true; + package = null; + xwayland.enable = false; + environment = [ + "XDG_CURRENT_DESKTOP=labwc:wlroots" + "XKB_DEFAULT_LAYOUT=us" + ]; + }; + + nmt.script = '' + labwcEnvironment=home-files/.config/labwc/environment + + assertFileExists "$labwcEnvironment" + assertFileContent "$labwcEnvironment" "${./environment}" + ''; +} diff --git a/tests/modules/services/window-managers/labwc/labwc-menu.nix b/tests/modules/services/window-managers/labwc/labwc-menu.nix new file mode 100644 index 000000000..f42b3e859 --- /dev/null +++ b/tests/modules/services/window-managers/labwc/labwc-menu.nix @@ -0,0 +1,99 @@ +{ + wayland.windowManager.labwc = { + enable = true; + package = null; + menu = [ + { + menuId = "client-menu"; + label = "Client Menu"; + icon = ""; + items = [ + { + label = "Maximize"; + icon = ""; + action = { + name = "ToggleMaximize"; + }; + } + { + label = "Fullscreen"; + action = { + name = "ToggleFullscreen"; + }; + } + { + label = "Always on Top"; + action = { + name = "ToggleAlwaysOnTop"; + }; + } + { + label = "Alacritty"; + action = { + name = "Execute"; + command = "alacritty"; + }; + } + { + separator = { }; + } + { + label = "Workspace"; + menuId = "workspace"; + icon = ""; + items = [ + { + label = "Move Left"; + action = { + name = "SendToDesktop"; + to = "left"; + }; + } + ]; + } + { + separator = { + label = "sep"; + }; + } + ]; + } + { + menuId = "menu-two"; + label = "Client Menu Two"; + icon = ""; + items = [ + { + label = "Menu In Menu"; + menuId = "menu-in-menu"; + icon = ""; + items = [ + { + label = "Menu In Menu In Menu"; + menuId = "menu-in-menu-in-menu"; + icon = ""; + items = [ + { + label = "Move Right"; + action = { + name = "SendToDesktop"; + to = "right"; + }; + } + ]; + } + ]; + } + ]; + } + { menuId = ""; } + ]; + }; + + nmt.script = '' + labwcMenuConfig=home-files/.config/labwc/menu.xml + + assertFileExists "$labwcMenuConfig" + assertFileContent "$labwcMenuConfig" "${./menu.xml}" + ''; +} diff --git a/tests/modules/services/window-managers/labwc/labwc-rc.nix b/tests/modules/services/window-managers/labwc/labwc-rc.nix new file mode 100644 index 000000000..3660d44e4 --- /dev/null +++ b/tests/modules/services/window-managers/labwc/labwc-rc.nix @@ -0,0 +1,81 @@ +{ + wayland.windowManager.labwc = { + enable = true; + package = null; + rc = { + theme = { + name = "nord"; + cornerRadius = 8; + font = { + "@name" = "FiraCode"; + "@place" = ""; + "@size" = "11"; + }; + }; + mouse = { + default = { }; + context = { + "@name" = "Root"; + mousebind = [ + { + "@button" = "Right"; + "@action" = "Press"; + action = { + "@name" = "ShowMenu"; + "@menu" = "some-custom-menu"; + }; + } + ]; + }; + }; + keyboard = { + default = true; + keybind = [ + { + "@key" = "W-Return"; + action = { + "@command" = "alacritty"; + "@name" = "Execute"; + }; + } + { + "@key" = "W-Esc"; + action = { + "@name" = "Execute"; + "@command" = "foot"; + }; + } + { + "@key" = "W-1"; + action = { + "@to" = "1"; + "@name" = "GoToDesktop"; + }; + } + ]; + }; + desktops = { + "@number" = 10; + }; + }; + extraConfig = '' + + + + + + + + + + + ''; + }; + + nmt.script = '' + labwcConfig=home-files/.config/labwc/rc.xml + + assertFileExists "$labwcConfig" + assertFileContent "$labwcConfig" "${./rc.xml}" + ''; +} diff --git a/tests/modules/services/window-managers/labwc/menu.xml b/tests/modules/services/window-managers/labwc/menu.xml new file mode 100644 index 000000000..6ba0821cd --- /dev/null +++ b/tests/modules/services/window-managers/labwc/menu.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/modules/services/window-managers/labwc/rc.xml b/tests/modules/services/window-managers/labwc/rc.xml new file mode 100644 index 000000000..4b1422e89 --- /dev/null +++ b/tests/modules/services/window-managers/labwc/rc.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + 8 + + nord + + + + + + + + + + + + +