diff --git a/.config/hypr/config/autostart.conf b/.config/hypr/config/autostart.conf
index c107eb0..63d7bba 100644
--- a/.config/hypr/config/autostart.conf
+++ b/.config/hypr/config/autostart.conf
@@ -5,7 +5,7 @@
source = ~/.config/hypr/config/defaults.conf
# Autostart wiki https://wiki.hyprland.org/0.45.0/Configuring/Keywords/#executing #
-exec-once = /home/michaelb/.config/hypr/scripts/launch-waybar.sh &
+exec-once = qs
exec-once = blueman-applet &
exec-once = fcitx5 -d &
exec-once = mako &
diff --git a/.config/hypr/config/monitor.conf b/.config/hypr/config/monitor.conf
index 87dab07..ee68e57 100644
--- a/.config/hypr/config/monitor.conf
+++ b/.config/hypr/config/monitor.conf
@@ -7,8 +7,8 @@
monitor = eDP-2, highres@highrr, 0x0, 1, vrr, 1
#monitor = HDMI-A-1, highres@highrr, auto-right, 1, transform, 1
-monitor = desc:Samsung Electric Company S24F350 H4ZR302705, highres@highrr, auto-right, 1
-monitor = desc:Samsung Electric Company S24F350 H4ZK111233, highres@highrr, auto-right, 1, transform, 1
+#monitor = desc:Samsung Electric Company S24F350 H4ZR302705, highres@highrr, auto-right, 1
+monitor = desc:Samsung Electric Company S24F350 H4ZK111233, highres@highrr, 1920x0, 1#, transform, 1
# monitor = HDMI-A-1, highres@highrr, auto-right, 1, vrr, 0
diff --git a/.config/quickshell/Bar.qml b/.config/quickshell/Bar.qml
new file mode 100644
index 0000000..9efc733
--- /dev/null
+++ b/.config/quickshell/Bar.qml
@@ -0,0 +1,60 @@
+// Bar.qml - top panel
+// Neighbouring types (Pill, ClockWidget, Theme, etc.) are auto-imported by QuickShell.
+import Quickshell
+import Quickshell.Wayland
+import QtQuick
+import QtQuick.Layouts
+import "./modules"
+
+PanelWindow {
+ id: root
+
+ WlrLayershell.namespace: "quickshell-bar"
+ WlrLayershell.layer: WlrLayer.Top
+ WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
+
+ anchors {
+ top: true
+ left: true
+ right: true
+ }
+
+ margins {
+ left: 2
+ right: 2
+ bottom: 1
+ top: 3
+ }
+
+ implicitHeight: Theme.barHeight
+ exclusiveZone: Theme.barHeight
+ color: "transparent"
+
+ RowLayout {
+ anchors.fill: parent
+ anchors.leftMargin: 4
+ anchors.rightMargin: 4
+ spacing: Theme.spacing
+
+ // ─── LEFT ──────────────────────────────────────────
+ ClockWidget {}
+ WeatherWidget {}
+ SysTrayWidget {}
+ WorkspacesWidget { screen: root.screen }
+ MediaWidget {}
+ WindowTitleWidget { screen: root.screen }
+
+ Item { Layout.fillWidth: true }
+
+ // ─── RIGHT ─────────────────────────────────────────
+ CavaWidget {}
+ AudioWidget {}
+ MemoryWidget {}
+ CpuWidget {}
+ TemperatureWidget {}
+ BatteryWidget {}
+ BluetoothWidget {}
+ PowerProfilesWidget {}
+ PowerMenuWidget {}
+ }
+}
diff --git a/.config/quickshell/Exec.qml b/.config/quickshell/Exec.qml
new file mode 100644
index 0000000..7f8b270
--- /dev/null
+++ b/.config/quickshell/Exec.qml
@@ -0,0 +1,23 @@
+// Exec.qml - fire-and-forget process launcher
+// Usage from any file: Exec.run(["kitty", "-e", "btop"])
+pragma Singleton
+import Quickshell
+import Quickshell.Io
+import QtQuick
+
+Singleton {
+ id: root
+
+ function run(cmd) {
+ const proc = procPool.createObject(root, { command: cmd });
+ proc.running = true;
+ }
+
+ Component {
+ id: procPool
+ Process {
+ running: false
+ onExited: destroy()
+ }
+ }
+}
diff --git a/.config/quickshell/Pill.qml b/.config/quickshell/Pill.qml
new file mode 100644
index 0000000..4432cf3
--- /dev/null
+++ b/.config/quickshell/Pill.qml
@@ -0,0 +1,39 @@
+// Pill.qml - styled module container
+import QtQuick
+import QtQuick.Layouts
+
+Rectangle {
+ id: root
+
+ default property alias content: inner.data
+
+ property bool hovered: mouseArea.containsMouse
+ signal clicked(var mouse)
+ signal scrolled(var wheel)
+
+ property real leftPadding: Theme.pillPadH
+ property real rightPadding: Theme.pillPadH
+ implicitWidth: inner.implicitWidth + leftPadding + rightPadding
+ implicitHeight: Theme.barHeight
+ radius: Theme.radius
+ color: hovered ? Theme.pillHover : Theme.pill
+
+ Behavior on color { ColorAnimation { duration: 150 } }
+
+ RowLayout {
+ id: inner
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: parent.left
+ anchors.leftMargin: root.leftPadding
+ spacing: 4
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
+ onClicked: (m) => root.clicked(m)
+ onWheel: (w) => root.scrolled(w)
+ }
+}
diff --git a/.config/quickshell/Theme.qml b/.config/quickshell/Theme.qml
new file mode 100644
index 0000000..faebb46
--- /dev/null
+++ b/.config/quickshell/Theme.qml
@@ -0,0 +1,38 @@
+// Theme.qml - global palette & dimensions
+// QuickShell auto-discovers this; access from any file as `Theme.colorN` etc.
+pragma Singleton
+import Quickshell
+import QtQuick
+
+Singleton {
+ // ── Wallust palette ──────────────────────────────────────
+ readonly property color background: "#252425"
+ readonly property color foreground: "#F9F1D9"
+ readonly property color color0: "#505051"
+ readonly property color color1: "#9C604E"
+ readonly property color color2: "#807A52"
+ readonly property color color3: "#908BAB"
+ readonly property color color4: "#B7815F"
+ readonly property color color5: "#B9BECA"
+ readonly property color color6: "#EED793"
+ readonly property color color7: "#EEE3C1"
+ readonly property color color8: "#A79F87"
+
+ // ── Derived / semantic ────────────────────────────────────
+ readonly property color pill: Qt.rgba(0.976, 0.945, 0.851, 0.15)
+ readonly property color pillHover: color2
+ readonly property color wsActive: color3
+ readonly property color wsUrgent: color1
+
+ // ── Typography ────────────────────────────────────────────
+ readonly property string fontSans: "Fira Sans Condensed"
+ readonly property string fontMono: "FiraCode Nerd Font"
+ readonly property int fontSize: 12
+
+ // ── Bar geometry ─────────────────────────────────────────
+ readonly property int barHeight: 28
+ readonly property int barPadding: 2
+ readonly property int radius: 5
+ readonly property int pillPadH: 10
+ readonly property int spacing: 4
+}
diff --git a/.config/quickshell/modules/AudioWidget.qml b/.config/quickshell/modules/AudioWidget.qml
new file mode 100644
index 0000000..1c50431
--- /dev/null
+++ b/.config/quickshell/modules/AudioWidget.qml
@@ -0,0 +1,48 @@
+// modules/AudioWidget.qml - wireplumber volume, matches waybar style.
+// Icon set mirrors waybar wireplumber format-icons (NerdFont).
+import QtQuick
+import QtQuick.Layouts
+import Quickshell.Services.Pipewire
+import ".."
+
+Pill {
+ id: root
+
+ property var node: Pipewire.defaultAudioSink
+ property bool muted: node?.audio.muted ?? false
+ property real vol: node?.audio.volume ?? 0
+
+ property string icon: {
+ if (muted || vol === 0) return " "
+ if (vol < 0.34) return " "
+ if (vol < 0.67) return " "
+ return " "
+ }
+
+ onClicked: (m) => {
+ if (m.button === Qt.LeftButton) Exec.run(["pavucontrol"])
+ if (m.button === Qt.RightButton && node)
+ node.audio.muted = !node.audio.muted
+ }
+
+ onScrolled: (w) => {
+ if (!node) return
+ var delta = w.angleDelta.y > 0 ? 0.04 : -0.04
+ node.audio.volume = Math.max(0, Math.min(1.5, node.audio.volume + delta))
+ }
+
+ // Cava feeds into this on the left → right border only
+ leftPadding: 0
+ rightPadding: Theme.pillPadH
+
+ Text {
+ text: root.icon
+ font { family: Theme.fontMono; pixelSize: Theme.fontSize; }
+ color: "#fab387" // peach accent matching waybar foreground color for icon
+ }
+ Text {
+ text: root.muted ? "muted" : Math.round(root.vol * 100) + "%"
+ font { family: Theme.fontSans; pixelSize: Theme.fontSize }
+ color: Theme.foreground
+ }
+}
diff --git a/.config/quickshell/modules/BatteryWidget.qml b/.config/quickshell/modules/BatteryWidget.qml
new file mode 100644
index 0000000..df9695e
--- /dev/null
+++ b/.config/quickshell/modules/BatteryWidget.qml
@@ -0,0 +1,56 @@
+// BatteryWidget.qml - battery icon + percentage
+import QtQuick
+import Quickshell.Io
+import ".."
+
+Pill {
+ id: root
+ property int capacity: 100
+ property string status: "Unknown"
+ property bool charging: status === "Charging"
+ property bool plugged: status === "Full" || status === "Not charging"
+ property bool critical: capacity <= 15 && !charging
+
+ property string icon: {
+ if (charging) return " "
+ if (plugged) return " "
+ if (capacity > 80) return ""
+ if (capacity > 60) return ""
+ if (capacity > 40) return ""
+ if (capacity > 20) return ""
+ return ""
+ }
+
+ Text {
+ text: root.icon + " " + root.capacity + "%"
+ font.family: Theme.fontSans
+ font.pixelSize: Theme.fontSize
+ color: root.critical ? Theme.color1 : Theme.foreground
+ }
+
+ Process {
+ id: batProc
+ running: false
+ command: ["bash", "-c",
+ "cat /sys/class/power_supply/BAT0/capacity 2>/dev/null; " +
+ "echo ---; " +
+ "cat /sys/class/power_supply/BAT0/status 2>/dev/null"]
+ stdout: StdioCollector {
+ onStreamFinished: {
+ const parts = this.text.split("---")
+ if (parts.length >= 2) {
+ const cap = parseInt(parts[0].trim())
+ if (!isNaN(cap)) root.capacity = cap
+ root.status = parts[1].trim()
+ }
+ }
+ }
+ onExited: batProc.running = false
+ }
+
+ Timer {
+ interval: 5000; running: true; repeat: true
+ triggeredOnStart: true
+ onTriggered: batProc.running = true
+ }
+}
diff --git a/.config/quickshell/modules/BluetoothWidget.qml b/.config/quickshell/modules/BluetoothWidget.qml
new file mode 100644
index 0000000..e980844
--- /dev/null
+++ b/.config/quickshell/modules/BluetoothWidget.qml
@@ -0,0 +1,61 @@
+// modules/BluetoothWidget.qml - ᛒ status / device alias
+// Uses bluetoothctl via a polled Process (no BlueZ QML bindings in QS yet).
+import QtQuick
+import Quickshell.Io
+import ".."
+
+Pill {
+ id: root
+ property string btStatus: "off" // "off" | "on" | "connected"
+ property string devAlias: ""
+
+ readonly property var bgColors: ({
+ "off": Qt.rgba(0.565, 0.545, 0.671, 0.3), // alpha(@color3, 0.3)
+ "on": Theme.color2,
+ "connected": Theme.color4,
+ })
+ // Override Pill's own color binding
+ color: bgColors[btStatus] ?? Theme.pill
+
+ onClicked: (m) => {
+ if (m.button === Qt.LeftButton)
+ Exec.run(["blueman-manager"])
+ }
+
+ Text {
+ text: {
+ var s = root.btStatus
+ if (s === "connected") return "ᛒ " + (root.devAlias || "connected")
+ if (s === "on") return "ᛒ on"
+ return "ᛒ off"
+ }
+ font { family: Theme.fontSans; pixelSize: Theme.fontSize }
+ color: Theme.foreground
+ }
+
+ // Poll bluetoothctl show + info every 5 s
+ Process {
+ id: btProc
+ running: false
+ command: ["bash", "-c",
+ "bluetoothctl show | grep -E 'Powered|Name'; " +
+ "bluetoothctl info 2>/dev/null | grep -E 'Name|Connected'"]
+ stdout: SplitParser {
+ onRead: (line) => {
+ if (/Powered:\s+no/i.test(line)) { root.btStatus = "off"; root.devAlias = "" }
+ if (/Powered:\s+yes/i.test(line)) { if (root.btStatus === "off") root.btStatus = "on" }
+ if (/Connected:\s+yes/i.test(line)) root.btStatus = "connected"
+ if (/Connected:\s+no/i.test(line)) { if (root.btStatus === "connected") { root.btStatus = "on"; root.devAlias = "" } }
+ var match = /^\s+Name:\s+(.+)/.exec(line)
+ if (match && root.btStatus === "connected") root.devAlias = match[1].trim()
+ }
+ }
+ onExited: btProc.running = false
+ }
+
+ Timer {
+ interval: 5000; running: true; repeat: true
+ triggeredOnStart: true
+ onTriggered: btProc.running = true
+ }
+}
diff --git a/.config/quickshell/modules/CavaWidget.qml b/.config/quickshell/modules/CavaWidget.qml
new file mode 100644
index 0000000..eb70f47
--- /dev/null
+++ b/.config/quickshell/modules/CavaWidget.qml
@@ -0,0 +1,91 @@
+// CavaWidget.qml - audio visualiser via cava raw output
+import QtQuick
+import QtQuick.Layouts
+import Quickshell.Io
+import ".."
+
+Rectangle {
+ id: root
+
+ property var bars: Array(12).fill(0)
+ property bool silence: bars.every(v => v === 0)
+
+ readonly property var blocks: [" ","▁","▂","▃","▄","▅","▆","▇","█"]
+
+ implicitWidth: cavaRow.implicitWidth + Theme.pillPadH * 2
+ implicitHeight: Theme.barHeight
+ radius: Theme.radius
+ color: cavaHover.containsMouse ? Theme.pillHover : Theme.pill
+ Behavior on color { ColorAnimation { duration: 150 } }
+
+ RowLayout {
+ id: cavaRow
+ anchors.centerIn: parent
+ spacing: 1
+
+ Repeater {
+ model: root.bars.length
+ Text {
+ required property int index
+ text: root.silence
+ ? " "
+ : root.blocks[Math.min(Math.floor(root.bars[index] / 28.5), 8)]
+ font.family: Theme.fontMono
+ font.pixelSize: Theme.fontSize + 1
+ color: Theme.foreground
+ }
+ }
+ }
+
+ MouseArea {
+ id: cavaHover
+ anchors.fill: parent
+ hoverEnabled: true
+ onClicked: Exec.run(["pavucontrol"])
+ }
+
+ // Write the cava config once at startup, then run cava pointing at it.
+ Component.onCompleted: writeCfg.running = true
+
+ Process {
+ id: writeCfg
+ running: false
+ // Plain double-quoted string - no JS interpolation, bash sees ${VAR} verbatim.
+ command: ["bash", "-c",
+ "mkdir -p /tmp/qs-cava && cat > /tmp/qs-cava/cava.ini <<'CFG'\n" +
+ "[general]\n" +
+ "framerate = 30\n" +
+ "bars = 12\n" +
+ "[input]\n" +
+ "method = pipewire\n" +
+ "source = auto\n" +
+ "[smoothing]\n" +
+ "noise_reduction = 77\n" +
+ "monstercat = 1\n" +
+ "[output]\n" +
+ "method = raw\n" +
+ "raw_target = /dev/stdout\n" +
+ "data_format = ascii\n" +
+ "ascii_max_range = 255\n" +
+ "bar_delimiter = 59\n" +
+ "CFG\n"
+ ]
+ onExited: cavaProc.running = true
+ }
+
+ Process {
+ id: cavaProc
+ running: false
+ command: ["cava", "-p", "/tmp/qs-cava/cava.ini"]
+ stdout: SplitParser {
+ onRead: (line) => {
+ const parts = line.trim().replace(/;$/, "").split(";");
+ if (parts.length >= root.bars.length) {
+ root.bars = parts.slice(0, root.bars.length)
+ .map(v => parseInt(v) || 0);
+ }
+ }
+ }
+ onExited: cavaProc.running = true
+ }
+}
diff --git a/.config/quickshell/modules/ClockWidget.qml b/.config/quickshell/modules/ClockWidget.qml
new file mode 100644
index 0000000..8f6f227
--- /dev/null
+++ b/.config/quickshell/modules/ClockWidget.qml
@@ -0,0 +1,30 @@
+// modules/ClockWidget.qml - " HH:MM DD Mon" (matches waybar clock format)
+import QtQuick
+import QtQuick.Layouts
+import Quickshell.Io
+import ".."
+
+Pill {
+ onClicked: (m) => {
+ if (m.button === Qt.LeftButton)
+ Exec.run(["kitty", "-e", "calcure", "--class=float", "-T", "calcure"])
+ }
+
+ Text {
+ text: " " + Qt.formatDateTime(clock.now, "HH:mm") +
+ " " + Qt.formatDateTime(clock.now, "d MMM")
+ font { family: Theme.fontSans; pixelSize: Theme.fontSize }
+ color: Theme.foreground
+ }
+
+ // Update every 10 s (no need for per-second ticks)
+ QtObject {
+ id: clock
+ property var now: new Date()
+ property var timer: Timer {
+ interval: 10000; running: true; repeat: true
+ triggeredOnStart: true
+ onTriggered: clock.now = new Date()
+ }
+ }
+}
diff --git a/.config/quickshell/modules/CpuWidget.qml b/.config/quickshell/modules/CpuWidget.qml
new file mode 100644
index 0000000..d96c714
--- /dev/null
+++ b/.config/quickshell/modules/CpuWidget.qml
@@ -0,0 +1,55 @@
+// CpuWidget.qml - "X.XGHz | Y%"
+import QtQuick
+import Quickshell.Io
+import ".."
+
+Pill {
+ id: root
+ property real freqGhz: 0
+ property int usagePct: 0
+
+ property int prevIdle: 0
+ property int prevTotal: 0
+
+ onClicked: (m) => {
+ if (m.button === Qt.LeftButton) Exec.run(["kitty", "-e", "btop"])
+ }
+
+ Text {
+ text: root.freqGhz.toFixed(1) + "GHz | " + root.usagePct + "%"
+ font.family: Theme.fontSans
+ font.pixelSize: Theme.fontSize
+ color: Theme.foreground
+ }
+
+ // /proc/stat - first line is total CPU
+ Process {
+ id: statProc
+ running: false
+ command: ["bash", "-c", "head -1 /proc/stat && cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq 2>/dev/null"]
+ stdout: StdioCollector {
+ onStreamFinished: {
+ const lines = this.text.split("\n")
+ // line 0: cpu user nice system idle iowait irq softirq steal
+ const nums = lines[0].replace(/^cpu\s+/, "").split(/\s+/).map(Number)
+ const idle = (nums[3] || 0) + (nums[4] || 0)
+ const total = nums.reduce((s, v) => s + v, 0)
+ const dIdle = idle - root.prevIdle
+ const dTotal = total - root.prevTotal
+ if (dTotal > 0) root.usagePct = Math.round((1 - dIdle / dTotal) * 100)
+ root.prevIdle = idle
+ root.prevTotal = total
+ // line 1: current frequency in kHz
+ const khz = parseInt(lines[1] || "0")
+ if (!isNaN(khz) && khz > 0) root.freqGhz = khz / 1e6
+ }
+ }
+ onExited: statProc.running = false
+ }
+
+ Timer {
+ interval: 1500; running: true; repeat: true
+ triggeredOnStart: true
+ onTriggered: statProc.running = true
+ }
+}
diff --git a/.config/quickshell/modules/MediaWidget.qml b/.config/quickshell/modules/MediaWidget.qml
new file mode 100644
index 0000000..41d5f42
--- /dev/null
+++ b/.config/quickshell/modules/MediaWidget.qml
@@ -0,0 +1,54 @@
+// modules/MediaWidget.qml
+// Mirrors waybar custom/spotify - uses MPRIS via Quickshell.Services.Mpris.
+// Shows: artist - title + spotify icon. Click to play/pause, scroll to skip.
+import QtQuick
+import Quickshell.Services.Mpris
+import ".."
+
+Pill {
+ id: root
+
+ // Pick the first active player (prefer spotify)
+ property MprisPlayer activePlayer: {
+ var players = Mpris.players.values
+ for (var i = 0; i < players.length; i++)
+ if (players[i].identity.toLowerCase() === "spotify") return players[i]
+ return players.length > 0 ? players[0] : null
+ }
+
+ property string trackText: {
+ if (!activePlayer) return ""
+ var p = activePlayer
+ var info = ""
+ if (p.trackArtists && p.trackTitle)
+ info = p.trackArtists.join(", ") + " - " + p.trackTitle
+ else if (p.trackTitle)
+ info = p.trackTitle
+ if (info.length > 45) info = info.substring(0, 45) + "..."
+ if (p.playbackState !== MprisPlaybackState.Playing && info)
+ info = " " + info
+ return info + " " // trailing Nerd Font Spotify icon
+ }
+
+ visible: trackText !== ""
+
+ Text {
+ text: root.trackText
+ font { family: Theme.fontSans; pixelSize: Theme.fontSize }
+ color: Theme.foreground
+ elide: Text.ElideRight
+ maximumLineCount: 1
+ }
+
+ onClicked: (m) => {
+ if (!root.activePlayer) return
+ if (m.button === Qt.LeftButton)
+ root.activePlayer.togglePlaying()
+ }
+
+ onScrolled: (w) => {
+ if (!root.activePlayer) return
+ if (w.angleDelta.y > 0) root.activePlayer.next()
+ else root.activePlayer.previous()
+ }
+}
diff --git a/.config/quickshell/modules/MemoryWidget.qml b/.config/quickshell/modules/MemoryWidget.qml
new file mode 100644
index 0000000..560d945
--- /dev/null
+++ b/.config/quickshell/modules/MemoryWidget.qml
@@ -0,0 +1,43 @@
+// MemoryWidget.qml - " X.XX / Y GB"
+import QtQuick
+import Quickshell.Io
+import ".."
+
+Pill {
+ id: root
+ property real usedGb: 0
+ property real totalGb: 0
+
+ onClicked: (m) => {
+ if (m.button === Qt.LeftButton) Exec.run(["kitty", "-e", "btop"])
+ }
+
+ Text {
+ text: " " + root.usedGb.toFixed(2) + " / " + root.totalGb.toFixed(0) + " GB"
+ font.family: Theme.fontSans
+ font.pixelSize: Theme.fontSize
+ color: Theme.foreground
+ }
+
+ Process {
+ id: memProc
+ running: false
+ command: ["cat", "/proc/meminfo"]
+ stdout: StdioCollector {
+ onStreamFinished: {
+ const text = this.text
+ const total = parseInt((/MemTotal:\s+(\d+)/.exec(text) || [])[1] || "0")
+ const avail = parseInt((/MemAvailable:\s+(\d+)/.exec(text) || [])[1] || "0")
+ root.totalGb = total / 1048576
+ root.usedGb = (total - avail) / 1048576
+ }
+ }
+ onExited: memProc.running = false
+ }
+
+ Timer {
+ interval: 5000; running: true; repeat: true
+ triggeredOnStart: true
+ onTriggered: memProc.running = true
+ }
+}
diff --git a/.config/quickshell/modules/PowerMenuWidget.qml b/.config/quickshell/modules/PowerMenuWidget.qml
new file mode 100644
index 0000000..a7eb7f4
--- /dev/null
+++ b/.config/quickshell/modules/PowerMenuWidget.qml
@@ -0,0 +1,83 @@
+// modules/PowerMenuWidget.qml - ⏻ button with inline popup menu.
+// Matches waybar custom/power with menu-actions.
+import QtQuick
+import QtQuick.Layouts
+import Quickshell.Io
+import ".."
+
+Pill {
+ id: root
+ onClicked: (m) => {
+ if (m.button === Qt.LeftButton) menu.visible = !menu.visible
+ }
+
+ Text {
+ text: "⏻ "
+ font { family: Theme.fontSans; pixelSize: Theme.fontSize }
+ color: Theme.foreground
+ }
+
+ // Inline drop-up popup - appears above the bar
+ Rectangle {
+ id: menu
+ visible: false
+ z: 100
+
+ width: 130
+ height: menuCol.implicitHeight + 16
+ radius: 5
+ color: Qt.rgba(0.086, 0.075, 0.125, 0.85)
+ border.color: Qt.rgba(1, 1, 1, 0.06)
+ border.width: 1
+
+ // Anchor above the pill
+ parent: root.parent // reparent to bar so z-ordering works
+ x: root.x + root.width - width
+ y: root.y - height - 4
+
+ ColumnLayout {
+ id: menuCol
+ anchors { fill: parent; margins: 8 }
+ spacing: 2
+
+ Repeater {
+ model: [
+ { label: "Suspend", cmd: ["systemctl", "suspend"] },
+ { label: "Hibernate", cmd: ["systemctl", "hibernate"] },
+ { label: "Logout", cmd: ["hyprctl", "dispatch", "exit"] },
+ { label: "Reboot", cmd: ["reboot"] },
+ { label: "Shutdown", cmd: ["shutdown", "now"] },
+ ]
+
+ delegate: Rectangle {
+ required property var modelData
+ Layout.fillWidth: true
+ height: 26
+ radius: 5
+ color: itemHover.containsMouse
+ ? Theme.pillHover
+ : "transparent"
+ Behavior on color { ColorAnimation { duration: 100 } }
+
+ Text {
+ anchors { left: parent.left; verticalCenter: parent.verticalCenter; leftMargin: 8 }
+ text: modelData.label
+ font { family: Theme.fontSans; pixelSize: Theme.fontSize }
+ color: Theme.foreground
+ }
+
+ MouseArea {
+ id: itemHover
+ anchors.fill: parent
+ hoverEnabled: true
+ onClicked: {
+ menu.visible = false
+ Exec.run(modelData.cmd)
+ }
+ }
+ }
+ }
+ }
+
+ }
+}
diff --git a/.config/quickshell/modules/PowerProfilesWidget.qml b/.config/quickshell/modules/PowerProfilesWidget.qml
new file mode 100644
index 0000000..1264606
--- /dev/null
+++ b/.config/quickshell/modules/PowerProfilesWidget.qml
@@ -0,0 +1,59 @@
+// PowerProfilesWidget.qml - ⚡/⚖/🔋 + click-to-cycle
+import QtQuick
+import Quickshell.Io
+import ".."
+
+Pill {
+ id: root
+ property string profile: "balanced"
+
+ readonly property var profileOrder: ["performance", "balanced", "power-saver"]
+ readonly property var icons: ({
+ "performance": "⚡",
+ "balanced": "⚖",
+ "power-saver": "🔋",
+ })
+
+ onClicked: (m) => {
+ if (m.button !== Qt.LeftButton) return;
+ const i = profileOrder.indexOf(root.profile);
+ const next = profileOrder[(i + 1) % profileOrder.length];
+ setProc.command = ["powerprofilesctl", "set", next];
+ setProc.running = true;
+ root.profile = next; // optimistic update
+ }
+
+ Text {
+ text: (root.icons[root.profile] ?? "⚡")
+ font.family: Theme.fontSans
+ font.pixelSize: Theme.fontSize
+ color: Theme.foreground
+ }
+
+ // Read current profile periodically
+ Process {
+ id: readProc
+ running: false
+ command: ["powerprofilesctl", "get"]
+ stdout: SplitParser {
+ onRead: (line) => root.profile = line.trim()
+ }
+ onExited: readProc.running = false
+ }
+
+ // Setter - command is rewritten on each click
+ Process {
+ id: setProc
+ running: false
+ command: ["true"]
+ onExited: setProc.running = false
+ }
+
+ Timer {
+ interval: 2000
+ running: true
+ repeat: true
+ triggeredOnStart: true
+ onTriggered: readProc.running = true
+ }
+}
diff --git a/.config/quickshell/modules/SysTrayWidget.qml b/.config/quickshell/modules/SysTrayWidget.qml
new file mode 100644
index 0000000..083f218
--- /dev/null
+++ b/.config/quickshell/modules/SysTrayWidget.qml
@@ -0,0 +1,63 @@
+// modules/SysTrayWidget.qml - SNI system tray (nm-applet, blueman ...)
+import QtQuick
+import QtQuick.Layouts
+import Quickshell.Services.SystemTray
+import ".."
+
+Rectangle {
+ id: root
+ color: "transparent"
+ radius: Theme.radius
+
+ implicitWidth: trayRow.implicitWidth + 6
+ implicitHeight: Theme.barHeight
+
+ RowLayout {
+ id: trayRow
+ anchors.centerIn: parent
+ spacing: Theme.spacing
+
+ Repeater {
+ model: SystemTray.items
+
+ Rectangle {
+ required property SystemTrayItem modelData
+ width: 22; height: 22
+ radius: Theme.radius
+ color: trayHover.containsMouse
+ ? Theme.pillHover
+ : Qt.rgba(0.976, 0.945, 0.851, 0.15)
+
+ Behavior on color { ColorAnimation { duration: 150 } }
+
+ Image {
+ anchors { fill: parent; margins: 4 }
+ source: modelData.icon
+ fillMode: Image.PreserveAspectFit
+ smooth: true
+ }
+
+ MouseArea {
+ id: trayHover
+ anchors.fill: parent
+ hoverEnabled: true
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ onClicked: (m) => {
+ if (m.button === Qt.LeftButton)
+ modelData.activate()
+ else
+ modelData.contextMenu(mapToGlobal(mouseX, mouseY))
+ }
+ }
+
+ // Attention indicator dot
+ Rectangle {
+ visible: modelData.status === SystemTrayItem.NeedsAttention
+ width: 5; height: 5; radius: 2.5
+ color: Theme.wsUrgent
+ anchors { bottom: parent.bottom; right: parent.right; margins: 1 }
+ }
+ }
+ }
+ }
+}
diff --git a/.config/quickshell/modules/TemperatureWidget.qml b/.config/quickshell/modules/TemperatureWidget.qml
new file mode 100644
index 0000000..9df2ce0
--- /dev/null
+++ b/.config/quickshell/modules/TemperatureWidget.qml
@@ -0,0 +1,43 @@
+// TemperatureWidget.qml - CPU package temperature
+import QtQuick
+import Quickshell.Io
+import ".."
+
+Pill {
+ id: root
+ property int tempC: 0
+ property bool critical: tempC >= 80
+
+ property string icon: tempC < 50 ? "" : tempC < 70 ? "" : ""
+
+ onClicked: (m) => {
+ if (m.button === Qt.LeftButton) Exec.run(["xsensors"])
+ }
+
+ Text {
+ text: root.icon + " " + root.tempC + "°C"
+ font.family: Theme.fontSans
+ font.pixelSize: Theme.fontSize
+ color: root.critical ? Theme.color1 : Theme.foreground
+ }
+
+ // Read first available CPU package sensor - works regardless of hwmon number
+ Process {
+ id: tempProc
+ running: false
+ command: ["bash", "-c", "cat /sys/class/hwmon/hwmon*/temp1_input 2>/dev/null | head -1"]
+ stdout: SplitParser {
+ onRead: (line) => {
+ const raw = parseInt(line.trim())
+ if (!isNaN(raw)) root.tempC = Math.round(raw / 1000)
+ }
+ }
+ onExited: tempProc.running = false
+ }
+
+ Timer {
+ interval: 4000; running: true; repeat: true
+ triggeredOnStart: true
+ onTriggered: tempProc.running = true
+ }
+}
diff --git a/.config/quickshell/modules/WeatherWidget.qml b/.config/quickshell/modules/WeatherWidget.qml
new file mode 100644
index 0000000..433569a
--- /dev/null
+++ b/.config/quickshell/modules/WeatherWidget.qml
@@ -0,0 +1,34 @@
+// modules/WeatherWidget.qml - wttr.in one-liner, refreshed hourly
+import QtQuick
+import Quickshell.Io
+import ".."
+
+Pill {
+ id: root
+ property string weatherText: "..."
+
+ Text {
+ text: weatherText
+ font { family: Theme.fontSans; pixelSize: Theme.fontSize }
+ color: Theme.foreground
+ }
+
+ // ── Fetch via curl ────────────────────────────────────────
+ Process {
+ id: curl
+ command: ["curl", "-s", "--max-time", "8", "https://wttr.in/?format=1"]
+ running: false
+ stdout: SplitParser {
+ onRead: (line) => root.weatherText = line.trim()
+ }
+ onExited: curl.running = false
+ }
+
+ Timer {
+ interval: 3600000 // 1 hour
+ running: true
+ repeat: true
+ triggeredOnStart: true
+ onTriggered: curl.running = true
+ }
+}
diff --git a/.config/quickshell/modules/WindowTitleWidget.qml b/.config/quickshell/modules/WindowTitleWidget.qml
new file mode 100644
index 0000000..15818de
--- /dev/null
+++ b/.config/quickshell/modules/WindowTitleWidget.qml
@@ -0,0 +1,28 @@
+// modules/WindowTitleWidget.qml - hyprland/window equivalent
+import QtQuick
+import Quickshell.Hyprland
+import ".."
+
+Pill {
+ id: root
+ required property var screen
+
+ property string title: {
+ var ws = Hyprland.focusedWorkspace
+ if (!ws) return ""
+ var win = ws.lastWindow
+ if (!win) return ""
+ var t = win.title ?? ""
+ return t.length > 60 ? t.substring(0, 60) + "..." : t
+ }
+
+ visible: title !== ""
+
+ Text {
+ text: root.title
+ font { family: Theme.fontSans; pixelSize: Theme.fontSize }
+ color: Theme.foreground
+ elide: Text.ElideRight
+ maximumLineCount: 1
+ }
+}
diff --git a/.config/quickshell/modules/WorkspacesWidget.qml b/.config/quickshell/modules/WorkspacesWidget.qml
new file mode 100644
index 0000000..29eb72c
--- /dev/null
+++ b/.config/quickshell/modules/WorkspacesWidget.qml
@@ -0,0 +1,70 @@
+// modules/WorkspacesWidget.qml
+// Kanji workspace labels, per-monitor, matches waybar hyprland/workspaces.
+import QtQuick
+import QtQuick.Layouts
+import Quickshell.Hyprland
+import ".."
+
+Rectangle {
+ id: root
+ required property var screen
+
+ color: "transparent"
+ implicitWidth: wsRow.implicitWidth
+ implicitHeight: Theme.barHeight
+
+ // Filter workspaces that belong to this screen's monitor
+ property string monitorName: {
+ for (var i = 0; i < Hyprland.monitors.values.length; i++) {
+ var m = Hyprland.monitors.values[i]
+ if (m.name === screen.name) return m.name
+ }
+ return ""
+ }
+
+ RowLayout {
+ id: wsRow
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacing
+
+ Repeater {
+ model: {
+ // sort visible workspaces for this monitor
+ var all = Hyprland.workspaces.values
+ return all.filter(ws => ws.monitor && ws.monitor.name === root.monitorName)
+ .sort((a, b) => a.id - b.id)
+ }
+
+ delegate: Rectangle {
+ required property var modelData
+ property bool isActive: modelData.id === (Hyprland.focusedWorkspace?.id ?? -1)
+
+ width: 32
+ height: Theme.barHeight - 4
+ radius: Theme.radius
+ Layout.alignment: Qt.AlignVCenter
+ color: isActive
+ ? Theme.wsActive
+ : (wsBtn.containsMouse ? Theme.pillHover : Theme.pill)
+
+ Behavior on color { ColorAnimation { duration: 200 } }
+
+ Text {
+ anchors.centerIn: parent
+ text: Math.min(modelData.id - 1, 10 - 1)
+ font { family: Theme.fontSans; pixelSize: 11 }
+ color: Theme.foreground
+ }
+
+ MouseArea {
+ id: wsBtn
+ anchors.fill: parent
+ hoverEnabled: true
+ onClicked: Hyprland.dispatch("workspace " + modelData.id)
+ onWheel: (w) => Hyprland.dispatch(
+ "workspace " + (w.angleDelta.y > 0 ? "e+1" : "e-1"))
+ }
+ }
+ }
+ }
+}
diff --git a/.config/quickshell/shell.qml b/.config/quickshell/shell.qml
new file mode 100644
index 0000000..5d37a3c
--- /dev/null
+++ b/.config/quickshell/shell.qml
@@ -0,0 +1,14 @@
+// shell.qml - entry point
+// QuickShell scans this directory and auto-imports neighbours (Bar, Theme, Exec, ...).
+// Do NOT create a qmldir file - QuickShell synthesises one automatically.
+import Quickshell
+
+ShellRoot {
+ Variants {
+ model: Quickshell.screens
+ Bar {
+ required property var modelData
+ screen: modelData
+ }
+ }
+}
diff --git a/.config/waybar/colors-wallust.css b/.config/waybar/colors-wallust.css
deleted file mode 100644
index 79de5dc..0000000
--- a/.config/waybar/colors-wallust.css
+++ /dev/null
@@ -1,19 +0,0 @@
-@define-color cursor #AAA27D;
-@define-color background #252425;
-@define-color foreground #F9F1D9;
-@define-color color0 #505051;
-@define-color color1 #9C604E;
-@define-color color2 #807A52;
-@define-color color3 #908BAB;
-@define-color color4 #B7815F;
-@define-color color5 #B9BECA;
-@define-color color6 #EED793;
-@define-color color7 #EEE3C1;
-@define-color color8 #A79F87;
-@define-color color9 #9C604E;
-@define-color color10 #807A52;
-@define-color color11 #908BAB;
-@define-color color12 #B7815F;
-@define-color color13 #B9BECA;
-@define-color color14 #EED793;
-@define-color color15 #EEE3C1;
diff --git a/.config/waybar/config.jsonc b/.config/waybar/config.jsonc
deleted file mode 100644
index 9cea439..0000000
--- a/.config/waybar/config.jsonc
+++ /dev/null
@@ -1,445 +0,0 @@
-{
- // -------------------------------------------------------------------------
- // Global configuration
- // -------------------------------------------------------------------------
-
- "layer": "top",
- "swap-icon-label": true,
- "position": "top",
-
- "height": 36,
-
- "margin-left": 2,
- "margin-bottom": 1,
- "margin-right": 2,
-
- "spacing": 1, // Gaps between modules (4px)
-
- "modules-left": [
- "clock",
- "custom/weather",
- "tray",
- //"custom/rofi",
- "hyprland/workspaces",
- //"hyprland/submap",
- //"idle_inhibitor",
- //"mpd",
- "custom/spotify",
- "hyprland/window",
- ],
- // "modules-center": [
- // //"custom/gammastep"
- // ],
- "modules-right": [
- "cava",
- "wireplumber",
- //"wlr/taskbar",
- //"custom/storage",
- "memory",
- "cpu",
- "temperature",
- "battery",
-
- //"pulseaudio",
- //"backlight",
- "bluetooth",
- //"custom/screenshot_t",
- "custom/power_profiles",
- "custom/power",
- ],
-
- // -------------------------------------------------------------------------
- // Modules
- // -------------------------------------------------------------------------
-
- "wlr/taskbar": {
- "on-click": "activate",
- "on-click-middle": "close",
- "format": "{icon}",
- "icon-size": 12,
- "icon-theme": "Numix-Circle",
- },
-
- "custom/sp1": {
- "format": " | ",
- "tooltip": false,
- },
- "custom/sp2": {
- "format": " |",
- "tooltip": false,
- },
-
- "custom/rofi": {
- "format": "",
- "tooltip": false,
- "on-click-right": "nwg-drawer",
- "on-click": "wofi --show run",
- "on-click-middle": "pkill -9 wofi",
- },
- "custom/screenshot_t": {
- "format": " ",
- "on-click": "~/.config/hypr/scripts/screenshot_full",
- "on-click-right": "~/.config/hypr/scripts/screenshot_area",
- },
-
- "clock#1": {
- "format": " {:%a}",
- "tooltip": false,
- "on-click": "kitty -e calcure --class=\"float\" -T calcure",
- },
- "clock#2": {
- "format": " {:%d-%h-%Y}",
- "tooltip": false,
- "on-click": "kitty -e calcure --class=\"float\" -T calcure",
- },
- "clock#3": {
- "format": " {:%H:%M:%S %p}",
- "tooltip": false,
- "on-click": "kitty -e calcure --class=\"float\" -T calcure",
- },
-
- "bluetooth": {
- // "controller": "controller1", // specify the alias of the controller if there are more than 1 on the system
- "format": "ᛒ {status}",
- "format-connected": "ᛒ {device_alias}",
- "format-disabled": "ᛒ off",
- "tooltip-format": "{controller_alias}\t{controller_address}",
- "tooltip-format-connected": "{controller_alias}\t{controller_address}\n\n{device_enumerate}",
- "tooltip-format-enumerate-connected": "{device_alias}\t{device_address}",
- "on-click": "blueman-manager",
- },
-
- "temperature": {
- // "thermal-zone": 1,
- "interval": 4,
- //"hwmon-path": "/sys/class/hwmon/hwmon3/temp1_input",
- "critical-threshold": 80,
- // "format-critical": " {temperatureC}°C",
- "format-critical": " {temperatureC}°C",
- "format": "{icon} {temperatureC}°C",
- "format-icons": ["", "", ""],
- "max-length": 7,
- "min-length": 7,
- "on-click": "xsensors",
- },
-
- "memory": {
- "interval": 30,
- "format": " {used:0.2f} / {total:0.0f} GB",
- "on-click": "kitty -e btop",
- },
-
- "battery": {
- "interval": 2,
- "states": {
- "good": 95,
- "warning": 30,
- "critical": 15,
- },
- "format": "{icon} {capacity}%",
- "format-charging": " {capacity}%",
- "format-plugged": " {capacity}%",
- "format-icons": ["", "", "", "", ""],
- },
- "network": {
- "format-wifi": " {essid} ({signalStrength}%)",
- "format-ethernet": "{ifname}: {ipaddr}/{cidr} ",
- "format-linked": "{ifname} (No IP) ",
- "format": "",
- "format-disconnected": "",
- "format-alt": "{ifname}: {ipaddr}/{cidr}",
- "on-click": "wl-copy $(ip address show up scope global | grep inet | head -n1 | cut -d/ -f 1 | tr -d [:space:] | cut -c5-)",
- "on-click-right": "wl-copy $(ip address show up scope global | grep inet6 | head -n1 | cut -d/ -f 1 | tr -d [:space:] | cut -c6-)",
- "tooltip-format": " {bandwidthUpBits} {bandwidthDownBits}\n{ifname}\n{ipaddr}/{cidr}\n",
- "tooltip-format-wifi": " {essid} {frequency}MHz\nStrength: {signaldBm}dBm ({signalStrength}%)\nIP: {ipaddr}/{cidr}\n {bandwidthUpBits} {bandwidthDownBits}",
- "interval": 10,
- },
- "custom/storage": {
- "format": " {}",
- "format-alt": "{percentage}% ",
- "format-alt-click": "click-right",
- "return-type": "json",
- "interval": 60,
- "exec": "~/.config/waybar/modules/storage.sh",
- },
-
- "backlight": {
- "device": "intel_backlight",
- "format": "{icon} {percent}%",
- "format-alt": "{percent}% {icon}",
- "format-alt-click": "click-right",
- //"format-icons": ["", ""],
- "format-icons": ["", ""],
- "on-scroll-down": "brightnessctl s 5%-",
- "on-scroll-up": "brightnessctl s +5%",
- },
- "idle_inhibitor": {
- "format": "{icon}",
- "format-icons": {
- "activated": "",
- "deactivated": "",
- },
- "tooltip": "true",
- },
- "custom/weather": {
- "format": "{}",
- "format-alt": "{alt}: {}",
- "format-alt-click": "click-right",
- "interval": 3600,
- "exec": "curl -s 'https://wttr.in/?format=1'",
- //"return-type": "json",
- //"exec": "~/.config/waybar/modules/weather.sh",
- "exec-if": "ping wttr.in -c1",
- },
- "custom/pacman": {
- "format": " {}",
- "interval": 3600, // every hour
- "exec": "checkupdates | wc -l", // # of updates
- "exec-if": "exit 0", // always run; consider advanced run conditions
- "on-click": "alacritty -e 'paru'; pkill -SIGRTMIN+8 waybar", // update system
- "signal": 8,
- "max-length": 5,
- "min-length": 3,
- },
-
- "custom/spotify": {
- "exec": "~/.config/waybar/mediaplayer.py --player spotify",
- "format": "{} ",
- "return-type": "json",
- "on-click": "playerctl play-pause",
- "on-scroll-up": "playerctl next",
- "on-scroll-down": "playerctl previous",
- },
-
- "custom/media": {
- "format": "{0} {1}",
- "return-type": "json",
- "max-length": 40,
- "format-icons": {
- "spotify": "",
- "default": "🎜",
- },
- "escape": true,
- //"exec": "~/.config/waybar/mediaplayer.py" // Script in resources folder
- // "exec": "~/.config/waybar/mediaplayer.py --player spotify 2> /dev/null" // Filter player based on name
- },
-
- "clock": {
- "format": " {:%H:%M %e %b}",
- "tooltip-format": "{:%Y %B}\n{calendar}",
- "today-format": "{}",
- "on-click": "kitty -e calcure --class=\"float\" -T calcure",
- },
-
- "clock#date": {
- "format": " {:%H:%M \n %e %b}",
- "tooltip-format": "{:%Y %B}\n{calendar}",
- "today-format": "{}",
- },
-
- "custom/gammastep": {
- "interval": 5,
- "return-type": "json",
- "exec": {
- "pre": "if unit_status=\"$(systemctl --user is-active gammastep)\"; then\nstatus=\"$unit_status ($(journalctl --user -u gammastep.service -g 'Period: ' | tail -1 | cut -d ':' -f6 | xargs))\"\nelse\nstatus=\"$unit_status\"\nfi",
- "alt": "${status:-inactive}",
- "tooltip": "Gammastep is $status",
- },
- "format": "{icon}",
- "format-icons": {
- "activating": " ",
- "deactivating": " ",
- "inactive": "? ",
- "active (Night)": " ",
- "active (Nighttime)": " ",
- "active (Transition (Night)": " ",
- "active (Transition (Nighttime)": " ",
- "active (Day)": " ",
- "active (Daytime)": " ",
- "active (Transition (Day)": " ",
- "active (Transition (Daytime)": " ",
- },
- "on-click": "systemctl --user is-active gammastep && systemctl --user stop gammastep || systemctl --user start gammastep",
- },
-
- "cpu": {
- "interval": 1,
- //"format": " {}%", // Icon: microchip
- "format": "{max_frequency}GHz | {usage}%",
- "max-length": 13,
- "min-length": 13,
- },
-
- "mpd": {
- "max-length": 25,
- "format": " {title}",
- "format-paused": " {title}",
- "format-stopped": "",
- "format-disconnected": "",
- "on-click": "mpc --quiet toggle",
- "on-click-right": "mpc update; mpc ls | mpc add",
- "on-click-middle": "alacritty -e ncmpcpp",
- "on-scroll-up": "mpc --quiet prev",
- "on-scroll-down": "mpc --quiet next",
- "smooth-scrolling-threshold": 5,
- "tooltip-format": "{title} - {artist} ({elapsedTime:%M:%S}/{totalTime:%H:%M:%S})",
- },
-
- "custom/title": {
- "format": "{}",
- "interval": 0,
- "return-type": "json",
- //"max-length": 35,
- "tooltip": false,
- },
-
- "custom/title#name": {
- "format": "{}",
- "interval": 0,
- "return-type": "json",
-
- "max-length": 35,
- "exec": "$HOME/.scripts/title",
- },
-
- /*"custom/keyboard": {
- "format": " {}",
- "interval": 1,
- "exec": "$HOME/.config/waybar/get_kbdlayout.sh"
- },*/
-
- "hyprland/workspaces": {
- "all-outputs": false,
- "format": "{name}",
- "format-icons": {
- "1": "一",
- "2": "二",
- "3": "三",
- "4": "四",
- "5": "五",
- "6": "六",
- "7": "七",
- "8": "八",
- "9": "九",
- "10": "十",
- },
- "on-scroll-up": "hyprctl dispatch workspace e+1 1>/dev/null",
- "on-scroll-down": "hyprctl dispatch workspace e-1 1>/dev/null",
- "sort-by-number": true,
- "active-only": false,
- },
-
- "hyprland/window": {
- "max-length": 200,
- "separate-outputs": true,
- },
-
- "pulseaudio": {
- "scroll-step": 3, // %, can be a float
- "format": "{icon} {volume}% {format_source}",
- "format-bluetooth": "{volume}% {icon} {format_source}",
- "format-bluetooth-muted": " {icon} {format_source}",
- "format-muted": " {format_source}",
- //"format-source": "{volume}% ",
- //"format-source-muted": "",
- "format-source": "",
- "format-source-muted": "",
- "format-icons": {
- "headphone": "",
- "hands-free": "",
- "headset": "",
- "phone": "",
- "portable": "",
- "car": "",
- "default": ["", "", ""],
- },
- "on-click": "pavucontrol",
- "on-click-right": "amixer sset Master toggle",
- },
-
- "wireplumber": {
- "on-click": "pavucontrol",
- "on-click-right": "amixer sset Master toggle 1>/dev/null",
- //on-click: "${wpctl} set-mute @DEFAULT_AUDIO_SINK@ toggle";
- //on-scroll-down: "${wpctl} set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 0.04+";
- //on-scroll-up: "${wpctl} set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 0.04-";
- "format": "{icon} {volume}%",
- "format-muted": " ",
- "format-source": "",
- "format-source-muted": "",
- //"format-muted": " ",
- //"format-icons": [ "" ]
- "format-icons": {
- "headphone": " ",
- "hands-free": " ",
- "headset": " ",
- "phone": " ",
- "portable": " ",
- "car": " ",
- "default": [" ", " ", " "],
- },
- },
-
- "tray": {
- "icon-size": 11,
- "spacing": 5,
- "show-passive-items": true,
- },
-
- "custom/power_profiles": {
- "format": "{}",
- "return-type": "json",
- "interval": 2,
- "exec": "~/.config/waybar/modules/power_profiles.py",
- "menu": "on-click",
- "menu-file": "~/.config/waybar/modules/power_profiles_menu.xml",
- "menu-actions": {
- "performance": "~/.config/waybar/modules/power_profiles.py --set performance",
- "balanced": "~/.config/waybar/modules/power_profiles.py --set balanced",
- "power-saver": "~/.config/waybar/modules/power_profiles.py --set power-saver",
- },
- "tooltip": true,
- },
-
- "custom/power": {
- "format": "⏻ ",
- "tooltip": false,
- "menu": "on-click",
- "menu-file": "~/.config/waybar/modules/power_menu.xml",
- "menu-actions": {
- "suspend": "systemctl suspend",
- "hibernate": "systemctl hibernate",
- "logout": "hyprctl dispatch exit",
- "shutdown": "shutdown",
- "reboot": "reboot",
- },
- },
-
- "cava": {
- "on-click": "pavucontrol",
- // "cava_config": "$XDG_CONFIG_HOME/cava/cava.conf",
- "framerate": 30,
- "sensitivity": 3,
- "autosens": 1,
- "bars": 12,
- "lower_cutoff_freq": 10,
- "higher_cutoff_freq": 10000,
- "hide_on_silence": false,
- "format_silent": "quiet",
- "method": "pipewire",
- "source": "auto",
- "stereo": false,
- "reverse": false,
- "bar_delimiter": 0,
- "bar_spacing": 1,
- "monstercat": true,
- "waves": true,
- "noise_reduction": 0.77,
- "input_delay": 2,
- "eq": { "1": 1.8, "2": 1.4, "3": 1.1, "4": 1.0, "5": 1.0 },
- "format-icons": ["", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"],
- "actions": {
- "on-click-right": "mode",
- },
- },
-}
diff --git a/.config/waybar/mediaplayer.py b/.config/waybar/mediaplayer.py
deleted file mode 100755
index 7404f48..0000000
--- a/.config/waybar/mediaplayer.py
+++ /dev/null
@@ -1,127 +0,0 @@
-#!/usr/bin/env python3
-import argparse
-import logging
-import sys
-import signal
-import gi
-import json
-gi.require_version('Playerctl', '2.0')
-from gi.repository import Playerctl, GLib
-
-logger = logging.getLogger(__name__)
-
-
-def write_output(text, player):
- logger.info('Writing output')
-
- output = {'text': text,
- 'class': 'custom-' + player.props.player_name,
- 'alt': player.props.player_name}
-
- sys.stdout.write(json.dumps(output) + '\n')
- sys.stdout.flush()
-
-
-def on_play(player, status, manager):
- logger.info('Received new playback status')
- on_metadata(player, player.props.metadata, manager)
-
-
-def on_metadata(player, metadata, manager):
- logger.info('Received new metadata')
- track_info = ''
-
- if player.props.player_name == 'spotify' and \
- 'mpris:trackid' in metadata.keys() and \
- ':ad:' in player.props.metadata['mpris:trackid']:
- track_info = 'AD PLAYING'
- elif player.get_artist() != '' and player.get_title() != '':
- track_info = '{artist} - {title}'.format(artist=player.get_artist(),
- title=player.get_title())
- else:
- track_info = player.get_title()
-
- if player.props.status != 'Playing' and track_info:
- track_info = ' ' + track_info
- write_output(track_info, player)
-
-
-def on_player_appeared(manager, player, selected_player=None):
- if player is not None and (selected_player is None or player.name == selected_player):
- init_player(manager, player)
- else:
- logger.debug("New player appeared, but it's not the selected player, skipping")
-
-
-def on_player_vanished(manager, player):
- logger.info('Player has vanished')
- sys.stdout.write('\n')
- sys.stdout.flush()
-
-
-def init_player(manager, name):
- logger.debug('Initialize player: {player}'.format(player=name.name))
- player = Playerctl.Player.new_from_name(name)
- player.connect('playback-status', on_play, manager)
- player.connect('metadata', on_metadata, manager)
- manager.manage_player(player)
- on_metadata(player, player.props.metadata, manager)
-
-
-def signal_handler(sig, frame):
- logger.debug('Received signal to stop, exiting')
- sys.stdout.write('\n')
- sys.stdout.flush()
- # loop.quit()
- sys.exit(0)
-
-
-def parse_arguments():
- parser = argparse.ArgumentParser()
-
- # Increase verbosity with every occurance of -v
- parser.add_argument('-v', '--verbose', action='count', default=0)
-
- # Define for which player we're listening
- parser.add_argument('--player')
-
- return parser.parse_args()
-
-
-def main():
- arguments = parse_arguments()
-
- # Initialize logging
- logging.basicConfig(stream=sys.stderr, level=logging.DEBUG,
- format='%(name)s %(levelname)s %(message)s')
-
- # Logging is set by default to WARN and higher.
- # With every occurrence of -v it's lowered by one
- logger.setLevel(max((3 - arguments.verbose) * 10, 0))
-
- # Log the sent command line arguments
- logger.debug('Arguments received {}'.format(vars(arguments)))
-
- manager = Playerctl.PlayerManager()
- loop = GLib.MainLoop()
-
- manager.connect('name-appeared', lambda *args: on_player_appeared(*args, arguments.player))
- manager.connect('player-vanished', on_player_vanished)
-
- signal.signal(signal.SIGINT, signal_handler)
- signal.signal(signal.SIGTERM, signal_handler)
-
- for player in manager.props.player_names:
- if arguments.player is not None and arguments.player != player.name:
- logger.debug('{player} is not the filtered player, skipping it'
- .format(player=player.name)
- )
- continue
-
- init_player(manager, player)
-
- loop.run()
-
-
-if __name__ == '__main__':
- main()
diff --git a/.config/waybar/modules/mail.py b/.config/waybar/modules/mail.py
deleted file mode 100755
index abc36ec..0000000
--- a/.config/waybar/modules/mail.py
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/usr/bin/python
-
-import os
-import imaplib
-
-import mailsecrets
-
-def getmails(username, password, server):
- imap = imaplib.IMAP4_SSL(server, 993)
- imap.login(username, password)
- imap.select('INBOX')
- ustatus, uresponse = imap.uid('search', None, 'UNSEEN')
- if ustatus == 'OK':
- unread_msg_nums = uresponse[0].split()
- else:
- unread_msg_nums = []
-
- fstatus, fresponse = imap.uid('search', None, 'FLAGGED')
- if fstatus == 'OK':
- flagged_msg_nums = fresponse[0].split()
- else:
- flagged_msg_nums = []
-
- return [len(unread_msg_nums), len(flagged_msg_nums)]
-
-ping = os.system("ping " + mailsecrets.server + " -c1 > /dev/null 2>&1")
-if ping == 0:
- mails = getmails(mailsecrets.username, mailsecrets.password, mailsecrets.server)
- text = ''
- alt = ''
-
- if mails[0] > 0:
- text = alt = str(mails[0])
- if mails[1] > 0:
- alt = str(mails[1]) + " " + alt
- else:
- exit(1)
-
- print('{"text":"' + text + '", "alt": "' + alt + '"}')
-
-else:
- exit(1)
diff --git a/.config/waybar/modules/power_menu.xml b/.config/waybar/modules/power_menu.xml
deleted file mode 100644
index a280e97..0000000
--- a/.config/waybar/modules/power_menu.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
-
-
diff --git a/.config/waybar/modules/power_profiles.py b/.config/waybar/modules/power_profiles.py
deleted file mode 100755
index 20744e8..0000000
--- a/.config/waybar/modules/power_profiles.py
+++ /dev/null
@@ -1,150 +0,0 @@
-#!/usr/bin/env python3
-import subprocess
-import json
-import sys
-from pathlib import Path
-
-def get_current_profile():
- """Get the current power profile"""
- try:
- result = subprocess.run(
- ["powerprofilesctl", "get"],
- capture_output=True,
- text=True,
- timeout=5
- )
- if result.returncode == 0:
- return result.stdout.strip()
- except Exception as e:
- print(f"Error getting profile: {e}", file=sys.stderr)
- return "unknown"
-
-def get_available_profiles():
- """Get list of available power profiles"""
- try:
- result = subprocess.run(
- ["powerprofilesctl", "list"],
- capture_output=True,
- text=True,
- timeout=5
- )
- if result.returncode == 0:
- profiles = []
- for line in result.stdout.split('\n'):
- line = line.strip()
- if line and ':' in line and not line.startswith('Cpu') and not line.startswith('Platform') and not line.startswith('Degraded'):
- profile_name = line.split(':')[0].strip()
- if profile_name and not profile_name.startswith('*'):
- profiles.append(profile_name)
- elif profile_name.startswith('*'):
- profiles.append(profile_name[1:].strip())
- return profiles
- except Exception as e:
- print(f"Error getting profiles: {e}", file=sys.stderr)
- return []
-
-def set_profile(profile):
- """Set the power profile"""
- try:
- subprocess.run(
- ["powerprofilesctl", "set", profile],
- capture_output=True,
- timeout=5
- )
- except Exception as e:
- print(f"Error setting profile: {e}", file=sys.stderr)
-
-def write_menu_file(menu_path, profiles, current):
- """Write a GTK menu XML file for Waybar."""
- items = []
- if not profiles:
- items.append(
- " \n"
- " \n"
- " \n"
- )
- else:
- for profile in profiles:
- label = profile
- if profile == current:
- label = f"{profile} (current)"
- items.append(
- " \n"
- f" \n"
- " \n"
- )
-
- content = (
- "\n"
- "\n"
- " \n"
- "\n"
- )
- try:
- menu_path.parent.mkdir(parents=True, exist_ok=True)
- menu_path.write_text(content)
- except Exception as e:
- print(f"Error writing menu file: {e}", file=sys.stderr)
-
-def format_output(profile):
- """Format output for waybar"""
- # Icons for each profile
- icons = {
- "performance": "⚡",
- "balanced": "⚖",
- "power-saver": "🔋"
- }
-
- icon = icons.get(profile, "⚡")
-
- output = {
- "text": f"{icon}",
- "alt": profile,
- "class": f"power-profile-{profile}",
- "tooltip": f"Current: {profile}"
- }
-
- return json.dumps(output)
-
-def main():
- menu_path = Path("~/.config/waybar/modules/power_profiles_menu.xml").expanduser()
-
- if len(sys.argv) > 2 and sys.argv[1] == "--set":
- target = sys.argv[2]
- set_profile(target)
- current = get_current_profile()
- available = get_available_profiles()
- write_menu_file(menu_path, available, current)
- print(format_output(current))
- return
-
- if len(sys.argv) > 1 and sys.argv[1] == "--next":
- # Cycle to next profile
- current = get_current_profile()
- available = get_available_profiles()
- write_menu_file(menu_path, available, current)
-
- if current in available and available:
- idx = available.index(current)
- next_profile = available[(idx + 1) % len(available)]
- set_profile(next_profile)
- print(format_output(next_profile))
- else:
- print(format_output(current))
- return
-
- # Just display current profile
- current = get_current_profile()
- available = get_available_profiles()
- write_menu_file(menu_path, available, current)
- print(format_output(current))
-
-if __name__ == "__main__":
- main()
diff --git a/.config/waybar/modules/power_profiles_menu.xml b/.config/waybar/modules/power_profiles_menu.xml
deleted file mode 100644
index 9f949be..0000000
--- a/.config/waybar/modules/power_profiles_menu.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
diff --git a/.config/waybar/modules/spotify.sh b/.config/waybar/modules/spotify.sh
deleted file mode 100755
index c00622b..0000000
--- a/.config/waybar/modules/spotify.sh
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/bin/sh
-
-class=$(playerctl metadata --player=spotify --format '{{lc(status)}}')
-icon=""
-
-if [[ $class == "playing" ]]; then
- info=$(playerctl metadata --player=spotify --format '{{artist}} - {{title}}')
- if [[ ${#info} > 40 ]]; then
- info=$(echo $info | cut -c1-40)"..."
- fi
- text=$info" "$icon
-elif [[ $class == "paused" ]]; then
- text=$icon
-elif [[ $class == "stopped" ]]; then
- text=""
-fi
-
-echo -e "{\"text\":\""$text"\", \"class\":\""$class"\"}"
diff --git a/.config/waybar/modules/storage.sh b/.config/waybar/modules/storage.sh
deleted file mode 100755
index ae2a5ce..0000000
--- a/.config/waybar/modules/storage.sh
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/bin/sh
-
-mount="/"
-warning=20
-critical=10
-
-df -h -P -l "$mount" | awk -v warning=$warning -v critical=$critical '
-/\/.*/ {
- text=$4
- tooltip="Filesystem: "$1"\rSize: "$2"\rUsed: "$3"\rAvail: "$4"\rUse%: "$5"\rMounted on: "$6
- use=$5
- exit 0
-}
-END {
- class=""
- gsub(/%$/,"",use)
- if ((100 - use) < critical) {
- class="critical"
- } else if ((100 - use) < warning) {
- class="warning"
- }
- print "{\"text\":\""text"\", \"percentage\":"use",\"tooltip\":\""tooltip"\", \"class\":\""class"\"}"
-}
-'
diff --git a/.config/waybar/modules/weather.sh b/.config/waybar/modules/weather.sh
deleted file mode 100755
index 06157ef..0000000
--- a/.config/waybar/modules/weather.sh
+++ /dev/null
@@ -1,79 +0,0 @@
-#!/bin/bash
-
-cachedir=~/.cache/rbn
-cachefile=${0##*/}-$1
-
-if [ ! -d $cachedir ]; then
- mkdir -p $cachedir
-fi
-
-if [ ! -f $cachedir/$cachefile ]; then
- touch $cachedir/$cachefile
-fi
-
-# Save current IFS
-SAVEIFS=$IFS
-# Change IFS to new line.
-IFS=$'\n'
-
-cacheage=$(($(date +%s) - $(stat -c '%Y' "$cachedir/$cachefile")))
-if [ $cacheage -gt 1740 ] || [ ! -s $cachedir/$cachefile ]; then
- data=($(curl -s https://en.wttr.in/$1\?0qnT 2>&1))
- echo ${data[0]} | cut -f1 -d, > $cachedir/$cachefile
- echo ${data[1]} | sed -E 's/^.{15}//' >> $cachedir/$cachefile
- echo ${data[2]} | sed -E 's/^.{15}//' >> $cachedir/$cachefile
-fi
-
-weather=($(cat $cachedir/$cachefile))
-
-# Restore IFSClear
-IFS=$SAVEIFS
-
-temperature=$(echo ${weather[2]} | sed -E 's/([[:digit:]])+\.\./\1 to /g')
-
-#echo ${weather[1]##*,}
-
-# https://fontawesome.com/icons?s=solid&c=weather
-case $(echo ${weather[1]##*,} | tr '[:upper:]' '[:lower:]') in
-"clear" | "sunny")
- condition=""
- ;;
-"partly cloudy")
- condition="杖"
- ;;
-"cloudy")
- condition=""
- ;;
-"overcast")
- condition=""
- ;;
-"mist" | "fog" | "freezing fog")
- condition=""
- ;;
-"patchy rain possible" | "patchy light drizzle" | "light drizzle" | "patchy light rain" | "light rain" | "light rain shower" | "rain")
- condition=""
- ;;
-"moderate rain at times" | "moderate rain" | "heavy rain at times" | "heavy rain" | "moderate or heavy rain shower" | "torrential rain shower" | "rain shower")
- condition=""
- ;;
-"patchy snow possible" | "patchy sleet possible" | "patchy freezing drizzle possible" | "freezing drizzle" | "heavy freezing drizzle" | "light freezing rain" | "moderate or heavy freezing rain" | "light sleet" | "ice pellets" | "light sleet showers" | "moderate or heavy sleet showers")
- condition="ﭽ"
- ;;
-"blowing snow" | "moderate or heavy sleet" | "patchy light snow" | "light snow" | "light snow showers")
- condition="流"
- ;;
-"blizzard" | "patchy moderate snow" | "moderate snow" | "patchy heavy snow" | "heavy snow" | "moderate or heavy snow with thunder" | "moderate or heavy snow showers")
- condition="ﰕ"
- ;;
-"thundery outbreaks possible" | "patchy light rain with thunder" | "moderate or heavy rain with thunder" | "patchy light snow with thunder")
- condition=""
- ;;
-*)
- condition=""
- echo -e "{\"text\":\""$condition"\", \"alt\":\""${weather[0]}"\", \"tooltip\":\""${weather[0]}: $temperature ${weather[1]}"\"}"
- ;;
-esac
-
-#echo $temp $condition
-
-echo -e "{\"text\":\""$temperature $condition"\", \"alt\":\""${weather[0]}"\", \"tooltip\":\""${weather[0]}: $temperature ${weather[1]}"\"}"
diff --git a/.config/waybar/style.css b/.config/waybar/style.css
deleted file mode 100644
index d097df9..0000000
--- a/.config/waybar/style.css
+++ /dev/null
@@ -1,245 +0,0 @@
-/* Import wallust colors */
-@import url("colors-wallust.css");
-
-/* Global defaults */
-* {
- font-family:
- "Fira Sans Condensed", "Font Awesome 6 Free", FontAwesome, Roboto,
- Helvetica, Arial, sans-serif;
- font-size: 12px;
- font-weight: 500;
- margin: 0;
- padding: 0;
- border: none;
- border-radius: 3px;
- min-height: 0;
- transition: background-color 0.5s;
-}
-
-.modules-left,
-.modules-right {
- background-color: rgba(17, 24, 39, 0.1);
- margin: 2px;
- padding: 1px 3px;
-}
-
-/* The whole bar */
-#waybar {
- background-color: alpha(@background, 0);
- color: @foreground;
- border-radius: 0px;
- margin: 0;
-}
-
-window#waybar.hidden {
- opacity: 0.2;
-}
-
-window#waybar.empty #window {
- background-color: transparent;
-}
-
-/* --- Workspaces --- */
-#workspaces button {
- padding: 3px 10px;
- margin: 2px;
- border-radius: 3px;
- color: @foreground;
- background-color: alpha(@foreground, 0.15);
- transition: all 0.3s ease-in-out;
- font-size: 9px;
-}
-
-#workspaces button.active {
- color: @foreground;
- background: @color3;
-}
-
-#workspaces button:hover {
- background: @color2;
-}
-
-#workspaces button.urgent {
- background-color: @color9;
-}
-
-#workspaces {
- background-color: transparent;
- margin: 0;
- padding: 0;
-}
-
-/* --- Tray --- */
-#tray {
- margin: 2px;
-}
-
-#tray > * {
- background-color: alpha(@foreground, 0.15);
- margin: 2px;
-}
-
-#tray > * > image,
-#tray > * image {
- padding: 0px 10px;
-}
-
-#tray > *:hover {
- background: @color2;
-}
-
-#tray > .needs-attention {
- background-color: @color9;
-}
-
-#window {
- background-color: alpha(@background, 0.1);
- color: @foreground;
- border-radius: 3px;
- padding: 1px 10px;
- margin: 2px;
-}
-
-/* --- Shared module styling --- */
-#clock,
-#battery,
-#cpu,
-#memory,
-#custom-storage,
-#custom-weather,
-#bluetooth,
-#custom-media,
-#custom-power_profiles,
-#custom-power,
-#temperature,
-#backlight,
-#network,
-#pulseaudio,
-#wireplumber,
-#cava {
- background-color: alpha(@foreground, 0.15);
- color: @foreground;
- padding: 0px 10px;
- margin: 2px;
-}
-
-#clock:hover,
-#battery:hover,
-#cpu:hover,
-#memory:hover,
-#custom-storage:hover,
-#custom-weather:hover,
-#bluetooth:hover,
-#custom-media:hover,
-#custom-power_profiles:hover,
-#custom-power:hover,
-#temperature:hover,
-#backlight:hover,
-#network:hover,
-#wireplumber:hover,
-#pulseaudio:hover,
-#cava:hover {
- background: @color2;
-}
-
-#wireplumber {
- border-radius: 0px 3px 3px 0px;
- margin-left: 0px;
-}
-
-#cava {
- border-radius: 3px 0px 0px 3px;
- margin-right: 0px;
-}
-
-#bluetooth {
- background-color: alpha(@color3, 0.3);
-}
-
-#bluetooth.disabled {
-}
-
-#bluetooth.on {
- background-color: alpha(@color2, 1);
-}
-
-#bluetooth.connected {
- background-color: alpha(@color4, 1);
-}
-
-#bluetooth.connected:hover,
-#bluetooth.on:hover {
- background-color: alpha(@color3, 0.3);
-}
-
-#battery.critical:not(.charging) {
- color: @color1;
- background-color: alpha(@color2, 1);
-}
-
-#temperature.critical {
- background-color: alpha(@color2, 1);
-}
-
-#idle_inhibitor {
- background-color: rgba(45, 52, 54, 0.5);
-}
-
-#idle_inhibitor.activated {
- background-color: rgba(236, 240, 241, 0.5); /* bleibt gleich */
- color: rgba(45, 52, 54, 1);
-}
-
-/* Language + keyboard state */
-#language,
-#keyboard-state {
- padding: 0 0px;
- margin: 0 5px;
- min-width: 16px;
-}
-
-#language {
- background: rgba(0, 176, 147, 0.5);
- color: rgba(116, 8, 100, 0.5);
-}
-
-#keyboard-state {
- background: rgba(151, 225, 173, 0.5);
- color: rgba(0, 0, 0, 0.5);
-}
-
-#keyboard-state > label {
- padding: 0 0px;
-}
-
-#keyboard-state > label.locked {
- background: rgba(0, 0, 0, 0.2);
-}
-
-#taskbar {
- padding: 0;
-}
-
-#taskbar button {
- padding: 0px 3px 0px 6px;
- margin: 2px 1px;
- border-radius: 6px;
- color: @foreground;
- transition: all 0.3s ease-in-out;
-}
-
-#taskbar button.active {
- background: alpha(@foreground, 0.2);
- transition: all 0.4s ease-in-out;
-}
-
-menu {
- border-radius: 5px;
- border: 0px;
- background: rgba(22, 19, 32, 0.5);
- color: #b5e8e0;
- padding: 5px;
-}
-menuitem {
- border-radius: 5px;
-}
diff --git a/.config/waybar/waybar.sh b/.config/waybar/waybar.sh
deleted file mode 100755
index d03b9eb..0000000
--- a/.config/waybar/waybar.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/usr/bin/env sh
-
-# Terminate already running bar instances
-killall -q waybar
-
-# Wait until the processes have been shut down
-while pgrep -x waybar >/dev/null; do sleep 1; done
-
-# Launch main
-waybar &