{ config, lib, pkgs, ... }: # docs: https://nixos.wiki/wiki/Sway # sway-config docs: `man 5 sway` let cfg = config.sane.programs.sway; wrapSway = configuredSway: let # `wrapSway` exists to launch sway with our desired debugging facilities. # i.e. redirect output to syslog. systemd-cat = "${lib.getBin pkgs.systemd}/bin/systemd-cat"; swayLauncher = pkgs.writeShellScriptBin "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: export SWAYSOCK="$XDG_RUNTIME_DIR/sway-ipc.sock" export XDG_CURRENT_DESKTOP=sway echo "launching sway (sway.desktop)..." | ${systemd-cat} --identifier=sway exec ${configuredSway}/bin/sway 2>&1 | ${systemd-cat} --identifier=sway ''; in pkgs.symlinkJoin { inherit (configuredSway) meta version; name = "sway-wrapped"; paths = [ swayLauncher configuredSway ]; }; swayPackage = wrapSway (pkgs.sway-unwrapped.overrideAttrs (_: { # isNixOS = true; #< doesn't matter # TODO: something else is dragging a xwayland-enabled wlroots into the environment, # making this actually kinda wasteful. enableXWayland = cfg.config.xwayland; })); 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 ''; }; background = mkOption { type = types.path; }; font = mkOption { type = types.str; default = "pango:monospace 11"; description = '' default font (for e.g. window titles) ''; }; 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 ''; }; xwayland = mkOption { type = types.bool; default = true; description = '' whether or not to enable xwayland (allows running X11 apps on sway). some electron apps (e.g. element-desktop) require xwayland. ''; }; screenshot_cmd = mkOption { type = types.str; default = "grimshot copy area"; description = "command to run when user wants to take a screenshot"; }; }; }; }; packageUnwrapped = swayPackage; suggestedPrograms = [ "guiApps" "blueberry" # GUI bluetooth manager "brightnessctl" "conky" # for a nice background "fontconfig" "fuzzel" # "gnome.gnome-bluetooth" # XXX(2023/05/14): broken # "gnome.gnome-control-center" # XXX(2023/06/28): depends on webkitgtk4_1 "playerctl" # for waybar & particularly to have playerctld running "pulsemixer" # for volume controls "splatmoji" # used by sway config "sway-contrib.grimshot" # used by sway config # "swayidle" # enable if you need it "swaylock" # used by sway config "swaynotificationcenter" # notification daemon "unl0kr" # greeter "waybar" # used by sway config "wdisplays" # like xrandr "wl-clipboard" "wob" # render volume changes on-screen "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 ]; secrets.".config/sane-sway/snippets.txt" = ../../../../secrets/common/snippets.txt.bin; fs.".config/xdg-desktop-portal/sway-portals.conf".symlink.text = '' # portals.conf docs: [preferred] default=wlr;gtk ''; fs.".config/sway/config".symlink.target = pkgs.callPackage ./sway-config.nix { inherit config; swayCfg = cfg.config; }; services.sway-session = { description = "sway-session: active iff sway desktop environment is baseline operational"; documentation = [ "https://github.com/swaywm/sway/issues/7862" "https://github.com/alebastr/sway-systemd" ]; # we'd like to start graphical-session after sway is ready, but it's marked `RefuseManualStart` because Lennart. # instead, create `sway-session.service` which `bindsTo` `graphical-session.target`. # we can manually start `sway-session`, and the `bindsTo` means that it will start `graphical-session`, # and then track `graphical-session`s state (that is: it'll stop when graphical-session stops). # # additionally, set `ConditionEnvironment` to guard that the sway environment variables *really have* been imported into systemd. unitConfig.ConditionEnvironment = "SWAYSOCK"; # requiredBy = [ "graphical-session.target" ]; before = [ "graphical-session.target" ]; bindsTo = [ "graphical-session.target" ]; serviceConfig = { ExecStart = "${pkgs.coreutils}/bin/true"; Type = "oneshot"; RemainAfterExit = true; }; }; }; sane.gui.gtk = lib.mkIf cfg.enabled { enable = lib.mkDefault true; # gtk-theme = lib.mkDefault "Fluent-Light-compact"; gtk-theme = lib.mkDefault "Tokyonight-Light-B"; # icon-theme = lib.mkDefault "HighContrast"; # 4/5 coverage on moby # icon-theme = lib.mkDefault "WhiteSur"; # 3.5/5 coverage on moby, but it provides a bunch for Fractal/Dino # icon-theme = lib.mkDefault "Humanity"; # 3.5/5 coverage on moby, but it provides the bookmark icon # icon-theme = lib.mkDefault "Paper"; # 3.5/5 coverage on moby, but it provides the bookmark icon # icon-theme = lib.mkDefault "Nordzy"; # 3/5 coverage on moby # icon-theme = lib.mkDefault "Fluent"; # 3/5 coverage on moby # icon-theme = lib.mkDefault "Colloid"; # 3/5 coverage on moby # icon-theme = lib.mkDefault "Qogir"; # 2.5/5 coverage on moby # icon-theme = lib.mkDefault "rose-pine-dawn"; # 2.5/5 coverage on moby # icon-theme = lib.mkDefault "Flat-Remix-Grey-Light"; # requires qtbase }; # TODO: port to sane.programs programs.xwayland = lib.mkIf cfg.enabled { enable = cfg.config.xwayland; }; services.gvfs = lib.mkIf cfg.enabled { enable = true; # allow nautilus to mount remote filesystems (e.g. ftp://...) package = lib.mkDefault (pkgs.gvfs.override { # i don't need to mount samba shares, and samba build is expensive/flaky (mostly for cross, but even problematic on native) samba = null; }); }; # unlike other DEs, sway configures no audio stack # administer with pw-cli, pw-mon, pw-top commands services.pipewire = lib.mkIf cfg.enabled { enable = true; alsa.enable = true; alsa.support32Bit = true; # ?? # emulate pulseaudio for legacy apps (e.g. sxmo-utils) pulse.enable = true; }; systemd.user.services."pipewire".wantedBy = lib.optionals cfg.enabled [ "graphical-session.target" ]; # rtkit/RealtimeKit: allow applications which want realtime audio (e.g. Dino? Pulseaudio server?) to request it. # this might require more configuration (e.g. polkit-related) to work exactly as desired. # - readme outlines requirements: # XXX(2023/10/12): rtkit does not play well on moby. any application sending audio out dies after 10s. # security.rtkit.enable = true; # persist per-device volume levels sane.user.persist.byStore.plaintext = lib.optionals cfg.enabled [ ".local/state/wireplumber" ]; # persist per-device volume settings across power cycles. # pipewire sits atop the kernel ALSA API, so alsa-utils knows about device volumes. # but wireplumber also tries to do some of this # systemd.services.alsa-store = { # # based on # description = "Store Sound Card State"; # wantedBy = [ "multi-user.target" ]; # serviceConfig = { # Type = "oneshot"; # RemainAfterExit = true; # ExecStart = "${pkgs.alsa-utils}/sbin/alsactl restore"; # ExecStop = "${pkgs.alsa-utils}/sbin/alsactl store --ignore"; # }; # }; # sane.persist.sys.byStore.plaintext = [ "/var/lib/alsa" ]; # TODO: this can go elsewhere # networking = lib.mkIf cfg.enabled { # networkmanager.enable = true; # wireless.enable = lib.mkForce false; # }; hardware.bluetooth = lib.mkIf cfg.enabled { enable = true; }; services.blueman = lib.mkIf cfg.enabled { enable = 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 }