I've recently installed my personal laptop to NixOS. Long time i3 user I took this opportunity to use sway instead. To manage some of my home configuration files I'm using the excellent home-manager project.

X to Wayland

Moving from i3 to sway is simple because the configuration is compatible. Even if sway provide XWayland support for non Wayland applications I tried to find Wayland native alternatives for the following applications:

Also, to manage displays automatically: kanshi

And finally waybar for a nicer status bar.

Starting sway in NixOS

At the time of this writing sway cannot be started from graphical login managers (but it should be fixed soon).

Anyway I'm used to login directly on the tty and run startx, but now with sway it is instead start-sway.

NixOS configuration

In /etc/nixos/configuration.nix I've added:

programs.sway.enable = true;

This does not only install the sway package but also activate pam configuration for swaylock, installs some default fonts, enable opengl...

Home-manager configuration

Now that sway is installed globally, I wanted to start sway and other tools like waybar, mako and kanshi as systemd user services.

One tricky thing is to properly share a D-Bus socket between all theses services. Hopefully I've found my answer on serverfault (read it for details).

Also waybar needs to know where to find the sway socket. sway exports $SWAYSOCK once started but it's only available inside sway. From a tty running sway --get-socket-path will return sway socket not detected. As a workaround it is possible to determine the socket location based on your user id and sway process id:

export SWAYSOCK=/run/user/$(id -u)/sway-ipc.$(id -u).$(pgrep -f 'sway$').sock

My home-manager configuration for sway is:

{ config, pkgs, ... }:

let

  start-sway = pkgs.writeShellScriptBin "start-sway" ''
    # first import environment variables from the login manager
    systemctl --user import-environment
    # then start the service
    exec systemctl --user start sway.service
  '';

  start-waybar = pkgs.writeShellScriptBin "start-waybar" ''
    export SWAYSOCK=/run/user/$(id -u)/sway-ipc.$(id -u).$(pgrep -f 'sway$').sock
    ${pkgs.waybar}/bin/waybar
  '';

in {

  home.packages = with pkgs; [
    start-sway
    wofi grim wl-clipboard imv slurp brightnessctl bemenu
  ];

  systemd.user.sockets.dbus = {
    Unit = {
      Description = "D-Bus User Message Bus Socket";
    };
    Socket = {
      ListenStream = "%t/bus";
      ExecStartPost = "${pkgs.systemd}/bin/systemctl --user set-environment DBUS_SESSION_BUS_ADDRESS=unix:path=%t/bus";
    };
    Install = {
      WantedBy = [ "sockets.target" ];
      Also = [ "dbus.service" ];
    };
  };

  systemd.user.services.dbus = {
    Unit = {
      Description = "D-Bus User Message Bus";
      Requires = [ "dbus.socket" ];
    };
    Service = {
      ExecStart = "${pkgs.dbus}/bin/dbus-daemon --session --address=systemd: --nofork --nopidfile --systemd-activation";
      ExecReload = "${pkgs.dbus}/bin/dbus-send --print-reply --session --type=method_call --dest=org.freedesktop.DBus / org.freedesktop.DBus.ReloadConfig";
    };
    Install = {
      Also = [ "dbus.socket" ];
    };
  };

  systemd.user.services.sway = {
    Unit = {
      Description = "Sway - Wayland window manager";
      Documentation = [ "man:sway(5)" ];
      BindsTo = [ "graphical-session.target" ];
      Wants = [ "graphical-session-pre.target" ];
      After = [ "graphical-session-pre.target" ];
    };
    Service = {
      Type = "simple";
      ExecStart = "${pkgs.sway}/bin/sway";
      Restart = "on-failure";
      RestartSec = 1;
      TimeoutStopSec = 10;
    };
  };

  systemd.user.services.mako = {
    Unit = {
      Description = "Mako notification daemon";
      PartOf = [ "graphical-session.target" ];
    };
    Install = {
      WantedBy = [ "graphical-session.target" ];
    };
    Service = {
      Type = "dbus";
      BusName = "org.freedesktop.Notifications";
      ExecStart = "${pkgs.mako}/bin/mako";
      RestartSec = 5;
      Restart = "always";
    };
  };

  systemd.user.services.kanshi = {
    Unit = {
      Description = "Kanshi dynamic display configuration";
      PartOf = [ "graphical-session.target" ];
    };
    Install = {
      WantedBy = [ "graphical-session.target" ];
    };
    Service = {
      Type = "simple";
      ExecStart = "${pkgs.kanshi}/bin/kanshi";
      RestartSec = 5;
      Restart = "always";
    };
  };

  xdg.configFile."kanshi/config".text = ''
    {
      output eDP-1 mode 1920x1080 position 0,0
      output "ViewSonic Corporation VP2770 SERIES T56131300326" mode 2560x1440 position 1920,0
    }
    {
      output eDP-1 mode 1920x1080 position 0,0
    }
  '';

  systemd.user.services.waybar = {
    Unit = {
      Description = "Wayland bar for Sway and Wlroots based compositors";
      PartOf = [ "graphical-session.target" ];
    };
    Install = {
      WantedBy = [ "graphical-session.target" ];
    };
    Service = {
      Type = "simple";
      ExecStart = "${start-waybar}/bin/start-waybar";
      RestartSec = 5;
      Restart = "always";
    };
  };

}

start-sway is run from a TTY, it imports my shell environment in the user systemd daemon and start sway.service.

The sway.service binds to graphical-session.target so that when it is started graphical-session.target and all dependent services are started as well:

⚡ systemctl --user list-dependencies sway.service
sway.service
● ├─-.slice
● └─graphical-session.target
●   ├─kanshi.service
●   ├─mako.service
●   ├─waybar.service
●   └─basic.target
●     ├─paths.target
●     ├─sockets.target
●     │ ├─dbus.socket
●     │ ├─gpg-agent-ssh.socket
●     │ ├─gpg-agent.socket
●     │ └─pulseaudio.socket
●     └─timers.target

⚡ systemctl --user show-environment | grep DBUS
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
⚡ env | grep DBUS
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus

If everything goes well you should be able to run:

⚡ nix-shell -p libnotify
[nix-shell:~]$ notify-send Test

And the notification should be properly displayed by mako.