Compare commits

...

3 commits

Author SHA1 Message Date
Damocles
5886f39b08 overview: background layer clock and date visible in niri overview gaps 2026-04-12 19:11:26 +02:00
Damocles
b80071db5c nix fmt 2026-04-12 19:05:24 +02:00
Damocles
367520df6a remove dead OsdState/Osd code, update README 2026-04-12 19:03:22 +02:00
7 changed files with 95 additions and 192 deletions

View file

@ -10,18 +10,19 @@ exactly when you should be most suspicious.
## "Features"
- Bar with workspaces, window title, clock, tray, and a regrettable number of widgets
- Niri IPC integration — workspace indicator, focused window title, power menu with `niri msg action quit`
- Context menus for tray icons with submenu support, because the robot watched celestia-shell do it and got jealous
- Per-module accent colors that change based on state (battery goes red when it's dying, bluetooth goes blue when connected, volume dims when muted — genuinely useful, by accident)
- 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)
- Bar with workspaces, window title, clock, tray, and a regrettable number of widgets grouped into color-coded sections with glowing borders, because subtlety is dead
- Niri IPC integration — workspace indicator, focused window title with app icon, power menu with `niri msg action quit`. All event-driven, no polling
- Interactive hover panels — volume (with per-app mixer and output device switcher), brightness (with slider), media (with album art, transport controls, progress bar). Hover to peek, click to expand, leave to dismiss. The OSD and tooltip in one, because having three separate UI patterns for the same information was too reasonable
- Context menus for tray icons with submenu support, network chooser (known available WiFi/ethernet), bluetooth device manager (connect/disconnect, battery levels)
- Privacy indicators — screenshare and microphone icons pulse red/green when PipeWire detects active video/audio capture streams. Finally, you'll know when your webcam is on, which is more than can be said for most laptop manufacturers
- Per-module accent colors that change based on state, with animated transitions. Battery blinks when critical and sends desktop notifications, because the robot cares about your hardware more than you do
- Screen corner rounding — tiny overlay windows with quarter-circle masks, click-transparent, 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
- 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
- Event-driven updates for network, bluetooth, and power profiles via dbus-monitor/nmcli monitor — no more 5-second polling lag
- Animated everything: flyout tooltips slide in/out, modules fade on visibility change, icons crossfade on state change, notification count pops. The bar is basically a screensaver at this point
- Home Manager module with stylix integration, per-module config objects (enable/disable + module-specific settings like polling intervals, thresholds, brightness step), and a theme system that hot-reloads
- treefmt + nixfmt for formatting, because even AI slop deserves consistent indentation
- Checks via `nix flake check` (the irony of testing AI garbage is not lost on anyone)
## Installation

View file

@ -1,146 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import "." as M
// OSD overlay slides out from the bar, centered on screen.
PanelWindow {
id: root
required property var screen
visible: _winVisible
color: "transparent"
property bool _winVisible: false
property bool _shown: M.OsdState.visible && M.OsdState.screen === root.screen
on_ShownChanged: {
if (_shown) {
_winVisible = true;
hideAnim.stop();
showAnim.start();
} else {
showAnim.stop();
hideAnim.start();
}
}
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.exclusiveZone: 0
WlrLayershell.namespace: "nova-osd"
mask: Region {}
anchors.top: true
anchors.left: true
margins.top: 0
margins.left: Math.max(0, Math.min(Math.round(M.OsdState.itemX - implicitWidth / 2), screen.width - implicitWidth))
implicitWidth: 200
implicitHeight: 48
ParallelAnimation {
id: showAnim
NumberAnimation {
target: content
property: "opacity"
to: 1
duration: 150
easing.type: Easing.OutCubic
}
NumberAnimation {
target: content
property: "y"
to: 0
duration: 200
easing.type: Easing.OutCubic
}
}
ParallelAnimation {
id: hideAnim
NumberAnimation {
target: content
property: "opacity"
to: 0
duration: 250
easing.type: Easing.InCubic
}
NumberAnimation {
target: content
property: "y"
to: -content.height
duration: 250
easing.type: Easing.InCubic
}
onFinished: root._winVisible = false
}
Item {
id: content
anchors.left: parent.left
anchors.right: parent.right
height: root.implicitHeight
opacity: 0
y: -height
Rectangle {
anchors.fill: parent
color: M.Theme.base00
opacity: Math.max(M.Theme.barOpacity, 0.85)
topLeftRadius: 0
topRightRadius: 0
bottomLeftRadius: M.Theme.radius
bottomRightRadius: M.Theme.radius
}
Row {
anchors.centerIn: parent
spacing: 10
Text {
text: M.OsdState.icon
color: M.Theme.base0D
font.pixelSize: M.Theme.fontSize + 6
font.family: M.Theme.iconFontFamily
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: 120
height: 6
anchors.verticalCenter: parent.verticalCenter
Rectangle {
anchors.fill: parent
color: M.Theme.base02
radius: 3
}
Rectangle {
width: parent.width * Math.min(1, Math.max(0, M.OsdState.value))
height: parent.height
color: M.Theme.base0D
radius: 3
Behavior on width {
NumberAnimation {
duration: 120
easing.type: Easing.OutCubic
}
}
}
}
Text {
text: Math.round(M.OsdState.value * 100) + "%"
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize
font.family: M.Theme.fontFamily
anchors.verticalCenter: parent.verticalCenter
width: 32
}
}
}
}

View file

@ -1,24 +0,0 @@
pragma Singleton
import QtQuick
QtObject {
property bool visible: false
property real value: 0 // 0.01.0
property string icon: ""
property real itemX: 0
property var screen: null
property Timer _timer: Timer {
interval: 1500
onTriggered: visible = false
}
function show(val, ico, x, scr) {
value = val;
icon = ico;
itemX = x;
screen = scr;
visible = true;
_timer.restart();
}
}

51
modules/Overview.qml Normal file
View file

@ -0,0 +1,51 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import "." as M
PanelWindow {
id: root
required property var screen
color: "transparent"
WlrLayershell.layer: WlrLayer.Background
WlrLayershell.exclusiveZone: -1
WlrLayershell.namespace: "nova-overview"
mask: Region {}
anchors.top: true
anchors.left: true
anchors.right: true
anchors.bottom: true
SystemClock {
id: clock
precision: SystemClock.Minutes
}
Column {
anchors.centerIn: parent
spacing: 8
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: Qt.formatDateTime(clock.date, "HH:mm")
color: M.Theme.base05
opacity: 0.7
font.pixelSize: 72
font.family: M.Theme.fontFamily
font.bold: true
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: Qt.formatDateTime(clock.date, "dddd, dd MMMM yyyy")
color: M.Theme.base05
opacity: 0.5
font.pixelSize: 18
font.family: M.Theme.fontFamily
}
}
}

View file

@ -9,21 +9,27 @@ Row {
// Only detect active client streams, not hardware sources/devices
readonly property bool _videoCapture: {
if (!Pipewire.nodes) return false;
if (!Pipewire.nodes)
return false;
for (const node of Pipewire.nodes.values) {
if (!node.isStream) continue;
if (!node.isStream)
continue;
const mc = node.properties?.["media.class"] ?? "";
if (mc === "Stream/Input/Video" || mc === "Stream/Output/Video") return true;
if (mc === "Stream/Input/Video" || mc === "Stream/Output/Video")
return true;
}
return false;
}
readonly property bool _audioIn: {
if (!Pipewire.nodes) return false;
if (!Pipewire.nodes)
return false;
for (const node of Pipewire.nodes.values) {
if (!node.isStream) continue;
if (!node.isStream)
continue;
const mc = node.properties?.["media.class"] ?? "";
if (mc === "Stream/Input/Audio") return true;
if (mc === "Stream/Input/Audio")
return true;
}
return false;
}
@ -51,8 +57,16 @@ Row {
SequentialAnimation on opacity {
running: root._videoCapture
loops: Animation.Infinite
NumberAnimation { to: 0.4; duration: 600; easing.type: Easing.InOutQuad }
NumberAnimation { to: 1; duration: 600; easing.type: Easing.InOutQuad }
NumberAnimation {
to: 0.4
duration: 600
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 1
duration: 600
easing.type: Easing.InOutQuad
}
}
}
@ -77,8 +91,16 @@ Row {
SequentialAnimation on opacity {
running: root._audioIn
loops: Animation.Infinite
NumberAnimation { to: 0.4; duration: 600; easing.type: Easing.InOutQuad }
NumberAnimation { to: 1; duration: 600; easing.type: Easing.InOutQuad }
NumberAnimation {
to: 0.4
duration: 600
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 1
duration: 600
easing.type: Easing.InOutQuad
}
}
}
}

View file

@ -1,7 +1,6 @@
module modules
singleton Theme 1.0 Theme.qml
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
@ -16,7 +15,6 @@ TrayMenu 1.0 TrayMenu.qml
PopupPanel 1.0 PopupPanel.qml
PowerMenu 1.0 PowerMenu.qml
ScreenCorners 1.0 ScreenCorners.qml
Osd 1.0 Osd.qml
ThemedIcon 1.0 ThemedIcon.qml
Battery 1.0 Battery.qml
Mpris 1.0 Mpris.qml
@ -35,3 +33,4 @@ IdleInhibitor 1.0 IdleInhibitor.qml
Notifications 1.0 Notifications.qml
Power 1.0 Power.qml
Privacy 1.0 Privacy.qml
Overview 1.0 Overview.qml

View file

@ -19,7 +19,7 @@ ShellRoot {
screen: scope.modelData
}
Osd {
Overview {
screen: scope.modelData
}