feat(desktop): Hyprland, Waybar, autostart/bindings/idle

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-06-28 06:48:07 +02:00
commit 6e87d8e186
5 changed files with 1317 additions and 0 deletions

View file

@ -0,0 +1,575 @@
{
config,
pkgs,
lib,
inputs,
...
}:
let
inherit (lib)
mkEnableOption
mkOption
mkIf
mkMerge
types
optionals
optionalString
;
cfg = config.omni.desktop;
omni = config.omni.lib;
user = config.omni.user;
in
{
imports = [
./hyprland/autostart.nix
./hyprland/bindings.nix
./hyprland/idle.nix
];
options.omni.desktop = {
enable = mkEnableOption "Omnixient Hyprland desktop environment";
monitors = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "DP-1,1920x1080@144,0x0,1" ];
description = "Monitor configuration for Hyprland";
};
defaultTerminal = mkOption {
type = types.str;
default = "ghostty";
description = "Default terminal emulator";
};
defaultBrowser = mkOption {
type = types.str;
default = "chromium";
description = "Default web browser";
};
wallpaper = mkOption {
type = types.nullOr types.path;
default = null;
description = "Path to wallpaper image (optional)";
};
idleSuspend = mkOption {
type = types.bool;
default = false;
description = "Enable system suspend after idle timeout";
};
};
config = mkIf (cfg.enable) {
# Enable Hyprland at the system level.
#
# `programs.hyprland.portalPackage` is set to the xdg-desktop-portal-hyprland
# built by the same Hyprland flake input that provides `package`, so
# the auto-added portal (from the upstream programs.hyprland module)
# matches the hyprland binary's source. Mixing sources causes two
# copies of the same package in the closure and a systemd user-unit
# symlink collision when both try to install
# xdg-desktop-portal-hyprland.service.
programs.hyprland = {
enable = true;
xwayland.enable = true;
package = inputs.hyprland.packages.${pkgs.stdenv.hostPlatform.system}.hyprland;
portalPackage = inputs.hyprland.packages.${pkgs.stdenv.hostPlatform.system}.xdg-desktop-portal-hyprland;
};
# XDG portal for Wayland. We DON'T add xdg-desktop-portal-hyprland
# here — `programs.hyprland.enable = true` already adds it via the
# upstream module's `xdg.portal.extraPortals = [ cfg.portalPackage ]`.
# Adding it again would create a duplicate.
xdg.portal = {
enable = true;
extraPortals = [
pkgs.xdg-desktop-portal-gtk
];
config.common.default = "*";
};
# Session variables
environment.sessionVariables = {
NIXOS_OZONE_WL = "1";
MOZ_ENABLE_WAYLAND = "1";
QT_QPA_PLATFORM = "wayland";
QT_WAYLAND_DISABLE_WINDOWDECORATION = "1";
GDK_BACKEND = "wayland";
WLR_NO_HARDWARE_CURSORS = "1";
TERMINAL = cfg.defaultTerminal;
BROWSER = cfg.defaultBrowser;
EDITOR = "nvim";
};
# Essential packages for Hyprland desktop
environment.systemPackages = with pkgs; [
wayland
wayland-protocols
wayland-utils
wlroots
hyprland-protocols
hyprpaper
hyprlock
hypridle
hyprpicker
waybar
wofi
rofi
walker
mako
libnotify
wl-clipboard
cliphist
wlr-randr
kanshi
grim
slurp
satty
wf-recorder
networkmanagerapplet
blueman
pasystray
impala
pamixer
pulsemixer
pavucontrol
wiremix
awww
nautilus
pkgs.thunar
ranger
yazi
polkit_gnome
adwaita-icon-theme
papirus-icon-theme
bibata-cursors
brightnessctl
playerctl
];
# --- Home-manager Hyprland config (structured, mergeable by themes) ---
home-manager.users.${user} = {
wayland.windowManager.hyprland = {
enable = true;
package = config.programs.hyprland.package;
# Keep the legacy hyprlang config format (home.stateVersion < 26.05).
# Switching to "lua" is a separate, deliberate migration.
configType = "hyprlang";
systemd.enable = true;
settings = {
# Monitor config — user monitors + fallback
monitor = cfg.monitors ++ [ ",preferred,auto,1" ];
input = {
kb_layout = "us";
kb_options = "caps:escape";
follow_mouse = 1;
mouse_refocus = false;
sensitivity = 0;
touchpad = {
natural_scroll = true;
disable_while_typing = true;
tap-to-click = true;
scroll_factor = 0.5;
};
};
general = {
gaps_in = 5;
gaps_out = 10;
border_size = 2;
layout = "dwindle";
allow_tearing = false;
};
decoration = {
rounding = 10;
blur = {
enabled = true;
size = 6;
passes = 2;
new_optimizations = true;
ignore_opacity = true;
};
# Hyprland 0.40+ moved shadow options from flat
# `drop_shadow`/`shadow_range`/`shadow_render_power` into a
# nested `shadow { enabled; range; render_power; color }`
# block. Theme modules contribute `shadow.color`.
shadow = {
enabled = true;
range = 20;
render_power = 3;
};
dim_inactive = false;
dim_strength = 0.1;
};
animations = {
enabled = true;
bezier = [
"overshot, 0.05, 0.9, 0.1, 1.05"
"smoothOut, 0.5, 0, 0.99, 0.99"
"smoothIn, 0.5, -0.5, 0.68, 1.5"
];
animation = [
"windows, 1, 5, overshot, slide"
"windowsOut, 1, 4, smoothOut, slide"
"windowsIn, 1, 4, smoothIn, slide"
"windowsMove, 1, 4, default"
"border, 1, 10, default"
"borderangle, 1, 8, default"
"fade, 1, 7, default"
"workspaces, 1, 6, default"
];
};
dwindle = {
preserve_split = true;
force_split = 2;
};
# NOTE: Hyprland 0.49+ removed the entire `gestures` section
# in favor of new `gesture` keyword bindings (similar to
# `bind`). Touchpad workspace-swipe will need to be re-added
# via the new syntax — tracking as a follow-up. The old
# block was:
# gestures = { workspace_swipe = true; ... };
misc = {
force_default_wallpaper = 0;
disable_hyprland_logo = true;
disable_splash_rendering = true;
mouse_move_enables_dpms = true;
key_press_enables_dpms = true;
enable_swallow = true;
swallow_regex = "^(ghostty|alacritty|kitty|footclient)$";
};
windowrule = [
"opacity 0.9 override 0.9 override, match:class (ghostty|Alacritty|kitty)"
"opacity 0.9 override 0.9 override, match:class (Code|code-oss)"
"float on, match:class (pavucontrol|nm-connection-editor|blueman-manager)"
"float on, match:class (org.gnome.Calculator|gnome-calculator)"
"float on, match:title (Picture-in-Picture)"
"pin on, match:title (Picture-in-Picture)"
"float on, match:class (imv|mpv|vlc)"
"center on, match:class (imv|mpv|vlc)"
"float on, match:class (com.gabm.satty)"
"size 800 600, match:class (com.gabm.satty)"
"center on, match:class (com.gabm.satty)"
"workspace 4, match:class (Spotify)"
"workspace 5, match:class (org.telegram.desktop)"
];
};
};
# Waybar
programs.waybar = {
enable = true;
systemd.enable = true;
settings = [
{
reload_style_on_change = true;
layer = "top";
position = "top";
spacing = 0;
height = 26;
modules-left = [
"custom/omni"
"hyprland/workspaces"
];
modules-center = [ "clock" ];
modules-right = [
"group/tray-expander"
"custom/screenshot"
"bluetooth"
"network"
"pulseaudio"
"cpu"
"backlight"
"battery"
"custom/power"
];
"custom/omni" = {
format = "󱄅";
on-click = "walker";
on-click-right = "${cfg.defaultTerminal}";
tooltip-format = "App Launcher";
};
"custom/screenshot" = {
format = "󰹑";
on-click = "bash -c 'f=~/Pictures/Screenshots/screenshot_$(date +%Y-%m-%d-%H%M%S).png; grim -g \"$(slurp)\" \"$f\" && satty --filename \"$f\" --output-filename \"$f\" --copy-command wl-copy'";
on-click-right = "bash -c 'f=~/Pictures/Screenshots/screenshot_$(date +%Y-%m-%d-%H%M%S).png; grim \"$f\" && satty --filename \"$f\" --output-filename \"$f\" --copy-command wl-copy'";
tooltip-format = "Screenshot (right-click: fullscreen)";
};
"hyprland/workspaces" = {
on-click = "activate";
format = "{icon}";
format-icons = {
"1" = "1";
"2" = "2";
"3" = "3";
"4" = "4";
"5" = "5";
"6" = "6";
"7" = "7";
"8" = "8";
"9" = "9";
"10" = "0";
active = "󱓻";
default = "";
};
persistent-workspaces = {
"1" = [ ];
"2" = [ ];
"3" = [ ];
"4" = [ ];
"5" = [ ];
};
};
clock = {
format = "{:L%A %H:%M}";
format-alt = "{:L%d %B W%V %Y}";
tooltip = false;
};
network = {
format-icons = [
"󰤯"
"󰤟"
"󰤢"
"󰤥"
"󰤨"
];
format = "{icon}";
format-wifi = "{icon}";
format-ethernet = "󰀂";
format-disconnected = "󰤮";
tooltip-format-wifi = "{essid} ({frequency} GHz)\n{bandwidthDownBytes} {bandwidthUpBytes}";
tooltip-format-ethernet = "{bandwidthDownBytes} {bandwidthUpBytes}";
tooltip-format-disconnected = "Disconnected";
interval = 3;
on-click = "${cfg.defaultTerminal} -e impala";
};
bluetooth = {
format = "";
format-off = "󰂲";
format-disabled = "󰂲";
format-connected = "󰂱";
format-no-controller = "";
tooltip-format = "Devices connected: {num_connections}";
on-click = "${cfg.defaultTerminal} -e bluetui";
};
pulseaudio = {
format = "{icon}";
format-muted = "󰝟";
format-icons = {
headphone = "󰋋";
headset = "󰋎";
default = [
"󰕿"
"󰖀"
"󰕾"
];
};
tooltip-format = "Playing at {volume}%";
scroll-step = 5;
on-click = "${cfg.defaultTerminal} -e wiremix";
on-click-right = "pamixer -t";
};
cpu = {
interval = 5;
format = "󰍛";
on-click = "${cfg.defaultTerminal} -e btop";
};
backlight = {
format = "{icon}";
format-icons = [
""
""
""
""
""
""
""
""
""
];
tooltip-format = "Brightness: {percent}%";
};
battery = {
format = "{icon}";
format-discharging = "{icon}";
format-charging = "{icon}";
format-plugged = "";
format-full = "󰂅";
format-icons = {
charging = [
"󰢜"
"󰂆"
"󰂇"
"󰂈"
"󰢝"
"󰂉"
"󰢞"
"󰂊"
"󰂋"
"󰂅"
];
default = [
"󰁺"
"󰁻"
"󰁼"
"󰁽"
"󰁾"
"󰁿"
"󰂀"
"󰂁"
"󰂂"
"󰁹"
];
};
tooltip-format-discharging = "{power:>1.0f}W {capacity}%";
tooltip-format-charging = "{power:>1.0f}W {capacity}%";
interval = 5;
states = {
warning = 20;
critical = 10;
};
on-click = "${pkgs.writeShellScript "power-menu" ''
choice=$(printf "Lock\nLogout\nReboot\nShutdown" | ${pkgs.wofi}/bin/wofi --dmenu --prompt "Power" --width 200 --height 200)
case "$choice" in
Lock) hyprlock ;;
Logout) hyprctl dispatch exit ;;
Reboot) systemctl reboot ;;
Shutdown) systemctl poweroff ;;
esac
''}";
};
"group/tray-expander" = {
orientation = "inherit";
drawer = {
transition-duration = 600;
children-class = "tray-group-item";
};
modules = [
"custom/expand-icon"
"tray"
];
};
"custom/expand-icon" = {
format = "";
tooltip = false;
};
tray = {
icon-size = 12;
spacing = 17;
};
"custom/power" = {
format = "";
tooltip-format = "Power menu";
on-click = "${pkgs.writeShellScript "power-menu-btn" ''
choice=$(printf "Lock\nLogout\nReboot\nShutdown" | ${pkgs.wofi}/bin/wofi --dmenu --prompt "Power" --width 200 --height 200)
case "$choice" in
Lock) hyprlock ;;
Logout) hyprctl dispatch exit ;;
Reboot) systemctl reboot ;;
Shutdown) systemctl poweroff ;;
esac
''}";
};
}
];
style = lib.mkDefault ''
* {
font-family: "JetBrainsMono Nerd Font";
font-size: 12px;
border: none;
border-radius: 0;
min-height: 0;
}
window#waybar {
background: rgba(30, 30, 46, 0.9);
color: #cdd6f4;
}
.modules-left {
margin-left: 8px;
}
.modules-right {
margin-right: 8px;
}
#workspaces button {
all: initial;
color: #cdd6f4;
padding: 0 6px;
margin: 0 1.5px;
min-width: 9px;
}
#workspaces button.empty {
opacity: 0.5;
}
#workspaces button.active {
color: #cba6f7;
}
#custom-omni {
margin: 0 12px 0 4px;
font-size: 14px;
color: #cba6f7;
}
#cpu,
#battery,
#pulseaudio,
#backlight {
min-width: 12px;
margin: 0 7.5px;
}
#custom-power {
min-width: 12px;
margin: 0 4px 0 7.5px;
color: #f38ba8;
}
#bluetooth {
margin-right: 17px;
}
#network {
margin-right: 13px;
}
#custom-screenshot {
margin-right: 17px;
}
#custom-expand-icon {
margin-right: 18px;
}
#tray {
margin-right: 16px;
}
#clock {
margin-left: 8.75px;
}
#custom-power:hover {
color: #1e1e2e;
background: #f38ba8;
border-radius: 5px;
}
#battery.charging { color: #a6e3a1; }
#battery.warning:not(.charging) { color: #fab387; }
#battery.critical:not(.charging) {
color: #f38ba8;
animation: blink 0.5s linear infinite alternate;
}
@keyframes blink {
to { background-color: #f38ba8; color: #1e1e2e; }
}
tooltip {
padding: 2px;
}
'';
};
};
};
}