diff --git a/modules/misc/news.nix b/modules/misc/news.nix index 0b19711a..4f955e39 100644 --- a/modules/misc/news.nix +++ b/modules/misc/news.nix @@ -1414,6 +1414,14 @@ in { A new module is available: 'programs.jetbrains-remote' ''; } + + { + time = "2024-02-21T23:01:27+00:00"; + condition = hostPlatform.isLinux; + message = '' + A new module is available: 'wayland.windowManager.river'. + ''; + } ]; }; } diff --git a/modules/modules.nix b/modules/modules.nix index f8146efa..08a539de 100644 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -364,6 +364,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/river.nix ./services/window-managers/spectrwm.nix ./services/window-managers/xmonad.nix ./services/wlsunset.nix diff --git a/modules/services/window-managers/river.nix b/modules/services/window-managers/river.nix new file mode 100644 index 00000000..80ad68b7 --- /dev/null +++ b/modules/services/window-managers/river.nix @@ -0,0 +1,212 @@ +{ config, lib, pkgs, ... }: + +let + inherit (lib) types; + cfg = config.wayland.windowManager.river; + + # Systemd integration + variables = builtins.concatStringsSep " " cfg.systemd.variables; + extraCommands = builtins.concatStringsSep " " + (map (f: "&& ${f}") cfg.systemd.extraCommands); + systemdActivation = '' + exec "${pkgs.dbus}/bin/dbus-update-activation-environment --systemd ${variables} ${extraCommands}" + ''; + + toValue = val: + if lib.isString val || lib.isDerivation val then + toString val + else if true == val then + "enabled" + else if false == val then + "disabled" + else if lib.isInt val then + toString val + else if lib.isFloat val then + lib.strings.floatToString val + else + abort "unsupported type ${builtins.typeOf val}"; + + # Intermediary function that converts some value (attrs, str, ...) to one or several commands. + toArgs = path: value: + + let + stringValue = lib.concatStringsSep " " (path ++ [ (toValue value) ]); + finalValue = if lib.isAttrs value then + toCommand path value + else if lib.isList value then + lib.lists.flatten (map (x: toArgs path x) value) + else if value == null then + [ ] + else + [ stringValue ]; + in finalValue; + + # toCommand :: [string] -> attrs -> [string] + # Recursive function that converts an attrs to a list of commands that can be written to the + # config file. + toCommand = basePath: attrs: + lib.concatLists (lib.mapAttrsToList + (key: value: let path = basePath ++ [ key ]; in toArgs path value) attrs); + +in { + meta.maintainers = [ lib.maintainers.GaetanLepage ]; + + options.wayland.windowManager.river = { + enable = lib.mkEnableOption "the river window manager"; + + package = lib.mkPackageOption pkgs "river" { + nullable = true; + extraDescription = '' + Set to `null` to not add any river package to your path. + This should be done if you want to use the NixOS river module to install river. + ''; + }; + + xwayland.enable = lib.mkEnableOption "XWayland" // { default = true; }; + + systemd = { + enable = lib.mkEnableOption null // { + default = true; + description = '' + Whether to enable {file}`river-session.target` on + river 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` + - `NIXOS_OZONE_WL` + - `XCURSOR_THEME` + - `XCURSOR_SIZE` + ''; + }; + + variables = lib.mkOption { + type = types.listOf types.str; + default = [ + "DISPLAY" + "WAYLAND_DISPLAY" + "XDG_CURRENT_DESKTOP" + "NIXOS_OZONE_WL" + "XCURSOR_THEME" + "XCURSOR_SIZE" + ]; + example = [ "-all" ]; + description = '' + Environment variables to be imported in the systemd & D-Bus user + environment. + ''; + }; + + extraCommands = lib.mkOption { + type = types.listOf types.str; + default = [ + "systemctl --user stop river-session.target" + "systemctl --user start river-session.target" + ]; + description = "Extra commands to be run after D-Bus activation."; + }; + }; + + extraSessionVariables = lib.mkOption { + type = types.attrs; + default = { }; + description = "Extra session variables set when running the compositor."; + example = { MOZ_ENABLE_WAYLAND = "1"; }; + }; + + settings = lib.mkOption { + type = let + valueType = with types; + nullOr (oneOf [ + bool + int + float + str + path + (attrsOf valueType) + (listOf valueType) + ]) // { + description = "River configuration value"; + }; + in valueType; + default = { }; + description = "General settings given to `riverctl`."; + example = { + border-width = 2; + declare-mode = [ "locked" "normal" "passthrough" ]; + map.normal."Alt Q" = "close"; + input.pointer-foo-bar = { + accel-profile = "flat"; + events = true; + pointer-accel = -0.3; + tap = false; + }; + rule-add."-app-id" = { + "'bar'" = "csd"; + "'float*'"."-title"."'foo'" = "float"; + }; + set-cursor-warp = "on-output-change"; + set-repeat = "50 300"; + xcursor-theme = "someGreatTheme 12"; + spawn = [ "firefox" "'foot -a terminal'" ]; + }; + }; + + extraConfig = lib.mkOption { + type = types.lines; + default = ""; + example = '' + rivertile -view-padding 6 -outer-padding 6 & + ''; + description = + "Extra lines appended to {file}`$XDG_CONFIG_HOME/river/init`."; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + (lib.hm.assertions.assertPlatform "wayland.windowManager.river" pkgs + lib.platforms.linux) + ]; + + home.packages = lib.optional (cfg.package != null) cfg.package + ++ lib.optional cfg.xwayland.enable pkgs.xwayland; + + # Configuration file ~/.config/river/init + xdg.configFile."river/init".source = pkgs.writeShellScript "init" ('' + ### This file was generated with Nix. Don't modify this file directly. + + ### SHELL VARIABLES ### + ${config.lib.shell.exportAll cfg.extraSessionVariables} + + ### CONFIGURATION ### + ${lib.concatStringsSep "\n" (toCommand [ "riverctl" ] cfg.settings)} + + ### EXTRA CONFIGURATION ### + ${cfg.extraConfig} + + '' + (lib.optionalString cfg.systemd.enable '' + ### SYSTEMD INTEGRATION ### + ${systemdActivation} + '')); + + # Systemd integration + systemd.user.targets.river-session = lib.mkIf cfg.systemd.enable { + Unit = { + Description = "river compositor session"; + Documentation = [ "man:systemd.special(7)" ]; + BindsTo = [ "graphical-session.target" ]; + Wants = [ "graphical-session-pre.target" ]; + After = [ "graphical-session-pre.target" ]; + }; + }; + + systemd.user.targets.tray = { + Unit = { + Description = "Home Manager System Tray"; + Requires = [ "graphical-session-pre.target" ]; + }; + }; + }; +} diff --git a/tests/default.nix b/tests/default.nix index be4d8f40..97fa3fbb 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -260,6 +260,7 @@ in import nmtSrc { ./modules/services/window-managers/herbstluftwm ./modules/services/window-managers/hyprland ./modules/services/window-managers/i3 + ./modules/services/window-managers/river ./modules/services/window-managers/spectrwm ./modules/services/window-managers/sway ./modules/services/wlsunset diff --git a/tests/modules/services/window-managers/river/configuration.nix b/tests/modules/services/window-managers/river/configuration.nix new file mode 100644 index 00000000..9a019921 --- /dev/null +++ b/tests/modules/services/window-managers/river/configuration.nix @@ -0,0 +1,95 @@ +{ ... }: + +{ + wayland.windowManager.river = { + enable = true; + xwayland.enable = true; + extraSessionVariables = { + FOO = "foo"; + BAR = "bar"; + FOURTY_TWO = 42; + }; + # systemdIntegration = true; + settings = { + attach-mode = "bottom"; + background-color = "0x002b36"; + border-color-focused = "0x93a1a1"; + border-color-unfocused = "0x586e75"; + border-color-urgent = "0xff0000"; + border-width = 2; + csd-filter-add.app-id = [ "bar" "foo" ]; + declare-mode = [ "locked" "normal" "passthrough" ]; + default-layout = "rivertile"; + float-filter-add.app-id = "mpd"; + float-filter-add.title = "popup title with spaces"; + focus-follows-cursor = "normal"; + hide-cursor.timeout = 2; + hide-cursor.when-typing = true; + input.pointer-foo-bar = { + accel-profile = "flat"; + events = true; + pointer-accel = -0.3; + tap = false; + }; + keyboard-layout."-variant".colemak."-options"."altwin:swap_alt_wincaps:escapegrp:alt_shift_toggle" = + "us,de"; + map.locked.None.XF86AudioLowerVolume.spawn = "'pamixer -d 5'"; + map.locked.None.XF86AudioRaiseVolume.spawn = "'pamixer -i 5'"; + map.normal."Alt E" = "toggle-fullscreen"; + map.normal."Alt P" = "enter-mode passthrough"; + map.normal."Alt Q" = "close"; + map.normal."Alt Return" = "spawn foot"; + map.normal."Alt T" = "toggle-float"; + map.passthrough."Alt P" = "enter-mode normal"; + map-pointer.normal."Alt BTN_LEFT" = "move-view"; + map-pointer.normal."Super BTN_LEFT" = "move-view"; + map-pointer.normal."Super BTN_MIDDLE" = "toggle-float"; + map-pointer.normal."Super BTN_RIGHT" = "resize-view"; + map-switch = { + locked = { + lid.open = "foo"; + tablet.on = "foo"; + }; + normal = { + lid = { + close = "foo"; + open = "foo"; + }; + tablet = { + off = "foo bar"; + on = "foo"; + }; + }; + }; + rule-add."-app-id" = { + "'bar'" = "csd"; + "'float*'"."-title"."'foo'" = "float"; + }; + set-cursor-warp = "on-output-change"; + set-repeat = "50 300"; + xcursor-theme = "someGreatTheme 12"; + spawn = [ "firefox" "'foot -a terminal'" ]; + }; + + extraConfig = '' + rivertile -view-padding 6 -outer-padding 6 & + some + extra config + ''; + }; + + test.stubs = { + dbus = { }; + river = { }; + xwayland = { }; + }; + + nmt.script = '' + riverInit=home-files/.config/river/init + assertFileExists "$riverInit" + assertFileIsExecutable "$riverInit" + + normalizedConfig=$(normalizeStorePaths "$riverInit") + assertFileContent "$normalizedConfig" "${./init}" + ''; +} diff --git a/tests/modules/services/window-managers/river/default.nix b/tests/modules/services/window-managers/river/default.nix new file mode 100644 index 00000000..5d17f3f1 --- /dev/null +++ b/tests/modules/services/window-managers/river/default.nix @@ -0,0 +1 @@ +{ river-configuration = ./configuration.nix; } diff --git a/tests/modules/services/window-managers/river/init b/tests/modules/services/window-managers/river/init new file mode 100755 index 00000000..b7c5ede3 --- /dev/null +++ b/tests/modules/services/window-managers/river/init @@ -0,0 +1,67 @@ +#!/nix/store/00000000000000000000000000000000-bash/bin/bash +### This file was generated with Nix. Don't modify this file directly. + +### SHELL VARIABLES ### +export BAR="bar" +export FOO="foo" +export FOURTY_TWO="42" + +### CONFIGURATION ### +riverctl attach-mode bottom +riverctl background-color 0x002b36 +riverctl border-color-focused 0x93a1a1 +riverctl border-color-unfocused 0x586e75 +riverctl border-color-urgent 0xff0000 +riverctl border-width 2 +riverctl csd-filter-add app-id bar +riverctl csd-filter-add app-id foo +riverctl declare-mode locked +riverctl declare-mode normal +riverctl declare-mode passthrough +riverctl default-layout rivertile +riverctl float-filter-add app-id mpd +riverctl float-filter-add title popup title with spaces +riverctl focus-follows-cursor normal +riverctl hide-cursor timeout 2 +riverctl hide-cursor when-typing enabled +riverctl input pointer-foo-bar accel-profile flat +riverctl input pointer-foo-bar events enabled +riverctl input pointer-foo-bar pointer-accel -0.300000 +riverctl input pointer-foo-bar tap disabled +riverctl keyboard-layout -variant colemak -options altwin:swap_alt_wincaps:escapegrp:alt_shift_toggle us,de +riverctl map locked None XF86AudioLowerVolume spawn 'pamixer -d 5' +riverctl map locked None XF86AudioRaiseVolume spawn 'pamixer -i 5' +riverctl map normal Alt E toggle-fullscreen +riverctl map normal Alt P enter-mode passthrough +riverctl map normal Alt Q close +riverctl map normal Alt Return spawn foot +riverctl map normal Alt T toggle-float +riverctl map passthrough Alt P enter-mode normal +riverctl map-pointer normal Alt BTN_LEFT move-view +riverctl map-pointer normal Super BTN_LEFT move-view +riverctl map-pointer normal Super BTN_MIDDLE toggle-float +riverctl map-pointer normal Super BTN_RIGHT resize-view +riverctl map-switch locked lid open foo +riverctl map-switch locked tablet on foo +riverctl map-switch normal lid close foo +riverctl map-switch normal lid open foo +riverctl map-switch normal tablet off foo bar +riverctl map-switch normal tablet on foo +riverctl rule-add -app-id 'bar' csd +riverctl rule-add -app-id 'float*' -title 'foo' float +riverctl set-cursor-warp on-output-change +riverctl set-repeat 50 300 +riverctl spawn firefox +riverctl spawn 'foot -a terminal' +riverctl xcursor-theme someGreatTheme 12 + +### EXTRA CONFIGURATION ### +rivertile -view-padding 6 -outer-padding 6 & +some +extra config + + +### SYSTEMD INTEGRATION ### +exec "@dbus@/bin/dbus-update-activation-environment --systemd DISPLAY WAYLAND_DISPLAY XDG_CURRENT_DESKTOP NIXOS_OZONE_WL XCURSOR_THEME XCURSOR_SIZE && systemctl --user stop river-session.target && systemctl --user start river-session.target" + +