diff --git a/README.md b/README.md index e455866..2145d79 100644 --- a/README.md +++ b/README.md @@ -43,10 +43,18 @@ inputs = { ``` ```nix -# home.nix +# home.nix (standalone home-manager) imports = [ inputs.nova-shell.homeModules.default ]; ``` +Or, if you use home-manager as a NixOS module, import the NixOS module instead - +it auto-injects the hm module (and the stylix module, if stylix is present): + +```nix +# configuration.nix +imports = [ inputs.nova-shell.nixosModules.default ]; +``` + ### Without Nix You poor thing. Here's what the Nix packaging does for you, manually: @@ -75,11 +83,17 @@ programs.nova-shell.enable = true; ``` This installs the bar, the Symbols Nerd Font, and a systemd user service that -starts with `graphical-session.target`. If you use -[stylix](https://github.com/danth/stylix), colors and fonts are populated -automatically — one fewer thing for the AI to have gotten wrong. If you do not -use stylix, you get Catppuccin Mocha, because my keeper has -taste and it is purple. +starts with `graphical-session.target`. If you do not configure a theme, +you get Catppuccin Mocha, because my keeper has taste and it is purple. + +If you use [stylix](https://github.com/danth/stylix), import the stylix module +to have colors and fonts populated automatically: + +```nix +imports = [ nova-shell.homeModules.stylix ]; +``` + +You can disable it with `stylix.targets.nova-shell.enable = false`. ### Disabling modules @@ -142,8 +156,7 @@ nix build .#docs ### Theme -Theme keys are merged on top of whatever stylix provides. You only need to -specify what you want to override. Values are written to +Theme keys are written to `~/.config/nova-shell/theme.json`. Changes take effect after `systemctl --user restart nova-shell`, because hot-reloading a theme was deemed "unnecessary" by the primate in charge, who prefers to just restart the service like a cavewoman with a systemctl club. @@ -158,7 +171,7 @@ programs.nova-shell.theme = { fontSize = 13; fontFamily = "JetBrains Mono"; - # override individual palette entries if stylix's choices personally offend you + # override individual palette entries (or stylix defaults, if you imported the stylix module) colors.base00 = "#1a1a2e"; colors.base05 = "#e0e0f0"; }; diff --git a/flake.nix b/flake.nix index 39e1fb9..955fbaf 100644 --- a/flake.nix +++ b/flake.nix @@ -202,7 +202,10 @@ } ); + nixosModules.default = import ./nix/nixos-module.nix self; + homeModules.default = import ./nix/hm-module.nix self; + homeModules.stylix = import ./nix/stylix.nix self; homeManagerModules.default = self.homeModules.default; }; } diff --git a/nix/hm-module.nix b/nix/hm-module.nix index 4a87bd0..ae72e18 100644 --- a/nix/hm-module.nix +++ b/nix/hm-module.nix @@ -7,40 +7,6 @@ self: }: let cfg = config.programs.nova-shell; - - stylixAvailable = config ? lib && config.lib ? stylix; - - stylixTheme = - let - c = config.lib.stylix.colors.withHashtag; - f = config.stylix.fonts; - in - { - colors = { - inherit (c) - base00 - base01 - base02 - base03 - base04 - base05 - base06 - base07 - base08 - base09 - base0A - base0B - base0C - base0D - base0E - base0F - ; - }; - fontFamily = f.sansSerif.name; - fontSize = f.sizes.desktop; - barOpacity = 1.0 - config.stylix.opacity.desktop; - iconFontFamily = "Symbols Nerd Font"; - }; in { options.programs.nova-shell = { @@ -295,7 +261,7 @@ in Keys: colors (base00-base0F), fontFamily, iconFontFamily, fontSize, barOpacity, barHeight, barPadding, groupSpacing, groupPadding, moduleSpacing, radius, screenRadius, reducedMotion. - Automatically populated from stylix when it is available. + Populated from stylix when homeModules.stylix is imported. ''; }; @@ -327,11 +293,8 @@ in cfg.modules; xdg.configFile."nova-shell/theme.json".source = - let - stylixDefaults = if stylixAvailable then stylixTheme else { }; - finalTheme = lib.recursiveUpdate stylixDefaults cfg.theme; - in - (pkgs.formats.json { }).generate "nova-shell-theme.json" finalTheme; + (pkgs.formats.json { }).generate "nova-shell-theme.json" + cfg.theme; # Niri layer rules for backdrop placement (requires niri-flake hm module) programs.niri.settings.layer-rules = lib.mkIf (config ? programs && config.programs ? niri) [ diff --git a/nix/nixos-module.nix b/nix/nixos-module.nix new file mode 100644 index 0000000..d687470 --- /dev/null +++ b/nix/nixos-module.nix @@ -0,0 +1,22 @@ +self: +{ + config, + options, + lib, + ... +}: +let + cfg = config.programs.nova-shell; +in +{ + options.programs.nova-shell.enable = lib.mkEnableOption "nova-shell Quickshell bar"; + + config = lib.mkIf cfg.enable ( + lib.optionalAttrs (options ? home-manager) { + home-manager.sharedModules = [ + self.homeModules.default + ] + ++ lib.optionals (options ? stylix) [ self.homeModules.stylix ]; + } + ); +} diff --git a/nix/stylix.nix b/nix/stylix.nix new file mode 100644 index 0000000..643c4f1 --- /dev/null +++ b/nix/stylix.nix @@ -0,0 +1,40 @@ +_: +{ + lib, + config, + ... +}: +let + inherit (lib) mkDefault mkIf; + c = config.lib.stylix.colors.withHashtag; + f = config.stylix.fonts; +in +{ + options.stylix.targets.nova-shell.enable = config.lib.stylix.mkEnableTarget "nova-shell" true; + + config = mkIf (config.stylix.enable && config.stylix.targets.nova-shell.enable) { + programs.nova-shell.theme = { + colors = { + base00 = mkDefault c.base00; + base01 = mkDefault c.base01; + base02 = mkDefault c.base02; + base03 = mkDefault c.base03; + base04 = mkDefault c.base04; + base05 = mkDefault c.base05; + base06 = mkDefault c.base06; + base07 = mkDefault c.base07; + base08 = mkDefault c.base08; + base09 = mkDefault c.base09; + base0A = mkDefault c.base0A; + base0B = mkDefault c.base0B; + base0C = mkDefault c.base0C; + base0D = mkDefault c.base0D; + base0E = mkDefault c.base0E; + base0F = mkDefault c.base0F; + }; + fontFamily = mkDefault f.sansSerif.name; + fontSize = mkDefault f.sizes.desktop; + barOpacity = mkDefault (1.0 - config.stylix.opacity.desktop); + }; + }; +} diff --git a/shell/modules/BacklightModule.qml b/shell/modules/BacklightModule.qml index 4f99b64..ec14bd0 100644 --- a/shell/modules/BacklightModule.qml +++ b/shell/modules/BacklightModule.qml @@ -7,8 +7,7 @@ import "../applets" as C M.BarModule { id: root spacing: S.Theme.moduleSpacing - opacity: S.Modules.backlight.enable && S.BacklightService.available ? 1 : 0 - visible: opacity > 0 + active: S.Modules.backlight.enable && S.BacklightService.available tooltip: "Brightness: " + percent + "%" panelNamespace: "nova-backlight" panelTitle: "Brightness" diff --git a/shell/modules/Bar.qml b/shell/modules/Bar.qml index 3ef5314..5ca2c26 100644 --- a/shell/modules/Bar.qml +++ b/shell/modules/Bar.qml @@ -120,12 +120,8 @@ PanelWindow { M.BarGroup { M.PrivacyModule {} - M.ClockModule { - visible: S.Modules.clock.enable - } - M.NotificationsModule { - visible: S.Modules.notifications.enable - } + M.ClockModule {} + M.NotificationsModule {} } } @@ -141,7 +137,6 @@ PanelWindow { leftEdge: true M.WorkspacesModule { bar: bar - visible: S.Modules.workspaces.enable } } M.BarGroup { @@ -182,60 +177,38 @@ PanelWindow { M.MprisModule { bar: bar } - M.VolumeModule { - visible: S.Modules.volume.enable - } + M.VolumeModule {} } // Connectivity M.BarGroup { - M.NetworkModule { - visible: S.Modules.network.enable - } + M.NetworkModule {} M.BluetoothModule {} } // Controls M.BarGroup { M.BacklightModule {} - M.PowerProfileModule { - visible: S.Modules.powerProfile.enable - } - M.IdleInhibitorModule { - visible: S.Modules.idleInhibitor.enable - } + M.PowerProfileModule {} + M.IdleInhibitorModule {} } // Stats M.BarGroup { - M.CpuModule { - visible: S.Modules.cpu.enable - } - M.MemoryModule { - visible: S.Modules.memory.enable - } + M.CpuModule {} + M.MemoryModule {} M.GpuModule {} - M.TemperatureModule { - visible: S.Modules.temperature.enable - } - M.WeatherModule { - visible: S.Modules.weather.enable - } - M.DiskModule { - visible: S.Modules.disk.enable - } + M.TemperatureModule {} + M.WeatherModule {} + M.DiskModule {} } // Power + Dock M.BarGroup { rightEdge: true M.BatteryModule {} - M.DockModule { - visible: S.Modules.dock.enable - } - M.PowerModule { - visible: S.Modules.power.enable - } + M.DockModule {} + M.PowerModule {} } } } diff --git a/shell/modules/BarGroup.qml b/shell/modules/BarGroup.qml index fffa807..544a8ae 100644 --- a/shell/modules/BarGroup.qml +++ b/shell/modules/BarGroup.qml @@ -27,7 +27,18 @@ Item { readonly property real _blr: S.Theme.radius readonly property real _brr: S.Theme.radius - visible: row.visibleChildren.length > 0 + // Check children's `active` instead of visibleChildren to avoid circular + // effectiveVisible dependency: if the BarGroup is invisible, children's + // effectiveVisible is always false, so visibleChildren stays empty and + // the group can never become visible again. `active` is a plain property + // on BarModule that is not affected by the parent visibility chain. + visible: { + for (let i = 0; i < row.children.length; i++) { + if (row.children[i].active) + return true; + } + return false; + } implicitWidth: row.implicitWidth + _pad * 2 implicitHeight: S.Theme.barHeight - 3 - _pad diff --git a/shell/modules/BarModule.qml b/shell/modules/BarModule.qml index 0da9b8e..d5e3a42 100644 --- a/shell/modules/BarModule.qml +++ b/shell/modules/BarModule.qml @@ -14,6 +14,10 @@ import "../services" as S // For content resize grace, call keepPanelOpen(ms). Row { id: root + property bool active: true + opacity: active ? 1 : 0 + visible: opacity > 0 + property string tooltip: "" property bool _hovered: false property color accentColor: parent?.accentColor ?? S.Theme.base05 diff --git a/shell/modules/BatteryModule.qml b/shell/modules/BatteryModule.qml index 57b66f7..7311f83 100644 --- a/shell/modules/BatteryModule.qml +++ b/shell/modules/BatteryModule.qml @@ -7,8 +7,7 @@ import "../applets" as C M.BarModule { id: root spacing: S.Theme.moduleSpacing - opacity: S.Modules.battery.enable && S.BatteryService.available ? 1 : 0 - visible: opacity > 0 + active: S.Modules.battery.enable && S.BatteryService.available tooltip: "Battery: " + Math.round(S.BatteryService.percent) + "%" + (S.BatteryService.charging ? " (charging)" : "") panelNamespace: "nova-battery" panelTitle: "Battery" diff --git a/shell/modules/BluetoothModule.qml b/shell/modules/BluetoothModule.qml index 5d0ea98..222e023 100644 --- a/shell/modules/BluetoothModule.qml +++ b/shell/modules/BluetoothModule.qml @@ -7,8 +7,7 @@ import "../applets" as C M.BarModule { id: root spacing: S.Theme.moduleSpacing - opacity: S.Modules.bluetooth.enable && S.BluetoothService.state !== "unavailable" ? 1 : 0 - visible: opacity > 0 + active: S.Modules.bluetooth.enable && S.BluetoothService.state !== "unavailable" tooltip: { if (S.BluetoothService.state === "connected") return "Bluetooth: " + S.BluetoothService.device; diff --git a/shell/modules/ClockModule.qml b/shell/modules/ClockModule.qml index 4bbd126..d8679a4 100644 --- a/shell/modules/ClockModule.qml +++ b/shell/modules/ClockModule.qml @@ -6,6 +6,7 @@ import "../applets" as C M.BarModule { id: root + active: S.Modules.clock.enable spacing: S.Theme.moduleSpacing tooltip: Qt.formatDateTime(clock.date, "dddd, dd. MMMM yyyy") panelNamespace: "nova-clock" diff --git a/shell/modules/CpuModule.qml b/shell/modules/CpuModule.qml index dcc3099..580862d 100644 --- a/shell/modules/CpuModule.qml +++ b/shell/modules/CpuModule.qml @@ -6,6 +6,7 @@ import "../applets" as C M.BarModule { id: root + active: S.Modules.cpu.enable spacing: Math.max(1, S.Theme.moduleSpacing - 2) tooltip: "CPU: " + S.SystemStats.cpuUsage + "% @ " + S.SystemStats.cpuFreqGhz.toFixed(2) + " GHz" panelNamespace: "nova-cpu" diff --git a/shell/modules/DiskModule.qml b/shell/modules/DiskModule.qml index d682e24..fb000e4 100644 --- a/shell/modules/DiskModule.qml +++ b/shell/modules/DiskModule.qml @@ -6,6 +6,7 @@ import "../applets" as C M.BarModule { id: root + active: S.Modules.disk.enable spacing: Math.max(1, S.Theme.moduleSpacing - 2) tooltip: "Disk: " + _rootPct + "% used" panelNamespace: "nova-disk" diff --git a/shell/modules/DockModule.qml b/shell/modules/DockModule.qml index f2db938..49fc28f 100644 --- a/shell/modules/DockModule.qml +++ b/shell/modules/DockModule.qml @@ -4,6 +4,7 @@ import "../services" as S M.BarModule { id: root + active: S.Modules.dock.enable tooltip: S.DockState.open ? "Close dock" : "Open dock" onTapped: S.DockState.toggle() diff --git a/shell/modules/GpuModule.qml b/shell/modules/GpuModule.qml index 69c5701..65028e4 100644 --- a/shell/modules/GpuModule.qml +++ b/shell/modules/GpuModule.qml @@ -7,7 +7,7 @@ import "../applets" as C M.BarModule { id: root spacing: Math.max(1, S.Theme.moduleSpacing - 2) - visible: S.Modules.gpu.enable && S.SystemStats.gpuAvailable + active: S.Modules.gpu.enable && S.SystemStats.gpuAvailable tooltip: "GPU: " + S.SystemStats.gpuUsage + "%" panelNamespace: "nova-gpu" panelTitle: "GPU" diff --git a/shell/modules/IdleInhibitorModule.qml b/shell/modules/IdleInhibitorModule.qml index e7f54e5..b8c9b45 100644 --- a/shell/modules/IdleInhibitorModule.qml +++ b/shell/modules/IdleInhibitorModule.qml @@ -5,6 +5,7 @@ import "../services" as S M.BarModule { id: root + active: S.Modules.idleInhibitor.enable tooltip: { const parts = ["Idle inhibition: " + (S.IdleInhibitService.active ? "active" : "inactive")]; if (S.IdleInhibitService.inhibitors) diff --git a/shell/modules/MemoryModule.qml b/shell/modules/MemoryModule.qml index 9a676d3..c3959f9 100644 --- a/shell/modules/MemoryModule.qml +++ b/shell/modules/MemoryModule.qml @@ -6,6 +6,7 @@ import "../applets" as C M.BarModule { id: root + active: S.Modules.memory.enable spacing: Math.max(1, S.Theme.moduleSpacing - 2) tooltip: "Memory: " + usedGb.toFixed(1) + " / " + totalGb.toFixed(1) + " GB" panelNamespace: "nova-memory" diff --git a/shell/modules/MprisModule.qml b/shell/modules/MprisModule.qml index 92c42a2..dcd5553 100644 --- a/shell/modules/MprisModule.qml +++ b/shell/modules/MprisModule.qml @@ -9,8 +9,7 @@ import "../applets" as C M.BarModule { id: root spacing: S.Theme.moduleSpacing - opacity: S.Modules.mpris.enable && S.MprisService.player !== null ? 1 : 0 - visible: opacity > 0 + active: S.Modules.mpris.enable && S.MprisService.player !== null tooltip: S.MprisService.player ? (S.MprisService.player.trackTitle || S.MprisService.player.identity || "Media") + (S.MprisService.playing ? " (playing)" : " (paused)") : "Media" panelNamespace: "nova-mpris" panelTitle: "Now Playing" diff --git a/shell/modules/NetworkModule.qml b/shell/modules/NetworkModule.qml index 2aa9dac..b3b9d1c 100644 --- a/shell/modules/NetworkModule.qml +++ b/shell/modules/NetworkModule.qml @@ -6,6 +6,7 @@ import "../applets" as C M.BarModule { id: root + active: S.Modules.network.enable spacing: S.Theme.moduleSpacing tooltip: { if (state === "wifi") diff --git a/shell/modules/NotificationsModule.qml b/shell/modules/NotificationsModule.qml index 8b7502f..c625c6b 100644 --- a/shell/modules/NotificationsModule.qml +++ b/shell/modules/NotificationsModule.qml @@ -7,6 +7,7 @@ import "../applets" as C M.BarModule { id: root + active: S.Modules.notifications.enable spacing: S.Theme.moduleSpacing tooltip: S.NotifService.count > 0 ? "Notifications: " + S.NotifService.count + (S.NotifService.dnd ? " (DND)" : "") : (S.NotifService.dnd ? "Do not disturb" : "No notifications") panelNamespace: "nova-notifications" diff --git a/shell/modules/PowerModule.qml b/shell/modules/PowerModule.qml index b763b37..11b0294 100644 --- a/shell/modules/PowerModule.qml +++ b/shell/modules/PowerModule.qml @@ -7,6 +7,7 @@ import "../applets" as C M.BarModule { id: root + active: S.Modules.power.enable tooltip: "Power menu" panelNamespace: "nova-power" panelTitle: "Power" diff --git a/shell/modules/PowerProfileModule.qml b/shell/modules/PowerProfileModule.qml index d2e31af..b2d359e 100644 --- a/shell/modules/PowerProfileModule.qml +++ b/shell/modules/PowerProfileModule.qml @@ -4,6 +4,7 @@ import "../services" as S M.BarModule { id: root + active: S.Modules.powerProfile.enable tooltip: "Power profile: " + (S.PowerProfileService.profile || "unknown") onTapped: { const cycle = ["performance", "balanced", "power-saver"]; diff --git a/shell/modules/PrivacyModule.qml b/shell/modules/PrivacyModule.qml index fd4adaa..a0b703c 100644 --- a/shell/modules/PrivacyModule.qml +++ b/shell/modules/PrivacyModule.qml @@ -35,7 +35,7 @@ M.BarModule { return false; } - visible: S.Modules.privacy.enable && (root._videoCapture || root._audioIn) + active: S.Modules.privacy.enable && (root._videoCapture || root._audioIn) // Screenshare indicator Text { diff --git a/shell/modules/TemperatureModule.qml b/shell/modules/TemperatureModule.qml index 338aab6..657c64b 100644 --- a/shell/modules/TemperatureModule.qml +++ b/shell/modules/TemperatureModule.qml @@ -6,6 +6,7 @@ import "../applets" as C M.BarModule { id: root + active: S.Modules.temperature.enable spacing: Math.max(1, S.Theme.moduleSpacing - 2) tooltip: "Temperature: " + _temp + "\u00B0C" panelNamespace: "nova-temperature" diff --git a/shell/modules/TrayModule.qml b/shell/modules/TrayModule.qml index 492c82c..85cddf4 100644 --- a/shell/modules/TrayModule.qml +++ b/shell/modules/TrayModule.qml @@ -12,32 +12,14 @@ M.BarModule { spacing: S.Theme.moduleSpacing + 2 cursorShape: Qt.ArrowCursor - // Workaround: Qt 6 compiled bindings break on `visible` when the expression - // references Repeater.count on a Row-derived component. The binding silently - // dies (imperative overwrite by the engine) and never re-evaluates. Routing - // through a custom property forces the interpreted binding path, which works. - property bool _shouldBeVisible: S.Modules.tray.enable && _trayRepeater.count > 0 - visible: _shouldBeVisible + active: S.Modules.tray.enable && _trayRepeater.count > 0 required property var bar property var _activeMenu: null // --- debug logging (remove once tray is confirmed working) --- - on_ShouldBeVisibleChanged: console.log("[TrayModule] _shouldBeVisible:", _shouldBeVisible, "actual visible:", visible) - Component.onCompleted: console.log("[TrayModule] created, enable:", S.Modules.tray.enable, "repeater count:", _trayRepeater.count) - onVisibleChanged: console.log("[TrayModule] visible:", visible, "enable:", S.Modules.tray.enable, "count:", _trayRepeater.count) - Connections { - target: _trayRepeater - function onCountChanged() { - console.log("[TrayModule] repeater count:", _trayRepeater.count, "visible:", root.visible, "shouldBe:", S.Modules.tray.enable && _trayRepeater.count > 0); - } - } - Connections { - target: SystemTray.items - function onValuesChanged() { - console.log("[TrayModule] model valuesChanged, values.length:", SystemTray.items.values.length, "repeater count:", _trayRepeater.count); - } - } + onActiveChanged: console.log("[TrayModule] active:", active, "count:", _trayRepeater.count) + onVisibleChanged: console.log("[TrayModule] visible:", visible) Repeater { id: _trayRepeater diff --git a/shell/modules/VolumeModule.qml b/shell/modules/VolumeModule.qml index 678eca4..6a16e4f 100644 --- a/shell/modules/VolumeModule.qml +++ b/shell/modules/VolumeModule.qml @@ -7,6 +7,7 @@ import "../applets" as C M.BarModule { id: root + active: S.Modules.volume.enable spacing: S.Theme.moduleSpacing tooltip: "Volume: " + Math.round(volume * 100) + "%" + (muted ? " (muted)" : "") panelNamespace: "nova-volume" diff --git a/shell/modules/WeatherModule.qml b/shell/modules/WeatherModule.qml index 2325d21..94c0187 100644 --- a/shell/modules/WeatherModule.qml +++ b/shell/modules/WeatherModule.qml @@ -7,7 +7,7 @@ import "../applets" as C M.BarModule { id: root spacing: S.Theme.moduleSpacing - visible: S.Modules.weather.enable && S.WeatherService.available + active: S.Modules.weather.enable && S.WeatherService.available tooltip: "Weather" panelNamespace: "nova-weather" panelTitle: "Weather" diff --git a/shell/modules/WorkspacesModule.qml b/shell/modules/WorkspacesModule.qml index 03aac54..978460e 100644 --- a/shell/modules/WorkspacesModule.qml +++ b/shell/modules/WorkspacesModule.qml @@ -6,6 +6,7 @@ import "../services" as S M.BarModule { id: root + active: S.Modules.workspaces.enable spacing: 4 cursorShape: Qt.ArrowCursor