2026-05-10 20:03:43 +01:00
|
|
|
# settings/hyprland.nix
|
|
|
|
|
{ config, pkgs, lib, inputs, ... }:
|
2026-05-13 22:40:54 +01:00
|
|
|
let
|
|
|
|
|
hyprland-pkgs = inputs.hyprland.packages.${pkgs.stdenv.hostPlatform.system};
|
2026-05-18 12:48:00 +01:00
|
|
|
anyrun-pkgs = inputs.anyrun.packages.${pkgs.stdenv.hostPlatform.system};
|
2026-05-13 22:40:54 +01:00
|
|
|
|
2026-05-14 13:10:32 +01:00
|
|
|
isMacbook = config.networking.hostName == "FredOS-Macbook";
|
2026-05-17 20:38:30 +01:00
|
|
|
isGaming = !isMacbook;
|
2026-05-14 10:44:32 +01:00
|
|
|
|
2026-05-13 22:40:54 +01:00
|
|
|
in
|
2026-05-10 20:03:43 +01:00
|
|
|
{
|
2026-05-14 10:44:32 +01:00
|
|
|
config = lib.mkIf (lib.elem config.networking.hostName [ "FredOS-Gaming" "FredOS-Macbook" ]) {
|
2026-05-10 20:03:43 +01:00
|
|
|
programs.hyprland = {
|
|
|
|
|
enable = true;
|
|
|
|
|
xwayland.enable = true;
|
2026-05-18 13:09:19 +01:00
|
|
|
package = hyprland-pkgs.hyprland;
|
2026-05-13 22:40:54 +01:00
|
|
|
portalPackage = hyprland-pkgs.xdg-desktop-portal-hyprland;
|
2026-05-10 20:03:43 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
xdg.portal = {
|
|
|
|
|
enable = true;
|
2026-05-11 12:25:00 +01:00
|
|
|
# xdg-desktop-portal-hyprland is registered automatically by
|
|
|
|
|
# programs.hyprland.portalPackage; listing it here too produced a
|
|
|
|
|
# duplicate user-unit symlink during nixos-rebuild.
|
2026-05-10 20:03:43 +01:00
|
|
|
extraPortals = with pkgs; [
|
|
|
|
|
xdg-desktop-portal-gtk
|
|
|
|
|
];
|
|
|
|
|
config.hyprland.default = [ "hyprland" "gtk" ];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
security.polkit.enable = true;
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
mako
|
|
|
|
|
grim
|
|
|
|
|
slurp
|
|
|
|
|
wl-clipboard
|
|
|
|
|
cliphist
|
|
|
|
|
brightnessctl
|
2026-05-16 17:37:02 +01:00
|
|
|
swayosd
|
2026-05-10 20:03:43 +01:00
|
|
|
playerctl
|
|
|
|
|
hyprpaper
|
|
|
|
|
hyprlock
|
|
|
|
|
hypridle
|
|
|
|
|
hyprshot
|
|
|
|
|
networkmanagerapplet
|
|
|
|
|
pavucontrol
|
|
|
|
|
polkit_gnome
|
2026-05-26 09:50:57 +01:00
|
|
|
quickshell
|
2026-05-26 10:55:21 +01:00
|
|
|
qt6.qt5compat
|
2026-05-10 20:03:43 +01:00
|
|
|
];
|
|
|
|
|
|
2026-05-18 12:48:00 +01:00
|
|
|
# Use upstream anyrun flake's HM module instead of the built-in one
|
|
|
|
|
# for working daemon mode.
|
|
|
|
|
home-manager.sharedModules = [
|
|
|
|
|
({ modulesPath, ... }: {
|
|
|
|
|
disabledModules = [ "${modulesPath}/programs/anyrun.nix" ];
|
|
|
|
|
})
|
|
|
|
|
inputs.anyrun.homeManagerModules.default
|
|
|
|
|
];
|
|
|
|
|
|
hyprland: fix Lua config errors caused by stylix's Hyprland target
Stylix's Hyprland target injects settings.{general,decoration,group,misc}
as top-level keys. In hyprlang mode these render as section blocks, but
in Lua mode (configType = "lua") they become hl.general(), hl.decoration(),
etc. — functions that don't exist, causing an emergency-mode config error.
Disable stylix.targets.hyprland and absorb all its colours into
settings.config.{general,decoration,group,misc} where they are correctly
rendered inside the single hl.config({}) call.
Also add a home.activation script that removes any stale hyprland.conf
(auto-generated by Hyprland before hyprland.lua was first placed) so the
autogenerated-config warning banner can never persist across restarts.
Keep services.hyprpaper.enable = true since it was previously set by the
now-disabled Hyprland target.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 22:24:12 +01:00
|
|
|
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;
|
|
|
|
|
|
2026-05-18 10:35:26 +01:00
|
|
|
# The disabled Hyprland target would normally enable this; do it
|
|
|
|
|
# manually. Stylix's hyprpaper target (auto-enabled) still handles
|
|
|
|
|
# preload/wallpaper settings.
|
hyprland: fix Lua config errors caused by stylix's Hyprland target
Stylix's Hyprland target injects settings.{general,decoration,group,misc}
as top-level keys. In hyprlang mode these render as section blocks, but
in Lua mode (configType = "lua") they become hl.general(), hl.decoration(),
etc. — functions that don't exist, causing an emergency-mode config error.
Disable stylix.targets.hyprland and absorb all its colours into
settings.config.{general,decoration,group,misc} where they are correctly
rendered inside the single hl.config({}) call.
Also add a home.activation script that removes any stale hyprland.conf
(auto-generated by Hyprland before hyprland.lua was first placed) so the
autogenerated-config warning banner can never persist across restarts.
Keep services.hyprpaper.enable = true since it was previously set by the
now-disabled Hyprland target.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 22:24:12 +01:00
|
|
|
services.hyprpaper.enable = true;
|
|
|
|
|
|
2026-05-10 20:03:43 +01:00
|
|
|
wayland.windowManager.hyprland = {
|
|
|
|
|
enable = true;
|
2026-05-17 20:38:30 +01:00
|
|
|
configType = "lua";
|
2026-05-10 20:03:43 +01:00
|
|
|
systemd.variables = [ "--all" ];
|
2026-05-18 13:09:19 +01:00
|
|
|
package = hyprland-pkgs.hyprland;
|
2026-05-10 20:03:43 +01:00
|
|
|
|
|
|
|
|
settings = {
|
2026-05-17 20:38:30 +01:00
|
|
|
# hl.config({...}) — all static named-section configuration.
|
|
|
|
|
# monitor is set per-host in hosts/FredOS-{Gaming,Macbook}.nix.
|
|
|
|
|
config = {
|
|
|
|
|
general = {
|
|
|
|
|
gaps_in = 6;
|
|
|
|
|
gaps_out = 12;
|
|
|
|
|
border_size = 2;
|
|
|
|
|
layout = "dwindle";
|
|
|
|
|
resize_on_border = true;
|
hyprland: fix Lua config errors caused by stylix's Hyprland target
Stylix's Hyprland target injects settings.{general,decoration,group,misc}
as top-level keys. In hyprlang mode these render as section blocks, but
in Lua mode (configType = "lua") they become hl.general(), hl.decoration(),
etc. — functions that don't exist, causing an emergency-mode config error.
Disable stylix.targets.hyprland and absorb all its colours into
settings.config.{general,decoration,group,misc} where they are correctly
rendered inside the single hl.config({}) call.
Also add a home.activation script that removes any stale hyprland.conf
(auto-generated by Hyprland before hyprland.lua was first placed) so the
autogenerated-config warning banner can never persist across restarts.
Keep services.hyprpaper.enable = true since it was previously set by the
now-disabled Hyprland target.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 22:24:12 +01:00
|
|
|
"col.active_border" = rgb c.base0D;
|
|
|
|
|
"col.inactive_border" = rgb c.base03;
|
2026-05-17 20:38:30 +01:00
|
|
|
};
|
2026-05-17 19:55:18 +01:00
|
|
|
|
2026-05-17 20:38:30 +01:00
|
|
|
decoration = {
|
|
|
|
|
rounding = 8;
|
|
|
|
|
blur = {
|
|
|
|
|
enabled = true;
|
|
|
|
|
};
|
hyprland: fix Lua config errors caused by stylix's Hyprland target
Stylix's Hyprland target injects settings.{general,decoration,group,misc}
as top-level keys. In hyprlang mode these render as section blocks, but
in Lua mode (configType = "lua") they become hl.general(), hl.decoration(),
etc. — functions that don't exist, causing an emergency-mode config error.
Disable stylix.targets.hyprland and absorb all its colours into
settings.config.{general,decoration,group,misc} where they are correctly
rendered inside the single hl.config({}) call.
Also add a home.activation script that removes any stale hyprland.conf
(auto-generated by Hyprland before hyprland.lua was first placed) so the
autogenerated-config warning banner can never persist across restarts.
Keep services.hyprpaper.enable = true since it was previously set by the
now-disabled Hyprland target.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 22:24:12 +01:00
|
|
|
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;
|
|
|
|
|
};
|
2026-05-17 20:38:30 +01:00
|
|
|
};
|
2026-05-10 20:03:43 +01:00
|
|
|
|
2026-05-17 20:38:30 +01:00
|
|
|
render = {
|
|
|
|
|
direct_scanout = false;
|
|
|
|
|
};
|
2026-05-10 20:03:43 +01:00
|
|
|
|
2026-05-17 20:38:30 +01:00
|
|
|
animations = {
|
2026-05-11 15:10:20 +01:00
|
|
|
enabled = true;
|
2026-05-10 20:03:43 +01:00
|
|
|
};
|
|
|
|
|
|
2026-05-17 20:38:30 +01:00
|
|
|
input = {
|
|
|
|
|
kb_layout = "gb,no";
|
|
|
|
|
kb_options = "grp:alt_shift_toggle";
|
|
|
|
|
follow_mouse = 1;
|
|
|
|
|
accel_profile = "flat";
|
|
|
|
|
sensitivity = 0;
|
|
|
|
|
} // lib.optionalAttrs isMacbook {
|
|
|
|
|
touchpad = {
|
2026-05-18 09:59:30 +01:00
|
|
|
tap_to_click = true;
|
2026-05-17 20:38:30 +01:00
|
|
|
tap_button_map = "lrm";
|
|
|
|
|
natural_scroll = true;
|
2026-05-18 17:11:52 +01:00
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-17 20:38:30 +01:00
|
|
|
cursor = {
|
|
|
|
|
no_warps = true;
|
|
|
|
|
};
|
2026-05-11 11:17:03 +01:00
|
|
|
|
2026-05-17 20:38:30 +01:00
|
|
|
dwindle = {
|
|
|
|
|
preserve_split = true;
|
|
|
|
|
};
|
2026-05-10 20:03:43 +01:00
|
|
|
|
2026-05-17 20:38:30 +01:00
|
|
|
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;
|
hyprland: fix Lua config errors caused by stylix's Hyprland target
Stylix's Hyprland target injects settings.{general,decoration,group,misc}
as top-level keys. In hyprlang mode these render as section blocks, but
in Lua mode (configType = "lua") they become hl.general(), hl.decoration(),
etc. — functions that don't exist, causing an emergency-mode config error.
Disable stylix.targets.hyprland and absorb all its colours into
settings.config.{general,decoration,group,misc} where they are correctly
rendered inside the single hl.config({}) call.
Also add a home.activation script that removes any stale hyprland.conf
(auto-generated by Hyprland before hyprland.lua was first placed) so the
autogenerated-config warning banner can never persist across restarts.
Keep services.hyprpaper.enable = true since it was previously set by the
now-disabled Hyprland target.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 22:24:12 +01:00
|
|
|
background_color = rgb c.base00;
|
2026-05-17 20:38:30 +01:00
|
|
|
};
|
2026-05-11 12:40:52 +01:00
|
|
|
|
2026-05-17 20:38:30 +01:00
|
|
|
# vfr moved from misc: to debug: in 0.55.0
|
|
|
|
|
debug = {
|
|
|
|
|
vfr = false; # keep compositor ticking, don't idle between frames
|
|
|
|
|
disable_logs = false;
|
|
|
|
|
};
|
2026-05-10 20:03:43 +01:00
|
|
|
};
|
2026-05-17 20:38:30 +01:00
|
|
|
};
|
2026-05-10 20:03:43 +01:00
|
|
|
|
2026-05-17 20:38:30 +01:00
|
|
|
extraConfig =
|
|
|
|
|
let
|
|
|
|
|
powerMenu = pkgs.writeShellScript "power-menu" ''
|
2026-05-18 12:51:41 +01:00
|
|
|
# Stop the daemon so standalone stdin mode can run cleanly.
|
|
|
|
|
# systemd restarts it automatically afterwards (Restart=on-failure).
|
|
|
|
|
systemctl --user stop anyrun.service 2>/dev/null || true
|
2026-05-14 10:21:23 +01:00
|
|
|
choice=$(printf '%s\n' \
|
2026-05-17 22:58:41 +01:00
|
|
|
$'\uf023 Lock' \
|
|
|
|
|
$'\uf08b Logout' \
|
|
|
|
|
$'\uf01e Reboot' \
|
|
|
|
|
$'\uf011 Shutdown' \
|
2026-05-18 12:48:00 +01:00
|
|
|
| ${anyrun-pkgs.anyrun}/bin/anyrun \
|
|
|
|
|
--plugins "${anyrun-pkgs.stdin}/lib/libstdin.so" \
|
2026-05-14 10:15:56 +01:00
|
|
|
--show-results-immediately true \
|
|
|
|
|
--hide-plugin-info true \
|
|
|
|
|
--close-on-click true)
|
2026-05-18 12:58:48 +01:00
|
|
|
# Restart the daemon service (reset-failed clears the start-rate limiter).
|
|
|
|
|
systemctl --user reset-failed anyrun.service 2>/dev/null
|
|
|
|
|
systemctl --user start anyrun.service 2>/dev/null
|
2026-05-14 10:15:56 +01:00
|
|
|
case "$choice" in
|
|
|
|
|
*Lock) ${pkgs.hyprlock}/bin/hyprlock ;;
|
|
|
|
|
*Logout) hyprctl dispatch exit ;;
|
|
|
|
|
*Reboot) systemctl reboot ;;
|
|
|
|
|
*Shutdown) systemctl poweroff ;;
|
|
|
|
|
esac
|
2026-05-17 20:38:30 +01:00
|
|
|
'';
|
|
|
|
|
kbdBrightUp = pkgs.writeShellScript "kbd-bright-up" ''
|
2026-05-17 13:47:59 +01:00
|
|
|
${pkgs.brightnessctl}/bin/brightnessctl -d smc::kbd_backlight set +10%
|
|
|
|
|
brightness=$(${pkgs.brightnessctl}/bin/brightnessctl -d smc::kbd_backlight get)
|
|
|
|
|
max=$(${pkgs.brightnessctl}/bin/brightnessctl -d smc::kbd_backlight max)
|
|
|
|
|
echo $(( brightness * 100 / max )) > "$XDG_RUNTIME_DIR/wob.fifo"
|
2026-05-17 20:38:30 +01:00
|
|
|
'';
|
|
|
|
|
kbdBrightDown = pkgs.writeShellScript "kbd-bright-down" ''
|
2026-05-17 13:47:59 +01:00
|
|
|
${pkgs.brightnessctl}/bin/brightnessctl -d smc::kbd_backlight set 10%-
|
|
|
|
|
brightness=$(${pkgs.brightnessctl}/bin/brightnessctl -d smc::kbd_backlight get)
|
|
|
|
|
max=$(${pkgs.brightnessctl}/bin/brightnessctl -d smc::kbd_backlight max)
|
|
|
|
|
echo $(( brightness * 100 / max )) > "$XDG_RUNTIME_DIR/wob.fifo"
|
2026-05-17 20:38:30 +01:00
|
|
|
'';
|
|
|
|
|
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")
|
|
|
|
|
''}
|
|
|
|
|
|
|
|
|
|
-- Startup
|
|
|
|
|
hl.on("hyprland.start", function()
|
2026-05-17 22:58:41 +01:00
|
|
|
-- 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")
|
2026-05-26 11:19:28 +01:00
|
|
|
hl.exec_cmd("qs")
|
2026-05-17 20:38:30 +01:00
|
|
|
hl.exec_cmd("mako")
|
2026-05-26 11:02:35 +01:00
|
|
|
${lib.optionalString isMacbook ''hl.exec_cmd("nm-applet --indicator")''}
|
2026-05-17 20:38:30 +01:00
|
|
|
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")
|
|
|
|
|
hl.exec_cmd("swayosd-server")
|
|
|
|
|
${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" })
|
|
|
|
|
hl.animation({ leaf = "workspaces", enabled = true, speed = 1, bezier = "snap" })
|
|
|
|
|
|
|
|
|
|
-- Window rules
|
|
|
|
|
-- 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",
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
-- Binds
|
|
|
|
|
local mod = "SUPER"
|
|
|
|
|
|
|
|
|
|
-- Apps
|
|
|
|
|
hl.bind(mod .. " + T", hl.dsp.exec_cmd("ghostty"))
|
|
|
|
|
hl.bind(mod .. " + E", hl.dsp.exec_cmd("nemo"))
|
2026-05-18 12:48:00 +01:00
|
|
|
hl.bind(mod .. " + R", hl.dsp.exec_cmd("anyrun close || anyrun"))
|
2026-05-17 20:38:30 +01:00
|
|
|
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" }))
|
2026-05-17 22:37:34 +01:00
|
|
|
hl.bind(mod .. " + F", hl.dsp.window.fullscreen())
|
2026-05-17 20:38:30 +01:00
|
|
|
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" }))
|
|
|
|
|
|
2026-05-18 12:48:00 +01:00
|
|
|
-- Power menu — dismiss launcher if open, then show menu
|
|
|
|
|
hl.bind(mod .. " + L", hl.dsp.exec_cmd("anyrun close 2>/dev/null; ${powerMenu}"))
|
2026-05-17 20:38:30 +01:00
|
|
|
|
|
|
|
|
-- 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
|
2026-05-25 20:18:41 +01:00
|
|
|
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
|
2026-05-17 20:38:30 +01:00
|
|
|
|
|
|
|
|
-- Screenshots — Shift+Super+S matches GNOME binding
|
|
|
|
|
hl.bind(mod .. " + SHIFT + S", hl.dsp.exec_cmd("hyprshot -m region --clipboard-only"))
|
|
|
|
|
hl.bind("Print", hl.dsp.exec_cmd("hyprshot -m output --clipboard-only"))
|
|
|
|
|
|
|
|
|
|
-- 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"))
|
|
|
|
|
|
|
|
|
|
-- 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)
|
|
|
|
|
hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("swayosd-client --output-volume raise"), { repeating = true })
|
|
|
|
|
hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("swayosd-client --output-volume lower"), { repeating = true })
|
|
|
|
|
hl.bind("XF86AudioMute", hl.dsp.exec_cmd("swayosd-client --output-volume mute-toggle"), { repeating = true })
|
|
|
|
|
hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("swayosd-client --brightness raise"), { repeating = true })
|
|
|
|
|
hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("swayosd-client --brightness lower"), { repeating = true })
|
|
|
|
|
${lib.optionalString isMacbook ''
|
|
|
|
|
hl.bind("XF86KbdBrightnessUp", hl.dsp.exec_cmd("${kbdBrightUp}"), { repeating = true })
|
|
|
|
|
hl.bind("XF86KbdBrightnessDown", hl.dsp.exec_cmd("${kbdBrightDown}"), { 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 })
|
|
|
|
|
'';
|
2026-05-10 20:03:43 +01:00
|
|
|
};
|
|
|
|
|
|
2026-05-14 10:15:56 +01:00
|
|
|
programs.anyrun = {
|
2026-05-11 12:50:51 +01:00
|
|
|
enable = true;
|
2026-05-14 10:15:56 +01:00
|
|
|
config = {
|
2026-05-18 12:51:41 +01:00
|
|
|
plugins = [ anyrun-pkgs.applications ];
|
2026-05-14 10:15:56 +01:00
|
|
|
x.fraction = 0.5;
|
2026-05-14 10:23:03 +01:00
|
|
|
y.fraction = 0.25;
|
2026-05-16 10:59:35 +01:00
|
|
|
width.absolute = 350;
|
2026-05-14 10:15:56 +01:00
|
|
|
height.absolute = 0;
|
|
|
|
|
hideIcons = false;
|
|
|
|
|
ignoreExclusiveZones = false;
|
|
|
|
|
layer = "overlay";
|
|
|
|
|
hidePluginInfo = true;
|
|
|
|
|
closeOnClick = true;
|
|
|
|
|
maxEntries = 8;
|
2026-05-11 12:50:51 +01:00
|
|
|
};
|
2026-05-14 10:15:56 +01:00
|
|
|
extraCss =
|
|
|
|
|
let c = config.lib.stylix.colors; in
|
|
|
|
|
''
|
|
|
|
|
* { all: unset; font-family: "FiraMono Nerd Font", monospace; font-size: 13px; }
|
|
|
|
|
window { background: transparent; }
|
|
|
|
|
box.main {
|
|
|
|
|
background: #${c.base00};
|
|
|
|
|
border: 1px solid #${c.base03};
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
padding: 8px;
|
|
|
|
|
margin: 16px;
|
|
|
|
|
}
|
|
|
|
|
text {
|
|
|
|
|
background: #${c.base01};
|
|
|
|
|
color: #${c.base05};
|
|
|
|
|
caret-color: #${c.base0D};
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
}
|
|
|
|
|
list.plugin { background: transparent; }
|
|
|
|
|
.matches { background: transparent; }
|
|
|
|
|
.match {
|
|
|
|
|
padding: 4px 16px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
color: #${c.base05};
|
|
|
|
|
background: transparent;
|
|
|
|
|
}
|
|
|
|
|
.match:selected {
|
|
|
|
|
background: #${c.base02};
|
|
|
|
|
border: none;
|
|
|
|
|
}
|
|
|
|
|
label.match.description { color: #${c.base04}; font-size: 11px; }
|
|
|
|
|
'';
|
|
|
|
|
extraConfigFiles."applications.ron".text = ''
|
|
|
|
|
Config(
|
|
|
|
|
desktop_actions: false,
|
2026-05-18 12:20:02 +01:00
|
|
|
max_entries: 8,
|
2026-05-18 12:35:07 +01:00
|
|
|
terminal: Some("ghostty"),
|
2026-05-14 10:15:56 +01:00
|
|
|
)
|
|
|
|
|
'';
|
2026-05-11 12:50:51 +01:00
|
|
|
};
|
|
|
|
|
|
2026-05-15 12:12:04 +01:00
|
|
|
programs.hyprlock.enable = true;
|
|
|
|
|
|
|
|
|
|
services.hypridle = lib.mkIf isMacbook {
|
|
|
|
|
enable = true;
|
|
|
|
|
settings = {
|
|
|
|
|
general = {
|
2026-05-17 20:38:30 +01:00
|
|
|
lock_cmd = "pidof hyprlock || hyprlock";
|
2026-05-15 12:12:04 +01:00
|
|
|
before_sleep_cmd = "loginctl lock-session";
|
|
|
|
|
after_sleep_cmd = "hyprctl dispatch dpms on";
|
|
|
|
|
};
|
|
|
|
|
listener = [
|
|
|
|
|
{
|
|
|
|
|
timeout = 300; # 5 min — lock
|
|
|
|
|
on-timeout = "loginctl lock-session";
|
|
|
|
|
}
|
|
|
|
|
{
|
|
|
|
|
timeout = 420; # 7 min — display off
|
|
|
|
|
on-timeout = "hyprctl dispatch dpms off";
|
|
|
|
|
on-resume = "hyprctl dispatch dpms on";
|
|
|
|
|
}
|
|
|
|
|
{
|
|
|
|
|
timeout = 600; # 10 min — suspend
|
|
|
|
|
on-timeout = "systemctl suspend";
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-26 11:15:01 +01:00
|
|
|
# Scope all HM Wayland services (hyprpaper, etc.) to the
|
2026-05-13 21:13:31 +01:00
|
|
|
# Hyprland session so they don't crash-loop in a GNOME session.
|
|
|
|
|
wayland.systemd.target = "hyprland-session.target";
|
|
|
|
|
|
2026-05-26 11:15:01 +01:00
|
|
|
xdg.configFile."quickshell/shell.qml" = {
|
2026-05-26 10:11:09 +01:00
|
|
|
text = ''
|
2026-05-26 10:29:47 +01:00
|
|
|
//@ pragma UseQApplication
|
2026-05-26 10:11:09 +01:00
|
|
|
import Quickshell
|
|
|
|
|
import Quickshell.Hyprland
|
|
|
|
|
import Quickshell.Services.SystemTray
|
2026-05-26 10:37:50 +01:00
|
|
|
import Quickshell.Widgets
|
2026-05-26 11:02:35 +01:00
|
|
|
import Quickshell.Io
|
2026-05-26 10:11:09 +01:00
|
|
|
import QtQuick
|
|
|
|
|
import QtQuick.Layouts
|
2026-05-26 10:55:21 +01:00
|
|
|
import Qt5Compat.GraphicalEffects
|
2026-05-26 10:11:09 +01:00
|
|
|
|
|
|
|
|
ShellRoot {
|
|
|
|
|
Variants {
|
|
|
|
|
model: Quickshell.screens
|
|
|
|
|
|
|
|
|
|
PanelWindow {
|
2026-05-26 10:16:55 +01:00
|
|
|
id: bar
|
2026-05-26 10:11:09 +01:00
|
|
|
required property var modelData
|
|
|
|
|
screen: modelData
|
|
|
|
|
|
|
|
|
|
anchors {
|
|
|
|
|
top: true
|
|
|
|
|
left: true
|
|
|
|
|
right: true
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-26 10:16:55 +01:00
|
|
|
implicitHeight: 30
|
2026-05-26 11:02:35 +01:00
|
|
|
color: "#D1${c.base00}"
|
2026-05-26 10:11:09 +01:00
|
|
|
|
|
|
|
|
RowLayout {
|
2026-05-26 12:05:04 +01:00
|
|
|
anchors.fill: parent
|
2026-05-26 10:11:09 +01:00
|
|
|
spacing: 0
|
|
|
|
|
|
|
|
|
|
// Workspaces
|
2026-05-26 12:05:04 +01:00
|
|
|
Row {
|
2026-05-26 10:11:09 +01:00
|
|
|
spacing: 0
|
|
|
|
|
Layout.leftMargin: 6
|
|
|
|
|
|
|
|
|
|
Repeater {
|
|
|
|
|
model: Hyprland.workspaces
|
|
|
|
|
|
2026-05-26 10:16:55 +01:00
|
|
|
Item {
|
2026-05-26 10:11:09 +01:00
|
|
|
required property var modelData
|
2026-05-26 12:05:04 +01:00
|
|
|
width: 28
|
|
|
|
|
height: 30
|
2026-05-26 10:11:09 +01:00
|
|
|
|
|
|
|
|
Text {
|
|
|
|
|
anchors.centerIn: parent
|
|
|
|
|
text: modelData.name
|
|
|
|
|
color: modelData.focused ? "#${c.base05}" : "#${c.base03}"
|
|
|
|
|
font.family: "FiraMono Nerd Font"
|
|
|
|
|
font.pixelSize: 13
|
2026-05-26 10:16:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Underline indicator
|
|
|
|
|
Rectangle {
|
|
|
|
|
anchors.bottom: parent.bottom
|
|
|
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
|
|
|
width: parent.width - 8
|
|
|
|
|
height: 2
|
|
|
|
|
color: "#${c.base05}"
|
|
|
|
|
visible: modelData.focused
|
2026-05-26 10:11:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MouseArea {
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
onClicked: modelData.activate()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Spacer
|
|
|
|
|
Item { Layout.fillWidth: true }
|
|
|
|
|
|
2026-05-26 11:15:01 +01:00
|
|
|
// Clock
|
|
|
|
|
Text {
|
|
|
|
|
id: clockText
|
|
|
|
|
property date now: new Date()
|
|
|
|
|
text: now.toLocaleTimeString(Qt.locale(), "HH:mm")
|
|
|
|
|
color: "#${c.base05}"
|
|
|
|
|
font.family: "FiraMono Nerd Font"
|
|
|
|
|
font.pixelSize: 13
|
|
|
|
|
font.weight: Font.Medium
|
|
|
|
|
|
|
|
|
|
Timer {
|
|
|
|
|
interval: 1000
|
|
|
|
|
running: true
|
|
|
|
|
repeat: true
|
|
|
|
|
onTriggered: clockText.now = new Date()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MouseArea {
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
onClicked: calPopup.visible = !calPopup.visible
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Spacer
|
|
|
|
|
Item { Layout.fillWidth: true }
|
|
|
|
|
|
2026-05-26 11:02:35 +01:00
|
|
|
// Network status
|
|
|
|
|
Item {
|
2026-05-26 11:04:34 +01:00
|
|
|
id: netWidget
|
2026-05-26 11:02:35 +01:00
|
|
|
Layout.preferredHeight: 30
|
|
|
|
|
Layout.preferredWidth: netRow.width
|
|
|
|
|
Layout.rightMargin: 10
|
|
|
|
|
|
|
|
|
|
property string netState: ""
|
|
|
|
|
property string netConn: ""
|
|
|
|
|
property string netIcon: "\u{f0b1}"
|
|
|
|
|
|
|
|
|
|
Timer {
|
|
|
|
|
interval: 5000
|
|
|
|
|
running: true
|
|
|
|
|
repeat: true
|
|
|
|
|
triggeredOnStart: true
|
|
|
|
|
onTriggered: netProc.running = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Process {
|
|
|
|
|
id: netProc
|
|
|
|
|
command: ["${pkgs.networkmanager}/bin/nmcli", "-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device"]
|
|
|
|
|
stdout: SplitParser {
|
|
|
|
|
onRead: data => {
|
|
|
|
|
let fields = data.split(":");
|
|
|
|
|
if (fields.length >= 4 && fields[1] === "ethernet") {
|
|
|
|
|
let state = fields[2];
|
|
|
|
|
let conn = fields[3];
|
|
|
|
|
if (state === "connected") {
|
2026-05-26 11:04:34 +01:00
|
|
|
netWidget.netState = "connected";
|
|
|
|
|
netWidget.netConn = conn;
|
|
|
|
|
netWidget.netIcon = "\u{f0200}";
|
2026-05-26 11:02:35 +01:00
|
|
|
} else {
|
2026-05-26 11:04:34 +01:00
|
|
|
netWidget.netState = "disconnected";
|
|
|
|
|
netWidget.netConn = "";
|
|
|
|
|
netWidget.netIcon = "\u{f0201}";
|
2026-05-26 11:02:35 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RowLayout {
|
|
|
|
|
id: netRow
|
|
|
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
|
spacing: 6
|
|
|
|
|
|
|
|
|
|
Text {
|
2026-05-26 11:04:34 +01:00
|
|
|
text: netWidget.netIcon
|
2026-05-26 11:02:35 +01:00
|
|
|
color: "#${c.base05}"
|
|
|
|
|
font.family: "FiraMono Nerd Font"
|
|
|
|
|
font.pixelSize: 14
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-26 11:07:14 +01:00
|
|
|
|
|
|
|
|
Process {
|
|
|
|
|
id: nmEditorProc
|
|
|
|
|
command: ["${pkgs.networkmanagerapplet}/bin/nm-connection-editor"]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MouseArea {
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
onClicked: nmEditorProc.running = true
|
|
|
|
|
}
|
2026-05-26 11:02:35 +01:00
|
|
|
}
|
|
|
|
|
|
2026-05-26 11:15:01 +01:00
|
|
|
${lib.optionalString isMacbook ''
|
|
|
|
|
// Battery
|
|
|
|
|
Item {
|
|
|
|
|
id: batteryWidget
|
|
|
|
|
Layout.preferredHeight: 30
|
|
|
|
|
Layout.preferredWidth: batteryRow.width
|
|
|
|
|
Layout.rightMargin: 10
|
|
|
|
|
|
|
|
|
|
property int batteryLevel: 0
|
|
|
|
|
property bool charging: false
|
|
|
|
|
property string batteryIcon: "\u{f008e}"
|
|
|
|
|
|
|
|
|
|
function updateIcon() {
|
|
|
|
|
if (charging) { batteryIcon = "\u{f0084}"; return; }
|
|
|
|
|
if (batteryLevel >= 90) batteryIcon = "\u{f0079}";
|
|
|
|
|
else if (batteryLevel >= 70) batteryIcon = "\u{f0082}";
|
|
|
|
|
else if (batteryLevel >= 50) batteryIcon = "\u{f007f}";
|
|
|
|
|
else if (batteryLevel >= 30) batteryIcon = "\u{f007c}";
|
|
|
|
|
else if (batteryLevel >= 15) batteryIcon = "\u{f007a}";
|
|
|
|
|
else batteryIcon = "\u{f008e}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Timer {
|
|
|
|
|
interval: 10000
|
|
|
|
|
running: true
|
|
|
|
|
repeat: true
|
|
|
|
|
triggeredOnStart: true
|
|
|
|
|
onTriggered: batteryProc.running = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Process {
|
|
|
|
|
id: batteryProc
|
|
|
|
|
command: ["sh", "-c", "cat /sys/class/power_supply/BAT0/capacity; cat /sys/class/power_supply/BAT0/status"]
|
|
|
|
|
stdout: SplitParser {
|
|
|
|
|
onRead: data => {
|
|
|
|
|
let trimmed = data.trim();
|
|
|
|
|
if (/^\\d+$/.test(trimmed)) {
|
|
|
|
|
batteryWidget.batteryLevel = parseInt(trimmed);
|
|
|
|
|
} else {
|
|
|
|
|
batteryWidget.charging = (trimmed === "Charging");
|
|
|
|
|
}
|
|
|
|
|
batteryWidget.updateIcon();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RowLayout {
|
|
|
|
|
id: batteryRow
|
|
|
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
|
spacing: 4
|
|
|
|
|
|
|
|
|
|
Text {
|
|
|
|
|
text: batteryWidget.batteryLevel + "%"
|
|
|
|
|
color: batteryWidget.batteryLevel <= 15 ? "#${c.base08}"
|
|
|
|
|
: batteryWidget.batteryLevel <= 30 ? "#${c.base0A}"
|
|
|
|
|
: "#${c.base05}"
|
|
|
|
|
font.family: "FiraMono Nerd Font"
|
|
|
|
|
font.pixelSize: 13
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Text {
|
|
|
|
|
text: batteryWidget.batteryIcon
|
|
|
|
|
color: batteryWidget.batteryLevel <= 15 ? "#${c.base08}"
|
|
|
|
|
: batteryWidget.batteryLevel <= 30 ? "#${c.base0A}"
|
|
|
|
|
: "#${c.base05}"
|
|
|
|
|
font.family: "FiraMono Nerd Font"
|
|
|
|
|
font.pixelSize: 14
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
''}
|
|
|
|
|
|
2026-05-26 10:55:21 +01:00
|
|
|
// Tray icons inline
|
|
|
|
|
RowLayout {
|
|
|
|
|
id: trayArea
|
|
|
|
|
spacing: 8
|
|
|
|
|
Layout.rightMargin: 8
|
2026-05-26 10:11:09 +01:00
|
|
|
|
2026-05-26 10:55:21 +01:00
|
|
|
Repeater {
|
|
|
|
|
model: SystemTray.items
|
2026-05-26 10:11:09 +01:00
|
|
|
|
2026-05-26 10:55:21 +01:00
|
|
|
Item {
|
|
|
|
|
required property var modelData
|
|
|
|
|
Layout.preferredWidth: 16
|
|
|
|
|
Layout.preferredHeight: 16
|
2026-05-26 10:16:55 +01:00
|
|
|
|
2026-05-26 10:55:21 +01:00
|
|
|
Image {
|
|
|
|
|
id: trayIcon
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
source: modelData.icon
|
|
|
|
|
sourceSize.width: 16
|
|
|
|
|
sourceSize.height: 16
|
|
|
|
|
smooth: true
|
|
|
|
|
mipmap: true
|
|
|
|
|
visible: false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ColorOverlay {
|
|
|
|
|
anchors.fill: trayIcon
|
|
|
|
|
source: trayIcon
|
|
|
|
|
color: "#${c.base05}"
|
|
|
|
|
}
|
2026-05-26 10:16:55 +01:00
|
|
|
|
2026-05-26 10:55:21 +01:00
|
|
|
MouseArea {
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
|
|
|
onClicked: (event) => {
|
|
|
|
|
if (event.button === Qt.RightButton && modelData.hasMenu) {
|
|
|
|
|
contextMenu.trayItem = modelData;
|
|
|
|
|
menuOpener.menu = modelData.menu;
|
|
|
|
|
contextMenu.visible = true;
|
|
|
|
|
} else {
|
|
|
|
|
modelData.activate();
|
2026-05-26 10:11:09 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-26 10:37:50 +01:00
|
|
|
|
|
|
|
|
// Custom-rendered context menu
|
|
|
|
|
PopupWindow {
|
|
|
|
|
id: contextMenu
|
|
|
|
|
property var trayItem: null
|
2026-05-26 10:55:21 +01:00
|
|
|
anchor.item: trayArea
|
2026-05-26 10:37:50 +01:00
|
|
|
anchor.edges: Edges.Bottom | Edges.Right
|
|
|
|
|
anchor.gravity: Edges.Bottom | Edges.Left
|
|
|
|
|
anchor.adjustment: PopupAdjustment.Slide
|
2026-05-26 10:58:40 +01:00
|
|
|
grabFocus: true
|
2026-05-26 10:37:50 +01:00
|
|
|
visible: false
|
|
|
|
|
color: "transparent"
|
|
|
|
|
implicitWidth: menuColumn.width + 2
|
|
|
|
|
implicitHeight: menuColumn.height + 2
|
|
|
|
|
|
|
|
|
|
QsMenuOpener {
|
|
|
|
|
id: menuOpener
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Rectangle {
|
|
|
|
|
id: menuColumn
|
|
|
|
|
width: menuItems.width + 16
|
|
|
|
|
height: menuItems.height + 12
|
|
|
|
|
color: "#${c.base00}"
|
|
|
|
|
border.color: "#${c.base03}"
|
|
|
|
|
border.width: 1
|
|
|
|
|
radius: 8
|
|
|
|
|
|
|
|
|
|
Column {
|
|
|
|
|
id: menuItems
|
|
|
|
|
anchors.centerIn: parent
|
|
|
|
|
width: 200
|
|
|
|
|
|
|
|
|
|
Repeater {
|
|
|
|
|
model: menuOpener.children
|
|
|
|
|
|
2026-05-26 10:39:19 +01:00
|
|
|
Rectangle {
|
2026-05-26 10:37:50 +01:00
|
|
|
required property var modelData
|
|
|
|
|
width: 200
|
2026-05-26 10:39:19 +01:00
|
|
|
height: modelData.isSeparator ? 9 : 28
|
|
|
|
|
color: !modelData.isSeparator && itemMouse.containsMouse && modelData.enabled
|
|
|
|
|
? "#${c.base02}" : "transparent"
|
|
|
|
|
radius: modelData.isSeparator ? 0 : 4
|
|
|
|
|
|
|
|
|
|
// Separator line
|
|
|
|
|
Rectangle {
|
|
|
|
|
visible: modelData.isSeparator
|
|
|
|
|
anchors.centerIn: parent
|
|
|
|
|
width: parent.width - 20
|
|
|
|
|
height: 1
|
|
|
|
|
color: "#${c.base03}"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Menu item content
|
|
|
|
|
RowLayout {
|
|
|
|
|
visible: !modelData.isSeparator
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
anchors.leftMargin: 10
|
|
|
|
|
anchors.rightMargin: 10
|
|
|
|
|
spacing: 8
|
|
|
|
|
|
|
|
|
|
Text {
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
|
text: modelData.text ?? ""
|
|
|
|
|
color: modelData.enabled ? "#${c.base05}" : "#${c.base03}"
|
|
|
|
|
font.family: "FiraMono Nerd Font"
|
|
|
|
|
font.pixelSize: 12
|
|
|
|
|
elide: Text.ElideRight
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Text {
|
|
|
|
|
visible: modelData.buttonType !== QsMenuButtonType.None
|
|
|
|
|
text: modelData.checkState === Qt.Checked ? "\u2713" : ""
|
|
|
|
|
color: "#${c.base0D}"
|
|
|
|
|
font.family: "FiraMono Nerd Font"
|
|
|
|
|
font.pixelSize: 12
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MouseArea {
|
|
|
|
|
id: itemMouse
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
hoverEnabled: true
|
|
|
|
|
enabled: !modelData.isSeparator && modelData.enabled
|
|
|
|
|
onClicked: {
|
|
|
|
|
modelData.triggered();
|
|
|
|
|
contextMenu.visible = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-26 10:37:50 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onVisibleChanged: {
|
|
|
|
|
if (!visible) {
|
|
|
|
|
menuOpener.menu = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-26 10:52:14 +01:00
|
|
|
}
|
2026-05-26 11:15:01 +01:00
|
|
|
|
|
|
|
|
// Calendar popup
|
|
|
|
|
PopupWindow {
|
|
|
|
|
id: calPopup
|
|
|
|
|
anchor.item: clockText
|
|
|
|
|
anchor.edges: Edges.Bottom
|
|
|
|
|
anchor.gravity: Edges.Bottom
|
|
|
|
|
anchor.adjustment: PopupAdjustment.Slide
|
|
|
|
|
grabFocus: true
|
|
|
|
|
visible: false
|
|
|
|
|
color: "transparent"
|
|
|
|
|
implicitWidth: calContent.width + 2
|
|
|
|
|
implicitHeight: calContent.height + 2
|
|
|
|
|
|
|
|
|
|
Rectangle {
|
|
|
|
|
id: calContent
|
|
|
|
|
width: calCol.width + 32
|
|
|
|
|
height: calCol.height + 24
|
|
|
|
|
color: "#${c.base00}"
|
|
|
|
|
border.color: "#${c.base03}"
|
|
|
|
|
border.width: 1
|
|
|
|
|
radius: 8
|
|
|
|
|
|
|
|
|
|
Column {
|
|
|
|
|
id: calCol
|
|
|
|
|
anchors.centerIn: parent
|
|
|
|
|
spacing: 8
|
|
|
|
|
|
|
|
|
|
// Date header
|
|
|
|
|
Text {
|
|
|
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
|
|
|
text: clockText.now.toLocaleDateString(Qt.locale(), "dddd, d MMMM yyyy")
|
|
|
|
|
color: "#${c.base05}"
|
|
|
|
|
font.family: "FiraMono Nerd Font"
|
|
|
|
|
font.pixelSize: 14
|
|
|
|
|
font.weight: Font.Medium
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Day headers
|
|
|
|
|
Row {
|
|
|
|
|
spacing: 0
|
|
|
|
|
Repeater {
|
|
|
|
|
model: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]
|
|
|
|
|
Text {
|
|
|
|
|
required property var modelData
|
|
|
|
|
width: 28
|
|
|
|
|
horizontalAlignment: Text.AlignHCenter
|
|
|
|
|
text: modelData
|
|
|
|
|
color: "#${c.base03}"
|
|
|
|
|
font.family: "FiraMono Nerd Font"
|
|
|
|
|
font.pixelSize: 11
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calendar grid
|
|
|
|
|
Grid {
|
|
|
|
|
columns: 7
|
|
|
|
|
spacing: 0
|
|
|
|
|
|
|
|
|
|
Repeater {
|
|
|
|
|
id: calRepeater
|
|
|
|
|
model: 42
|
|
|
|
|
|
|
|
|
|
Rectangle {
|
|
|
|
|
required property int index
|
|
|
|
|
width: 28
|
|
|
|
|
height: 24
|
|
|
|
|
radius: 4
|
|
|
|
|
color: {
|
|
|
|
|
let d = clockText.now;
|
|
|
|
|
let first = new Date(d.getFullYear(), d.getMonth(), 1);
|
|
|
|
|
let startDay = (first.getDay() + 6) % 7;
|
|
|
|
|
let dayNum = index - startDay + 1;
|
|
|
|
|
let daysInMonth = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate();
|
|
|
|
|
return (dayNum === d.getDate() && dayNum >= 1 && dayNum <= daysInMonth)
|
|
|
|
|
? "#${c.base02}" : "transparent";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Text {
|
|
|
|
|
anchors.centerIn: parent
|
|
|
|
|
text: {
|
|
|
|
|
let d = clockText.now;
|
|
|
|
|
let first = new Date(d.getFullYear(), d.getMonth(), 1);
|
|
|
|
|
let startDay = (first.getDay() + 6) % 7;
|
|
|
|
|
let dayNum = parent.index - startDay + 1;
|
|
|
|
|
let daysInMonth = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate();
|
|
|
|
|
return (dayNum >= 1 && dayNum <= daysInMonth) ? dayNum.toString() : "";
|
|
|
|
|
}
|
|
|
|
|
color: {
|
|
|
|
|
let d = clockText.now;
|
|
|
|
|
let first = new Date(d.getFullYear(), d.getMonth(), 1);
|
|
|
|
|
let startDay = (first.getDay() + 6) % 7;
|
|
|
|
|
let dayNum = parent.index - startDay + 1;
|
|
|
|
|
return (dayNum === d.getDate()) ? "#${c.base05}" : "#${c.base04}";
|
|
|
|
|
}
|
|
|
|
|
font.family: "FiraMono Nerd Font"
|
|
|
|
|
font.pixelSize: 11
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-26 10:11:09 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
'';
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-10 20:03:43 +01:00
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
}
|