Compare commits

..
29 changed files with 142 additions and 122 deletions

View file

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

View file

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

View file

@ -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) [

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 {
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"

View file

@ -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 {}
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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"

View file

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

View file

@ -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"

View file

@ -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"

View file

@ -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"

View file

@ -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()

View file

@ -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"

View file

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

View file

@ -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"

View file

@ -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"

View file

@ -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")

View file

@ -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"

View file

@ -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"

View file

@ -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"];

View file

@ -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 {

View file

@ -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"

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

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