# settings/hyprland.nix { config, pkgs, lib, inputs, ... }: let hyprland-pkgs = inputs.hyprland.packages.${pkgs.stdenv.hostPlatform.system}; anyrun-pkgs = inputs.anyrun.packages.${pkgs.stdenv.hostPlatform.system}; isMacbook = config.networking.hostName == "FredOS-Macbook"; isGaming = !isMacbook; in { config = lib.mkIf (lib.elem config.networking.hostName [ "FredOS-Gaming" "FredOS-Macbook" ]) { programs.hyprland = { enable = true; xwayland.enable = true; package = hyprland-pkgs.hyprland; portalPackage = hyprland-pkgs.xdg-desktop-portal-hyprland; }; 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; # 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 swayosd playerctl hyprpaper hyprlock hypridle hyprshot networkmanagerapplet pavucontrol polkit_gnome quickshell qt6.qt5compat zenity libcanberra-gtk3 ]; # 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 ]; 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; # The disabled Hyprland target would normally enable this; do it # manually. Stylix's hyprpaper target (auto-enabled) still handles # preload/wallpaper settings. services.hyprpaper.enable = true; wayland.windowManager.hyprland = { enable = true; configType = "lua"; systemd.variables = [ "--all" ]; package = hyprland-pkgs.hyprland; settings = { # 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; "col.active_border" = rgb c.base0D; "col.inactive_border" = rgb c.base03; }; decoration = { rounding = 8; blur = { enabled = true; }; 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:alt_shift_toggle"; follow_mouse = 1; accel_profile = "flat"; sensitivity = 0; } // lib.optionalAttrs isMacbook { touchpad = { tap_to_click = true; tap_button_map = "lrm"; natural_scroll = true; }; }; cursor = { no_warps = true; }; 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; }; # 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 powerMenu = pkgs.writeShellScript "power-menu" '' # 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 choice=$(printf '%s\n' \ $'\uf023 Lock' \ $'\uf08b Logout' \ $'\uf01e Reboot' \ $'\uf011 Shutdown' \ | ${anyrun-pkgs.anyrun}/bin/anyrun \ --plugins "${anyrun-pkgs.stdin}/lib/libstdin.so" \ --show-results-immediately true \ --hide-plugin-info true \ --close-on-click true) # 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 case "$choice" in *Lock) ${pkgs.hyprlock}/bin/hyprlock ;; *Logout) hyprctl dispatch exit ;; *Reboot) systemctl reboot ;; *Shutdown) systemctl poweroff ;; esac ''; kbdBrightUp = pkgs.writeShellScript "kbd-bright-up" '' ${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" ''; kbdBrightDown = pkgs.writeShellScript "kbd-bright-down" '' ${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" ''; 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() -- 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") -- 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") 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")) hl.bind(mod .. " + R", hl.dsp.exec_cmd("anyrun close || anyrun")) 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 — dismiss launcher if open, then show menu hl.bind(mod .. " + L", hl.dsp.exec_cmd("anyrun close 2>/dev/null; ${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 -- 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 }) ''; }; programs.anyrun = { enable = true; config = { plugins = [ anyrun-pkgs.applications ]; x.fraction = 0.5; y.fraction = 0.25; width.absolute = 350; height.absolute = 0; hideIcons = false; ignoreExclusiveZones = false; layer = "overlay"; hidePluginInfo = true; closeOnClick = true; maxEntries = 8; }; 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, max_entries: 8, terminal: Some("ghostty"), ) ''; }; programs.hyprlock = { enable = true; settings = { general = { grace = 5; hide_cursor = true; }; background = { blur_passes = 3; blur_size = 8; brightness = 0.6; }; input-field = { size = "280, 44"; outline_thickness = 2; dots_size = 0.25; dots_spacing = 0.2; dots_center = true; fade_on_empty = true; fade_timeout = 3000; placeholder_text = ""; fail_text = ""; position = "0, -40"; halign = "center"; valign = "center"; rounding = 8; }; label = [ { text = "$TIME"; color = "rgb(${c.base05})"; font_size = 72; font_family = "Inter"; position = "0, 120"; halign = "center"; valign = "center"; } { text = "cmd[update:60000] date +'%A, %d %B'"; color = "rgb(${c.base04})"; font_size = 16; font_family = "Inter"; position = "0, 60"; halign = "center"; valign = "center"; } ]; }; }; services.hypridle = lib.mkIf isMacbook { enable = true; settings = { general = { lock_cmd = "pidof hyprlock || hyprlock"; 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"; } ]; }; }; # 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"; systemd.user.services.quickshell = { Unit = { Description = "Quickshell desktop shell"; PartOf = [ "graphical-session.target" ]; After = [ "graphical-session.target" ]; }; Service = { ExecStart = "${pkgs.quickshell}/bin/qs"; Restart = "always"; RestartSec = 2; }; Install.WantedBy = [ "hyprland-session.target" ]; }; xdg.configFile = let qsRestart = '' ${pkgs.systemd}/bin/systemctl --user restart quickshell.service 2>/dev/null || true ''; wifiConnectScript = pkgs.writeShellScript "wifi-connect" '' ssid="$1" ${pkgs.networkmanager}/bin/nmcli device wifi connect "$ssid" 2>/dev/null && exit 0 pw=$(${pkgs.zenity}/bin/zenity --password --title="WiFi Password" 2>/dev/null) [ -n "$pw" ] && ${pkgs.networkmanager}/bin/nmcli device wifi connect "$ssid" password "$pw" ''; nmcli = "${pkgs.networkmanager}/bin/nmcli"; powerprofilesctl = "${pkgs.power-profiles-daemon}/bin/powerprofilesctl"; in { "quickshell/qmldir" = { onChange = qsRestart; text = '' singleton Theme 1.0 Theme.qml singleton Commands 1.0 Commands.qml Bar 1.0 Bar.qml NotificationToast 1.0 NotificationToast.qml ''; }; "quickshell/Theme.qml" = { onChange = qsRestart; text = '' pragma Singleton import QtQuick QtObject { readonly property color base00: "#${c.base00}" readonly property color base01: "#${c.base01}" readonly property color base02: "#${c.base02}" readonly property color base03: "#${c.base03}" readonly property color base04: "#${c.base04}" readonly property color base05: "#${c.base05}" readonly property color base08: "#${c.base08}" readonly property color base0A: "#${c.base0A}" readonly property color base0B: "#${c.base0B}" readonly property color base0C: "#${c.base0C}" readonly property color base0D: "#${c.base0D}" readonly property color barBg: "#D1${c.base00}" readonly property color toastBg: "#E6${c.base00}" } ''; }; "quickshell/Commands.qml" = { onChange = qsRestart; text = '' pragma Singleton import QtQuick QtObject { readonly property string nmcli: "${nmcli}" readonly property string wifiConnect: "${wifiConnectScript}" readonly property string powerprofilesctl: "${powerprofilesctl}" readonly property string notifSound: "${pkgs.libcanberra-gtk3}/bin/canberra-gtk-play" } ''; }; "quickshell/shell.qml" = { onChange = qsRestart; text = '' //@ pragma UseQApplication import Quickshell import Quickshell.Services.Notifications import QtQuick ShellRoot { id: root property var latestNotification: null signal notificationReceived() NotificationServer { id: _notifServer bodySupported: true actionsSupported: true imageSupported: true persistenceSupported: true keepOnReload: true onNotification: (notification) => { notification.tracked = true; root.latestNotification = notification; root.notificationReceived(); } } Variants { model: Quickshell.screens Bar { notifServer: _notifServer } } NotificationToast { shellRoot: root } } ''; }; "quickshell/Bar.qml" = { onChange = qsRestart; text = '' import Quickshell import Quickshell.Hyprland import Quickshell.Services.SystemTray import Quickshell.Services.Notifications import Quickshell.Services.Pipewire import Quickshell.Widgets import Quickshell.Io import QtQuick import QtQuick.Layouts import Qt5Compat.GraphicalEffects PanelWindow { id: bar required property var modelData required property NotificationServer notifServer screen: modelData anchors { top: true left: true right: true } implicitHeight: 30 color: Theme.barBg property var activeDropdown: null function closeAllDropdowns() { if (activeDropdown && activeDropdown.visible) { activeDropdown.animateClose(); } activeDropdown = null; } function toggleDropdown(dd, setupFn) { if (dd.visible && !dd.closing) { dd.animateClose(); activeDropdown = null; } else { if (activeDropdown && activeDropdown !== dd && activeDropdown.visible) { activeDropdown.animateClose(); } if (setupFn) setupFn(); if (dd.closing) { dd.closing = false; dd.open = true; } else { dd.visible = true; } activeDropdown = dd; } } // Left — workspaces Row { anchors.left: parent.left anchors.leftMargin: 6 anchors.verticalCenter: parent.verticalCenter spacing: 0 Repeater { model: Hyprland.workspaces Item { required property var modelData width: 28 height: 30 Text { anchors.centerIn: parent text: modelData.name color: modelData.focused ? Theme.base05 : Theme.base03 font.family: "FiraMono Nerd Font" font.pixelSize: 13 } Rectangle { anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter width: parent.width - 8 height: 2 color: Theme.base05 visible: modelData.focused } MouseArea { anchors.fill: parent onClicked: modelData.activate() } } } } // Center — clock Text { id: clockText anchors.centerIn: parent property date now: new Date() text: now.toLocaleTimeString(Qt.locale(), "HH:mm") color: Theme.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 hoverEnabled: true onClicked: bar.toggleDropdown(calPopup) onEntered: { if (bar.activeDropdown) { if (bar.activeDropdown !== calPopup) bar.toggleDropdown(calPopup); else bar.activeDropdown.resetAutoClose(); } } } } // Right — network, battery, tray Row { anchors.right: parent.right anchors.rightMargin: 8 anchors.verticalCenter: parent.verticalCenter spacing: 10 // Volume Item { id: volWidget width: volText.width height: 30 property PwNode sink: Pipewire.defaultAudioSink PwObjectTracker { objects: [volWidget.sink] } property int vol: sink && sink.audio ? Math.round(sink.audio.volume * 100) : 0 property bool muted: sink && sink.audio ? sink.audio.muted : false property string volIcon: muted ? "\u{f0581}" : vol > 66 ? "\u{f057e}" : vol > 33 ? "\u{f0580}" : vol > 0 ? "\u{f057f}" : "\u{f0581}" function openVolDropdown() { bar.toggleDropdown(volDropdown, function() { let pos = volWidget.mapToItem(bar.contentItem, volWidget.width / 2, 0); volDropdown.dropdownX = pos.x; }); } Text { id: volText anchors.verticalCenter: parent.verticalCenter text: volWidget.volIcon + " " + volWidget.vol + "%" color: volWidget.muted ? Theme.base03 : Theme.base05 font.family: "FiraMono Nerd Font" font.pixelSize: 13 } MouseArea { anchors.fill: parent hoverEnabled: true acceptedButtons: Qt.LeftButton | Qt.MiddleButton onClicked: (event) => { if (event.button === Qt.MiddleButton) { if (volWidget.sink && volWidget.sink.audio) volWidget.sink.audio.muted = !volWidget.sink.audio.muted; } else { volWidget.openVolDropdown(); } } onEntered: { if (bar.activeDropdown) { if (bar.activeDropdown !== volDropdown) volWidget.openVolDropdown(); else bar.activeDropdown.resetAutoClose(); } } } } // Network status Item { id: netWidget width: 16 height: 30 property string netState: "disconnected" property string netConn: "" property string netType: "" property string netIcon: "\u{f0b0}" property var wifiNetworks: [] property string netDevice: "" property string _pendingState: "disconnected" property string _pendingConn: "" property string _pendingType: "" property string _pendingDevice: "" property var _pendingNets: [] Timer { interval: 5000 running: true repeat: true triggeredOnStart: true onTriggered: netWidget.refreshNet() } function refreshNet() { netWidget._pendingState = "disconnected"; netWidget._pendingConn = ""; netWidget._pendingType = ""; netProc.running = true; } Process { id: netProc command: [Commands.nmcli, "-t", "-f", "DEVICE,TYPE,STATE,CONNECTION", "device"] stdout: SplitParser { onRead: data => { let fields = data.split(":"); if (fields.length < 4) return; let type = fields[1]; let state = fields[2]; let conn = fields[3]; if (type !== "ethernet" && type !== "wifi") return; if (type === "wifi") { netWidget._pendingDevice = fields[0]; } if (state === "connected") { netWidget._pendingState = "connected"; netWidget._pendingConn = conn; netWidget._pendingType = type; } } } onRunningChanged: { if (!running) { netWidget.netState = netWidget._pendingState; netWidget.netConn = netWidget._pendingConn; netWidget.netType = netWidget._pendingType.length > 0 ? netWidget._pendingType : netWidget.netType; netWidget.netDevice = netWidget._pendingDevice.length > 0 ? netWidget._pendingDevice : netWidget.netDevice; if (netWidget.netState === "connected") { netWidget.netIcon = netWidget.netType === "wifi" ? "\u{f05a9}" : "\u{f0200}"; } else { netWidget.netIcon = netWidget.netType === "wifi" ? "\u{f05aa}" : "\u{f0201}"; } } } } Text { anchors.centerIn: parent text: netWidget.netIcon color: Theme.base05 font.family: "FiraMono Nerd Font" font.pixelSize: 14 } Timer { id: netRefreshDelay interval: 2000 onTriggered: netWidget.refreshNet() } Process { id: wifiScanProc command: [Commands.nmcli, "-t", "-f", "SSID,SIGNAL,SECURITY,IN-USE", "device", "wifi", "list", "--rescan", "auto"] stdout: SplitParser { onRead: data => { let fields = data.split(":"); if (fields.length < 4 || fields[0] === "") return; for (let i = 0; i < netWidget._pendingNets.length; i++) { if (netWidget._pendingNets[i].ssid === fields[0]) return; } netWidget._pendingNets.push({ ssid: fields[0], signal: parseInt(fields[1]) || 0, security: fields[2], active: fields[3] === "*" }); } } onRunningChanged: { if (!running) { netWidget.wifiNetworks = netWidget._pendingNets; netWidget._pendingNets = []; } } } Process { id: wifiConnectProc property string targetSsid: "" command: [Commands.wifiConnect, targetSsid] } Process { id: netDisconnectProc property string targetDevice: "" command: [Commands.nmcli, "device", "disconnect", targetDevice] } function openNetDropdown() { bar.toggleDropdown(netDropdown, function() { wifiScanProc.running = true; let pos = netWidget.mapToItem(bar.contentItem, netWidget.width / 2, 0); netDropdown.dropdownX = pos.x; }); } MouseArea { anchors.fill: parent hoverEnabled: true onClicked: netWidget.openNetDropdown() onEntered: { if (bar.activeDropdown) { if (bar.activeDropdown !== netDropdown) netWidget.openNetDropdown(); else bar.activeDropdown.resetAutoClose(); } } } } ${lib.optionalString isMacbook '' // Battery Item { id: batteryWidget width: batteryText.width + 4 + batteryIconText.width height: 30 property int batteryLevel: 0 property bool charging: false property string batteryIcon: "\u{f008e}" property real powerDraw: 0.0 property real energyNow: 0.0 property real energyFull: 0.0 property string timeRemaining: "" property string powerProfile: "balanced" 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: 5000 running: true repeat: true triggeredOnStart: true onTriggered: { batteryProc.running = true; profileProc.running = true; } } Process { id: batteryProc command: ["sh", "-c", "cat /sys/class/power_supply/BAT0/capacity; cat /sys/class/power_supply/BAT0/status; cat /sys/class/power_supply/BAT0/power_now 2>/dev/null || echo 0; cat /sys/class/power_supply/BAT0/energy_now 2>/dev/null || echo 0; cat /sys/class/power_supply/BAT0/energy_full 2>/dev/null || echo 0"] stdout: SplitParser { property int lineNum: 0 onRead: data => { let trimmed = data.trim(); let num = parseInt(trimmed); lineNum++; if (lineNum === 1) { if (!isNaN(num)) batteryWidget.batteryLevel = num; } else if (lineNum === 2) { batteryWidget.charging = (trimmed === "Charging"); } else if (lineNum === 3) { if (!isNaN(num)) batteryWidget.powerDraw = num / 1000000.0; } else if (lineNum === 4) { if (!isNaN(num)) batteryWidget.energyNow = num / 1000000.0; } else if (lineNum === 5) { if (!isNaN(num)) batteryWidget.energyFull = num / 1000000.0; lineNum = 0; if (batteryWidget.powerDraw > 0.5) { let hours; if (batteryWidget.charging) { hours = (batteryWidget.energyFull - batteryWidget.energyNow) / batteryWidget.powerDraw; } else { hours = batteryWidget.energyNow / batteryWidget.powerDraw; } let h = Math.floor(hours); let m = Math.round((hours - h) * 60); batteryWidget.timeRemaining = h + "h " + m + "m"; } else { batteryWidget.timeRemaining = ""; } } batteryWidget.updateIcon(); } } } Process { id: profileProc command: [Commands.powerprofilesctl, "get"] stdout: SplitParser { onRead: data => { batteryWidget.powerProfile = data.trim(); } } } Process { id: setProfileProc property string target: "balanced" command: [Commands.powerprofilesctl, "set", target] } Row { anchors.verticalCenter: parent.verticalCenter spacing: 4 Text { id: batteryText text: batteryWidget.batteryLevel + "%" color: batteryWidget.batteryLevel <= 15 ? Theme.base08 : batteryWidget.batteryLevel <= 30 ? Theme.base0A : Theme.base05 font.family: "FiraMono Nerd Font" font.pixelSize: 13 } Text { id: batteryIconText text: batteryWidget.batteryIcon color: batteryWidget.batteryLevel <= 15 ? Theme.base08 : batteryWidget.batteryLevel <= 30 ? Theme.base0A : Theme.base05 font.family: "FiraMono Nerd Font" font.pixelSize: 14 } } function openBatteryDropdown() { bar.toggleDropdown(batteryDropdown, function() { batteryProc.running = true; profileProc.running = true; let pos = batteryWidget.mapToItem(bar.contentItem, batteryWidget.width / 2, 0); batteryDropdown.dropdownX = pos.x; }); } MouseArea { anchors.fill: parent hoverEnabled: true onClicked: batteryWidget.openBatteryDropdown() onEntered: { if (bar.activeDropdown) { if (bar.activeDropdown !== batteryDropdown) batteryWidget.openBatteryDropdown(); else bar.activeDropdown.resetAutoClose(); } } } } ''} // Tray icons Row { id: trayArea spacing: 8 height: 30 anchors.verticalCenter: parent.verticalCenter HoverHandler { onHoveredChanged: { if (hovered && bar.activeDropdown) bar.activeDropdown.resetAutoClose(); } } Repeater { model: SystemTray.items Item { required property var modelData width: 24 height: 30 Image { id: trayIcon anchors.centerIn: parent width: 16 height: 16 source: modelData.icon sourceSize.width: 16 sourceSize.height: 16 smooth: true mipmap: true visible: false } ColorOverlay { anchors.fill: trayIcon source: trayIcon color: Theme.base05 } MouseArea { anchors.fill: parent hoverEnabled: true acceptedButtons: Qt.NoButton onEntered: { if (bar.activeDropdown) { bar.activeDropdown.resetAutoClose(); if (modelData.hasMenu && !(bar.activeDropdown === contextMenu && contextMenu.trayItem === modelData)) { if (bar.activeDropdown === contextMenu) { // Same dropdown, just switch content let pos = parent.mapToItem(bar.contentItem, parent.width / 2, 0); contextMenu.dropdownX = pos.x; contextMenu.trayItem = modelData; menuOpener.menu = modelData.menu; contextMenu.resetAutoClose(); } else { bar.toggleDropdown(contextMenu, function() { let pos = parent.mapToItem(bar.contentItem, parent.width / 2, 0); contextMenu.dropdownX = pos.x; contextMenu.trayItem = modelData; menuOpener.menu = modelData.menu; }); } } } } } MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: (event) => { if (modelData.hasMenu) { bar.toggleDropdown(contextMenu, function() { let pos = parent.mapToItem(bar.contentItem, parent.width / 2, 0); contextMenu.dropdownX = pos.x; contextMenu.trayItem = modelData; menuOpener.menu = modelData.menu; }); } else { modelData.activate(); } } } } } } } // Reusable dropdown component component BarDropdown: PopupWindow { id: dropdown property bool open: false property bool closing: false property real dropdownX: 0 property real fullWidth: 200 property real fullHeight: 200 property int autoCloseMs: 1500 property bool alignRight: false default property alias content: dropdownContent.data function animateClose() { if (!visible || closing) return; closing = true; open = false; _autoClose.stop(); _closeDelay.start(); } function resetAutoClose() { if (visible && !closing) _autoClose.restart(); } anchor.window: bar anchor.rect.x: alignRight ? bar.width - fullWidth - 8 : dropdownX - (fullWidth + 16) / 2 anchor.rect.y: bar.height anchor.edges: Edges.Top | Edges.Left anchor.gravity: Edges.Bottom | Edges.Right anchor.adjustment: alignRight ? PopupAdjustment.None : PopupAdjustment.Slide visible: false color: "transparent" implicitWidth: fullWidth + (alignRight ? 8 : 16) implicitHeight: fullHeight + 4 + (alignRight ? 8 : 0) onVisibleChanged: { if (visible) { closing = false; open = true; _autoClose.restart(); } else { open = false; closing = false; _autoClose.stop(); } } Timer { id: _autoClose interval: dropdown.autoCloseMs onTriggered: bar.closeAllDropdowns() } Timer { id: _closeDelay interval: 230 onTriggered: { dropdown.visible = false; dropdown.closing = false; } } HoverHandler { onHoveredChanged: { if (hovered) _autoClose.stop(); else _autoClose.restart(); } } // Left ear Item { anchors.right: _dropdownRect.left anchors.top: parent.top width: 8 height: Math.min(8, _dropdownRect.height) clip: true visible: _dropdownRect.height > 0 Canvas { anchors.top: parent.top width: 8; height: 8 onPaint: { var ctx = getContext("2d"); ctx.clearRect(0, 0, 8, 8); ctx.fillStyle = Theme.barBg; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(8, 0); ctx.lineTo(8, 8); ctx.arc(0, 8, 8, 0, -Math.PI / 2, true); ctx.closePath(); ctx.fill(); } } } // Right ear (for centered dropdowns) Item { anchors.left: _dropdownRect.right anchors.top: parent.top width: 8 height: Math.min(8, _dropdownRect.height) clip: true visible: _dropdownRect.height > 0 && !dropdown.alignRight Canvas { anchors.top: parent.top width: 8; height: 8 onPaint: { var ctx = getContext("2d"); ctx.clearRect(0, 0, 8, 8); ctx.fillStyle = Theme.barBg; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(8, 0); ctx.arc(8, 8, 8, -Math.PI / 2, Math.PI, true); ctx.closePath(); ctx.fill(); } } } Rectangle { id: _dropdownRect anchors.right: dropdown.alignRight ? parent.right : undefined anchors.horizontalCenter: dropdown.alignRight ? undefined : parent.horizontalCenter anchors.top: parent.top width: dropdown.fullWidth height: dropdown.open ? dropdown.fullHeight : 0 color: Theme.barBg radius: 8 topLeftRadius: 0 topRightRadius: 0 bottomRightRadius: dropdown.alignRight ? 0 : 8 clip: true Behavior on height { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } } Item { id: dropdownContent anchors.fill: parent } } // Bottom-right concave ear — connects dropdown bottom to right screen edge Item { visible: dropdown.alignRight && _dropdownRect.height > 0 anchors.right: _dropdownRect.right anchors.top: _dropdownRect.bottom width: 8 height: Math.min(8, _dropdownRect.height) clip: true Canvas { width: 8; height: 8 onPaint: { var ctx = getContext("2d"); ctx.clearRect(0, 0, 8, 8); ctx.fillStyle = Theme.barBg; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(8, 0); ctx.lineTo(8, 8); ctx.arc(0, 8, 8, 0, -Math.PI / 2, true); ctx.fill(); } } } } // Context menu BarDropdown { id: contextMenu alignRight: true property var trayItem: null fullWidth: menuItems.width + 16 fullHeight: menuItems.height + 12 onVisibleChanged: { if (!visible) menuOpener.menu = null; } QsMenuOpener { id: menuOpener } Rectangle { anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.topMargin: 6 width: menuItems.width + 16 height: menuItems.height + 12 radius: 10 color: Theme.base01 Column { id: menuItems anchors.centerIn: parent width: 200 Repeater { model: menuOpener.children Rectangle { required property var modelData width: 200 height: modelData.isSeparator ? 9 : 28 color: !modelData.isSeparator && itemMouse.containsMouse && modelData.enabled ? Theme.base02 : "transparent" radius: modelData.isSeparator ? 0 : 4 Rectangle { visible: modelData.isSeparator anchors.centerIn: parent width: parent.width - 20 height: 1 color: Theme.base03 } 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 ? Theme.base05 : Theme.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: Theme.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(); bar.closeAllDropdowns(); } } } } } } } // Volume dropdown BarDropdown { id: volDropdown alignRight: true fullWidth: volDropdownCol.width + 24 fullHeight: volDropdownCol.height + 16 autoCloseMs: 3000 Rectangle { anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.topMargin: 8 width: volDropdownCol.width + 20 height: volDropdownCol.height + 16 radius: 10 color: Theme.base01 Column { id: volDropdownCol anchors.centerIn: parent width: 260 spacing: 8 // Master volume Text { text: "\u{f057e} Master" color: Theme.base05 font.family: "FiraMono Nerd Font" font.pixelSize: 13 font.weight: Font.Medium } Row { width: parent.width spacing: 8 Rectangle { id: masterSliderBg width: parent.width - masterVolLabel.width - 8 height: 20 radius: 4 color: Theme.base02 anchors.verticalCenter: parent.verticalCenter Rectangle { width: volWidget.sink && volWidget.sink.audio ? Math.min(1, volWidget.sink.audio.volume) * parent.width : 0 height: parent.height radius: 4 color: volWidget.muted ? Theme.base03 : Theme.base0D Behavior on width { NumberAnimation { duration: 80 } } } MouseArea { anchors.fill: parent onPressed: (mouse) => setVolume(mouse) onPositionChanged: (mouse) => { if (pressed) setVolume(mouse); } function setVolume(mouse) { if (!volWidget.sink || !volWidget.sink.audio) return; let v = Math.max(0, Math.min(1, mouse.x / width)); volWidget.sink.audio.volume = v; } } } Text { id: masterVolLabel width: 36 text: volWidget.vol + "%" color: Theme.base05 font.family: "FiraMono Nerd Font" font.pixelSize: 11 horizontalAlignment: Text.AlignRight anchors.verticalCenter: parent.verticalCenter } } // Mute button Rectangle { width: parent.width height: 28 color: masterMuteMa.containsMouse ? Theme.base02 : "transparent" radius: 4 Text { anchors.centerIn: parent text: volWidget.muted ? "\u{f0581} Unmute" : "\u{f057e} Mute" color: Theme.base05 font.family: "FiraMono Nerd Font" font.pixelSize: 12 } MouseArea { id: masterMuteMa anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { if (volWidget.sink && volWidget.sink.audio) volWidget.sink.audio.muted = !volWidget.sink.audio.muted; } } } // Separator Rectangle { width: parent.width - 20 anchors.horizontalCenter: parent.horizontalCenter height: 1 color: Theme.base02 visible: appStreamsCol.childrenRect.height > 0 } // App streams header Text { visible: appStreamsCol.childrenRect.height > 0 text: "\u{f0641} Applications" color: Theme.base05 font.family: "FiraMono Nerd Font" font.pixelSize: 13 font.weight: Font.Medium } // Per-app streams Column { id: appStreamsCol width: parent.width spacing: 6 Repeater { id: appStreamsRepeater model: Pipewire.nodes Column { required property var modelData width: parent.width spacing: 2 visible: modelData.isStream && modelData.audio !== null PwObjectTracker { objects: [modelData] } Text { text: modelData.properties["application.name"] || modelData.name || "Unknown" color: Theme.base04 font.family: "FiraMono Nerd Font" font.pixelSize: 11 elide: Text.ElideRight width: parent.width } Row { width: parent.width spacing: 8 Rectangle { width: parent.width - appVolLabel.width - 8 height: 16 radius: 3 color: Theme.base02 anchors.verticalCenter: parent.verticalCenter Rectangle { width: modelData.audio ? Math.min(1, modelData.audio.volume) * parent.width : 0 height: parent.height radius: 3 color: modelData.audio && modelData.audio.muted ? Theme.base03 : Theme.base0C Behavior on width { NumberAnimation { duration: 80 } } } MouseArea { anchors.fill: parent onPressed: (mouse) => setVol(mouse) onPositionChanged: (mouse) => { if (pressed) setVol(mouse); } function setVol(mouse) { if (!modelData.audio) return; let v = Math.max(0, Math.min(1, mouse.x / width)); modelData.audio.volume = v; } } } Text { id: appVolLabel width: 36 text: modelData.audio ? Math.round(modelData.audio.volume * 100) + "%" : "0%" color: Theme.base04 font.family: "FiraMono Nerd Font" font.pixelSize: 10 horizontalAlignment: Text.AlignRight anchors.verticalCenter: parent.verticalCenter } } } } } } } } // Network dropdown BarDropdown { id: netDropdown alignRight: true fullWidth: netDropdownCol.width + 24 fullHeight: netDropdownCol.height + 16 Rectangle { anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.topMargin: 8 width: netDropdownCol.width + 20 height: netDropdownCol.height + 16 radius: 10 color: Theme.base01 Column { id: netDropdownCol anchors.centerIn: parent width: 220 spacing: 4 Text { width: parent.width text: netWidget.netState === "connected" ? "\u{f05a9} " + netWidget.netConn : "\u{f05aa} Not connected" color: Theme.base05 font.family: "FiraMono Nerd Font" font.pixelSize: 13 font.weight: Font.Medium elide: Text.ElideRight } Rectangle { visible: netWidget.netState === "connected" width: parent.width height: 28 color: disconnectMouse.containsMouse ? Theme.base02 : "transparent" radius: 4 Text { anchors.centerIn: parent text: "Disconnect" color: Theme.base08 font.family: "FiraMono Nerd Font" font.pixelSize: 12 } MouseArea { id: disconnectMouse anchors.fill: parent hoverEnabled: true onClicked: { netDisconnectProc.targetDevice = netWidget.netDevice; netDisconnectProc.running = true; netWidget.netState = "disconnected"; netWidget.netConn = ""; netWidget.netIcon = "\u{f05aa}"; bar.closeAllDropdowns(); netRefreshDelay.start(); } } } Rectangle { width: parent.width - 20 anchors.horizontalCenter: parent.horizontalCenter height: 1 color: Theme.base03 } Text { text: "Available networks" color: Theme.base03 font.family: "FiraMono Nerd Font" font.pixelSize: 11 topPadding: 2 } Repeater { model: netWidget.wifiNetworks Rectangle { required property var modelData width: 220 height: 32 color: netItemMouse.containsMouse ? Theme.base02 : "transparent" radius: 4 Row { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left anchors.leftMargin: 8 anchors.right: parent.right anchors.rightMargin: 8 spacing: 8 Text { text: { let s = modelData.signal; if (s >= 75) return "\u{f05a9}"; if (s >= 50) return "\u{f05a9}"; if (s >= 25) return "\u{f05a9}"; return "\u{f05aa}"; } color: modelData.active ? Theme.base0B : Theme.base04 font.family: "FiraMono Nerd Font" font.pixelSize: 13 anchors.verticalCenter: parent.verticalCenter } Text { text: modelData.ssid color: modelData.active ? Theme.base0B : Theme.base05 font.family: "FiraMono Nerd Font" font.pixelSize: 12 elide: Text.ElideRight width: 140 anchors.verticalCenter: parent.verticalCenter } Text { visible: modelData.security !== "" && modelData.security !== "--" text: "\u{f0341}" color: Theme.base03 font.family: "FiraMono Nerd Font" font.pixelSize: 10 anchors.verticalCenter: parent.verticalCenter } } MouseArea { id: netItemMouse anchors.fill: parent hoverEnabled: true onClicked: { if (!modelData.active) { wifiConnectProc.targetSsid = modelData.ssid; wifiConnectProc.running = true; netRefreshDelay.start(); } bar.closeAllDropdowns(); } } } } } } } ${lib.optionalString isMacbook '' // Battery dropdown BarDropdown { id: batteryDropdown alignRight: true fullWidth: batteryDropdownCol.width + 24 fullHeight: batteryDropdownCol.height + 16 Rectangle { anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.topMargin: 8 width: batteryDropdownCol.width + 20 height: batteryDropdownCol.height + 16 radius: 10 color: Theme.base01 Column { id: batteryDropdownCol anchors.centerIn: parent width: 200 spacing: 8 Row { width: parent.width spacing: 8 Text { text: batteryWidget.batteryIcon color: Theme.base05 font.family: "FiraMono Nerd Font" font.pixelSize: 18 anchors.verticalCenter: parent.verticalCenter } Column { anchors.verticalCenter: parent.verticalCenter Text { text: batteryWidget.batteryLevel + "%" + (batteryWidget.charging ? " — Charging" : "") color: Theme.base05 font.family: "FiraMono Nerd Font" font.pixelSize: 13 font.weight: Font.Medium } Text { text: batteryWidget.powerDraw.toFixed(1) + " W" + (batteryWidget.timeRemaining !== "" ? " \u2022 " + batteryWidget.timeRemaining + (batteryWidget.charging ? " to full" : " left") : "") color: Theme.base04 font.family: "FiraMono Nerd Font" font.pixelSize: 11 } } } Rectangle { width: parent.width - 10 anchors.horizontalCenter: parent.horizontalCenter height: 1 color: Theme.base03 } Text { text: "Power Profile" color: Theme.base03 font.family: "FiraMono Nerd Font" font.pixelSize: 11 } Row { width: parent.width spacing: 4 Repeater { model: [ { name: "power-saver", label: "\u{f0425}", tip: "Saver" }, { name: "balanced", label: "\u{f0376}", tip: "Balanced" }, { name: "performance", label: "\u{f0e0e}", tip: "Performance" } ] Rectangle { required property var modelData width: (parent.width - 8) / 3 height: 36 radius: 6 color: batteryWidget.powerProfile === modelData.name ? Theme.base02 : profMouse.containsMouse ? Theme.base01 : "transparent" border.width: batteryWidget.powerProfile === modelData.name ? 1 : 0 border.color: Theme.base03 Column { anchors.centerIn: parent spacing: 1 Text { anchors.horizontalCenter: parent.horizontalCenter text: modelData.label color: batteryWidget.powerProfile === modelData.name ? Theme.base0D : Theme.base05 font.family: "FiraMono Nerd Font" font.pixelSize: 14 } Text { anchors.horizontalCenter: parent.horizontalCenter text: modelData.tip color: Theme.base04 font.family: "FiraMono Nerd Font" font.pixelSize: 9 } } MouseArea { id: profMouse anchors.fill: parent hoverEnabled: true onClicked: { setProfileProc.target = modelData.name; setProfileProc.running = true; batteryWidget.powerProfile = modelData.name; } } } } } } } } ''} // Calendar popup BarDropdown { id: calPopup dropdownX: bar.width / 2 fullWidth: calCol.width + 32 fullHeight: calCol.height + 24 Column { id: calCol anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.topMargin: 12 spacing: 8 opacity: calPopup.open ? 1.0 : 0.0 Behavior on opacity { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } } Rectangle { width: 7 * 32 + 24 height: calWrapCol.height + 16 radius: 10 color: Theme.base01 anchors.horizontalCenter: parent.horizontalCenter Column { id: calWrapCol anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.topMargin: 8 spacing: 8 Rectangle { width: 7 * 32 + 8 height: calTitle.height + 12 radius: 8 color: Theme.base02 anchors.horizontalCenter: parent.horizontalCenter Text { id: calTitle anchors.centerIn: parent text: clockText.now.toLocaleDateString(Qt.locale(), "dddd, d MMMM yyyy") color: Theme.base05 font.family: "FiraMono Nerd Font" font.pixelSize: 16 font.weight: Font.Medium } } Rectangle { width: 7 * 32 + 8 height: calInner.height + 12 radius: 8 color: Theme.base02 anchors.horizontalCenter: parent.horizontalCenter Column { id: calInner anchors.centerIn: parent spacing: 4 Rectangle { width: 7 * 32 height: weekdayRow.height + 8 radius: 6 color: Theme.base03 Row { id: weekdayRow anchors.centerIn: parent spacing: 0 Repeater { model: ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"] Text { required property var modelData width: 32 horizontalAlignment: Text.AlignHCenter text: modelData color: Theme.base00 font.family: "FiraMono Nerd Font" font.pixelSize: 13 } } } } Grid { columns: 7 spacing: 0 Repeater { id: calRepeater model: 42 Rectangle { required property int index width: 32 height: 26 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) ? Theme.base03 : "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()) ? Theme.base05 : Theme.base04; } font.family: "FiraMono Nerd Font" font.pixelSize: 13 } } } } } } Rectangle { width: 7 * 32 + 8 height: 1 color: Theme.base02 anchors.horizontalCenter: parent.horizontalCenter } Row { width: 7 * 32 + 8 anchors.horizontalCenter: parent.horizontalCenter Text { text: "Notifications" color: Theme.base05 font.family: "FiraMono Nerd Font" font.pixelSize: 13 font.weight: Font.Medium } Item { Layout.fillWidth: true; width: 10 } Text { anchors.right: parent.right text: bar.notifServer.trackedNotifications.values.length > 0 ? "Clear all" : "" color: Theme.base04 font.family: "FiraMono Nerd Font" font.pixelSize: 11 MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { let notifs = bar.notifServer.trackedNotifications.values; for (let i = notifs.length - 1; i >= 0; i--) { notifs[i].dismiss(); } } } } } Column { spacing: 4 width: 7 * 32 + 8 anchors.horizontalCenter: parent.horizontalCenter Text { visible: bar.notifServer.trackedNotifications.values.length === 0 text: "No notifications" color: Theme.base03 font.family: "FiraMono Nerd Font" font.pixelSize: 11 anchors.horizontalCenter: parent.horizontalCenter } Repeater { model: bar.notifServer.trackedNotifications Rectangle { id: notifItem required property var modelData width: 7 * 32 + 8 height: notifCol.height + 12 radius: 6 color: Theme.base02 Column { id: notifCol anchors.left: parent.left anchors.right: dismissBtn.left anchors.top: parent.top anchors.margins: 6 spacing: 2 Text { width: parent.width text: notifItem.modelData.summary || notifItem.modelData.appName color: Theme.base05 font.family: "FiraMono Nerd Font" font.pixelSize: 11 font.weight: Font.Medium elide: Text.ElideRight } Text { width: parent.width text: notifItem.modelData.body || "" color: Theme.base04 font.family: "FiraMono Nerd Font" font.pixelSize: 10 elide: Text.ElideRight maximumLineCount: 2 wrapMode: Text.Wrap visible: text !== "" } Row { spacing: 4 visible: notifItem.modelData.actions.length > 0 Repeater { model: notifItem.modelData.actions Rectangle { required property var modelData width: actionText.width + 12 height: actionText.height + 4 radius: 4 color: actionMa.containsMouse ? Theme.base03 : Theme.base02 border.width: 1 border.color: Theme.base03 Text { id: actionText anchors.centerIn: parent text: modelData.text color: Theme.base05 font.family: "FiraMono Nerd Font" font.pixelSize: 10 } MouseArea { id: actionMa anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: modelData.invoke() } } } } } Text { id: dismissBtn anchors.right: parent.right anchors.top: parent.top anchors.margins: 6 text: "\u{f0156}" color: dismissMa.containsMouse ? Theme.base05 : Theme.base03 font.family: "FiraMono Nerd Font" font.pixelSize: 12 MouseArea { id: dismissMa anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: notifItem.modelData.dismiss() } } } } } } } } } } ''; }; "quickshell/NotificationToast.qml" = { onChange = qsRestart; text = '' import Quickshell import Quickshell.Wayland import Quickshell.Io import QtQuick PanelWindow { id: notifToast required property var shellRoot screen: Quickshell.screens[0] property var currentNotif: null property bool open: false readonly property var mutedApps: ["discord", "Discord", "Spotify", "spotify", "vlc", "mpv"] WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.namespace: "quickshell-toast" exclusionMode: ExclusionMode.Ignore Process { id: notifSoundProc command: [Commands.notifSound, "-i", "message"] } Connections { target: shellRoot function onNotificationReceived() { notifToast.show(shellRoot.latestNotification); } } function show(notification) { currentNotif = notification; visible = true; open = true; _toastTimer.restart(); if (!mutedApps.includes(notification.appName)) { notifSoundProc.running = true; } } function dismiss() { open = false; _toastCloseDelay.start(); } anchors.top: true margins.top: 30 visible: false implicitWidth: 320 + 16 implicitHeight: _toastRect.height + 4 color: "transparent" Timer { id: _toastTimer interval: 5000 onTriggered: notifToast.dismiss() } Timer { id: _toastCloseDelay interval: 230 onTriggered: { notifToast.visible = false; notifToast.open = false; } } HoverHandler { onHoveredChanged: { if (hovered) _toastTimer.stop(); else _toastTimer.restart(); } } // Left inverse corner ear Item { anchors.right: _toastRect.left anchors.top: parent.top width: 8 height: Math.min(8, _toastRect.height) clip: true visible: _toastRect.height > 0 Canvas { anchors.top: parent.top width: 8; height: 8 onPaint: { var ctx = getContext("2d"); ctx.clearRect(0, 0, 8, 8); ctx.fillStyle = Theme.barBg; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(8, 0); ctx.lineTo(8, 8); ctx.arc(0, 8, 8, 0, -Math.PI / 2, true); ctx.closePath(); ctx.fill(); } } } // Right inverse corner ear Item { anchors.left: _toastRect.right anchors.top: parent.top width: 8 height: Math.min(8, _toastRect.height) clip: true visible: _toastRect.height > 0 Canvas { anchors.top: parent.top width: 8; height: 8 onPaint: { var ctx = getContext("2d"); ctx.clearRect(0, 0, 8, 8); ctx.fillStyle = Theme.barBg; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(8, 0); ctx.arc(8, 8, 8, -Math.PI / 2, Math.PI, true); ctx.closePath(); ctx.fill(); } } } Rectangle { id: _toastRect anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top width: 320 height: notifToast.open ? toastCol.height + 16 : 0 color: Theme.barBg radius: 8 topLeftRadius: 0 topRightRadius: 0 clip: true Behavior on height { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } } Column { id: toastCol anchors.left: parent.left anchors.right: toastDismiss.left anchors.top: parent.top anchors.margins: 8 spacing: 2 Text { width: parent.width text: notifToast.currentNotif ? (notifToast.currentNotif.summary || notifToast.currentNotif.appName) : "" color: Theme.base05 font.family: "FiraMono Nerd Font" font.pixelSize: 12 font.weight: Font.Medium elide: Text.ElideRight } Text { width: parent.width text: notifToast.currentNotif ? (notifToast.currentNotif.body || "") : "" color: Theme.base04 font.family: "FiraMono Nerd Font" font.pixelSize: 11 elide: Text.ElideRight maximumLineCount: 3 wrapMode: Text.Wrap visible: text !== "" } Row { spacing: 4 visible: notifToast.currentNotif && notifToast.currentNotif.actions.length > 0 Repeater { model: notifToast.currentNotif ? notifToast.currentNotif.actions : [] Rectangle { required property var modelData width: toastActionText.width + 12 height: toastActionText.height + 6 radius: 4 color: toastActionMa.containsMouse ? Theme.base02 : Theme.base01 border.width: 1 border.color: Theme.base02 Text { id: toastActionText anchors.centerIn: parent text: modelData.text color: Theme.base05 font.family: "FiraMono Nerd Font" font.pixelSize: 10 } MouseArea { id: toastActionMa anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { modelData.invoke(); notifToast.dismiss(); } } } } } } Text { id: toastDismiss anchors.right: parent.right anchors.top: parent.top anchors.margins: 8 text: "\u{f0156}" color: toastDismissMa.containsMouse ? Theme.base05 : Theme.base03 font.family: "FiraMono Nerd Font" font.pixelSize: 13 MouseArea { id: toastDismissMa anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: { notifToast.currentNotif.dismiss(); notifToast.dismiss(); } } } } } ''; }; }; }; }; }