fixes waybar, adds quickshell
This commit is contained in:
@@ -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 {}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user