Compare commits

...

3 commits

Author SHA1 Message Date
Damocles
9e1716aa39 nix fmt 2026-04-12 15:58:13 +02:00
Damocles
4df704844e rename power 2026-04-12 15:57:22 +02:00
Damocles
48c58e2fbf group modules 2026-04-12 15:54:58 +02:00
19 changed files with 213 additions and 111 deletions

View file

@ -17,7 +17,7 @@ exactly when you should be most suspicious.
- OSD overlay for volume and brightness changes — works with niri hotkeys, not just the bar
- Screen corner rounding — tiny overlay windows that draw quarter-circle masks, configurable via `screenRadius` (set to 0 if you prefer your corners sharp and your desktop ugly)
- Weather via wttrbar with configurable arguments and rich HTML tooltips
- Power menu with lock, suspend, logout, reboot, shutdown — no more `wlogout` dependency
- Power menu with lock, suspend, logout, reboot, shutdown
- Flyout tooltips that actually filter by screen on multi-monitor setups, which only took three attempts to get right
- Home Manager module with stylix integration, per-module enable/disable, and a theme system that hot-reloads
- treefmt + nixfmt for formatting, because even AI slop deserves consistent indentation
@ -75,14 +75,14 @@ programs.nova-shell.modules = {
battery = false; # see above, but for power
temperature = false; # what you don't measure can't alarm you
disk = false; # the number will only make you anxious
wlogout = false; # if you enjoy living dangerously without a logout button
power = false; # if you enjoy living dangerously without a logout button
};
```
Full list of things you can disable: `workspaces`, `tray`, `windowTitle`,
`clock`, `notifications`, `mpris`, `volume`, `bluetooth`, `backlight`,
`network`, `powerProfile`, `idleInhibitor`, `weather`, `temperature`, `cpu`,
`memory`, `disk`, `battery`, `wlogout`.
`memory`, `disk`, `battery`, `power`.
### Theme

View file

@ -9,7 +9,8 @@ M.BarSection {
tooltip: "Brightness: " + root.percent + "%"
property int percent: 0
onPercentChanged: if (percent > 0) M.OsdState.show(percent / 100, "\uF185")
onPercentChanged: if (percent > 0)
M.OsdState.show(percent / 100, "\uF185")
Process {
id: adjProc

View file

@ -39,8 +39,12 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter
spacing: M.Theme.barSpacing
M.Clock { visible: M.Modules.clock }
M.Notifications { visible: M.Modules.notifications }
M.Clock {
visible: M.Modules.clock
}
M.Notifications {
visible: M.Modules.notifications
}
}
// ---- left ----
@ -50,7 +54,10 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter
spacing: M.Theme.barSpacing
M.Workspaces { bar: bar; visible: M.Modules.workspaces }
M.Workspaces {
bar: bar
visible: M.Modules.workspaces
}
M.Tray {
bar: bar
visible: M.Modules.tray
@ -59,7 +66,9 @@ PanelWindow {
Layout.maximumWidth: 400
visible: M.Modules.windowTitle
}
Item { Layout.fillWidth: true }
Item {
Layout.fillWidth: true
}
}
// ---- right ----
@ -72,20 +81,63 @@ PanelWindow {
Item {
Layout.fillWidth: true
}
M.Mpris {}
M.Volume { visible: M.Modules.volume }
M.Bluetooth {}
M.Backlight {}
M.Network { visible: M.Modules.network }
M.PowerProfile { visible: M.Modules.powerProfile }
M.IdleInhibitor { visible: M.Modules.idleInhibitor }
M.Weather { visible: M.Modules.weather }
M.Temperature { visible: M.Modules.temperature }
M.Cpu { visible: M.Modules.cpu }
M.Memory { visible: M.Modules.memory }
M.Disk { visible: M.Modules.disk }
M.Battery {}
M.Wlogout { bar: bar; visible: M.Modules.wlogout }
// Media
M.BarGroup {
M.Mpris {}
M.Volume {
visible: M.Modules.volume
}
}
// Connectivity
M.BarGroup {
M.Network {
visible: M.Modules.network
}
M.Bluetooth {}
}
// Controls
M.BarGroup {
M.Backlight {}
M.PowerProfile {
visible: M.Modules.powerProfile
}
M.IdleInhibitor {
visible: M.Modules.idleInhibitor
}
}
// System
M.BarGroup {
M.Cpu {
visible: M.Modules.cpu
}
M.Memory {
visible: M.Modules.memory
}
M.Temperature {
visible: M.Modules.temperature
}
}
// Status
M.BarGroup {
M.Weather {
visible: M.Modules.weather
}
M.Disk {
visible: M.Modules.disk
}
M.Battery {}
}
// Power
M.Power {
bar: bar
visible: M.Modules.power
}
}
}
}

26
modules/BarGroup.qml Normal file
View file

@ -0,0 +1,26 @@
import QtQuick
import "." as M
Rectangle {
id: root
default property alias content: row.children
color: "transparent"
border.color: M.Theme.base02
border.width: 1
radius: M.Theme.radius
visible: row.visibleChildren.length > 0
implicitWidth: row.implicitWidth + _pad * 2
implicitHeight: row.implicitHeight + _pad * 2
readonly property int _pad: 6
Row {
id: row
anchors.centerIn: parent
spacing: M.Theme.moduleSpacing + 2
}
}

View file

@ -17,10 +17,7 @@ M.BarSection {
readonly property var dev: UPower.displayDevice
readonly property real pct: (dev?.percentage ?? 0) * 100
readonly property bool charging: dev?.state === UPowerDeviceState.Charging
readonly property color _stateColor: charging ? M.Theme.base0B
: pct < 15 ? M.Theme.base08
: pct < 30 ? M.Theme.base09
: M.Theme.base0B
readonly property color _stateColor: charging ? M.Theme.base0B : pct < 15 ? M.Theme.base08 : pct < 30 ? M.Theme.base09 : M.Theme.base0B
M.BarIcon {
icon: {

View file

@ -7,8 +7,10 @@ M.BarSection {
spacing: M.Theme.moduleSpacing
visible: M.Modules.bluetooth && root.state !== "unavailable"
tooltip: {
if (root.state === "off") return "Bluetooth: off";
if (root.state === "connected") return "Bluetooth: " + root.device;
if (root.state === "off")
return "Bluetooth: off";
if (root.state === "connected")
return "Bluetooth: " + root.device;
return "Bluetooth: on";
}
@ -18,20 +20,14 @@ M.BarSection {
function _parse(text) {
const t = text.trim();
const sep = t.indexOf(":");
root.state = sep === -1 ? t : t.slice(0, sep);
root.state = sep === -1 ? t : t.slice(0, sep);
root.device = sep === -1 ? "" : t.slice(sep + 1);
}
Process {
id: proc
running: true
command: ["sh", "-c",
"s=$(bluetoothctl show 2>/dev/null); " +
"[ -z \"$s\" ] && echo unavailable && exit; " +
"echo \"$s\" | grep -q 'Powered: yes' || { echo off:; exit; }; " +
"d=$(bluetoothctl info 2>/dev/null | awk -F': ' '/\\tName:/{n=$2}/Connected: yes/{c=1}END{if(c)print n}'); " +
"[ -n \"$d\" ] && echo \"connected:$d\" || echo on:"
]
command: ["sh", "-c", "s=$(bluetoothctl show 2>/dev/null); " + "[ -z \"$s\" ] && echo unavailable && exit; " + "echo \"$s\" | grep -q 'Powered: yes' || { echo off:; exit; }; " + "d=$(bluetoothctl info 2>/dev/null | awk -F': ' '/\\tName:/{n=$2}/Connected: yes/{c=1}END{if(c)print n}'); " + "[ -n \"$d\" ] && echo \"connected:$d\" || echo on:"]
stdout: StdioCollector {
onStreamFinished: root._parse(text)
}
@ -47,14 +43,13 @@ M.BarSection {
id: toggle
property string cmd: ""
command: ["bluetoothctl", "power", cmd]
onRunningChanged: if (!running && cmd !== "") proc.running = true
onRunningChanged: if (!running && cmd !== "")
proc.running = true
}
M.BarIcon {
icon: "\uF294"
color: root.state === "connected" ? M.Theme.base0D
: root.state === "off" ? M.Theme.base04
: M.Theme.base0D
color: root.state === "connected" ? M.Theme.base0D : root.state === "off" ? M.Theme.base04 : M.Theme.base0D
anchors.verticalCenter: parent.verticalCenter
}
M.BarLabel {

View file

@ -20,10 +20,7 @@ PanelWindow {
// Flush below bar, centered on hovered item
margins.top: 0
margins.left: Math.max(0, Math.min(
Math.round(M.FlyoutState.itemX - implicitWidth / 2),
screen.width - implicitWidth
))
margins.left: Math.max(0, Math.min(Math.round(M.FlyoutState.itemX - implicitWidth / 2), screen.width - implicitWidth))
implicitWidth: label.implicitWidth + M.Theme.barPadding * 2
implicitHeight: label.implicitHeight + M.Theme.barPadding * 2

View file

@ -25,7 +25,7 @@ QtObject {
property bool memory: true
property bool disk: true
property bool battery: true
property bool wlogout: true
property bool power: true
property var weatherArgs: ["--nerd"]

View file

@ -1,5 +1,6 @@
import QtQuick
import Quickshell
import Quickshell.Io
import "." as M
M.BarIcon {

View file

@ -10,7 +10,7 @@ PanelWindow {
required property var screen
required property real anchorX
signal menuClosed()
signal menuClosed
signal runCommand(var cmd)
readonly property bool _isNiri: Quickshell.env("NIRI_SOCKET") !== ""
@ -40,10 +40,7 @@ PanelWindow {
Item {
id: panel
x: Math.max(0, Math.min(
Math.round(menuWindow.anchorX - menuCol.width / 2),
menuWindow.width - menuCol.width
))
x: Math.max(0, Math.min(Math.round(menuWindow.anchorX - menuCol.width / 2), menuWindow.width - menuCol.width))
y: 0
width: menuCol.width
@ -73,11 +70,36 @@ PanelWindow {
Repeater {
model: [
{ label: "Lock", icon: "\uF023", cmd: ["loginctl", "lock-session"], color: M.Theme.base0D },
{ label: "Suspend", icon: "\uF186", cmd: ["systemctl", "suspend"], color: M.Theme.base0E },
{ label: "Logout", icon: "\uF2F5", cmd: menuWindow._isNiri ? ["niri", "msg", "action", "quit"] : ["loginctl", "terminate-user", ""], color: M.Theme.base0A },
{ label: "Reboot", icon: "\uF021", cmd: ["systemctl", "reboot"], color: M.Theme.base09 },
{ label: "Shutdown", icon: "\uF011", cmd: ["systemctl", "poweroff"], color: M.Theme.base08 }
{
label: "Lock",
icon: "\uF023",
cmd: ["loginctl", "lock-session"],
color: M.Theme.base0D
},
{
label: "Suspend",
icon: "\uF186",
cmd: ["systemctl", "suspend"],
color: M.Theme.base0E
},
{
label: "Logout",
icon: "\uF2F5",
cmd: menuWindow._isNiri ? ["niri", "msg", "action", "quit"] : ["loginctl", "terminate-user", ""],
color: M.Theme.base0A
},
{
label: "Reboot",
icon: "\uF021",
cmd: ["systemctl", "reboot"],
color: M.Theme.base09
},
{
label: "Shutdown",
icon: "\uF011",
cmd: ["systemctl", "poweroff"],
color: M.Theme.base08
}
]
delegate: Item {

View file

@ -8,9 +8,7 @@ M.BarIcon {
property string profile: ""
color: root.profile === "performance" ? M.Theme.base09
: root.profile === "power-saver" ? M.Theme.base0B
: M.Theme.base05
color: root.profile === "performance" ? M.Theme.base09 : root.profile === "power-saver" ? M.Theme.base0B : M.Theme.base05
icon: {
if (root.profile === "performance")

View file

@ -70,8 +70,24 @@ Item {
}
}
Corner { corner: 0; anchors.top: true; anchors.left: true }
Corner { corner: 1; anchors.top: true; anchors.right: true }
Corner { corner: 2; anchors.bottom: true; anchors.left: true }
Corner { corner: 3; anchors.bottom: true; anchors.right: true }
Corner {
corner: 0
anchors.top: true
anchors.left: true
}
Corner {
corner: 1
anchors.top: true
anchors.right: true
}
Corner {
corner: 2
anchors.bottom: true
anchors.left: true
}
Corner {
corner: 3
anchors.bottom: true
anchors.right: true
}
}

View file

@ -8,9 +8,7 @@ M.BarSection {
tooltip: "Temperature: " + root.celsius + "\u00B0C"
property int celsius: 0
readonly property color _stateColor: celsius > 80 ? M.Theme.base08
: celsius > 60 ? M.Theme.base09
: M.Theme.base0C
readonly property color _stateColor: celsius > 80 ? M.Theme.base08 : celsius > 60 ? M.Theme.base09 : M.Theme.base0C
FileView {
id: thermal

View file

@ -30,8 +30,7 @@ RowLayout {
HoverHandler {
onHoveredChanged: {
const tip = [iconItem.modelData.tooltipTitle, iconItem.modelData.tooltipDescription]
.filter(s => s).join("\n") || iconItem.modelData.title;
const tip = [iconItem.modelData.tooltipTitle, iconItem.modelData.tooltipDescription].filter(s => s).join("\n") || iconItem.modelData.title;
if (hovered && tip) {
M.FlyoutState.text = tip;
M.FlyoutState.itemX = iconItem.mapToGlobal(iconItem.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0);

View file

@ -13,7 +13,7 @@ PanelWindow {
required property var screen
required property real anchorX
signal menuClosed()
signal menuClosed
// Current menu level swapped when entering/leaving submenus
property var _currentHandle: handle
@ -41,10 +41,7 @@ PanelWindow {
Item {
id: panel
x: Math.max(0, Math.min(
Math.round(menuWindow.anchorX - menuCol.width / 2),
menuWindow.width - menuCol.width
))
x: Math.max(0, Math.min(Math.round(menuWindow.anchorX - menuCol.width / 2), menuWindow.width - menuCol.width))
y: 0
width: menuCol.width
@ -143,8 +140,7 @@ PanelWindow {
anchors.fill: parent
anchors.leftMargin: 4
anchors.rightMargin: 4
color: rowArea.containsMouse && entryItem.modelData.enabled
? M.Theme.base02 : "transparent"
color: rowArea.containsMouse && entryItem.modelData.enabled ? M.Theme.base02 : "transparent"
radius: M.Theme.radius
}

View file

@ -14,7 +14,8 @@ M.BarSection {
property string _title: ""
property string _appId: ""
readonly property string _iconSource: {
if (!root._appId) return "";
if (!root._appId)
return "";
const entry = DesktopEntries.heuristicLookup(root._appId);
return entry ? Quickshell.iconPath(entry.icon) : "";
}

View file

@ -44,8 +44,7 @@ Row {
try {
const ev = JSON.parse(line);
if (ev.WorkspacesChanged !== undefined) {
root._allWorkspaces = ev.WorkspacesChanged.workspaces
.sort((a, b) => a.idx - b.idx);
root._allWorkspaces = ev.WorkspacesChanged.workspaces.sort((a, b) => a.idx - b.idx);
} else if (ev.WorkspaceActivated !== undefined) {
if (ev.WorkspaceActivated.focused)
root._activeId = ev.WorkspaceActivated.id;

View file

@ -4,6 +4,7 @@ singleton FlyoutState 1.0 FlyoutState.qml
singleton OsdState 1.0 OsdState.qml
singleton Modules 1.0 Modules.qml
Bar 1.0 Bar.qml
BarGroup 1.0 BarGroup.qml
BarSection 1.0 BarSection.qml
Flyout 1.0 Flyout.qml
Workspaces 1.0 Workspaces.qml
@ -29,4 +30,4 @@ Weather 1.0 Weather.qml
PowerProfile 1.0 PowerProfile.qml
IdleInhibitor 1.0 IdleInhibitor.qml
Notifications 1.0 Notifications.qml
Wlogout 1.0 Wlogout.qml
Power 1.0 Power.qml

View file

@ -56,33 +56,37 @@ in
description = "Enable or disable individual bar modules.";
default = { };
type = lib.types.submodule {
options = lib.genAttrs
[
"workspaces"
"tray"
"windowTitle"
"clock"
"notifications"
"mpris"
"volume"
"bluetooth"
"backlight"
"network"
"powerProfile"
"idleInhibitor"
"weather"
"temperature"
"cpu"
"memory"
"disk"
"battery"
"wlogout"
]
(name: lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable the ${name} module.";
});
options =
lib.genAttrs
[
"workspaces"
"tray"
"windowTitle"
"clock"
"notifications"
"mpris"
"volume"
"bluetooth"
"backlight"
"network"
"powerProfile"
"idleInhibitor"
"weather"
"temperature"
"cpu"
"memory"
"disk"
"battery"
"power"
]
(
name:
lib.mkOption {
type = lib.types.bool;
default = true;
description = "Enable the ${name} module.";
}
);
};
};
@ -123,12 +127,11 @@ in
};
config = lib.mkIf cfg.enable {
home.packages =
[
self.packages.${pkgs.stdenv.hostPlatform.system}.nova-shell-cli
pkgs.nerd-fonts.symbols-only
]
++ lib.optional cfg.modules.weather pkgs.wttrbar;
home.packages = [
self.packages.${pkgs.stdenv.hostPlatform.system}.nova-shell-cli
pkgs.nerd-fonts.symbols-only
]
++ lib.optional cfg.modules.weather pkgs.wttrbar;
xdg.configFile."nova-shell/modules.json".source =
(pkgs.formats.json { }).generate "nova-shell-modules.json"