Compare commits

..
7 Commits
17 changed files with 954 additions and 260 deletions
@@ -1,5 +1,5 @@
#!/bin/bash
# Start the Quickshell bar on the current Wayland session.
# Start the systemd-managed Quickshell bar on the current Wayland session.
qs -c bar kill 2>/dev/null || true
qs -c bar --daemonize
systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP HYPRLAND_INSTANCE_SIGNATURE
systemctl --user restart quickshell-bar.service
@@ -0,0 +1,48 @@
import QtQuick
import Quickshell
Block {
id: root
required property var sink
required property var source
signal detailsRequested(var anchorItem)
blockColor: sink && sink.audio && sink.audio.muted
? Theme.foreground : Theme.primary
textColor: sink && sink.audio && sink.audio.muted
? Theme.foregroundDark : Theme.foreground
interactive: true
text: {
if (!sink || !sink.audio)
return "\uf026";
return sink.audio.muted
? "\uf026" : "\uf028 " + Math.round(sink.audio.volume * 100) + "%";
}
secondaryIcon: source && source.audio
? (source.audio.muted
? "file://" + Quickshell.shellPath("icons/microphone-muted.svg")
: "file://" + Quickshell.shellPath("icons/microphone.svg"))
: ""
secondaryText: source && source.audio
? (source.audio.muted ? "OFF"
: Math.round(source.audio.volume * 100) + "%")
: ""
onClicked: mouse => {
if (mouse.button === Qt.LeftButton)
detailsRequested(root);
else if (mouse.button === Qt.MiddleButton && sink && sink.audio)
sink.audio.muted = !sink.audio.muted;
else
Quickshell.execDetached(["qjackctl"]);
}
onWheel: wheel => {
if (!sink || !sink.audio)
return;
const change = wheel.angleDelta.y > 0 ? 0.05 : -0.05;
sink.audio.volume = Math.max(0, Math.min(1.5,
sink.audio.volume + change));
}
}
+5 -31
View File
@@ -1,42 +1,16 @@
import QtQuick
import Quickshell
import Quickshell.Io
Block {
id: root
property int percentage: -1
required property var service
visible: percentage >= 0
blockColor: "#2980b9"
visible: service.brightness >= 0
blockColor: Theme.secondary
interactive: true
text: "\uf185 " + percentage + "%"
text: "\uf185 " + service.brightness + "%"
onWheel: wheel => {
const change = wheel.angleDelta.y > 0 ? "+5%" : "5%-";
Quickshell.execDetached(["brightnessctl", "set", change]);
refreshTimer.restart();
}
Process {
id: brightnessQuery
command: ["brightnessctl", "-m"]
running: true
stdout: StdioCollector {
onStreamFinished: {
const fields = text.trim().split(",");
root.percentage = fields.length >= 4
? parseInt(fields[3].replace("%", ""))
: -1;
}
}
}
Timer {
id: refreshTimer
interval: 500
repeat: true
running: true
onTriggered: brightnessQuery.running = true
service.setBrightness(change);
}
}
+36 -178
View File
@@ -2,9 +2,7 @@ import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Services.Pipewire
import Quickshell.Services.SystemTray
import Quickshell.Services.UPower
import Quickshell.Widgets
PanelWindow {
id: bar
@@ -12,14 +10,15 @@ PanelWindow {
property var sink: Pipewire.defaultAudioSink
property var source: Pipewire.defaultAudioSource
property var battery: UPower.displayDevice
property var hardware
anchors {
top: true
left: true
right: true
}
implicitHeight: 38
color: "#80000000"
implicitHeight: Theme.barHeight
color: Theme.background
PwObjectTracker {
objects: [bar.sink, bar.source]
@@ -30,6 +29,15 @@ PanelWindow {
precision: SystemClock.Seconds
}
DetailPopup {
id: detailsPopup
hardware: bar.hardware
sink: bar.sink
source: bar.source
battery: bar.battery
clock: clock
}
Row {
id: leftModules
anchors {
@@ -37,70 +45,23 @@ PanelWindow {
top: parent.top
bottom: parent.bottom
}
spacing: 8
spacing: Theme.moduleSpacing
Row {
Workspaces {
height: parent.height
spacing: 0
Repeater {
model: Hyprland.workspaces
Rectangle {
id: workspaceButton
required property var modelData
property bool active: modelData.active
visible: modelData.id > 0
width: 34
height: parent.height
color: modelData.urgent ? "#e91e63"
: active ? "#0288d1" : "transparent"
Rectangle {
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
height: 3
color: workspaceButton.active ? "#b3e5fc" : "transparent"
}
Text {
anchors.centerIn: parent
text: modelData.id === 2 ? "\uf120"
: modelData.id === 13 ? "\uf001" : modelData.name
color: "#ffffff"
font.family: "SF Pro Display, Font Awesome 6 Free"
font.pixelSize: 18
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: Quickshell.execDetached([
"hyprctl",
"dispatch",
"hl.dsp.focus({ workspace = " + workspaceButton.modelData.id + " })"
])
}
}
}
}
}
Text {
anchors.centerIn: parent
width: Math.max(0, Math.min(implicitWidth,
parent.width - leftModules.width - rightModules.width - 32))
parent.width - leftModules.width - rightModules.width - Theme.blockHeight))
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
text: Hyprland.activeToplevel ? Hyprland.activeToplevel.title : ""
color: "#ffffff"
font.family: "SF Pro Display, Helvetica, Arial, sans-serif"
font.pixelSize: 18
color: Theme.foreground
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
}
Row {
@@ -109,135 +70,32 @@ PanelWindow {
right: parent.right
top: parent.top
bottom: parent.bottom
rightMargin: 4
rightMargin: Theme.moduleSpacing / 2
}
spacing: 8
spacing: Theme.moduleSpacing
Block {
id: audioBlock
blockColor: bar.sink && bar.sink.audio && bar.sink.audio.muted
? "#ffffff" : "#0288d1"
textColor: bar.sink && bar.sink.audio && bar.sink.audio.muted
? "#000000" : "#ffffff"
interactive: true
text: {
if (!bar.sink || !bar.sink.audio)
return "\uf026";
return bar.sink.audio.muted
? "\uf026" : "\uf028 " + Math.round(bar.sink.audio.volume * 100) + "%";
}
secondaryIcon: bar.source && bar.source.audio
? (bar.source.audio.muted
? "file://" + Quickshell.shellPath("icons/microphone-muted.svg")
: "file://" + Quickshell.shellPath("icons/microphone.svg"))
: ""
secondaryText: bar.source && bar.source.audio
? (bar.source.audio.muted ? "OFF"
: Math.round(bar.source.audio.volume * 100) + "%")
: ""
onClicked: mouse => {
if (mouse.button === Qt.MiddleButton && bar.sink && bar.sink.audio)
bar.sink.audio.muted = !bar.sink.audio.muted;
else
Quickshell.execDetached(["qjackctl"]);
}
onWheel: wheel => {
if (!bar.sink || !bar.sink.audio)
return;
const change = wheel.angleDelta.y > 0 ? 0.05 : -0.05;
bar.sink.audio.volume = Math.max(0, Math.min(1.5,
bar.sink.audio.volume + change));
}
AudioBlock {
sink: bar.sink
source: bar.source
onDetailsRequested: anchorItem => detailsPopup.toggle("audio", anchorItem)
}
Network {}
Backlight {}
Block {
property int percentage: Math.round(bar.battery.percentage * 100)
visible: bar.battery.ready && bar.battery.isLaptopBattery
blockColor: percentage <= 15 && UPower.onBattery
? "#e91e63" : UPower.onBattery ? "#ffffff" : "#2980b9"
textColor: UPower.onBattery && percentage > 15
? "#000000" : "#ffffff"
text: {
if (!UPower.onBattery)
return "\uf1e6 " + percentage + "%";
if (percentage <= 10) return "\uf244 " + percentage + "%";
if (percentage <= 35) return "\uf243 " + percentage + "%";
if (percentage <= 60) return "\uf242 " + percentage + "%";
if (percentage <= 85) return "\uf241 " + percentage + "%";
return "\uf240 " + percentage + "%";
Network {
service: bar.hardware
onDetailsRequested: anchorItem => detailsPopup.toggle("network", anchorItem)
}
Backlight { service: bar.hardware }
BatteryBlock {
battery: bar.battery
onDetailsRequested: anchorItem => detailsPopup.toggle("battery", anchorItem)
}
Rectangle {
visible: trayRepeater.count > 0
implicitWidth: trayItems.implicitWidth + 20
implicitHeight: 32
color: "#0288d1"
Tray { barWindow: bar }
Row {
id: trayItems
anchors.centerIn: parent
spacing: 10
Repeater {
id: trayRepeater
model: SystemTray.items
Item {
id: trayItem
required property var modelData
width: 26
height: 30
IconImage {
anchors {
fill: parent
margins: 3
}
source: trayItem.modelData.icon
opacity: trayMouse.containsMouse ? 1 : 0.82
Behavior on opacity {
NumberAnimation { duration: 120 }
}
}
MouseArea {
id: trayMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.MiddleButton)
trayItem.modelData.secondaryActivate();
else if (mouse.button === Qt.RightButton || trayItem.modelData.onlyMenu) {
const point = mapToItem(bar.contentItem, 0, height);
trayItem.modelData.display(bar, point.x, point.y);
} else {
trayItem.modelData.activate();
}
}
onWheel: wheel => trayItem.modelData.scroll(
wheel.angleDelta.y, false)
}
}
}
}
}
Block {
property bool detailed: false
interactive: true
text: Qt.formatDateTime(clock.date,
detailed ? "yyyy-MM-dd hh:mm:ss" : "hh:mm")
onClicked: detailed = !detailed
ClockBlock {
clock: clock
onDetailsRequested: anchorItem => detailsPopup.toggle("clock", anchorItem)
}
}
}
@@ -0,0 +1,26 @@
import Quickshell.Services.UPower
Block {
id: root
required property var battery
property int percentage: Math.round(battery.percentage * 100)
signal detailsRequested(var anchorItem)
visible: battery.ready && battery.isLaptopBattery
interactive: true
blockColor: percentage <= 15 && UPower.onBattery
? Theme.urgent : UPower.onBattery ? Theme.foreground : Theme.secondary
textColor: UPower.onBattery && percentage > 15
? Theme.foregroundDark : Theme.foreground
text: {
if (!UPower.onBattery)
return "\uf1e6 " + percentage + "%";
if (percentage <= 10) return "\uf244 " + percentage + "%";
if (percentage <= 35) return "\uf243 " + percentage + "%";
if (percentage <= 60) return "\uf242 " + percentage + "%";
if (percentage <= 85) return "\uf241 " + percentage + "%";
return "\uf240 " + percentage + "%";
}
onClicked: detailsRequested(root)
}
+9 -7
View File
@@ -4,18 +4,19 @@ import Quickshell.Widgets
Rectangle {
id: root
property color blockColor: "#0288d1"
property color textColor: "#ffffff"
property color blockColor: Theme.primary
property color textColor: Theme.foreground
property bool interactive: false
property string text: ""
property string fontFamily: "SF Pro Display, Font Awesome 6 Free"
property string fontFamily: Theme.iconFontFamily
property string secondaryIcon: ""
property string secondaryText: ""
signal clicked(var mouse)
signal doubleClicked(var mouse)
signal wheel(var wheel)
implicitWidth: content.implicitWidth + 20
implicitHeight: 32
implicitWidth: content.implicitWidth + Theme.contentPadding * 2
implicitHeight: Theme.blockHeight
color: blockColor
Row {
@@ -27,7 +28,7 @@ Rectangle {
text: root.text
color: root.textColor
font.family: root.fontFamily
font.pixelSize: 18
font.pixelSize: Theme.fontSize
}
IconImage {
@@ -44,7 +45,7 @@ Rectangle {
text: root.secondaryText
color: root.textColor
font.family: root.fontFamily
font.pixelSize: 18
font.pixelSize: Theme.fontSize
}
}
@@ -54,6 +55,7 @@ Rectangle {
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
onClicked: mouse => root.clicked(mouse)
onDoubleClicked: mouse => root.doubleClicked(mouse)
onWheel: wheel => root.wheel(wheel)
}
}
@@ -0,0 +1,20 @@
Block {
id: root
required property var clock
property bool showSeconds: false
signal detailsRequested(var anchorItem)
function formattedTime() {
const date = clock.date;
const hour = date.getHours() % 12 || 12;
const minute = date.getMinutes().toString().padStart(2, "0");
const second = date.getSeconds().toString().padStart(2, "0");
return hour + ":" + minute + (showSeconds ? ":" + second : "");
}
interactive: true
text: formattedTime()
onClicked: detailsRequested(root)
onDoubleClicked: showSeconds = !showSeconds
}
@@ -0,0 +1,386 @@
import QtQuick
import Quickshell
import Quickshell.Services.UPower
PopupWindow {
id: root
property var hardware
property var sink
property var source
property var battery
property var clock
property var anchorItem: null
property string page: ""
function toggle(nextPage, item) {
if (visible && page === nextPage) {
visible = false;
return;
}
page = nextPage;
anchorItem = item;
if (page === "network")
hardware.refreshWifi();
anchor.updateAnchor();
visible = true;
}
function duration(seconds) {
if (seconds <= 0)
return "Calculating";
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return hours + "h " + minutes + "m";
}
function batteryState() {
switch (battery.state) {
case UPowerDeviceState.Charging: return "Charging";
case UPowerDeviceState.Discharging: return "Discharging";
case UPowerDeviceState.FullyCharged: return "Fully charged";
case UPowerDeviceState.Empty: return "Empty";
case UPowerDeviceState.PendingCharge: return "Waiting to charge";
case UPowerDeviceState.PendingDischarge: return "Waiting to discharge";
default: return "Unknown";
}
}
visible: false
implicitWidth: Theme.popupWidth
implicitHeight: contentLoader.item
? contentLoader.item.implicitHeight + Theme.popupPadding * 2 : 100
color: "transparent"
anchor {
item: root.anchorItem
edges: Edges.Bottom | Edges.Right
gravity: Edges.Bottom | Edges.Left
margins.top: 4
}
Rectangle {
anchors.fill: parent
color: Theme.popupBackground
Loader {
id: contentLoader
anchors {
left: parent.left
right: parent.right
top: parent.top
margins: Theme.popupPadding
}
sourceComponent: root.page === "clock" ? clockPage
: root.page === "audio" ? audioPage
: root.page === "network" ? networkPage
: batteryPage
}
Rectangle {
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
height: Theme.activeLineWidth
color: Theme.accent
}
}
Component {
id: clockPage
Item {
id: clockContent
property date now: root.clock.date
property int year: now.getFullYear()
property int month: now.getMonth()
property int firstDay: new Date(year, month, 1).getDay()
property int days: new Date(year, month + 1, 0).getDate()
implicitWidth: Theme.popupContentWidth
implicitHeight: 310
PopupHeader {
width: parent.width
text: Qt.formatDateTime(parent.now, "dddd, MMMM d")
}
Text {
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
topMargin: 40
}
text: Qt.formatDateTime(parent.now, "h:mm:ss AP")
color: Theme.foreground
font.family: Theme.fontFamily
font.pixelSize: Theme.fontClock
}
Row {
anchors {
left: parent.left
top: parent.top
topMargin: 86
}
spacing: 4
Repeater {
model: ["S", "M", "T", "W", "T", "F", "S"]
Text {
required property string modelData
width: 37
text: modelData
color: Theme.muted
font.family: Theme.fontFamily
horizontalAlignment: Text.AlignHCenter
font.bold: true
}
}
}
Grid {
anchors {
left: parent.left
top: parent.top
topMargin: 112
}
columns: 7
spacing: 4
Repeater {
model: 42
Rectangle {
required property int index
property int day: index - clockContent.firstDay + 1
property bool today: day === clockContent.now.getDate()
width: 37
height: 29
radius: 0
color: today ? Theme.primary : "transparent"
Rectangle {
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
height: Theme.activeLineWidth
visible: parent.today
color: Theme.accent
}
Text {
anchors.centerIn: parent
text: parent.day > 0 && parent.day <= clockContent.days
? parent.day : ""
color: Theme.foreground
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSmall
}
}
}
}
}
}
Component {
id: audioPage
Column {
width: Theme.popupContentWidth
spacing: 12
PopupHeader {
width: parent.width
text: "Audio"
}
PopupSlider {
width: parent.width
label: "Output"
value: root.sink && root.sink.audio ? root.sink.audio.volume : 0
muted: root.sink && root.sink.audio ? root.sink.audio.muted : false
onValueRequested: value => {
if (root.sink && root.sink.audio)
root.sink.audio.volume = value;
}
onMuteRequested: {
if (root.sink && root.sink.audio)
root.sink.audio.muted = !root.sink.audio.muted;
}
}
PopupSlider {
width: parent.width
label: "Microphone"
value: root.source && root.source.audio ? root.source.audio.volume : 0
muted: root.source && root.source.audio ? root.source.audio.muted : false
onValueRequested: value => {
if (root.source && root.source.audio)
root.source.audio.volume = value;
}
onMuteRequested: {
if (root.source && root.source.audio)
root.source.audio.muted = !root.source.audio.muted;
}
}
}
}
Component {
id: networkPage
Column {
width: Theme.popupContentWidth
spacing: 10
PopupHeader {
width: parent.width
text: "Network"
}
Text {
text: "Connected to " + root.hardware.connection
color: Theme.foreground
font.family: Theme.fontFamily
font.pixelSize: Theme.fontBody
}
Column {
id: networks
width: parent.width
spacing: 5
Repeater {
model: root.hardware.wifiNetworks
Rectangle {
required property var modelData
width: networks.width
height: 29
radius: 0
color: modelData.active ? Theme.primary : Theme.background
Rectangle {
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
height: Theme.activeLineWidth
visible: parent.modelData.active
color: Theme.accent
}
Text {
anchors {
left: parent.left
leftMargin: 9
verticalCenter: parent.verticalCenter
}
width: parent.width - 70
elide: Text.ElideRight
text: modelData.ssid
color: Theme.foreground
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSmall
}
Text {
anchors {
right: parent.right
rightMargin: 9
verticalCenter: parent.verticalCenter
}
text: modelData.signal + "%"
color: Theme.foreground
font.family: Theme.fontFamily
font.pixelSize: Theme.fontCaption
}
}
}
}
Rectangle {
width: parent.width
height: 30
radius: 0
color: Theme.primary
Text {
anchors.centerIn: parent
text: "Open network settings"
color: Theme.foreground
font.family: Theme.fontFamily
font.pixelSize: Theme.fontCaption
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: Quickshell.execDetached(["nm-connection-editor"])
}
}
}
}
Component {
id: batteryPage
Column {
width: Theme.popupContentWidth
spacing: 12
PopupHeader {
width: parent.width
text: "Battery"
}
Text {
text: Math.round(root.battery.percentage * 100) + "% "
+ root.batteryState()
color: Theme.foreground
font.family: Theme.fontFamily
font.pixelSize: Theme.fontDisplay
}
Rectangle {
width: parent.width
height: 8
radius: 0
color: Theme.track
Rectangle {
width: parent.width * root.battery.percentage
height: parent.height
radius: 0
color: root.battery.percentage <= 0.15 ? Theme.urgent : Theme.primary
}
}
Text {
text: UPower.onBattery
? "Remaining: " + root.duration(root.battery.timeToEmpty)
: "Until full: " + root.duration(root.battery.timeToFull)
color: Theme.accent
font.family: Theme.fontFamily
font.pixelSize: Theme.fontBody
}
Text {
text: "Rate: " + Math.abs(root.battery.changeRate).toFixed(1) + " W"
+ (root.battery.healthSupported
? " Health: " + Math.round(root.battery.healthPercentage * 100) + "%"
: "")
color: Theme.foreground
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSmall
}
}
}
}
@@ -0,0 +1,130 @@
import QtQuick
import Quickshell
import Quickshell.Io
Scope {
id: root
property string connection: "Disconnected"
property int brightness: -1
property var wifiNetworks: []
function setBrightness(change) {
Quickshell.execDetached(["brightnessctl", "set", change]);
}
function refreshNetwork() {
if (!networkQuery.running)
networkQuery.running = true;
}
function refreshWifi() {
if (!wifiQuery.running)
wifiQuery.running = true;
}
Process {
id: networkQuery
command: ["nmcli", "-t", "-f", "TYPE,NAME", "connection", "show", "--active"]
running: true
stdout: StdioCollector {
onStreamFinished: {
const lines = text.trim().split("\n");
for (let line of lines) {
const separator = line.indexOf(":");
const type = line.slice(0, separator);
if (type === "802-11-wireless" || type === "wifi") {
root.connection = line.slice(separator + 1);
return;
}
}
for (let line of lines) {
const separator = line.indexOf(":");
const type = line.slice(0, separator);
if (type === "802-3-ethernet" || type === "ethernet") {
root.connection = line.slice(separator + 1);
return;
}
}
root.connection = "Disconnected";
}
}
}
Process {
id: networkMonitor
command: ["nmcli", "monitor"]
running: true
stdout: SplitParser {
onRead: {
root.refreshNetwork();
if (root.wifiNetworks.length > 0)
root.refreshWifi();
}
}
onExited: networkRestart.start()
}
Timer {
id: networkRestart
interval: 1000
onTriggered: networkMonitor.running = true
}
function updateBrightness() {
if (!brightnessFile.loaded || !maximumBrightnessFile.loaded)
return;
const current = parseInt(brightnessFile.text().trim());
const maximum = parseInt(maximumBrightnessFile.text().trim());
root.brightness = maximum > 0 && !isNaN(current)
? Math.round(current / maximum * 100) : -1;
}
FileView {
id: brightnessFile
path: "file:///sys/class/backlight/intel_backlight/brightness"
watchChanges: true
onFileChanged: reload()
onLoaded: root.updateBrightness()
}
FileView {
id: maximumBrightnessFile
path: "file:///sys/class/backlight/intel_backlight/max_brightness"
onLoaded: root.updateBrightness()
}
Process {
id: wifiQuery
command: ["nmcli", "-t", "-f", "IN-USE,SSID,SIGNAL", "device", "wifi", "list", "--rescan", "auto"]
stdout: StdioCollector {
onStreamFinished: {
const networks = [];
const seen = {};
for (let line of text.trim().split("\n")) {
const fields = line.split(":");
if (fields.length < 3)
continue;
const active = fields.shift() === "*";
const signal = parseInt(fields.pop());
const ssid = fields.join(":").replace(/\\:/g, ":");
if (!ssid || seen[ssid])
continue;
seen[ssid] = true;
networks.push({
"ssid": ssid,
"signal": signal,
"active": active
});
if (networks.length >= 8)
break;
}
root.wifiNetworks = networks;
}
}
}
}
+10 -41
View File
@@ -1,51 +1,20 @@
import QtQuick
import Quickshell
import Quickshell.Io
Block {
id: root
property string connection: "Disconnected"
property bool connected: connection !== "Disconnected"
required property var service
property bool connected: service.connection !== "Disconnected"
signal detailsRequested(var anchorItem)
blockColor: connected ? "#2980b9" : "#e91e63"
blockColor: connected ? Theme.secondary : Theme.urgent
interactive: true
text: connected ? "\uf1eb " + connection : "Disconnected !"
onClicked: Quickshell.execDetached(["nm-connection-editor"])
Process {
id: networkQuery
command: ["nmcli", "-t", "-f", "TYPE,NAME", "connection", "show", "--active"]
running: true
stdout: StdioCollector {
onStreamFinished: {
const lines = text.trim().split("\n");
for (let line of lines) {
const separator = line.indexOf(":");
const type = line.slice(0, separator);
if (type === "802-11-wireless" || type === "wifi") {
root.connection = line.slice(separator + 1);
return;
}
}
for (let line of lines) {
const separator = line.indexOf(":");
const type = line.slice(0, separator);
if (type === "802-3-ethernet" || type === "ethernet") {
root.connection = line.slice(separator + 1);
return;
}
}
root.connection = "Disconnected";
}
}
}
Timer {
interval: 5000
repeat: true
running: true
onTriggered: networkQuery.running = true
text: connected ? "\uf1eb " + service.connection : "Disconnected !"
onClicked: mouse => {
if (mouse.button === Qt.LeftButton)
detailsRequested(root);
else
Quickshell.execDetached(["nm-connection-editor"]);
}
}
@@ -0,0 +1,23 @@
import QtQuick
Rectangle {
id: root
property string text: ""
implicitHeight: Theme.blockHeight
color: Theme.primary
Text {
anchors {
left: parent.left
leftMargin: Theme.contentPadding
verticalCenter: parent.verticalCenter
}
text: root.text
color: Theme.foreground
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
font.bold: true
}
}
@@ -0,0 +1,82 @@
import QtQuick
Item {
id: root
property string label: ""
property real value: 0
property bool muted: false
signal valueRequested(real value)
signal muteRequested()
implicitWidth: 280
implicitHeight: 58
Text {
anchors.left: parent.left
text: root.label
color: Theme.foreground
font.family: Theme.fontFamily
font.pixelSize: Theme.fontBody
}
Rectangle {
anchors {
right: parent.right
top: parent.top
}
width: 58
height: 24
radius: 0
color: root.muted ? Theme.urgent : Theme.primary
Text {
anchors.centerIn: parent
text: root.muted ? "MUTED" : Math.round(root.value * 100) + "%"
color: Theme.foreground
font.pixelSize: Theme.fontTiny
font.bold: true
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.muteRequested()
}
}
Rectangle {
id: track
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
height: 8
radius: 0
color: Theme.track
Rectangle {
width: Math.min(parent.width, parent.width * root.value / 1.5)
height: parent.height
radius: 0
color: root.muted ? Theme.urgent : Theme.primary
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
function updateValue(mouse) {
root.valueRequested(Math.max(0,
Math.min(1.5, mouse.x / width * 1.5)));
}
onPressed: mouse => updateValue(mouse)
onPositionChanged: mouse => {
if (pressed)
updateValue(mouse);
}
}
}
}
+36
View File
@@ -0,0 +1,36 @@
pragma Singleton
import QtQuick
import Quickshell
Singleton {
readonly property color background: "#80000000"
readonly property color popupBackground: "#e0000000"
readonly property color primary: "#0288d1"
readonly property color secondary: "#2980b9"
readonly property color accent: "#b3e5fc"
readonly property color urgent: "#e91e63"
readonly property color foreground: "#ffffff"
readonly property color foregroundDark: "#000000"
readonly property color muted: "#7aa6da"
readonly property color track: "#40000000"
readonly property string fontFamily: "SF Pro Display, Helvetica, Arial, sans-serif"
readonly property string iconFontFamily: "SF Pro Display, Font Awesome 6 Free"
readonly property int barHeight: 38
readonly property int blockHeight: 32
readonly property int fontSize: 18
readonly property int fontTiny: 11
readonly property int fontCaption: 13
readonly property int fontSmall: 14
readonly property int fontBody: 15
readonly property int fontDisplay: 25
readonly property int fontClock: 30
readonly property int moduleSpacing: 8
readonly property int contentPadding: 10
readonly property int activeLineWidth: 3
readonly property int popupWidth: 320
readonly property int popupPadding: 12
readonly property int popupContentWidth: popupWidth - popupPadding * 2
}
+66
View File
@@ -0,0 +1,66 @@
import QtQuick
import Quickshell.Services.SystemTray
import Quickshell.Widgets
Rectangle {
id: root
required property var barWindow
visible: trayRepeater.count > 0
implicitWidth: trayItems.implicitWidth + Theme.contentPadding * 2
implicitHeight: Theme.blockHeight
color: Theme.primary
Row {
id: trayItems
anchors.centerIn: parent
spacing: Theme.contentPadding
Repeater {
id: trayRepeater
model: SystemTray.items
Item {
id: trayItem
required property var modelData
width: 26
height: 30
IconImage {
anchors {
fill: parent
margins: 3
}
source: trayItem.modelData.icon
opacity: trayMouse.containsMouse ? 1 : 0.82
Behavior on opacity {
NumberAnimation { duration: 120 }
}
}
MouseArea {
id: trayMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.MiddleButton) {
trayItem.modelData.secondaryActivate();
} else if (mouse.button === Qt.RightButton || trayItem.modelData.onlyMenu) {
const point = mapToItem(root.barWindow.contentItem, 0, height);
trayItem.modelData.display(root.barWindow, point.x, point.y);
} else {
trayItem.modelData.activate();
}
}
onWheel: wheel => trayItem.modelData.scroll(
wheel.angleDelta.y, false)
}
}
}
}
}
@@ -0,0 +1,53 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
Row {
spacing: 0
Repeater {
model: Hyprland.workspaces
Rectangle {
id: workspaceButton
required property var modelData
property bool active: modelData.active
visible: modelData.id > 0
width: Theme.blockHeight + 2
height: parent.height
color: modelData.urgent ? Theme.urgent
: active ? Theme.primary : "transparent"
Rectangle {
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
height: Theme.activeLineWidth
color: workspaceButton.active ? Theme.accent : "transparent"
}
Text {
anchors.centerIn: parent
text: modelData.id === 2 ? "\uf120"
: modelData.id === 13 ? "\uf001" : modelData.name
color: Theme.foreground
font.family: Theme.iconFontFamily
font.pixelSize: Theme.fontSize
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: Quickshell.execDetached([
"hyprctl",
"dispatch",
"hl.dsp.focus({ workspace = " + workspaceButton.modelData.id + " })"
])
}
}
}
}
+5
View File
@@ -1,12 +1,17 @@
import Quickshell
ShellRoot {
HardwareService {
id: hardwareService
}
Variants {
model: Quickshell.screens
Bar {
required property var modelData
screen: modelData
hardware: hardwareService
}
}
}
@@ -0,0 +1,16 @@
[Unit]
Description=Quickshell desktop bar
Documentation=https://quickshell.org/
After=graphical-session.target
PartOf=graphical-session.target
ConditionEnvironment=WAYLAND_DISPLAY
[Service]
Type=simple
ExecStart=/usr/bin/qs -c bar --no-duplicate
Restart=on-failure
RestartSec=2
TimeoutStopSec=5
[Install]
WantedBy=graphical-session.target