The Complete Computer

RiverWM

LifeTechEmacsArcology
#+ARROYO_NIXOS_MODULE: nixos/riverwm.nix #+ARROYO_HOME_MODULE: hm/riverwm.nix #+ARROYO_SYSTEM_ROLE: endpoint

Since I was in high school I have been a user of Plasma KDE, in various forms and configurations. Perhaps the height of my idea of "linux on the desktop" was KDE Plasma 4 with KDEWM=/usr/bin/xmonad in the session variables, instructing KDE to launch XMonad instead of KWin. This meant that I had a nice stacking window manager with automated layouts and a "batteries included" desktop that meant that I didn't have to pick a bunch of software to piece together and configure a bunch of shit like screen-locking scripts and programmatic system bars.

Well here we are, back again; this is impossible with Plasma 6 on Wayland, to run another WM within Plasma and there is no stacking tiling WM manager like Bismuth that works in Plasma 6. Plasma 5's wayland session crashes on both of my linux machines and is otherwise under/un-maintained, so we're gonna set up something new — RiverWM — and try to return to the world of UnixPorn minimalist neofetch fetishism that no one else in my life would ever want to use; at least I could disable tiling on Plasma and have basically a normal desktop, now we're doomed!

The Hey Smell This levels are quite high here. I don't think I can convince you or explain to you why using a tiling window manager with configurable keyboard controls and automatic window placement is good and why you should use it over a tradition window manager. But if you know why, this may be helpful to you. Below you'll find a set of loosely-held opinions on the tools and configuration required to provide enough features for a tolerable lightweight wayland session on a laptop that functions both as a docked desktop computer and a lightweight high performance machine to write code and prose on the go.

I would love if some day I could run River inside of Plasma 6 and eliminate most of the fiddly crap on this page. In theory some day KWin and River could implement the same protocols which Plasma needs, and then i could have my cake and eat it too, right, right???

NixOS (SDDM, RiverWM and utility package installation)

Packages for River and the Desktop Environment setup below are available within the context of the desktop manager and WM, but not in the system profile path. This sets my endpoints up with autologin etc, and ensures things like polkit are provided to the session. Finally, to work around some stuff and to replicate configuration which was provided by KDE until now, there is a system-wide configuration to use the Kvantum Qt SVG theme engine instead of breeze. See why at the very bottom of this doc.

nix source: :tangle ~/arroyo-nix/nixos/riverwm.nix
{ config, pkgs, lib, ... }: { programs.river.enable = true; programs.river = { extraPackages = with pkgs; [ way-displays waybar fuzzel fnott swww swaylock-effects swayidle swaytools wlogout river-ultitile river-bnf river-tag-overlay ]; xwayland.enable = true; }; environment.systemPackages = with pkgs; [ brightnessctl playerctl pamixer udiskie networkmanagerapplet kdePackages.qtwayland libsForQt5.qtwayland grim slurp wl-color-picker wl-clipboard-x11 breeze-icons ]; services.displayManager.sddm = { enable = true; enableHidpi = true; wayland.enable = true; }; services.displayManager = { autoLogin.enable = true; autoLogin.user = "rrix"; defaultSession = "river"; }; # security.pam.services.login.kwallet.enable = true; # security.pam.services.login.enableGnomeKeyring = true; # services.gnome.gnome-keyring.enable = true; security.polkit.enable = true; qt.enable = true; qt = { platformTheme = "lxqt"; style = "kvantum"; }; # surely this will never go wrong! the closest i can get to KDE's "don't suspend while there is an external display plugged in" since services.logind.lidSwitchDocked doesn't consider the disabled display services.logind.lidSwitchExternalPower = "lock"; environment.variables = lib.mkIf (config.networking.hostName == "rose-quine") { WLR_DRM_NO_ATOMIC = "1"; }; }

Home Manager River Configuration (actual configuration and desktop service inclusion)

The River configuration itself and all the bolt-on desktop service configurations are managed in Home Manager; in theory this could be used on an Ubuntu install or whatever else (lol):

nix source: :tangle ~/arroyo-nix/hm/riverwm.nix
{ config, pkgs, lib, ... }: { imports = [ # ./wayland/svc-run-once.nix ./wayland/clipman.nix ./wayland/fnott.nix ./wayland/swayidle.nix ./wayland/swww.nix ./wayland/squeekboard.nix ./wayland/way-displays.nix ./wayland/waybar.nix ./wayland/wlogout.nix ./wayland/fucking-hacks.nix ]; wayland.windowManager.river.enable = true; wayland.windowManager.river = { package = null; # use nixos-provided river settings = { declare-mode = [ "locked" "normal" "passthrough" ]; input = { "'pointer-PIXA*'" = { accel-profile = "adaptive"; events = true; pointer-accel = 0.3; tap = false; }; # GPD Pocket 3 touchscreen and stylus "'*GXTP7380:00*'".map-to-output = "DSI-1"; }; map-pointer.normal = { "Super BTN_LEFT" = "move-view"; "Super BTN_RIGHT" = "resize-view"; }; map = { normal = { "Super backspace" = "close"; "Super delete" = "close"; "Super+Control backspace" = "exit"; "Alt space" = "spawn 'fuzzel --list-executables-in-path --show-actions'"; "Super space" = "toggle-float"; "None Print" = "spawn 'slurp | grim -g - - | wl-copy'"; "Super Y" = "spawn 'slurp | grim -g - - | wl-copy'"; # "Super Print" = "spawn 'slurp | grim -g - - | tesseract - - | wl-copy'"; # "Super+Control Print" = "spawn 'recshot'"; "Super period" = "spawn 'fnottctl dismiss'"; "Super J" = "focus-view next"; "Super K" = "focus-view previous"; "Super+Control J" = "focus-output next"; "Super+Control K" = "focus-output previous"; "Super+Shift+Control J" = "send-to-output next"; "Super+Shift+Control K" = "send-to-output previous"; "Super+Shift J" = "swap next"; "Super+Shift K" = "swap previous"; "Super Return" = "zoom"; "Super H" = ''send-layout-cmd river-ultitile "set integer main-size -= 5"''; "Super L" = ''send-layout-cmd river-ultitile "set integer main-size += 5"''; "Super+Shift H" = ''send-layout-cmd river-ultitile "set integer main-count -= 1"''; "Super+Shift L" = ''send-layout-cmd river-ultitile "set integer main-count += 1"''; "Super M" = ''send-layout-cmd river-ultitile "set string layout @ main monocle hstack vstack"''; "Super+Control M" = ''send-layout-cmd river-ultitile "set string layout = main "''; "Super N" = "toggle-fullscreen"; "Super E" = "spawn 'dolphin'"; "Super W" = "spawn 'konsole'"; "Super A" = "spawn 'river-bnf $(( 1 << (1-1) ))'"; "Super S" = "spawn 'river-bnf $(( 1 << (2-1) ))'"; "Super D" = "spawn 'river-bnf $(( 1 << (3-1) ))'"; "Super F" = "spawn 'river-bnf $(( 1 << (4-1) ))'"; "Super G" = "spawn 'river-bnf $(((1 << 32) - 1))'"; "Super+Shift A" = "set-view-tags $(( 1 << (1-1) ))"; "Super+Shift S" = "set-view-tags $(( 1 << (2-1) ))"; "Super+Shift D" = "set-view-tags $(( 1 << (3-1) ))"; "Super+Shift F" = "set-view-tags $(( 1 << (4-1) ))"; "Super+Shift G" = "set-view-tags $(((1 << 32) - 1))"; "Super+Control A" = "toggle-focused-tags 1"; "Super+Control S" = "toggle-focused-tags 2"; "Super+Control D" = "toggle-focused-tags 3"; "Super+Control F" = "toggle-focused-tags 4"; "Super Q" = "spawn 'gpd-rotate-display'"; }; }; rule-add = { # zoom pop-up notifications, but not zoom window "-title"."zoom" = { "-app-id"."zoom" = "float"; }; "-title"."Picture-in-Picture" = { "-app-id"."firefox" = "tags $(((1 << 32) - 1))"; }; "-app-id"."opensnitch_ui" = "float"; "-app-id"."firefox" = "ssd"; "-app-id"."emacs" = "ssd"; "-app-id"."'bar'" = "csd"; }; set-cursor-warp = "on-output-change"; set-repeat = "50 300"; keyboard-layout = "punctual"; # this is https://cce.whatthefuck.computer/xkb-layout spawn = [ "konsole" "firefox" "emacs" ]; xcursor-theme = "breeze_cursors 24"; default-layout = "river-ultitile"; background-color = "0x111111"; border-color-focused = "0x00552f"; border-color-unfocused = "0x303230"; border-width = 5; }; systemd.enable = true; systemd.variables = [ "GTK_USE_PORTAL" "QT_PLUGIN_PATH" "WAYLAND_DISPLAY" "XDG_SEAT" "XDG_SEAT_PATH" "XDG_SESSION_PATH" "XDG_SESSION_TYPE" "XDG_VTNR" ]; extraSessionVariables = { XDG_CURRENT_DESKTOP = "river"; # QT_AUTO_SCREEN_SCALE_FACTOR = "true"; QT_SCALE_FACTOR="1.5"; }; extraConfig = '' for mode in normal locked do # Control pulse audio volume with pamixer (https://github.com/cdemoulins/pamixer) riverctl map $mode None XF86AudioRaiseVolume spawn 'pamixer -i 5' riverctl map $mode None XF86AudioLowerVolume spawn 'pamixer -d 5' riverctl map $mode None XF86AudioMute spawn 'pamixer --toggle-mute' # Control MPRIS aware media players with playerctl (https://github.com/altdesktop/playerctl) riverctl map $mode None XF86AudioMedia spawn 'playerctl play-pause' riverctl map $mode None XF86AudioPlay spawn 'playerctl play-pause' riverctl map $mode None XF86AudioPrev spawn 'playerctl previous' riverctl map $mode None XF86AudioNext spawn 'playerctl next' # Control screen backlight brightness with brightnessctl (https://github.com/Hummer12007/brightnessctl) riverctl map $mode None XF86MonBrightnessUp spawn 'brightnessctl set +5%' riverctl map $mode None XF86MonBrightnessDown spawn 'brightnessctl set 5%-' done ''; }; systemd.user.services = with pkgs; let mkServiceUnit = (callPackage ./wayland/lib.nix {}).mkServiceUnit; in { polkit-gnome-authentication-agent-1 = mkServiceUnit "${polkit_gnome}/libexec/polkit-gnome-authentication-agent-1" {} ; river-ultitile = mkServiceUnit "${river-ultitile}/bin/river-ultitile" {}; nm-applet = mkServiceUnit "${networkmanagerapplet}/bin/nm-applet --indicator" {}; kdeconnect-indicator = mkServiceUnit "${config.services.kdeconnect.package}/bin/kdeconnect-indicator" {}; river-tag-overlay = mkServiceUnit "${river-tag-overlay}/bin/river-tag-overlay --tag-amount 4 --background-colour 0x111111 --border-colour 0x00552f --tags-per-row 2 --border-width 3" {}; udiskie = mkServiceUnit "${udiskie}/bin/udiskie --no-automount --smart-tray" {}; }; }

NEXT document keybinds

it is maybe time for a page that has my keyboard layout, the monticello drive keymap, evil, and riverwm stuff

NEXT document the rest of this

Run-once user service management

riverctl spawn doesn't make sure the spawned process is kept running, nor does it keep it from spawning more than once. To deal with that, we can try to use the SystemD user session to manage it:

CANCELLED systemd-run dynamic user units

these weren't robust enough for my tastes but this svcCommand stuff is a nice pattern for [[id:cce/cce][CCE]] extensions of nixos and HM built-in modules

Here we are trying to use systemd-run to create ephemeral services makes sense here, but it's a bit of a pain when using the Home Manager River module so that it can merge the settings attrset to define key bindings all over the CCE , add hardware-specific configurations in my hardware support/host configuration files, etc...

So this simple module adds an option to the river configuration with a list of commands to wrap in a systemd service runner; it also has to fold in config.wayland.windowManager.river.systemd features since those have to run before the services are started, but the HM module adds it after the extraConfig...

nix source: :tangle ~/arroyo-nix/hm/wayland/svc-run-once.nix
{ config, lib, pkgs, ... }: { options = with lib; { wayland.windowManager.river.rrix.svcCommands = mkOption { type = types.listOf types.str; default = []; example = [ "waybar" ]; description = '' list of commands which a systemd user service will be started to manage. ''; }; }; config = { wayland.windowManager.river.extraConfig = let toSvcCommand = command: # "svc ${lib.escapeShellArg command}" "svc ${command}"; svcCommands = lib.map toSvcCommand config.wayland.windowManager.river.rrix.svcCommands; variables = config.wayland.windowManager.river.systemd.variables; in '' ## instead of wayland.windowManager.river.systemd.enable... ${pkgs.dbus}/bin/dbus-update-activation-environment --systemd ${builtins.concatStringsSep " " variables} svc() { systemd-run --user \ --unit="$(basename $1)@$WAYLAND_DISPLAY" \ --service-type=exec \ --slice app.slice \ "$@" } ${lib.concatStringsSep "\n" svcCommands} ''; }; }

SystemD user-unit helpers

Rather than bodge systemd-run commands in to the River init, Home Manager can be instructed to manage the units with actual service destinations where auto-restart can be handled and timers can be directly managed:

nix source: :tangle ~/arroyo-nix/hm/wayland/lib.nix
{ pkgs, ... }: { mkServiceUnit = packageCommand: extras: pkgs.lib.recursiveUpdate { Unit = { Description = "user unit ${packageCommand}"; After = [ "river-session.target" ]; }; Service = { Type = "simple"; ExecStart = packageCommand; Restart = "on-failure"; RestartSec = 1; TimeoutStopSec = 10; }; Install = { WantedBy = [ "river-session.target" ]; }; } extras; mkTimerUnit = unitName: extras: pkgs.lib.recursiveUpdate { Unit = { Description = "timer for ${unitName}"; }; Timer = { OnActiveSec = 30; Unit = unitName; }; Install = { WantedBy = [ "river-session.target" ]; }; } extras; }

These are probably something which could be made to be nicely generic and useful throughout the CCE but here they are. river-session target is entered "manually" by the River init and HM's built in "systemd support" submodule for it.

window placement engine river-ultitile

This package definition is stuck in rixpkgs . river-ultitile is like rivertile but works with widescreen displays a bit better and has a "monocle" layout where windows are stacked taking up the whole display, ideal for laptop work and especially on my GPD Pocket 3 .

nix source: :tangle ~/arroyo-nix/pkgs/river-ultitile.nix
{ lib , stdenv , callPackage , fetchFromSourcehut , pandoc , pkg-config , wayland , wayland-scanner , wayland-protocols , zig_0_12 }: stdenv.mkDerivation (final: rec { pname = "river-ultitile"; version = "1.1.1"; src = fetchFromSourcehut { owner = "~midgard"; repo = pname; rev = "v${version}"; sha256 = "sha256-iGpedbZEnaWaYE9SC1rwsqyMj+UXV8fEhLRYB/f6Qs8="; }; deps = callPackage ./river-ultitile-deps.nix { }; postPatch = '' ln -s ${callPackage ./river-ultitile-deps.nix { }} $ZIG_GLOBAL_CACHE_DIR/p ''; nativeBuildInputs = [ pkg-config wayland wayland-scanner wayland-protocols pandoc zig_0_12.hook ]; })

System Status Bar waybar

You gotta have a system status bar! I sure wish this worked in systemd taskbar doesn't have icons and I gave up trying to figure out why so River just spawns one that may be duplicated or crash when the configuration is updated.

nix source: :tangle ~/arroyo-nix/hm/wayland/waybar.nix :mkdirp yes
{ config, pkgs, ... }: { wayland.windowManager.river.settings.spawn = [ "waybar" ]; # wayland.windowManager.river.rrix.svcCommands = [ "waybar" ]; # systemd.user.services = with pkgs; let # mkServiceUnit = (callPackage ./lib.nix {}).mkServiceUnit; # in { # waybar = mkServiceUnit "${pkgs.waybar}/bin/waybar" {}; # }; programs.waybar.enable = true; programs.waybar = { settings = { bar1 = { name = "bar1"; mode = "dock"; layer = "top"; position = "bottom"; spacing = 12; modules-left = [ "river/tags" ]; modules-center = [ "custom/fuzzy" "wlr/taskbar" "tray" "network" ]; modules-right = [ "river/window" "idle_inhibitor" "pulseaudio" "battery" ]; "river/tags" = { num-tags = 4; }; "custom/fuzzy" = let exc = pkgs.stdenv.mkDerivation { name = "fuzzy-clock"; propagatedBuildInputs = [ (pkgs.python3.withPackages (pp: with pp; [])) ]; dontUnpack = true; installPhase = "install -Dm755 ${../../files/fuzzy-clock.py} $out/bin/fuzzy-clock"; }; in { restart-interval = 120; return-type = "json"; exec = "${exc}/bin/fuzzy-clock"; }; "wlr/taskbar" = { format = "{icon}"; icon-size = 24; tooltip-format = "{title}"; on-click = "activate"; }; "pulseaudio" = { on-click = "pavucontrol"; format = "{volume}% {icon}"; format-bluetooth = "{volume}% 🛜{icon}"; format-icons = { "alsa_output.usb-VIA_Technologies_Inc._VIA_USB_Device-00.analog-stereo" = "📻"; "alsa_output.usb-VIA_Technologies_Inc._VIA_USB_Device-00.analog-stereo-muted" = "📻🔇"; "headphone" = "🎧"; "headphone-muted" = "🎧🔇"; "default" = ["" ""]; }; }; "network" = { format = "{ifname}"; format-wifi = "🛜 {essid}"; format-ethernet = "🖥️"; format-disconnected = "📵"; tooltip-format = "{ifname} via {gwaddr} 󰊗"; tooltip-format-wifi = "{essid} ({signalStrength}%) "; tooltip-format-ethernet = "{ifname} {ipaddr}/{cidr} "; tooltip-format-disconnected = "Disconnected"; on-click = "nm-connection-editor"; }; "river/window" = { max-length = 40; }; "battery" = { interval = 60; states = { warning = 30; critical = 15; }; format = "{capacity}% {icon}"; format-icons = ["" "" "" "" ""]; max-length = 25; }; "idle_inhibitor" = { format = "{icon}"; format-icons.activated = ""; format-icons.deactivated = ""; }; }; }; style = ../../files/waybar-style.css; }; }

Waybar CSS

These colors are taken from ef-bio-theme

css source: :tangle ~/arroyo-nix/files/waybar-style.css
window#waybar { font-family: Vulf Mono; font-style: italic; font-size: 14px; background: rgba(17, 17, 17, 0.33); } .modules-center { background: rgba(17,17,17,0.75); padding-left: 15px; padding-right: 15px; border-radius: 15px; margin-top: 3px; margin-bottom: 3px; } #tags button.urgent { background-color: #ef6560; } #tags button.occupied { background-color: #3a3027; } #tags button.focused { background-color: #00331f; } #battery.warning:not(.charging) { background: #f53c3c; color: white; animation-name: blink; animation-duration: 0.5s; animation-timing-function: steps(12); animation-iteration-count: infinite; animation-direction: alternate; } #idle_inhibitor.activated { color: #ef6560; }

Fuzzy Clock Command

I have a Python Fuzzy Clock here:

python source: :tangle ~/arroyo-nix/files/fuzzy-clock.py :shebang #!/usr/bin/env python3
from datetime import datetime import json hours = [""] + ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve"] * 2 partials = ["", "five", "ten", "quarter", "twenty", "twenty five", "half", "xXx"] def pick_idxs(h, m): if m == 0: return (0, 0, f"{hours[h]} o'clock") elif m <= 3: return (0, h, "just after") elif m <=32 and m > 3: return (round(m / 5), h, "past") elif m >=33 and m < 57: return (12 - round(m / 5), h+1, "to") elif m >= 57: return (0, h+1, "nearly") if __name__ == "__main__": now = datetime.now() (pidx, hidx, direction) = pick_idxs(now.hour, now.minute) fuzzy = f"{partials[pidx]} {direction} {hours[hidx]}" print(json.dumps(dict( text=fuzzy, tooltip=now.strftime("%H:%M\r%a %d %b W%W"), ))) # for h in range(0,24): # for m in range(0,59,3): # (pidx, hidx, direction) = pick_idxs(h, m) # fuzzy = f"{partials[pidx]} {direction} {hours[hidx]}" # print(json.dumps(dict( # text=fuzzy, # tooltip=now.strftime("%H:%M\r%a %d %b W%W"), # t=f"{h}:{m}" # )))

Logout/Lock/Etc helper wlogout

nix source: :tangle ~/arroyo-nix/hm/wayland/wlogout.nix
{ pkgs, ... }: let lockCommand = "${pkgs.swaylock-effects}/bin/swaylock --grace 5 -c 0x303230 --clock -S --effect-pixelate 8"; in { wayland.windowManager.river.settings.map.normal. "Control+Alt backspace" = "spawn 'wlogout'"; home.file.".config/wlogout/layout".text = '' { "label" : "lock", "action" : "${lockCommand}", "text" : "Lock", "keybind" : "l" } { "label" : "hibernate", "action" : "systemctl hibernate", "text" : "Hibernate", "keybind" : "h" } { "label" : "logout", "action" : "riverctl exit", "text" : "Logout", "keybind" : "e" } { "label" : "shutdown", "action" : "systemctl poweroff", "text" : "Shutdown", "keybind" : "s" } { "label" : "suspend", "action" : "systemctl suspend", "text" : "Suspend", "keybind" : "u" } { "label" : "reboot", "action" : "systemctl reboot", "text" : "Reboot", "keybind" : "r" } ''; }

Automatic screen locking and display blanking swayidle etc

swayidle + swaylock + wlopm.

I use two instances of swayidle, one for locking and one for blanking so that I can SIGUSR1 the locking process without also blanking:

nix source: :tangle ~/arroyo-nix/hm/wayland/swayidle.nix
{ pkgs, ... }: let locker = pkgs.writeScript "swaylock-rr" '' ${pkgs.swaylock-effects}/bin/swaylock --grace 5 -c 0x303230 --clock -S --effect-pixelate 8 ''; dpms = pkgs.writeScript "wlopm-rr" '' # ARG1 is either on or off ${pkgs.wlopm}/bin/wlopm --$1 '*' ''; idleCmdDpms = pkgs.writeScript "swayidle-dpms" '' #!${pkgs.stdenv.shell} ${pkgs.swayidle}/bin/swayidle \ timeout 1200 "${dpms} off" \ resume "${dpms} on" ''; idleCmd = pkgs.writeScript "swayidle-locker" '' #!${pkgs.stdenv.shell} ${pkgs.swayidle}/bin/swayidle \ before-sleep "${locker}" \ timeout 300 "${locker}" \ resume "${locker}" ''; in { systemd.user.services = with pkgs; let mkServiceUnit = (callPackage ./lib.nix {}).mkServiceUnit; in { swayidle-locker = mkServiceUnit "${idleCmd}" {}; swayidle-dpms = mkServiceUnit "${idleCmdDpms}" {}; }; wayland.windowManager.river.settings.map.normal = { "None XF86ScreenSaver" = "spawn 'pkill -USR1 -f swaylock-rr'"; "None XF86Applications" = "spawn 'pkill -USR1 -f swaylock-rr'"; }; }

Multi-monitor configuration engine way-displays

I hae a script to handle dynamic configuration on laptop lid close, and scaling configuration stuff.

i don't like this script so much, it still needs some work, i think but it's good enough to disable my laptop display when i close my docked laptop:

nix source: :tangle ~/arroyo-nix/hm/wayland/way-displays.nix
{ pkgs, ... }: let display-flipper = pkgs.writeScriptBin "flip-display" '' ENABLED_QUERY='.STATE.HEADS[] | select(.NAME == "eDP-1" and .CURRENT.ENABLED)' EXTERNAL_QUERY='.STATE.HEADS[] | select(.NAME != "eDP-1")' INTERNAL_CONNECTED=$(way-displays -y -g | ${pkgs.yq}/bin/yq -e "$ENABLED_QUERY") EXTERNAL_CONNECTED=$(way-displays -y -g | ${pkgs.yq}/bin/yq -e "$EXTERNAL_QUERY") if [ -n "$INTERNAL_CONNECTED" ]; then # add to the disabled list if another display is connected if [ -n "$EXTERNAL_CONNECTED" ]; then way-displays -s DISABLED "eDP-1" else swaylock fi else # remove from the disabled list way-displays -d DISABLED "eDP-1" fi ''; in { home.packages = [ display-flipper ]; wayland.windowManager.river.settings.map-switch.normal = { "lid close" = "spawn ${display-flipper}/bin/flip-display"; "lid open" = "spawn ${display-flipper}/bin/flip-display"; }; wayland.windowManager.river.extraConfig = '' way-displays > $XDG_RUNTIME_DIR/way-displays.$XDG_VTNR.log 2>&1 & ''; home.file.".config/way-displays/cfg.yaml".text = builtins.toJSON { ARRANGE = "COLUMN"; ALIGN = "MIDDLE"; AUTO_SCALE = true; ORDER = [ "DP-2" "eDP-1" "DSI-1" ]; SCALE = [ { NAME_DESC = "eDP-1"; SCALE = 1.25; } { NAME_DESC = "DSI-1"; SCALE = 1.5; } ]; TRANSFORM = [ { NAME_DESC = "DSI-1"; TRANSFORM = 270; } ]; }; }

Notifications with fnott

nix source: :tangle ~/arroyo-nix/hm/wayland/fnott.nix :mkdirp yes
{ pkgs, ... }: { systemd.user.services = with pkgs; let mkServiceUnit = (callPackage ./lib.nix {}).mkServiceUnit; in { fnott = mkServiceUnit "${pkgs.fnott}/bin/fnott" {}; }; home.file.".config/fnott/fnott.ini".text = '' min-width = 500 max-width = 500 icon-theme = breeze anchor = bottom-right stacking-order = top-down # figure out selection-helper stuff and actions background = 303230ff border-color = 222522ff dpi-aware = yes title-font = Vulf Mono:weight=bold title-color = cfdfd5ff summary-font = Vulf Mono:slant=italic:size=14 summary-color = cfdfd5ff body-font = Vulf Mono:slant=italic:size=12 body-color = cfdfd5ff max-timeout = 30 default-timeout = 30 idle-timeout = 300 [low] max-timeout = 15 default-timeout = 5 ''; }

Wallpaper Daemon swww

And a script to instruct it to switch random

nix source: :tangle ~/arroyo-nix/hm/wayland/swww.nix
{ config, pkgs, ... }: let swwwitch = pkgs.writeShellApplication { name = "swww-img-dyn-switch"; runtimeInputs = with pkgs; [ coreutils findutils swww ]; text = '' set +o pipefail DIRS="/home/rrix/sync/wallpapers/ /home/rrix/Pictures/Backgrounds/" # shellcheck disable=2086 find $DIRS -type f | sort -R | head -1 | xargs -I{} swww img -t any {} ''; }; in { home.packages = [ swwwitch ]; systemd.user.services = with pkgs; let mkServiceUnit = (callPackage ./lib.nix {}).mkServiceUnit; in { swww-daemon = mkServiceUnit "${swww}/bin/swww-daemon" {}; swww-img-dyn-switch = mkServiceUnit "${swwwitch}/bin/swww-img-dyn-switch" { Service.Type = "oneshot"; }; }; systemd.user.timers = with pkgs; let mkTimerUnit = (callPackage ./lib.nix {}).mkTimerUnit; in { swww-img-dyn-switch = mkTimerUnit "swww-img-dyn-switch.service" { Timer.OnActiveSec = 60 * 5; Timer.OnCalendar = "*:0/5:00"; }; }; }

Clipboard Management clipman

God i'm gonna miss klipper too, huh. This is basically good enough, though:

nix source: :tangle ~/arroyo-nix/hm/wayland/clipman.nix
{ ... }: { services.clipman.enable = true; wayland.windowManager.river.settings.map.normal. "Super v" = ''spawn 'clipman pick -t CUSTOM --tool-args "fuzzel -d"' ''; }

Virtual Keyboard squeekboard

nix source: :tangle ~/arroyo-nix/hm/wayland/squeekboard.nix
{ pkgs , lib , ... }: let preamble = '' export XDG_DATA_DIRS=$XDG_DATA_DIRS:${pkgs.glib}/share export GIO_EXTRA_MODULES=$GIO_EXTRA_MODULES:"${lib.getLib pkgs.dconf}/lib/gio/modules" export XDG_DATA_DIRS=$XDG_DATA_DIRS:"${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}" export XDG_DATA_DIRS=$XDG_DATA_DIRS:"${pkgs.hicolor-icon-theme}/share" ''; startSqk = pkgs.writeScriptBin "start-sqk" '' ${preamble} riverctl spawn squeekboard ${pkgs.glib}/bin/gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled true ''; stopSqk = pkgs.writeScriptBin "stop-sqk" '' ${preamble} killall squeekboard ${pkgs.glib}/bin/gsettings set org.gnome.desktop.a11y.applications screen-keyboard-enabled false ''; in { home.packages = [ pkgs.squeekboard startSqk stopSqk ]; # if this works this should be a systemd unit... wayland.windowManager.river.settings.map-switch.normal = { "tablet on" = "spawn ${startSqk}/bin/start-sqk"; "tablet off" = "spawn ${stopSqk}/bin/stop-sqk"; }; }

Emacs Commands and helpers

emacs-lisp source: 
(global-set-key (kbd "s-o") #'other-window)

Desktop Shit that's still broken

  • if i close my laptop and disable my monitor with DPMS, logind believes there are no connected displays and suspends the machine; this kills the tailscale.

  • gpd pocket has gpu/display issues maybe every other session start

    • if i autoload my session on the GPD pocket 3, it locks up the desktop and no longer updates the display

    • if i start river from a terminal it works but the swww image doesnt take the whole display and:

  • on GPD Pocket way-displays 1.0 pixel scaling looks good on Firefox and Emacs but bad on Qt apps and Waybar, too small

  • if i loginctl terminate the session it leaves the user units around because theyre not scoped to the seat, left in failed state and need to be restarted next time the session loads

  • waybar started in a systemd unit doesnt load icons, but does if i spawn it in a konsole or riverctl.

  • when waybar re-execs on configuration change from a home-manager generation update, the multiple processes may continue with the configuration.

  • pantalaiamon user unit cant spawn kwalletd6 to get the secrets but kwalletd6 starts if i spawn it in konsole and then i can restart pantalaimon to talk to it

    • i would love to switch back to breeze Qt theme some day; but this was causing kwalletd5 to load incorrectly w/o the secrets DBus interface.

Here are some bodges, mostly trying to

nix source: :tangle ~/arroyo-nix/hm/wayland/fucking-hacks.nix
{ pkgs, lib, osConfig, ... }: { home.sessionVariables = lib.mkIf (osConfig.networking.hostName == "rose-quine") { WLR_DRM_NO_ATOMIC = "1"; XDG_CURRENT_DESKTOP = "river"; }; xdg.portal.enable = true; xdg.portal.extraPortals = with pkgs; [ xdg-desktop-portal-gtk xdg-desktop-portal-wlr ]; xdg.portal.config.river.default = [ "gtk" "wlr" ]; # "wlr" "gtk" ]; xdg.portal.config.river = { "org.freedesktop.impl.portal.Secret" = [ "kwallet" ]; "org.freedesktop.impl.portal.Screenshot" = [ "wlr" ]; "org.freedesktop.impl.portal.ScreenCast" = [ "wlr" ]; }; wayland.windowManager.river.settings.spawn = [ "kwalletd6" "'sleep 1 && systemctl --user restart xdg-desktop-portal-gtk'" # need to restart this to deal with "cannot connect to display" error "'sleep 1 && systemctl --user restart xdg-desktop-portal-wlr'" # wlr wont start itself "'sleep 1 && systemctl --user restart xdg-desktop-portal'" # portal needs to start after wlr "'systemctl --user restart pantalaimon'" # need to wait for kwalletd to start up... ]; }

panta can't start kwalletd to provide org.freedesktop.secrets

method call time=1722033555.323031 sender=:1.44 -> destination=org.freedesktop.DBus serial=21 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=StartServiceByName
   string "org.kde.kwalletd5"
   uint32 0
error time=1722033555.332560 sender=org.freedesktop.DBus -> destination=:1.44 error_name=org.freedesktop.DBus.Error.Spawn.ChildSignaled reply_serial=21
   string "Process org.kde.kwalletd5 received signal 6"

it's starting kwallet 5 which is pulled in by breeze-qt5 transitively, rather than kwallet 6, so i can't use breeze any more, i guess