Compare commits

...
29 changed files with 142 additions and 122 deletions

View file

@ -43,10 +43,18 @@ inputs = {
``` ```
```nix ```nix
# home.nix # home.nix (standalone home-manager)
imports = [ inputs.nova-shell.homeModules.default ]; 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 ### Without Nix
You poor thing. Here's what the Nix packaging does for you, manually: 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 This installs the bar, the Symbols Nerd Font, and a systemd user service that
starts with `graphical-session.target`. If you use starts with `graphical-session.target`. If you do not configure a theme,
[stylix](https://github.com/danth/stylix), colors and fonts are populated you get Catppuccin Mocha, because my keeper has taste and it is purple.
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 If you use [stylix](https://github.com/danth/stylix), import the stylix module
taste and it is purple. 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 ### Disabling modules
@ -142,8 +156,7 @@ nix build .#docs
### Theme ### Theme
Theme keys are merged on top of whatever stylix provides. You only need to Theme keys are written to
specify what you want to override. Values are written to
`~/.config/nova-shell/theme.json`. Changes take effect after `~/.config/nova-shell/theme.json`. Changes take effect after
`systemctl --user restart nova-shell`, because hot-reloading a theme `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. 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; fontSize = 13;
fontFamily = "JetBrains Mono"; 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.base00 = "#1a1a2e";
colors.base05 = "#e0e0f0"; colors.base05 = "#e0e0f0";
}; };

View file

@ -202,7 +202,10 @@
} }
); );
nixosModules.default = import ./nix/nixos-module.nix self;
homeModules.default = import ./nix/hm-module.nix self; homeModules.default = import ./nix/hm-module.nix self;
homeModules.stylix = import ./nix/stylix.nix self;
homeManagerModules.default = self.homeModules.default; homeManagerModules.default = self.homeModules.default;
}; };
} }

View file

@ -7,40 +7,6 @@ self:
}: }:
let let
cfg = config.programs.nova-shell; 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 in
{ {
options.programs.nova-shell = { options.programs.nova-shell = {
@ -295,7 +261,7 @@ in
Keys: colors (base00-base0F), fontFamily, iconFontFamily, fontSize, Keys: colors (base00-base0F), fontFamily, iconFontFamily, fontSize,
barOpacity, barHeight, barPadding, groupSpacing, groupPadding, moduleSpacing, barOpacity, barHeight, barPadding, groupSpacing, groupPadding, moduleSpacing,
radius, screenRadius, reducedMotion. 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; cfg.modules;
xdg.configFile."nova-shell/theme.json".source = xdg.configFile."nova-shell/theme.json".source =
let (pkgs.formats.json { }).generate "nova-shell-theme.json"
stylixDefaults = if stylixAvailable then stylixTheme else { }; cfg.theme;
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) # Niri layer rules for backdrop placement (requires niri-flake hm module)
programs.niri.settings.layer-rules = lib.mkIf (config ? programs && config.programs ? niri) [ programs.niri.settings.layer-rules = lib.mkIf (config ? programs && config.programs ? niri) [

22
nix/nixos-module.nix Normal file
View file

@ -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 ];
}
);
}

40
nix/stylix.nix Normal file
View file

@ -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);
};
};
}

View file

@ -7,8 +7,7 @@ import "../applets" as C
M.BarModule { M.BarModule {
id: root id: root
spacing: S.Theme.moduleSpacing spacing: S.Theme.moduleSpacing
opacity: S.Modules.backlight.enable && S.BacklightService.available ? 1 : 0 active: S.Modules.backlight.enable && S.BacklightService.available
visible: opacity > 0
tooltip: "Brightness: " + percent + "%" tooltip: "Brightness: " + percent + "%"
panelNamespace: "nova-backlight" panelNamespace: "nova-backlight"
panelTitle: "Brightness" panelTitle: "Brightness"

View file

@ -120,12 +120,8 @@ PanelWindow {
M.BarGroup { M.BarGroup {
M.PrivacyModule {} M.PrivacyModule {}
M.ClockModule { M.ClockModule {}
visible: S.Modules.clock.enable M.NotificationsModule {}
}
M.NotificationsModule {
visible: S.Modules.notifications.enable
}
} }
} }
@ -141,7 +137,6 @@ PanelWindow {
leftEdge: true leftEdge: true
M.WorkspacesModule { M.WorkspacesModule {
bar: bar bar: bar
visible: S.Modules.workspaces.enable
} }
} }
M.BarGroup { M.BarGroup {
@ -182,60 +177,38 @@ PanelWindow {
M.MprisModule { M.MprisModule {
bar: bar bar: bar
} }
M.VolumeModule { M.VolumeModule {}
visible: S.Modules.volume.enable
}
} }
// Connectivity // Connectivity
M.BarGroup { M.BarGroup {
M.NetworkModule { M.NetworkModule {}
visible: S.Modules.network.enable
}
M.BluetoothModule {} M.BluetoothModule {}
} }
// Controls // Controls
M.BarGroup { M.BarGroup {
M.BacklightModule {} M.BacklightModule {}
M.PowerProfileModule { M.PowerProfileModule {}
visible: S.Modules.powerProfile.enable M.IdleInhibitorModule {}
}
M.IdleInhibitorModule {
visible: S.Modules.idleInhibitor.enable
}
} }
// Stats // Stats
M.BarGroup { M.BarGroup {
M.CpuModule { M.CpuModule {}
visible: S.Modules.cpu.enable M.MemoryModule {}
}
M.MemoryModule {
visible: S.Modules.memory.enable
}
M.GpuModule {} M.GpuModule {}
M.TemperatureModule { M.TemperatureModule {}
visible: S.Modules.temperature.enable M.WeatherModule {}
} M.DiskModule {}
M.WeatherModule {
visible: S.Modules.weather.enable
}
M.DiskModule {
visible: S.Modules.disk.enable
}
} }
// Power + Dock // Power + Dock
M.BarGroup { M.BarGroup {
rightEdge: true rightEdge: true
M.BatteryModule {} M.BatteryModule {}
M.DockModule { M.DockModule {}
visible: S.Modules.dock.enable M.PowerModule {}
}
M.PowerModule {
visible: S.Modules.power.enable
}
} }
} }
} }

View file

@ -27,7 +27,18 @@ Item {
readonly property real _blr: S.Theme.radius readonly property real _blr: S.Theme.radius
readonly property real _brr: 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 implicitWidth: row.implicitWidth + _pad * 2
implicitHeight: S.Theme.barHeight - 3 - _pad implicitHeight: S.Theme.barHeight - 3 - _pad

View file

@ -14,6 +14,10 @@ import "../services" as S
// For content resize grace, call keepPanelOpen(ms). // For content resize grace, call keepPanelOpen(ms).
Row { Row {
id: root id: root
property bool active: true
opacity: active ? 1 : 0
visible: opacity > 0
property string tooltip: "" property string tooltip: ""
property bool _hovered: false property bool _hovered: false
property color accentColor: parent?.accentColor ?? S.Theme.base05 property color accentColor: parent?.accentColor ?? S.Theme.base05

View file

@ -7,8 +7,7 @@ import "../applets" as C
M.BarModule { M.BarModule {
id: root id: root
spacing: S.Theme.moduleSpacing spacing: S.Theme.moduleSpacing
opacity: S.Modules.battery.enable && S.BatteryService.available ? 1 : 0 active: S.Modules.battery.enable && S.BatteryService.available
visible: opacity > 0
tooltip: "Battery: " + Math.round(S.BatteryService.percent) + "%" + (S.BatteryService.charging ? " (charging)" : "") tooltip: "Battery: " + Math.round(S.BatteryService.percent) + "%" + (S.BatteryService.charging ? " (charging)" : "")
panelNamespace: "nova-battery" panelNamespace: "nova-battery"
panelTitle: "Battery" panelTitle: "Battery"

View file

@ -7,8 +7,7 @@ import "../applets" as C
M.BarModule { M.BarModule {
id: root id: root
spacing: S.Theme.moduleSpacing spacing: S.Theme.moduleSpacing
opacity: S.Modules.bluetooth.enable && S.BluetoothService.state !== "unavailable" ? 1 : 0 active: S.Modules.bluetooth.enable && S.BluetoothService.state !== "unavailable"
visible: opacity > 0
tooltip: { tooltip: {
if (S.BluetoothService.state === "connected") if (S.BluetoothService.state === "connected")
return "Bluetooth: " + S.BluetoothService.device; return "Bluetooth: " + S.BluetoothService.device;

View file

@ -6,6 +6,7 @@ import "../applets" as C
M.BarModule { M.BarModule {
id: root id: root
active: S.Modules.clock.enable
spacing: S.Theme.moduleSpacing spacing: S.Theme.moduleSpacing
tooltip: Qt.formatDateTime(clock.date, "dddd, dd. MMMM yyyy") tooltip: Qt.formatDateTime(clock.date, "dddd, dd. MMMM yyyy")
panelNamespace: "nova-clock" panelNamespace: "nova-clock"

View file

@ -6,6 +6,7 @@ import "../applets" as C
M.BarModule { M.BarModule {
id: root id: root
active: S.Modules.cpu.enable
spacing: Math.max(1, S.Theme.moduleSpacing - 2) spacing: Math.max(1, S.Theme.moduleSpacing - 2)
tooltip: "CPU: " + S.SystemStats.cpuUsage + "% @ " + S.SystemStats.cpuFreqGhz.toFixed(2) + " GHz" tooltip: "CPU: " + S.SystemStats.cpuUsage + "% @ " + S.SystemStats.cpuFreqGhz.toFixed(2) + " GHz"
panelNamespace: "nova-cpu" panelNamespace: "nova-cpu"

View file

@ -6,6 +6,7 @@ import "../applets" as C
M.BarModule { M.BarModule {
id: root id: root
active: S.Modules.disk.enable
spacing: Math.max(1, S.Theme.moduleSpacing - 2) spacing: Math.max(1, S.Theme.moduleSpacing - 2)
tooltip: "Disk: " + _rootPct + "% used" tooltip: "Disk: " + _rootPct + "% used"
panelNamespace: "nova-disk" panelNamespace: "nova-disk"

View file

@ -4,6 +4,7 @@ import "../services" as S
M.BarModule { M.BarModule {
id: root id: root
active: S.Modules.dock.enable
tooltip: S.DockState.open ? "Close dock" : "Open dock" tooltip: S.DockState.open ? "Close dock" : "Open dock"
onTapped: S.DockState.toggle() onTapped: S.DockState.toggle()

View file

@ -7,7 +7,7 @@ import "../applets" as C
M.BarModule { M.BarModule {
id: root id: root
spacing: Math.max(1, S.Theme.moduleSpacing - 2) 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 + "%" tooltip: "GPU: " + S.SystemStats.gpuUsage + "%"
panelNamespace: "nova-gpu" panelNamespace: "nova-gpu"
panelTitle: "GPU" panelTitle: "GPU"

View file

@ -5,6 +5,7 @@ import "../services" as S
M.BarModule { M.BarModule {
id: root id: root
active: S.Modules.idleInhibitor.enable
tooltip: { tooltip: {
const parts = ["Idle inhibition: " + (S.IdleInhibitService.active ? "active" : "inactive")]; const parts = ["Idle inhibition: " + (S.IdleInhibitService.active ? "active" : "inactive")];
if (S.IdleInhibitService.inhibitors) if (S.IdleInhibitService.inhibitors)

View file

@ -6,6 +6,7 @@ import "../applets" as C
M.BarModule { M.BarModule {
id: root id: root
active: S.Modules.memory.enable
spacing: Math.max(1, S.Theme.moduleSpacing - 2) spacing: Math.max(1, S.Theme.moduleSpacing - 2)
tooltip: "Memory: " + usedGb.toFixed(1) + " / " + totalGb.toFixed(1) + " GB" tooltip: "Memory: " + usedGb.toFixed(1) + " / " + totalGb.toFixed(1) + " GB"
panelNamespace: "nova-memory" panelNamespace: "nova-memory"

View file

@ -9,8 +9,7 @@ import "../applets" as C
M.BarModule { M.BarModule {
id: root id: root
spacing: S.Theme.moduleSpacing spacing: S.Theme.moduleSpacing
opacity: S.Modules.mpris.enable && S.MprisService.player !== null ? 1 : 0 active: S.Modules.mpris.enable && S.MprisService.player !== null
visible: opacity > 0
tooltip: S.MprisService.player ? (S.MprisService.player.trackTitle || S.MprisService.player.identity || "Media") + (S.MprisService.playing ? " (playing)" : " (paused)") : "Media" tooltip: S.MprisService.player ? (S.MprisService.player.trackTitle || S.MprisService.player.identity || "Media") + (S.MprisService.playing ? " (playing)" : " (paused)") : "Media"
panelNamespace: "nova-mpris" panelNamespace: "nova-mpris"
panelTitle: "Now Playing" panelTitle: "Now Playing"

View file

@ -6,6 +6,7 @@ import "../applets" as C
M.BarModule { M.BarModule {
id: root id: root
active: S.Modules.network.enable
spacing: S.Theme.moduleSpacing spacing: S.Theme.moduleSpacing
tooltip: { tooltip: {
if (state === "wifi") if (state === "wifi")

View file

@ -7,6 +7,7 @@ import "../applets" as C
M.BarModule { M.BarModule {
id: root id: root
active: S.Modules.notifications.enable
spacing: S.Theme.moduleSpacing 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") tooltip: S.NotifService.count > 0 ? "Notifications: " + S.NotifService.count + (S.NotifService.dnd ? " (DND)" : "") : (S.NotifService.dnd ? "Do not disturb" : "No notifications")
panelNamespace: "nova-notifications" panelNamespace: "nova-notifications"

View file

@ -7,6 +7,7 @@ import "../applets" as C
M.BarModule { M.BarModule {
id: root id: root
active: S.Modules.power.enable
tooltip: "Power menu" tooltip: "Power menu"
panelNamespace: "nova-power" panelNamespace: "nova-power"
panelTitle: "Power" panelTitle: "Power"

View file

@ -4,6 +4,7 @@ import "../services" as S
M.BarModule { M.BarModule {
id: root id: root
active: S.Modules.powerProfile.enable
tooltip: "Power profile: " + (S.PowerProfileService.profile || "unknown") tooltip: "Power profile: " + (S.PowerProfileService.profile || "unknown")
onTapped: { onTapped: {
const cycle = ["performance", "balanced", "power-saver"]; const cycle = ["performance", "balanced", "power-saver"];

View file

@ -35,7 +35,7 @@ M.BarModule {
return false; return false;
} }
visible: S.Modules.privacy.enable && (root._videoCapture || root._audioIn) active: S.Modules.privacy.enable && (root._videoCapture || root._audioIn)
// Screenshare indicator // Screenshare indicator
Text { Text {

View file

@ -6,6 +6,7 @@ import "../applets" as C
M.BarModule { M.BarModule {
id: root id: root
active: S.Modules.temperature.enable
spacing: Math.max(1, S.Theme.moduleSpacing - 2) spacing: Math.max(1, S.Theme.moduleSpacing - 2)
tooltip: "Temperature: " + _temp + "\u00B0C" tooltip: "Temperature: " + _temp + "\u00B0C"
panelNamespace: "nova-temperature" panelNamespace: "nova-temperature"

View file

@ -12,32 +12,14 @@ M.BarModule {
spacing: S.Theme.moduleSpacing + 2 spacing: S.Theme.moduleSpacing + 2
cursorShape: Qt.ArrowCursor cursorShape: Qt.ArrowCursor
// Workaround: Qt 6 compiled bindings break on `visible` when the expression active: S.Modules.tray.enable && _trayRepeater.count > 0
// 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 required property var bar
property var _activeMenu: null property var _activeMenu: null
// --- debug logging (remove once tray is confirmed working) --- // --- debug logging (remove once tray is confirmed working) ---
on_ShouldBeVisibleChanged: console.log("[TrayModule] _shouldBeVisible:", _shouldBeVisible, "actual visible:", visible) onActiveChanged: console.log("[TrayModule] active:", active, "count:", _trayRepeater.count)
Component.onCompleted: console.log("[TrayModule] created, enable:", S.Modules.tray.enable, "repeater count:", _trayRepeater.count) onVisibleChanged: console.log("[TrayModule] visible:", visible)
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 { Repeater {
id: _trayRepeater id: _trayRepeater

View file

@ -7,6 +7,7 @@ import "../applets" as C
M.BarModule { M.BarModule {
id: root id: root
active: S.Modules.volume.enable
spacing: S.Theme.moduleSpacing spacing: S.Theme.moduleSpacing
tooltip: "Volume: " + Math.round(volume * 100) + "%" + (muted ? " (muted)" : "") tooltip: "Volume: " + Math.round(volume * 100) + "%" + (muted ? " (muted)" : "")
panelNamespace: "nova-volume" panelNamespace: "nova-volume"

View file

@ -7,7 +7,7 @@ import "../applets" as C
M.BarModule { M.BarModule {
id: root id: root
spacing: S.Theme.moduleSpacing spacing: S.Theme.moduleSpacing
visible: S.Modules.weather.enable && S.WeatherService.available active: S.Modules.weather.enable && S.WeatherService.available
tooltip: "Weather" tooltip: "Weather"
panelNamespace: "nova-weather" panelNamespace: "nova-weather"
panelTitle: "Weather" panelTitle: "Weather"

View file

@ -6,6 +6,7 @@ import "../services" as S
M.BarModule { M.BarModule {
id: root id: root
active: S.Modules.workspaces.enable
spacing: 4 spacing: 4
cursorShape: Qt.ArrowCursor cursorShape: Qt.ArrowCursor