New settings/ui.nix holds `radius` as the one source of truth. Theme derives radiusCard/Small/Tiny from it and hyprland's decoration.rounding reads the same value, so one number rescales the whole desktop — previously the shell and the window corners were unrelated constants, which is why doubling Theme.radius left windows at 8. Cards were using the SAME radius as the panel containing them. Nested corners need outer − gap; matching them makes the inner corner over-round for its size, which is the inflated/balloon look. All nine inner surfaces now use radiusCard (radius − panelGap). Invisible at r=8, obvious at r=32. Radius 32 -> 24. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
426 lines
18 KiB
Nix
426 lines
18 KiB
Nix
# settings/hyprland.nix
|
|
{ config, pkgs, lib, inputs, ... }:
|
|
let
|
|
isMacbook = config.networking.hostName == "FredOS-Macbook";
|
|
isGaming = !isMacbook;
|
|
ui = import ./ui.nix; # shared corner-rounding knob
|
|
|
|
in
|
|
{
|
|
config = lib.mkIf (lib.elem config.networking.hostName [ "FredOS-Gaming" "FredOS-Macbook" ]) {
|
|
programs.hyprland = {
|
|
enable = true;
|
|
xwayland.enable = true;
|
|
};
|
|
|
|
xdg.portal = {
|
|
enable = true;
|
|
# xdg-desktop-portal-hyprland is registered automatically by
|
|
# programs.hyprland.portalPackage; listing it here too produced a
|
|
# duplicate user-unit symlink during nixos-rebuild.
|
|
extraPortals = with pkgs; [
|
|
xdg-desktop-portal-gtk
|
|
];
|
|
config.hyprland.default = [ "hyprland" "gtk" ];
|
|
};
|
|
|
|
security.polkit.enable = true;
|
|
|
|
# graphical-desktop.nix mkDefault-enables this; unused (no screen reader,
|
|
# no Discord TTS).
|
|
services.speechd.enable = false;
|
|
|
|
# Polkit GUI agent for GUI sudo prompts under Hyprland
|
|
systemd.user.services.polkit-gnome-authentication-agent-1 = {
|
|
description = "polkit-gnome-authentication-agent-1";
|
|
wantedBy = [ "graphical-session.target" ];
|
|
partOf = [ "graphical-session.target" ];
|
|
after = [ "graphical-session.target" ];
|
|
serviceConfig = {
|
|
Type = "simple";
|
|
ExecStart = "${pkgs.polkit_gnome}/libexec/polkit-gnome-authentication-agent-1";
|
|
Restart = "on-failure";
|
|
};
|
|
};
|
|
|
|
environment.systemPackages = with pkgs; [
|
|
ghostty
|
|
libnotify
|
|
grim
|
|
slurp
|
|
wl-clipboard
|
|
cliphist
|
|
brightnessctl
|
|
playerctl
|
|
hypridle
|
|
hyprshot
|
|
hyprpicker
|
|
networkmanagerapplet
|
|
pavucontrol
|
|
polkit_gnome
|
|
zenity
|
|
libcanberra-gtk3
|
|
];
|
|
|
|
home-manager.users.fred = { config, lib, pkgs, inputs, ... }:
|
|
let
|
|
c = config.lib.stylix.colors;
|
|
rgb = hex: "rgb(${hex})";
|
|
rgba = hex: a: "rgba(${hex}${a})";
|
|
in {
|
|
# Stylix's Hyprland target injects settings.{general,decoration,group,misc}
|
|
# as top-level keys, which render as hl.general()/hl.decoration()/… in Lua
|
|
# mode — functions that don't exist. Disable it and absorb the colours
|
|
# into settings.config below.
|
|
stylix.targets.hyprland.enable = false;
|
|
|
|
# Wallpaper is drawn by quickshell (settings/quickshell.nix); no
|
|
# separate hyprpaper daemon.
|
|
stylix.targets.hyprpaper.enable = false;
|
|
|
|
# Screenshare picker: auto-allow restore tokens (the "allow a restore
|
|
# token" checkbox) so Vesktop/Discord gets a token on the first pick
|
|
# instead of re-prompting. XDPH parses "[SELECTION]<flags>/..." from the
|
|
# picker's stdout; flag 'r' = token allowed, so inject it always.
|
|
xdg.configFile."hypr/xdph.conf".text = ''
|
|
screencopy {
|
|
custom_picker_binary = ${pkgs.writeShellScript "share-picker-auto-restore" ''
|
|
${pkgs.xdg-desktop-portal-hyprland}/bin/hyprland-share-picker "$@" \
|
|
| ${pkgs.gnused}/bin/sed 's/^\[SELECTION\]/[SELECTION]r/'
|
|
''}
|
|
}
|
|
'';
|
|
|
|
wayland.windowManager.hyprland = {
|
|
enable = true;
|
|
configType = "lua";
|
|
systemd.variables = [ "--all" ];
|
|
|
|
settings = {
|
|
# hl.config({...}) — all static named-section configuration.
|
|
# monitor is set per-host in hosts/FredOS-{Gaming,Macbook}.nix.
|
|
config = {
|
|
general = {
|
|
gaps_in = 6;
|
|
# 6px of the outer gap is occupied by the quickshell screen
|
|
# frame (Theme.frameWidth) — 18 on framed sides keeps the
|
|
# visible frame↔window gap equal to the inter-window gap (12).
|
|
# Left stays 12: the bar lives there and reserves its own space.
|
|
gaps_out = { top = 18; right = 18; bottom = 18; left = 12; };
|
|
border_size = 2;
|
|
layout = "dwindle";
|
|
resize_on_border = true;
|
|
"col.active_border" = rgb c.base0D;
|
|
"col.inactive_border" = rgb c.base03;
|
|
};
|
|
|
|
decoration = {
|
|
# tracks Theme.radius via settings/ui.nix
|
|
rounding = ui.radius;
|
|
blur = {
|
|
enabled = true;
|
|
size = 1;
|
|
passes = 3;
|
|
};
|
|
shadow.color = rgba c.base00 "99";
|
|
};
|
|
|
|
group = {
|
|
"col.border_active" = rgb c.base0D;
|
|
"col.border_inactive" = rgb c.base03;
|
|
"col.border_locked_active" = rgb c.base0C;
|
|
groupbar = {
|
|
text_color = rgb c.base05;
|
|
"col.active" = rgb c.base0D;
|
|
"col.inactive" = rgb c.base03;
|
|
};
|
|
};
|
|
|
|
render = {
|
|
direct_scanout = false;
|
|
};
|
|
|
|
animations = {
|
|
enabled = true;
|
|
};
|
|
|
|
input = {
|
|
kb_layout = "gb,no";
|
|
kb_options = "grp:win_space_toggle" + lib.optionalString isGaming ",altwin:swap_lalt_lwin";
|
|
follow_mouse = 1;
|
|
accel_profile = "flat";
|
|
sensitivity = 0;
|
|
} // lib.optionalAttrs isMacbook {
|
|
touchpad = {
|
|
tap_to_click = true;
|
|
tap_button_map = "lrm";
|
|
natural_scroll = true;
|
|
};
|
|
};
|
|
|
|
binds = {
|
|
# default 300ms swallows rapid super+scroll workspace switches
|
|
scroll_event_delay = 50;
|
|
};
|
|
|
|
cursor = {
|
|
no_warps = true;
|
|
# Force real HW cursor plane (default "auto" can fall back to a
|
|
# software cursor on i915 → cursor waits a full repaint = laggy).
|
|
no_hardware_cursors = false;
|
|
};
|
|
|
|
dwindle = {
|
|
preserve_split = true;
|
|
};
|
|
|
|
misc = {
|
|
disable_hyprland_logo = true;
|
|
disable_splash_rendering = true;
|
|
# Apps demanding attention don't get to yank focus — they'll
|
|
# show as urgent in the bar instead.
|
|
focus_on_activate = false;
|
|
vrr = 2;
|
|
background_color = rgb c.base00;
|
|
# If quickshell dies while holding the session lock, let its
|
|
# restarted instance re-attach instead of a dead red screen.
|
|
allow_session_lock_restore = true;
|
|
};
|
|
|
|
# vfr moved from misc: to debug: in 0.55.0
|
|
debug = {
|
|
vfr = false; # keep compositor ticking, don't idle between frames
|
|
disable_logs = false;
|
|
};
|
|
};
|
|
};
|
|
|
|
extraConfig =
|
|
let
|
|
wpctl = "${pkgs.wireplumber}/bin/wpctl";
|
|
qs = "${pkgs.quickshell}/bin/qs";
|
|
in
|
|
''
|
|
-- Environment
|
|
hl.env("XCURSOR_THEME", "Bibata-Modern-Ice")
|
|
hl.env("XCURSOR_SIZE", "24")
|
|
hl.env("HYPRCURSOR_THEME", "Bibata-Modern-Ice")
|
|
hl.env("HYPRCURSOR_SIZE", "24")
|
|
hl.env("ELECTRON_OZONE_PLATFORM_HINT", "wayland")
|
|
hl.env("MOZ_ENABLE_WAYLAND", "1")
|
|
hl.env("QT_QPA_PLATFORM", "wayland;xcb")
|
|
hl.env("SDL_VIDEODRIVER", "wayland")
|
|
hl.env("_JAVA_AWT_WM_NONREPARENTING", "1")
|
|
${lib.optionalString isGaming ''
|
|
-- GPU pinning — Navi 22 is card1 on the dual-GPU gaming box.
|
|
hl.env("AQ_DRM_DEVICES", "/dev/dri/card1")
|
|
hl.env("DRI_PRIME", "pci-0000_03_00_0")
|
|
''}
|
|
|
|
-- Layer rules: blur behind bar and toasts
|
|
hl.layer_rule({ match = { namespace = "quickshell-bar" }, blur = true, ignore_alpha = 0.3 })
|
|
|
|
-- Startup
|
|
hl.on("hyprland.start", function()
|
|
-- Ensure hyprland-session.target starts even if HM's
|
|
-- dbus-update-activation-environment chain fails upstream.
|
|
hl.exec_cmd("systemctl --user start hyprland-session.target")
|
|
-- greetd's pam leaves gnome-keyring-daemon in half-initialized
|
|
-- --login mode (autologin = no password, and nothing in a
|
|
-- non-GNOME session runs the second stage). --start completes
|
|
-- init so the lockscreen's pam unlock actually reaches it.
|
|
hl.exec_cmd("${pkgs.gnome-keyring}/bin/gnome-keyring-daemon --start --components=secrets")
|
|
-- mako removed; notifications handled by quickshell
|
|
|
|
hl.exec_cmd("wl-paste --type text --watch cliphist store")
|
|
hl.exec_cmd("wl-paste --type image --watch cliphist store")
|
|
hl.exec_cmd("hyprctl setcursor Bibata-Modern-Ice 24")
|
|
-- swayosd removed; volume/brightness OSD handled by quickshell
|
|
${lib.optionalString isMacbook ''hl.exec_cmd("hypridle")''}
|
|
end)
|
|
|
|
-- Animation curve and definitions
|
|
hl.curve("snap", { type = "bezier", points = { {0.05, 0.9}, {0.1, 1.0} } })
|
|
hl.animation({ leaf = "windows", enabled = true, speed = 1, bezier = "snap" })
|
|
hl.animation({ leaf = "windowsOut", enabled = true, speed = 1, bezier = "snap", style = "popin 80%" })
|
|
hl.animation({ leaf = "layers", enabled = true, speed = 1, bezier = "snap" })
|
|
hl.animation({ leaf = "border", enabled = true, speed = 2, bezier = "default" })
|
|
hl.animation({ leaf = "fade", enabled = true, speed = 1, bezier = "default" })
|
|
-- slidevert: workspaces slide up/down to match the vertical
|
|
-- workspace column in the bar (default "slide" is horizontal).
|
|
hl.animation({ leaf = "workspaces", enabled = true, speed = 1, bezier = "snap", style = "slidevert" })
|
|
|
|
-- Window rules
|
|
-- Don't lock/idle while any window is fullscreen (video, games).
|
|
hl.window_rule({
|
|
match = { class = ".*" },
|
|
idle_inhibit = "fullscreen",
|
|
})
|
|
|
|
-- Steam games often open floating (fixed XWayland size hints);
|
|
-- force tile generically. Rule order matters: the Battle.net
|
|
-- tray-icon rule below is more specific and runs after, so it
|
|
-- still wins and keeps the tray icon floated on its hidden
|
|
-- workspace.
|
|
hl.window_rule({
|
|
match = { class = "^steam_app_.*" },
|
|
tile = true,
|
|
})
|
|
|
|
-- Battle.net tray icon leaks as a tiny floating XWayland window.
|
|
hl.window_rule({
|
|
match = { class = "steam_app_0", title = "^$", float = true },
|
|
workspace = "special silent",
|
|
})
|
|
|
|
-- WoW keeps requesting fullscreen/maximize; ignore so it stays tiled.
|
|
hl.window_rule({
|
|
match = { class = "steam_app_default", title = "^World of Warcraft$" },
|
|
suppress_event = "fullscreen maximize",
|
|
})
|
|
|
|
-- Battle.net (a non-Steam shortcut, so class steam_app_0) spams
|
|
-- window-activation events that clear quickshell's focus grab and
|
|
-- instantly close the launcher / power menu. Drop those events.
|
|
hl.window_rule({
|
|
match = { class = "steam_app_0" },
|
|
suppress_event = "activate activatefocus",
|
|
})
|
|
|
|
-- Binds
|
|
local mod = "SUPER"
|
|
|
|
-- Apps
|
|
hl.bind(mod .. " + T", hl.dsp.exec_cmd("ghostty"))
|
|
hl.bind(mod .. " + E", hl.dsp.exec_cmd("nemo"))
|
|
hl.bind(mod .. " + R", hl.dsp.exec_cmd("${pkgs.quickshell}/bin/qs ipc call launcher toggle"))
|
|
hl.bind(mod .. " + Q", hl.dsp.window.close())
|
|
hl.bind(mod .. " + SHIFT + E", hl.dsp.exit())
|
|
|
|
-- Floating / layout
|
|
hl.bind(mod .. " + V", hl.dsp.window.float({ action = "toggle" }))
|
|
hl.bind(mod .. " + F", hl.dsp.window.fullscreen())
|
|
hl.bind(mod .. " + P", hl.dsp.window.pseudo())
|
|
hl.bind(mod .. " + S", hl.dsp.layout("togglesplit"))
|
|
|
|
-- Focus
|
|
hl.bind(mod .. " + left", hl.dsp.focus({ direction = "left" }))
|
|
hl.bind(mod .. " + right", hl.dsp.focus({ direction = "right" }))
|
|
hl.bind(mod .. " + up", hl.dsp.focus({ direction = "up" }))
|
|
hl.bind(mod .. " + down", hl.dsp.focus({ direction = "down" }))
|
|
hl.bind(mod .. " + H", hl.dsp.focus({ direction = "left" }))
|
|
hl.bind(mod .. " + K", hl.dsp.focus({ direction = "up" }))
|
|
hl.bind(mod .. " + J", hl.dsp.focus({ direction = "down" }))
|
|
|
|
-- Power menu (quickshell launcher in power mode)
|
|
hl.bind(mod .. " + L", hl.dsp.exec_cmd("${pkgs.quickshell}/bin/qs ipc call launcher powermenu"))
|
|
|
|
-- Move windows
|
|
hl.bind(mod .. " + SHIFT + left", hl.dsp.window.move({ direction = "left" }))
|
|
hl.bind(mod .. " + SHIFT + right", hl.dsp.window.move({ direction = "right" }))
|
|
hl.bind(mod .. " + SHIFT + up", hl.dsp.window.move({ direction = "up" }))
|
|
hl.bind(mod .. " + SHIFT + down", hl.dsp.window.move({ direction = "down" }))
|
|
|
|
-- Workspaces
|
|
for i = 0, 9 do
|
|
local workspace_id = tostring((i == 0) and 10 or i)
|
|
hl.bind(mod .. " + " .. i, hl.dsp.focus({ workspace = workspace_id }))
|
|
hl.bind(mod .. " + SHIFT + " .. i, hl.dsp.window.move({ workspace = workspace_id, follow = false }))
|
|
end
|
|
|
|
-- Scroll through workspaces
|
|
hl.bind(mod .. " + mouse_down", hl.dsp.focus({ workspace = "e+1" }))
|
|
hl.bind(mod .. " + mouse_up", hl.dsp.focus({ workspace = "e-1" }))
|
|
|
|
${lib.optionalString isMacbook ''
|
|
-- Three-finger touchpad swipe between workspaces
|
|
hl.gesture({ fingers = 3, direction = "horizontal", action = "workspace" })
|
|
''}
|
|
|
|
-- Screenshots — Shift+Super+S matches GNOME binding
|
|
-- Pin/unpin quickshell's focus grab around the region select so an
|
|
-- open menu survives slurp's input grab (no-ops if qs isn't up).
|
|
hl.bind(mod .. " + SHIFT + S", hl.dsp.exec_cmd("sh -c 'qs ipc call screenshot pin; hyprshot -m region; qs ipc call screenshot unpin'"))
|
|
hl.bind("Print", hl.dsp.exec_cmd("hyprshot -m output"))
|
|
|
|
-- Settings shortcut — Super+I matches GNOME binding
|
|
hl.bind(mod .. " + I", hl.dsp.exec_cmd("pavucontrol"))
|
|
|
|
-- Custom shortcuts
|
|
hl.bind(mod .. " + Z", hl.dsp.exec_cmd("zen-beta"))
|
|
|
|
-- Color picker — copies hex to clipboard
|
|
hl.bind(mod .. " + A", hl.dsp.exec_cmd("hyprpicker -a"))
|
|
|
|
-- Mouse window manipulation
|
|
hl.bind(mod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true })
|
|
hl.bind(mod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true })
|
|
|
|
-- Volume / brightness (repeating). The OSD overlay is quickshell's
|
|
-- (see settings/quickshell.nix) — swayosd and wob are both gone.
|
|
-- Volume needs no IPC: quickshell watches Pipewire, so the OSD
|
|
-- also shows for changes made in pavucontrol or a mixer.
|
|
hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("${wpctl} set-volume @DEFAULT_AUDIO_SINK@ 5%+ -l 1.0"), { repeating = true })
|
|
hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("${wpctl} set-volume @DEFAULT_AUDIO_SINK@ 5%-"), { repeating = true })
|
|
hl.bind("XF86AudioMute", hl.dsp.exec_cmd("${wpctl} set-mute @DEFAULT_AUDIO_SINK@ toggle"))
|
|
hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("${qs} ipc call osd brightnessUp"), { repeating = true })
|
|
hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("${qs} ipc call osd brightnessDown"), { repeating = true })
|
|
${lib.optionalString isMacbook ''
|
|
hl.bind("XF86KbdBrightnessUp", hl.dsp.exec_cmd("${qs} ipc call osd kbdUp"), { repeating = true })
|
|
hl.bind("XF86KbdBrightnessDown", hl.dsp.exec_cmd("${qs} ipc call osd kbdDown"), { repeating = true })
|
|
''}
|
|
|
|
-- Media keys (locked — work through lockscreen)
|
|
hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true })
|
|
hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true })
|
|
hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true })
|
|
'';
|
|
};
|
|
|
|
# Night light — hyprsunset applies the gamma; the schedule and
|
|
# temperature are driven by quickshell's quick-settings menu
|
|
# (settings/quickshell.nix) via `hyprctl hyprsunset`.
|
|
services.hyprsunset.enable = true;
|
|
|
|
# Lock screen is quickshell's WlSessionLock (settings/quickshell.nix),
|
|
# triggered via `qs ipc call lock lock`.
|
|
services.hypridle = lib.mkIf isMacbook (
|
|
let
|
|
# Skip the action if any MPRIS player is playing — covers windowed
|
|
# video (Jellyfin in a browser, mpv, …) that the fullscreen
|
|
# idle_inhibit rule misses. Browsers expose MPRIS via playerctl.
|
|
unlessPlaying = cmd: "playerctl -a status 2>/dev/null | grep -q Playing || ${cmd}";
|
|
in {
|
|
enable = true;
|
|
settings = {
|
|
general = {
|
|
lock_cmd = "qs ipc call lock lock";
|
|
before_sleep_cmd = "loginctl lock-session";
|
|
after_sleep_cmd = "hyprctl dispatch dpms on";
|
|
};
|
|
listener = [
|
|
{
|
|
timeout = 300; # 5 min — lock
|
|
on-timeout = unlessPlaying "loginctl lock-session";
|
|
}
|
|
{
|
|
timeout = 420; # 7 min — display off
|
|
on-timeout = unlessPlaying "hyprctl dispatch dpms off";
|
|
on-resume = "hyprctl dispatch dpms on";
|
|
}
|
|
{
|
|
timeout = 600; # 10 min — suspend
|
|
on-timeout = unlessPlaying "systemctl suspend";
|
|
}
|
|
];
|
|
};
|
|
}
|
|
);
|
|
|
|
# Scope all HM Wayland services (hyprpaper, etc.) to the
|
|
# Hyprland session so they don't crash-loop in a GNOME session.
|
|
wayland.systemd.target = "hyprland-session.target";
|
|
|
|
};
|
|
};
|
|
}
|