diff --git a/README.md b/README.md index 2145d79..e455866 100644 --- a/README.md +++ b/README.md @@ -43,18 +43,10 @@ inputs = { ``` ```nix -# home.nix (standalone home-manager) +# home.nix 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: @@ -83,17 +75,11 @@ 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 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`. +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. ### Disabling modules @@ -156,7 +142,8 @@ nix build .#docs ### Theme -Theme keys are written to +Theme keys are merged on top of whatever stylix provides. You only need to +specify what you want to override. Values 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. @@ -171,7 +158,7 @@ programs.nova-shell.theme = { fontSize = 13; fontFamily = "JetBrains Mono"; - # override individual palette entries (or stylix defaults, if you imported the stylix module) + # override individual palette entries if stylix's choices personally offend you colors.base00 = "#1a1a2e"; colors.base05 = "#e0e0f0"; }; diff --git a/flake.nix b/flake.nix index 955fbaf..39e1fb9 100644 --- a/flake.nix +++ b/flake.nix @@ -202,10 +202,7 @@ } ); - 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 ae72e18..4a87bd0 100644 --- a/nix/hm-module.nix +++ b/nix/hm-module.nix @@ -7,6 +7,40 @@ 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 = { @@ -261,7 +295,7 @@ in Keys: colors (base00-base0F), fontFamily, iconFontFamily, fontSize, barOpacity, barHeight, barPadding, groupSpacing, groupPadding, moduleSpacing, radius, screenRadius, reducedMotion. - Populated from stylix when homeModules.stylix is imported. + Automatically populated from stylix when it is available. ''; }; @@ -293,8 +327,11 @@ in cfg.modules; xdg.configFile."nova-shell/theme.json".source = - (pkgs.formats.json { }).generate "nova-shell-theme.json" - cfg.theme; + let + stylixDefaults = if stylixAvailable then stylixTheme else { }; + finalTheme = lib.recursiveUpdate stylixDefaults cfg.theme; + in + (pkgs.formats.json { }).generate "nova-shell-theme.json" finalTheme; # 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 deleted file mode 100644 index d687470..0000000 --- a/nix/nixos-module.nix +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 643c4f1..0000000 --- a/nix/stylix.nix +++ /dev/null @@ -1,40 +0,0 @@ -_: -{ - 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 ec14bd0..4f99b64 100644 --- a/shell/modules/BacklightModule.qml +++ b/shell/modules/BacklightModule.qml @@ -7,7 +7,8 @@ import "../applets" as C M.BarModule { id: root spacing: S.Theme.moduleSpacing - active: S.Modules.backlight.enable && S.BacklightService.available + opacity: S.Modules.backlight.enable && S.BacklightService.available ? 1 : 0 + visible: opacity > 0 tooltip: "Brightness: " + percent + "%" panelNamespace: "nova-backlight" panelTitle: "Brightness" diff --git a/shell/modules/Bar.qml b/shell/modules/Bar.qml index 5ca2c26..3ef5314 100644 --- a/shell/modules/Bar.qml +++ b/shell/modules/Bar.qml @@ -120,8 +120,12 @@ PanelWindow { M.BarGroup { M.PrivacyModule {} - M.ClockModule {} - M.NotificationsModule {} + M.ClockModule { + visible: S.Modules.clock.enable + } + M.NotificationsModule { + visible: S.Modules.notifications.enable + } } } @@ -137,6 +141,7 @@ PanelWindow { leftEdge: true M.WorkspacesModule { bar: bar + visible: S.Modules.workspaces.enable } } M.BarGroup { @@ -177,38 +182,60 @@ PanelWindow { M.MprisModule { bar: bar } - M.VolumeModule {} + M.VolumeModule { + visible: S.Modules.volume.enable + } } // Connectivity M.BarGroup { - M.NetworkModule {} + M.NetworkModule { + visible: S.Modules.network.enable + } M.BluetoothModule {} } // Controls M.BarGroup { M.BacklightModule {} - M.PowerProfileModule {} - M.IdleInhibitorModule {} + M.PowerProfileModule { + visible: S.Modules.powerProfile.enable + } + M.IdleInhibitorModule { + visible: S.Modules.idleInhibitor.enable + } } // Stats M.BarGroup { - M.CpuModule {} - M.MemoryModule {} + M.CpuModule { + visible: S.Modules.cpu.enable + } + M.MemoryModule { + visible: S.Modules.memory.enable + } M.GpuModule {} - M.TemperatureModule {} - M.WeatherModule {} - M.DiskModule {} + M.TemperatureModule { + visible: S.Modules.temperature.enable + } + M.WeatherModule { + visible: S.Modules.weather.enable + } + M.DiskModule { + visible: S.Modules.disk.enable + } } // Power + Dock M.BarGroup { rightEdge: true M.BatteryModule {} - M.DockModule {} - M.PowerModule {} + M.DockModule { + visible: S.Modules.dock.enable + } + M.PowerModule { + visible: S.Modules.power.enable + } } } } diff --git a/shell/modules/BarGroup.qml b/shell/modules/BarGroup.qml index 544a8ae..fffa807 100644 --- a/shell/modules/BarGroup.qml +++ b/shell/modules/BarGroup.qml @@ -27,18 +27,7 @@ Item { readonly property real _blr: S.Theme.radius readonly property real _brr: S.Theme.radius - // 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; - } + visible: row.visibleChildren.length > 0 implicitWidth: row.implicitWidth + _pad * 2 implicitHeight: S.Theme.barHeight - 3 - _pad diff --git a/shell/modules/BarModule.qml b/shell/modules/BarModule.qml index d5e3a42..0da9b8e 100644 --- a/shell/modules/BarModule.qml +++ b/shell/modules/BarModule.qml @@ -14,10 +14,6 @@ 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 7311f83..57b66f7 100644 --- a/shell/modules/BatteryModule.qml +++ b/shell/modules/BatteryModule.qml @@ -7,7 +7,8 @@ import "../applets" as C M.BarModule { id: root spacing: S.Theme.moduleSpacing - active: S.Modules.battery.enable && S.BatteryService.available + opacity: S.Modules.battery.enable && S.BatteryService.available ? 1 : 0 + visible: opacity > 0 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 222e023..5d0ea98 100644 --- a/shell/modules/BluetoothModule.qml +++ b/shell/modules/BluetoothModule.qml @@ -7,7 +7,8 @@ import "../applets" as C M.BarModule { id: root spacing: S.Theme.moduleSpacing - active: S.Modules.bluetooth.enable && S.BluetoothService.state !== "unavailable" + opacity: S.Modules.bluetooth.enable && S.BluetoothService.state !== "unavailable" ? 1 : 0 + visible: opacity > 0 tooltip: { if (S.BluetoothService.state === "connected") return "Bluetooth: " + S.BluetoothService.device; diff --git a/shell/modules/ClockModule.qml b/shell/modules/ClockModule.qml index d8679a4..4bbd126 100644 --- a/shell/modules/ClockModule.qml +++ b/shell/modules/ClockModule.qml @@ -6,7 +6,6 @@ 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 580862d..dcc3099 100644 --- a/shell/modules/CpuModule.qml +++ b/shell/modules/CpuModule.qml @@ -6,7 +6,6 @@ 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 fb000e4..d682e24 100644 --- a/shell/modules/DiskModule.qml +++ b/shell/modules/DiskModule.qml @@ -6,7 +6,6 @@ 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 49fc28f..f2db938 100644 --- a/shell/modules/DockModule.qml +++ b/shell/modules/DockModule.qml @@ -4,7 +4,6 @@ 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 65028e4..69c5701 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) - active: S.Modules.gpu.enable && S.SystemStats.gpuAvailable + visible: 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 b8c9b45..e7f54e5 100644 --- a/shell/modules/IdleInhibitorModule.qml +++ b/shell/modules/IdleInhibitorModule.qml @@ -5,7 +5,6 @@ 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 c3959f9..9a676d3 100644 --- a/shell/modules/MemoryModule.qml +++ b/shell/modules/MemoryModule.qml @@ -6,7 +6,6 @@ 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 dcd5553..92c42a2 100644 --- a/shell/modules/MprisModule.qml +++ b/shell/modules/MprisModule.qml @@ -9,7 +9,8 @@ import "../applets" as C M.BarModule { id: root spacing: S.Theme.moduleSpacing - active: S.Modules.mpris.enable && S.MprisService.player !== null + opacity: S.Modules.mpris.enable && S.MprisService.player !== null ? 1 : 0 + visible: opacity > 0 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 b3b9d1c..2aa9dac 100644 --- a/shell/modules/NetworkModule.qml +++ b/shell/modules/NetworkModule.qml @@ -6,7 +6,6 @@ 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 c625c6b..8b7502f 100644 --- a/shell/modules/NotificationsModule.qml +++ b/shell/modules/NotificationsModule.qml @@ -7,7 +7,6 @@ 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 11b0294..b763b37 100644 --- a/shell/modules/PowerModule.qml +++ b/shell/modules/PowerModule.qml @@ -7,7 +7,6 @@ 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 b2d359e..d2e31af 100644 --- a/shell/modules/PowerProfileModule.qml +++ b/shell/modules/PowerProfileModule.qml @@ -4,7 +4,6 @@ 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 a0b703c..fd4adaa 100644 --- a/shell/modules/PrivacyModule.qml +++ b/shell/modules/PrivacyModule.qml @@ -35,7 +35,7 @@ M.BarModule { return false; } - active: S.Modules.privacy.enable && (root._videoCapture || root._audioIn) + visible: S.Modules.privacy.enable && (root._videoCapture || root._audioIn) // Screenshare indicator Text { diff --git a/shell/modules/TemperatureModule.qml b/shell/modules/TemperatureModule.qml index 657c64b..338aab6 100644 --- a/shell/modules/TemperatureModule.qml +++ b/shell/modules/TemperatureModule.qml @@ -6,7 +6,6 @@ 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 85cddf4..492c82c 100644 --- a/shell/modules/TrayModule.qml +++ b/shell/modules/TrayModule.qml @@ -12,14 +12,32 @@ M.BarModule { spacing: S.Theme.moduleSpacing + 2 cursorShape: Qt.ArrowCursor - active: S.Modules.tray.enable && _trayRepeater.count > 0 + // 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 required property var bar property var _activeMenu: null // --- debug logging (remove once tray is confirmed working) --- - onActiveChanged: console.log("[TrayModule] active:", active, "count:", _trayRepeater.count) - onVisibleChanged: console.log("[TrayModule] visible:", visible) + 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); + } + } Repeater { id: _trayRepeater diff --git a/shell/modules/VolumeModule.qml b/shell/modules/VolumeModule.qml index 6a16e4f..678eca4 100644 --- a/shell/modules/VolumeModule.qml +++ b/shell/modules/VolumeModule.qml @@ -7,7 +7,6 @@ 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 94c0187..2325d21 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 - active: S.Modules.weather.enable && S.WeatherService.available + visible: 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 978460e..03aac54 100644 --- a/shell/modules/WorkspacesModule.qml +++ b/shell/modules/WorkspacesModule.qml @@ -6,7 +6,6 @@ import "../services" as S M.BarModule { id: root - active: S.Modules.workspaces.enable spacing: 4 cursorShape: Qt.ArrowCursor