{ config, lib, pkgs, ... }: # docs: https://nixos.wiki/wiki/Sway # sway-config docs: `man 5 sway` let cfg = config.sane.programs.sway; enableXWayland = config.sane.programs.xwayland.enabled; wrapSway = configuredSway: let swayLauncher = pkgs.writeShellScriptBin "sway" '' test -e "$(dirname "$SWAYSOCK")" || \ echo "warning: required directory not found (create it?): $(dirname "$SWAYSOCK")" test -e "$(dirname "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY")" || \ echo "warning: required directory not found (create it?): $(dirname "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY")" test -e /tmp/.X11-unix || \ echo "warning: required directory not found (create it?): /tmp/.X11-unix" # delete DISPLAY-related vars from env before launch, else sway will try to connect to a remote display. # (consider: nested sway sessions, where sway actually has a reason to read these) exec env -u DISPLAY -u WAYLAND_DISPLAY "DESIRED_WAYLAND_DISPLAY=$WAYLAND_DISPLAY" ${configuredSway}/bin/sway 2>&1 ''; in pkgs.symlinkJoin { inherit (configuredSway) meta version; name = "sway-wrapped"; paths = [ swayLauncher configuredSway ]; passthru.sway-unwrapped = configuredSway; }; wlroots = (pkgs.nixpkgs-wayland.wlroots.override { # wlroots seems to launch Xwayland itself, and i can't easily just do that myself externally. # so in order for the Xwayland it launches to be sandboxed, i need to patch the sandboxed version in here. xwayland = config.sane.programs.xwayland.package; }).overrideAttrs (upstream: { # 2023/09/08: fix so clicking a notification can activate the corresponding window. # - test: run dino, receive a message while tabbed away, click the desktop notification. # - if sway activates the dino window (i.e. colors the workspace and tab), then all good # - do all of this with only a touchscreen (e.g. on mobile phone) -- NOT a mouse/pointer # 2023/12/17: this patch is still necessary ## what this patch does: # - allows any wayland window to request activation, at any time. # - traditionally, wayland only allows windows to request activation if # the client requesting to transfer control has some connection to a recent user interaction. # - e.g. the active window may transfer control to any window # - a window which was very recently active may transfer control to itself ## alternative (longer-term) solutions: # - fix this class of bug in gtk: # - # - N.B.: this linked PR doesn't actually fix it # - add xdg_activation_v1 support to SwayNC (my notification daemon): # - # - mako notification daemon supports activation, can use as a reference # - all of ~30 LoC, looks straight-forward # - however, it's not clear that gtk4 (or dino) actually support this mode of activation. # - i.e. my experience with dino is the same using mako as with SwayNC postPatch = (upstream.postPatch or "") + '' substituteInPlace types/wlr_xdg_activation_v1.c \ --replace-fail 'if (token->seat != NULL)' 'if (false && token->seat != NULL)' ''; }); swayPackage = wrapSway ( (pkgs.nixpkgs-wayland.sway-unwrapped.override { inherit wlroots; # about xwayland: # - required by many electron apps, though some electron apps support NIXOS_OZONE_WL=1 for native wayland. # - when xwayland is enabled, KOreader incorrectly chooses the X11 backend # -> slower; blurrier # - xwayland uses a small amount of memory (like 30MiB, IIRC?) inherit enableXWayland; }).overrideAttrs (upstream: { # fix to create SWAYSOCK and WAYLAND_DISPLAY directly in a sandboxable subdirectory: # i can't simply move it after creation i think because that would # be an unsupported cross-device `mv`? postPatch = (upstream.postPatch or "") + '' substituteInPlace sway/ipc-server.c --replace-fail \ '"%s/sway-ipc.%u.%i.sock", dir, getuid(), getpid())' \ '"%s/sway/sway-ipc.sock", dir)' substituteInPlace sway/server.c --replace-fail \ '"wayland-%u"' '"wl/wayland-%u"' ''; }) ); in { sane.programs.sway = { configOption = with lib; mkOption { default = {}; type = types.submodule { options = { extra_lines = mkOption { type = types.lines; description = '' extra lines to append to the sway config ''; default = '' # XXX: sway needs exclusive control of XF86Audio{Raise,Lower}Volume, so assign this from a block that it can override. # TODO: factor the bindings out into proper options and be less hacky? bindsym --locked XF86AudioRaiseVolume exec $volume_up bindsym --locked XF86AudioLowerVolume exec $volume_down ''; }; font = mkOption { type = types.str; default = "pango:monospace 11"; description = '' default font (for e.g. window titles) ''; }; locker = mkOption { type = types.str; default = "swaylock"; description = '' name of program to use as the screenlocker ''; example = "schlock"; }; mod = mkOption { type = types.str; default = "Mod4"; description = '' Super key (for non-application shortcuts). - "Mod1" for Alt - "Mod4" for logo key ''; }; workspace_layout = mkOption { type = types.str; default = "default"; description = '' how to arrange windows within new workspaces, by default: - "default" (split) - "tabbed" - etc ''; }; }; }; }; packageUnwrapped = swayPackage; suggestedPrograms = [ "guiApps" "blueberry" # GUI bluetooth manager "brightnessctl" "conky" # for a nice background # "fcitx5" # input method; emoji # TODO(2024/05/10): this broke 2-ish weeks ago, glitches input into any textbox, possibly only when xwayland is enabled? "fontconfig" # "gnome.gnome-bluetooth" # XXX(2023/05/14): broken # "gnome.gnome-control-center" # XXX(2023/06/28): depends on webkitgtk4_1 "nwg-panel" "pipewire" "playerctl" # for waybar & particularly to have playerctld running "rofi" # menu/launcher "rofi-snippets" "s6-rc" # used by sway config "sane-screenshot" "sane-open" "sane-theme" "seatd" # "splatmoji" # used by sway config "sway-contrib.grimshot" # used by sway config "swayidle" # enable if you need it "swaynotificationcenter" # notification daemon "switchboard" # network/bluetooth/sound control panel "sysvol" # volume notifier "unl0kr" # greeter # "waybar" "wdisplays" # like xrandr "wireplumber" # used by sway config "wl-clipboard" "xdg-desktop-portal" # xdg-desktop-portal-gtk provides portals for: # - org.freedesktop.impl.portal.Access # - org.freedesktop.impl.portal.Account # - org.freedesktop.impl.portal.DynamicLauncher # - org.freedesktop.impl.portal.Email # - org.freedesktop.impl.portal.FileChooser # - org.freedesktop.impl.portal.Inhibit # - org.freedesktop.impl.portal.Notification # - org.freedesktop.impl.portal.Print # and conditionally (i.e. unless buildPortalsInGnome = false) for: # - org.freedesktop.impl.portal.AppChooser (@appchooser_iface@) # - org.freedesktop.impl.portal.Lockdown (@lockdown_iface@) # - org.freedesktop.impl.portal.Settings (@settings_iface@) # - org.freedesktop.impl.portal.Wallpaper (@wallpaper_iface@) "xdg-desktop-portal-gtk" # xdg-desktop-portal-wlr provides portals for screenshots/screen sharing "xdg-desktop-portal-wlr" "xdg-terminal-exec" # used by sway config ] ++ [ cfg.config.locker ]; sandbox.method = "bwrap"; sandbox.wrapperType = "inplace"; #< `sway` is a wrapper around `sway-unwrapped`, so cheap to sandbox sandbox.net = "all"; # TODO: shouldn't be needed! but without this, mouse/kb hotplug doesn't work. sandbox.whitelistAudio = true; # it runs playerctl directly sandbox.whitelistDbus = [ "system" "user" ]; # to e.g. launch apps sandbox.whitelistDri = true; sandbox.whitelistX = true; # sway invokes xwayland itself sandbox.whitelistWayland = true; sandbox.extraRuntimePaths = [ "sway" ]; sandbox.extraPaths = [ # "/dev/input" "/run/seatd.sock" #< required if not using `logind` systemd login manager # "/run/systemd/sessions" "/run/udev" "/sys/class/backlight" "/sys/class/drm" "/sys/class/input" "/sys/dev/char" "/sys/devices" ]; sandbox.extraHomePaths = [ # bind ALL of ~/.config/sway into the sandbox instead of just the individual configs. # this way `swaymsg -- reload` can work even if the fd for ~/.config/sway/config changes. ".config/sway" ]; fs.".config/xdg-desktop-portal/sway-portals.conf".symlink.text = '' # portals.conf docs: [preferred] default=wlr;gtk ''; fs.".config/sway/config".symlink.target = pkgs.substituteAll { src = ./sway-config; inherit (cfg.config) extra_lines font locker mod workspace_layout ; xwayland = if enableXWayland then "enable" else "disable"; }; env.XDG_CURRENT_DESKTOP = "sway"; # sway defaults to auto-generating a unix domain socket named "sway-ipc.$UID.NNNN.sock", # which allows for multiple sway sessions under the same user. # but the unpredictability makes static sandboxing & such difficult, so hardcode it. # place it in a subdirectory for the same reason: env.SWAYSOCK = "$XDG_RUNTIME_DIR/sway/sway-ipc.sock"; # TODO: ensure this is reliable? might not work across sway restarts, etc. env.DISPLAY = ":0"; # docs: # N.B.: gtk apps support absolute paths for this; webkit apps (e.g. geary) support only relative paths (relative to $XDG_RUNTIME_DIR) env.WAYLAND_DISPLAY = "wl/wayland-1"; services.sway = { description = "sway: tiling wayland desktop environment"; partOf = [ "wayland" ] ++ lib.optionals enableXWayland [ "x11" ]; # partOf = lib.mkMerge [ # "wayland" # (lib.mkIf enableXWayland "x11") # ]; command = pkgs.writeShellScript "sway-start" '' # have to create these directories before launching sway so that they're available in the sandbox mkdir -p "$(dirname "$SWAYSOCK")" mkdir -p "$(dirname "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY")" mkdir -p /tmp/.X11-unix # for Xwayland exec sway ''; readiness.waitExists = [ "$SWAYSOCK" "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" ]; }; # launch all graphical apps by default services."graphical-session".partOf = [ "default" ]; }; # TODO: this can go elsewhere hardware.bluetooth.enable = lib.mkIf cfg.enabled true; services.blueman.enable = lib.mkIf cfg.enabled true; # gsd provides Rfkill, which is required for the bluetooth pane in gnome-control-center to work # services.gnome.gnome-settings-daemon.enable = true; # start the components of gsd we need at login # systemd.user.targets."org.gnome.SettingsDaemon.Rfkill".wantedBy = [ "graphical-session.target" ]; # go ahead and `systemctl --user cat gnome-session-initialized.target`. i dare you. # the only way i can figure out how to get Rfkill to actually load is to just disable all the shit it depends on. # it doesn't actually seem to need ANY of them in the first place T_T # systemd.user.targets."gnome-session-initialized".enable = false; # bluez can't connect to audio devices unless pipewire is running. # a system service can't depend on a user service, so just launch it at graphical-session }