initial commit

This commit is contained in:
Damocles 2026-04-10 10:49:48 +02:00
commit 9fde6d4fc6
22 changed files with 1110 additions and 0 deletions

38
modules/Backlight.qml Normal file
View file

@ -0,0 +1,38 @@
import QtQuick
import Quickshell.Io
import "." as M
Row {
id: root
spacing: 4
visible: percent > 0
property int percent: 0
FileView {
id: current
path: "/sys/class/backlight/intel_backlight/brightness"
watchChanges: true
onFileChanged: reload()
onLoaded: root._update()
}
FileView {
id: max
path: "/sys/class/backlight/intel_backlight/max_brightness"
onLoaded: root._update()
}
function _update() {
const c = parseInt(current.text());
const m = parseInt(max.text());
if (m > 0) root.percent = Math.round((c / m) * 100);
}
Text {
text: root.percent + "% "
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize
font.family: M.Theme.fontFamily
anchors.verticalCenter: parent.verticalCenter
}
}

79
modules/Bar.qml Normal file
View file

@ -0,0 +1,79 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import "." as M
PanelWindow {
id: bar
required property var screen
color: "transparent"
anchors {
top: true
left: true
right: true
}
implicitHeight: M.Theme.barHeight
exclusiveZone: implicitHeight
Rectangle {
anchors.fill: parent
color: M.Theme.base00
opacity: M.Theme.barOpacity
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
spacing: 8
// ---- left ----
RowLayout {
Layout.alignment: Qt.AlignLeft
spacing: 8
M.Workspaces {}
M.Tray { bar: bar }
M.WindowTitle { Layout.maximumWidth: 400 }
}
Item { Layout.fillWidth: true }
// ---- center ----
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 8
M.Clock {}
M.Notifications {}
}
Item { Layout.fillWidth: true }
// ---- right ----
RowLayout {
Layout.alignment: Qt.AlignRight
spacing: 12
M.Mpris {}
M.Volume {}
M.Bluetooth {}
M.Backlight {}
M.Network {}
M.PowerProfile {}
M.IdleInhibitor {}
M.Weather {}
M.Temperature {}
M.Cpu {}
M.Memory {}
M.Disk {}
M.Battery {}
M.Wlogout {}
}
}
}

32
modules/Battery.qml Normal file
View file

@ -0,0 +1,32 @@
import QtQuick
import Quickshell.Services.UPower
import "." as M
Row {
id: root
spacing: 4
visible: UPower.displayDevice?.isLaptopBattery ?? false
readonly property var dev: UPower.displayDevice
readonly property real pct: (dev?.percentage ?? 0) * 100
readonly property bool charging: dev?.state === UPowerDeviceState.Charging
Text {
text: {
if (root.charging) return "";
const icons = ["󰂎","󰁺","󰁻","󰁼","󰁽","󰁾","󰁿","󰂀","󰂁","󰂂","󱟢"];
return icons[Math.min(10, Math.floor(root.pct / 10))];
}
color: root.pct < 15 ? M.Theme.base08 : M.Theme.base05
font.pixelSize: M.Theme.fontSize + 2
font.family: M.Theme.fontFamily
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: Math.round(root.pct) + "%"
color: root.pct < 15 ? M.Theme.base08 : M.Theme.base05
font.pixelSize: M.Theme.fontSize
font.family: M.Theme.fontFamily
anchors.verticalCenter: parent.verticalCenter
}
}

38
modules/Bluetooth.qml Normal file
View file

@ -0,0 +1,38 @@
import QtQuick
import Quickshell.Io
import "." as M
Row {
id: root
spacing: 4
property string status: "off"
property string device: ""
Process {
id: proc
running: true
command: ["sh", "-c", "bluetoothctl info 2>/dev/null | awk -F': ' '/Name/ {n=$2} /Connected: yes/ {c=1} END {if (c) print n; else print \"\"}'"]
stdout: StdioCollector {
onStreamFinished: {
const t = text.trim();
root.device = t;
root.status = t ? "connected" : "on";
}
}
}
Timer {
interval: 10000
running: true
repeat: true
onTriggered: proc.running = true
}
Text {
text: root.status === "connected" ? (" " + root.device) : ""
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize + 1
font.family: M.Theme.fontFamily
anchors.verticalCenter: parent.verticalCenter
}
}

16
modules/Clock.qml Normal file
View file

@ -0,0 +1,16 @@
import QtQuick
import Quickshell
import "." as M
Text {
SystemClock {
id: clock
precision: SystemClock.Minutes
}
text: Qt.formatDateTime(clock.date, "ddd, dd. MMM HH:mm")
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize + 1
font.family: M.Theme.fontFamily
verticalAlignment: Text.AlignVCenter
}

51
modules/Cpu.qml Normal file
View file

@ -0,0 +1,51 @@
import QtQuick
import Quickshell.Io
import "." as M
Text {
id: root
property int usage: 0
property real freqGhz: 0
property var _prev: null
FileView {
id: stat
path: "/proc/stat"
onLoaded: {
const line = text().split("\n")[0];
const f = line.trim().split(/\s+/).slice(1).map(Number);
const idle = f[3] + f[4];
const total = f.reduce((a, b) => a + b, 0);
if (root._prev) {
const dIdle = idle - root._prev.idle;
const dTotal = total - root._prev.total;
if (dTotal > 0) root.usage = Math.round((1 - dIdle / dTotal) * 100);
}
root._prev = { idle, total };
}
}
FileView {
id: cpuinfo
path: "/proc/cpuinfo"
onLoaded: {
const lines = text().split("\n").filter(l => l.startsWith("cpu MHz"));
if (lines.length === 0) return;
const sum = lines.reduce((a, l) => a + parseFloat(l.split(":")[1]), 0);
root.freqGhz = sum / lines.length / 1000;
}
}
Timer {
interval: 1000
running: true
repeat: true
onTriggered: { stat.reload(); cpuinfo.reload(); }
}
text: " " + root.usage.toString().padStart(2) + "%@" + root.freqGhz.toFixed(2)
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize
font.family: M.Theme.fontFamily
verticalAlignment: Text.AlignVCenter
}

38
modules/Disk.qml Normal file
View file

@ -0,0 +1,38 @@
import QtQuick
import Quickshell.Io
import "." as M
Text {
id: root
property int freePct: 0
property real totalTb: 0
Process {
id: proc
running: true
command: ["sh", "-c", "df -B1 --output=size,avail / | tail -1"]
stdout: StdioCollector {
onStreamFinished: {
const parts = text.trim().split(/\s+/).map(Number);
const size = parts[0], avail = parts[1];
if (size > 0) {
root.freePct = Math.round((avail / size) * 100);
root.totalTb = size / 1e12;
}
}
}
}
Timer {
interval: 30000
running: true
repeat: true
onTriggered: proc.running = true
}
text: " " + root.freePct + "% " + root.totalTb.toFixed(1)
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize
font.family: M.Theme.fontFamily
verticalAlignment: Text.AlignVCenter
}

31
modules/IdleInhibitor.qml Normal file
View file

@ -0,0 +1,31 @@
import QtQuick
import Quickshell.Io
import "." as M
Text {
id: root
property bool active: false
text: root.active ? "" : ""
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize + 1
font.family: M.Theme.fontFamily
verticalAlignment: Text.AlignVCenter
Process {
id: toggle
command: ["sh", "-c", root.active
? "pkill -x systemd-inhibit || true"
: "systemd-inhibit --what=idle --who=nova-shell --why=user sleep infinity &"]
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
root.active = !root.active;
toggle.running = true;
}
}
}

36
modules/Memory.qml Normal file
View file

@ -0,0 +1,36 @@
import QtQuick
import Quickshell.Io
import "." as M
Text {
id: root
property int percent: 0
FileView {
id: meminfo
path: "/proc/meminfo"
onLoaded: {
const m = {};
text().split("\n").forEach(l => {
const [k, v] = l.split(":");
if (v) m[k.trim()] = parseInt(v.trim());
});
const total = m.MemTotal;
const avail = m.MemAvailable;
if (total > 0) root.percent = Math.round(((total - avail) / total) * 100);
}
}
Timer {
interval: 2000
running: true
repeat: true
onTriggered: meminfo.reload()
}
text: " " + root.percent + "%"
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize
font.family: M.Theme.fontFamily
verticalAlignment: Text.AlignVCenter
}

32
modules/Mpris.qml Normal file
View file

@ -0,0 +1,32 @@
import QtQuick
import Quickshell.Services.Mpris
import "." as M
Row {
id: root
spacing: 4
visible: player !== null
readonly property MprisPlayer player: Mpris.players.values[0] ?? null
readonly property bool playing: player?.playbackState === MprisPlaybackState.Playing
Text {
text: root.playing ? "" : (root.player?.playbackState === MprisPlaybackState.Paused ? "󰏤" : "󰓛")
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize + 1
font.family: M.Theme.fontFamily
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: root.player?.identity ?? ""
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize
font.family: M.Theme.fontFamily
anchors.verticalCenter: parent.verticalCenter
}
MouseArea {
anchors.fill: parent
onClicked: root.player?.togglePlaying()
}
}

51
modules/Network.qml Normal file
View file

@ -0,0 +1,51 @@
import QtQuick
import Quickshell.Io
import "." as M
Row {
id: root
spacing: 4
property string ifname: ""
property string essid: ""
property string state: "disconnected"
Process {
id: proc
running: true
command: ["sh", "-c", "nmcli -t -f NAME,TYPE,DEVICE connection show --active | head -1"]
stdout: StdioCollector {
onStreamFinished: {
const line = text.trim();
if (!line) {
root.state = "disconnected";
root.essid = "";
root.ifname = "";
return;
}
const parts = line.split(":");
root.essid = parts[0] || "";
root.ifname = parts[2] || "";
root.state = (parts[1] || "").includes("wireless") ? "wifi" : "eth";
}
}
}
Timer {
interval: 5000
running: true
repeat: true
onTriggered: proc.running = true
}
Text {
text: {
if (root.state === "wifi") return " " + root.essid;
if (root.state === "eth") return "󰈀";
return "󰣽";
}
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize + 1
font.family: M.Theme.fontFamily
anchors.verticalCenter: parent.verticalCenter
}
}

58
modules/Notifications.qml Normal file
View file

@ -0,0 +1,58 @@
import QtQuick
import Quickshell.Io
import "." as M
Row {
id: root
spacing: 4
property int count: 0
property bool dnd: false
Process {
id: sub
running: true
command: ["swaync-client", "--subscribe-waybar"]
stdout: SplitParser {
splitMarker: "\n"
onRead: (line) => {
try {
const d = JSON.parse(line);
root.count = d.count ?? 0;
root.dnd = (d.class ?? "").includes("dnd");
} catch (e) {}
}
}
}
Text {
text: {
if (root.dnd) return root.count > 0 ? "󰂠" : "󰪓";
return root.count > 0 ? "󱅫" : "󰂜";
}
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize + 2
font.family: M.Theme.fontFamily
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: root.count > 0 ? String(root.count) : ""
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize
font.family: M.Theme.fontFamily
anchors.verticalCenter: parent.verticalCenter
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (m) => {
const cmd = m.button === Qt.RightButton
? ["swaync-client", "--toggle-dnd", "--skip-wait"]
: ["swaync-client", "--toggle-panel", "--skip-wait"];
clicker.command = cmd;
clicker.running = true;
}
}
Process { id: clicker }
}

35
modules/PowerProfile.qml Normal file
View file

@ -0,0 +1,35 @@
import QtQuick
import Quickshell.Io
import "." as M
Text {
id: root
property string profile: ""
Process {
id: proc
running: true
command: ["powerprofilesctl", "get"]
stdout: StdioCollector {
onStreamFinished: root.profile = text.trim()
}
}
Timer {
interval: 5000
running: true
repeat: true
onTriggered: proc.running = true
}
text: {
if (root.profile === "performance") return "";
if (root.profile === "power-saver") return "";
if (root.profile === "balanced") return "";
return "";
}
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize + 1
font.family: M.Theme.fontFamily
verticalAlignment: Text.AlignVCenter
}

27
modules/Temperature.qml Normal file
View file

@ -0,0 +1,27 @@
import QtQuick
import Quickshell.Io
import "." as M
Text {
id: root
property int celsius: 0
FileView {
id: thermal
path: "/sys/class/thermal/thermal_zone0/temp"
onLoaded: root.celsius = Math.round(parseInt(text()) / 1000)
}
Timer {
interval: 2000
running: true
repeat: true
onTriggered: thermal.reload()
}
text: " " + root.celsius + "°C"
color: root.celsius > 80 ? M.Theme.base08 : M.Theme.base05
font.pixelSize: M.Theme.fontSize
font.family: M.Theme.fontFamily
verticalAlignment: Text.AlignVCenter
}

53
modules/Theme.qml Normal file
View file

@ -0,0 +1,53 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
QtObject {
id: root
// base16 palette, overwritten from ~/.config/nova-shell/theme.json
property color base00: "#1e1e2e"
property color base01: "#181825"
property color base02: "#313244"
property color base03: "#45475a"
property color base04: "#585b70"
property color base05: "#cdd6f4"
property color base06: "#f5e0dc"
property color base07: "#b4befe"
property color base08: "#f38ba8"
property color base09: "#fab387"
property color base0A: "#f9e2af"
property color base0B: "#a6e3a1"
property color base0C: "#94e2d5"
property color base0D: "#89b4fa"
property color base0E: "#cba6f7"
property color base0F: "#f2cdcd"
property string fontFamily: "sans-serif"
property int fontSize: 12
property real barOpacity: 0.9
property int barHeight: 32
property FileView _themeFile: FileView {
path: (Quickshell.env("XDG_CONFIG_HOME") || (Quickshell.env("HOME") + "/.config"))
+ "/nova-shell/theme.json"
watchChanges: true
onFileChanged: reload()
onLoaded: root._apply(text())
}
function _apply(raw) {
let data;
try { data = JSON.parse(raw); } catch (e) { return; }
const c = data.colors || {};
for (const k of Object.keys(c)) {
if (k in root) root[k] = c[k];
}
if (data.fontFamily) root.fontFamily = data.fontFamily;
if (data.fontSize) root.fontSize = data.fontSize;
if (data.barOpacity !== undefined) root.barOpacity = data.barOpacity;
if (data.barHeight !== undefined) root.barHeight = data.barHeight;
}
}

40
modules/Tray.qml Normal file
View file

@ -0,0 +1,40 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.SystemTray
RowLayout {
id: root
spacing: 6
required property var bar
Repeater {
model: SystemTray.items
delegate: Item {
id: iconItem
required property SystemTrayItem modelData
implicitWidth: 18
implicitHeight: 18
IconImage {
anchors.fill: parent
source: iconItem.modelData.icon
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse) => {
if (mouse.button === Qt.LeftButton) {
iconItem.modelData.activate();
} else if (mouse.button === Qt.RightButton) {
iconItem.modelData.display(root.bar, mouse.x, mouse.y);
}
}
}
}
}
}

37
modules/Volume.qml Normal file
View file

@ -0,0 +1,37 @@
import QtQuick
import Quickshell.Services.Pipewire
import "." as M
Row {
id: root
spacing: 4
PwObjectTracker {
objects: [Pipewire.defaultAudioSink]
}
readonly property var sink: Pipewire.defaultAudioSink
readonly property real volume: sink?.audio?.volume ?? 0
readonly property bool muted: sink?.audio?.muted ?? false
Text {
text: root.muted ? "" : (root.volume > 0.5 ? "" : (root.volume > 0 ? "" : ""))
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize + 1
font.family: M.Theme.fontFamily
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: Math.round(root.volume * 100) + "%"
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize
font.family: M.Theme.fontFamily
anchors.verticalCenter: parent.verticalCenter
}
MouseArea {
anchors.fill: parent
onClicked: if (root.sink?.audio) root.sink.audio.muted = !root.sink.audio.muted
}
}

37
modules/Weather.qml Normal file
View file

@ -0,0 +1,37 @@
import QtQuick
import Quickshell.Io
import "." as M
Text {
id: root
property string label: ""
Process {
id: proc
running: true
command: ["wttrbar", "--nerd"]
stdout: StdioCollector {
onStreamFinished: {
try {
const data = JSON.parse(text);
root.label = data.text ?? "";
} catch (e) {
root.label = "";
}
}
}
}
Timer {
interval: 3600000
running: true
repeat: true
onTriggered: proc.running = true
}
text: root.label
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize
font.family: M.Theme.fontFamily
verticalAlignment: Text.AlignVCenter
}

12
modules/WindowTitle.qml Normal file
View file

@ -0,0 +1,12 @@
import QtQuick
import Quickshell.Services.Niri
import "." as M
Text {
text: Niri.focusedWindow?.title ?? ""
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize
font.family: M.Theme.fontFamily
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}

19
modules/Wlogout.qml Normal file
View file

@ -0,0 +1,19 @@
import QtQuick
import Quickshell.Io
import "." as M
Text {
text: ""
color: M.Theme.base05
font.pixelSize: M.Theme.fontSize + 2
font.family: M.Theme.fontFamily
verticalAlignment: Text.AlignVCenter
Process { id: proc; command: ["wlogout"] }
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: proc.running = true
}
}

Some files were not shown because too many files have changed in this diff Show more