Noctalia V5 + LabWC on NixOS
Noctalia V5 isn't released yet but you already can try it on Linux distro like Artix, Arch, NixOS. Please follow the official Noctalia documentation or copy the parts you need from my configuration.nix. The ram usage on NixOS is 366Mb for Noctalia V5 + LabWC, I also use Ly as minimal login manager.
# /etc/nixos/configuration.nix
{ config, pkgs, lib, ... }:
let
noctalia = import (fetchTarball "https://github.com/noctalia-dev/noctalia/archive/cachix.tar.gz") {};
in
{
imports = [
./hardware-configuration.nix
noctalia.nixosModule
];
nix.package = pkgs.nixVersions.stable;
nix.settings = {
experimental-features = [ "nix-command" "flakes" ];
extra-substituters = [ "https://noctalia.cachix.org" ];
extra-trusted-public-keys = [ "noctalia.cachix.org-1:pCOR47nnMEo5thcxNDtzWpOxNFQsBRglJzxWPp3dkU4=" ];
# auto-optimise-store = true; # disabled: slow on virtio HDD, run `nix store optimise` periodically
};
# system.autoUpgrade.enable = true; # disabled: consumes CPU/RAM on single-core VM
nix.gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 7d";
};
# Bootloader & kernel
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
boot.kernelPackages = pkgs.linuxPackages_7_1;
boot.tmp.useTmpfs = true;
# Networking
networking.hostName = "nixos";
networking.networkmanager.enable = true;
networking.firewall.enable = true;
networking.firewall.allowedTCPPorts = [ 3000 ];
# Host filesystem mounts
# mount host dir
fileSystems."/mnt/shared_host" = {
fsType = "virtiofs";
device = "shared_host";
options = [ "rw" "noatime" "_netdev" "nofail" ];
};
# Time & locale
time.timeZone = "Europe/Rome";
i18n.defaultLocale = "en_US.UTF-8";
i18n.extraLocaleSettings = {
LC_ADDRESS = "it_IT.UTF-8";
LC_IDENTIFICATION = "it_IT.UTF-8";
LC_MEASUREMENT = "it_IT.UTF-8";
LC_MONETARY = "it_IT.UTF-8";
LC_NAME = "it_IT.UTF-8";
LC_NUMERIC = "it_IT.UTF-8";
LC_PAPER = "it_IT.UTF-8";
LC_TELEPHONE = "it_IT.UTF-8";
LC_TIME = "it_IT.UTF-8";
};
# Display server & WM
services.xserver.enable = true;
services.displayManager.ly.enable = true;
services.displayManager.autoLogin.enable = true;
services.displayManager.autoLogin.user = "g";
services.displayManager.defaultSession = "labwc";
services.displayManager.sessionPackages = [ pkgs.labwc ];
programs.noctalia = {
enable = true;
systemd.enable = true;
};
systemd.user.services.noctalia.serviceConfig = {
ExecStart = lib.mkForce "${pkgs.writeShellScriptBin "noctalia-wrap" ''
# Wait for the Wayland socket (labwc may not have created it yet)
while [ ! -S "$XDG_RUNTIME_DIR/''${WAYLAND_DISPLAY-wayland-0}" ]; do
sleep 0.2
done
exec ${noctalia.package}/bin/noctalia
''}/bin/noctalia-wrap";
RestartSec = lib.mkForce "2";
};
programs.xwayland.enable = true;
programs.dconf.enable = true;
security.polkit.enable = true;
services.xserver.desktopManager.runXdgAutostartIfNone = true;
xdg.portal = {
enable = true;
extraPortals = [ pkgs.xdg-desktop-portal-wlr pkgs.xdg-desktop-portal-gtk ];
configPackages = [ pkgs.labwc ];
};
# Audio (PipeWire)
services.pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
};
security.rtkit.enable = true;
# SPICE guest tools (VM clipboard, mouse, resolution)
services.spice-vdagentd.enable = true;
systemd.user.services.spice-vdagent = {
description = "Spice vdagent";
wantedBy = [ "graphical-session.target" ];
serviceConfig = {
ExecStart = "${pkgs.writeShellScriptBin "spice-vdagent-wrap" ''
# Wait for Xwayland socket (spice-vdagent needs X even with -x)
while [ ! -S /tmp/.X11-unix/X0 ]; do
sleep 0.2
done
export DISPLAY=:0
exec ${pkgs.spice-vdagent}/bin/spice-vdagent -x
''}/bin/spice-vdagent-wrap";
Restart = "always";
RestartSec = "2";
StartLimitBurst = 5;
StartLimitIntervalSec = 30;
};
};
systemd.user.services.spice-clipboard-bridge = {
description = "Bridge Wayland clipboard to X11 for SPICE";
wantedBy = [ "graphical-session.target" ];
partOf = [ "graphical-session.target" ];
environment = {
WAYLAND_DISPLAY = "wayland-0";
};
serviceConfig = {
Restart = "on-failure";
RestartSec = "2";
ExecStart = "${pkgs.writeShellScriptBin "spice-clipboard-bridge" ''
while true; do
# Wait for the Wayland socket (labwc may not have created it yet)
while [ ! -S "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" ]; do
sleep 0.2
done
# Wait for the X server socket (xclip needs it)
while [ ! -S /tmp/.X11-unix/X0 ]; do
sleep 0.2
done
export DISPLAY=:0
# Wayland -> X11 direction
${pkgs.wl-clipboard}/bin/wl-paste --watch ${pkgs.xclip}/bin/xclip -selection clipboard -i &
WLPID=$!
# X11 -> Wayland direction
last=""
while true; do
# if wl-paste died (Wayland disconnect), restart outer loop
kill -0 "$WLPID" 2>/dev/null || break
# if either socket disappeared (relogin), restart outer loop
[ -S "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" ] || break
[ -S /tmp/.X11-unix/X0 ] || break
clip=$(${pkgs.xclip}/bin/xclip -selection clipboard -o 2>/dev/null)
if [ -n "$clip" ] && [ "$clip" != "$last" ]; then
printf '%s' "$clip" | ${pkgs.wl-clipboard}/bin/wl-copy 2>/dev/null || true
last="$clip"
fi
sleep 1
done
kill "$WLPID" 2>/dev/null
done
''}/bin/spice-clipboard-bridge";
};
};
systemd.user.services.set-resolution = {
description = "Set display resolution";
wantedBy = [ "graphical-session.target" ];
after = [ "graphical-session.target" ];
environment = {
WAYLAND_DISPLAY = "wayland-0";
};
partOf = [ "graphical-session.target" ];
serviceConfig = {
Type = "simple";
Restart = "on-failure";
RestartSec = "2";
ExecStart = "${pkgs.writeShellScriptBin "set-resolution" ''
while true; do
# wait for the Wayland socket
while [ ! -S "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" ]; do
sleep 0.2
done
# wait for the Virtual-1 output to appear
while ! ${pkgs.wlr-randr}/bin/wlr-randr 2>/dev/null | grep -q "^Virtual-1"; do
sleep 0.2
done
# set resolution and keep re-applying
while true; do
${pkgs.wlr-randr}/bin/wlr-randr --output Virtual-1 --mode 1680x1050 2>/dev/null
sleep 10
# if the Wayland socket disappeared, restart the outer loop
[ -S "$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY" ] || break
done
done
''}/bin/set-resolution";
};
};
# Hardware & power
hardware.cpu.amd.updateMicrocode = true;
#hardware.bluetooth.enable = true;
services.fstrim.enable = true;
zramSwap.enable = true;
# User accounts
programs.fuse.userAllowOther = true;
security.wrappers.fusermount3 = {
owner = "root";
group = "root";
setuid = true;
source = "${pkgs.fuse3}/bin/fusermount3";
};
users.users.g = {
isNormalUser = true;
description = "user";
extraGroups = [ "networkmanager" "wheel" "docker" "fuse" ];
};
fonts.packages = with pkgs; [
noto-fonts
fira-code
];
environment.systemPackages = with pkgs; [
labwc
wl-clipboard
xclip
wlr-randr
kitty
micro
htop
fzf
ncdu
fastfetch
nano
nautilus
brave
opencode
gnome-text-editor
fuse3
];
nixpkgs.config.allowUnfree = true;
system.stateVersion = "26.11";
}