init
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Services.UPower
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property var battery: UPower.displayDevice
|
||||
readonly property bool charging: !UPower.onBattery
|
||||
|
||||
implicitHeight: label.implicitHeight
|
||||
implicitWidth: label.implicitWidth
|
||||
|
||||
function fmtTime(seconds: real): string {
|
||||
if (!seconds || seconds <= 0)
|
||||
return "\u2014";
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
return h > 0 ? `${h}h ${m}m` : `${m}m`;
|
||||
}
|
||||
|
||||
function batteryGlyph(): string {
|
||||
if (!root.battery)
|
||||
return "\u{F0091}";
|
||||
const percent = Math.round(root.battery.percentage * 100);
|
||||
if (root.charging)
|
||||
return "\u{F0084}";
|
||||
if (percent > 80)
|
||||
return "\u{F0079}";
|
||||
if (percent > 60)
|
||||
return "\u{F0080}";
|
||||
if (percent > 40)
|
||||
return "\u{F007F}";
|
||||
if (percent > 20)
|
||||
return "\u{F007E}";
|
||||
return "\u{F007A}";
|
||||
}
|
||||
|
||||
function stateText(s: int): string {
|
||||
switch (s) {
|
||||
case UPowerDeviceState.Charging:
|
||||
return "Charging";
|
||||
case UPowerDeviceState.Discharging:
|
||||
return "Discharging";
|
||||
case UPowerDeviceState.Empty:
|
||||
return "Empty";
|
||||
case UPowerDeviceState.FullyCharged:
|
||||
return "Fully charged";
|
||||
case UPowerDeviceState.PendingCharge:
|
||||
return "Pending charge";
|
||||
case UPowerDeviceState.PendingDischarge:
|
||||
return "Pending discharge";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mousearea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
Text {
|
||||
id: label
|
||||
anchors.centerIn: parent
|
||||
font.family: "JetBrainsMono Nerd Font"
|
||||
color: "#cdd6f4"
|
||||
text: {
|
||||
if (!root.battery)
|
||||
return "No Battery";
|
||||
return root.batteryGlyph() + " " + Math.round(root.battery.percentage * 100) + "%";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PopupWindow {
|
||||
id: popup
|
||||
|
||||
visible: mousearea.containsMouse && root.battery
|
||||
|
||||
anchor {
|
||||
item: label
|
||||
edges: Edges.Bottom
|
||||
rect {
|
||||
y: 30
|
||||
}
|
||||
}
|
||||
|
||||
color: "transparent"
|
||||
implicitWidth: 270
|
||||
implicitHeight: card.height
|
||||
|
||||
Rectangle {
|
||||
id: card
|
||||
|
||||
width: 270
|
||||
height: col.implicitHeight + 24
|
||||
radius: 16
|
||||
color: "#f21e1e2e"
|
||||
border.color: "#45475a"
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
id: col
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: parent.top
|
||||
margins: 12
|
||||
}
|
||||
|
||||
spacing: 8
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
font.family: "JetBrainsMono Nerd Font"
|
||||
font.pixelSize: 30
|
||||
color: root.charging ? "#a6e3a1" : "#89b4fa"
|
||||
text: root.batteryGlyph()
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
Text {
|
||||
color: "#cdd6f4"
|
||||
font.pixelSize: 22
|
||||
font.bold: true
|
||||
text: root.battery ? Math.round(root.battery.percentage * 100) + "%" : "\u2014"
|
||||
}
|
||||
|
||||
Text {
|
||||
color: root.charging ? "#a6e3a1" : "#a6adc8"
|
||||
font.pixelSize: 12
|
||||
text: root.battery ? root.stateText(root.battery.state) : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 1
|
||||
color: "#313244"
|
||||
}
|
||||
|
||||
InfoRow {
|
||||
label: "Power draw"
|
||||
value: root.battery ? Math.abs(root.battery.changeRate).toFixed(1) + " W" : "\u2014"
|
||||
}
|
||||
|
||||
InfoRow {
|
||||
visible: root.charging
|
||||
label: "Time to full"
|
||||
value: root.battery ? root.fmtTime(root.battery.timeToFull) : "\u2014"
|
||||
}
|
||||
|
||||
InfoRow {
|
||||
visible: !root.charging
|
||||
label: "Time to empty"
|
||||
value: root.battery ? root.fmtTime(root.battery.timeToEmpty) : "\u2014"
|
||||
}
|
||||
|
||||
InfoRow {
|
||||
label: "Energy"
|
||||
value: root.battery ? root.battery.energy.toFixed(1) + " / " + root.battery.energyCapacity.toFixed(1) + " Wh" : "\u2014"
|
||||
}
|
||||
|
||||
InfoRow {
|
||||
visible: root.battery && root.battery.healthSupported
|
||||
label: "Health"
|
||||
value: root.battery ? Math.round(root.battery.healthPercentage) + "%" : "\u2014"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component InfoRow: RowLayout {
|
||||
id: infoRow
|
||||
|
||||
property string label: ""
|
||||
property string value: ""
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
color: "#a6adc8"
|
||||
font.pixelSize: 12
|
||||
text: infoRow.label
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Text {
|
||||
color: "#cdd6f4"
|
||||
font.pixelSize: 12
|
||||
text: infoRow.value
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var popups: null
|
||||
readonly property bool expanded: popups ? popups.current === "bluetooth" : false
|
||||
readonly property var adapter: Bluetooth.defaultAdapter
|
||||
readonly property bool enabled: adapter?.enabled ?? false
|
||||
|
||||
// Reactive: reading each device's `connected` inside the binding makes it
|
||||
// re-evaluate whenever any device connects/disconnects.
|
||||
readonly property bool anyConnected: {
|
||||
const ds = Bluetooth.devices?.values ?? [];
|
||||
return ds.some(d => d.connected);
|
||||
}
|
||||
|
||||
implicitWidth: icon.implicitWidth
|
||||
implicitHeight: 30
|
||||
|
||||
Text {
|
||||
id: icon
|
||||
anchors.centerIn: parent
|
||||
font.family: "JetBrainsMono Nerd Font"
|
||||
font.pixelSize: 17
|
||||
color: root.enabled ? (root.anyConnected ? "#a6e3a1" : "#89b4fa") : "#6c7086"
|
||||
text: !root.enabled ? "\u{F00B2}" : (root.anyConnected ? "\u{F00B1}" : "\u{F00AF}")
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: if (root.popups) root.popups.toggle("bluetooth")
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
active: root.expanded
|
||||
|
||||
PanelWindow {
|
||||
id: win
|
||||
anchors.top: true
|
||||
anchors.right: true
|
||||
margins.top: 6
|
||||
margins.right: 8
|
||||
implicitWidth: 340
|
||||
implicitHeight: card.implicitHeight
|
||||
color: "transparent"
|
||||
exclusiveZone: 0
|
||||
|
||||
HyprlandFocusGrab {
|
||||
active: true
|
||||
windows: [win]
|
||||
onCleared: if (root.popups) root.popups.current = ""
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: card
|
||||
width: parent.width
|
||||
implicitHeight: col.implicitHeight + 24
|
||||
radius: 16
|
||||
color: "#f21e1e2e"
|
||||
border.color: "#45475a"
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
id: col
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: 12
|
||||
spacing: 10
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
font.family: "JetBrainsMono Nerd Font"
|
||||
font.pixelSize: 16
|
||||
color: "#cdd6f4"
|
||||
text: "\u{F00AF} Bluetooth"
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Pill {
|
||||
visible: root.enabled
|
||||
active: root.adapter?.discovering ?? false
|
||||
text: (root.adapter?.discovering ?? false) ? "Scanning…" : "Scan"
|
||||
onClicked: {
|
||||
if (root.adapter)
|
||||
root.adapter.discovering = !root.adapter.discovering;
|
||||
}
|
||||
}
|
||||
|
||||
Pill {
|
||||
active: root.enabled
|
||||
text: root.enabled ? "On" : "Off"
|
||||
onClicked: {
|
||||
if (root.adapter)
|
||||
root.adapter.enabled = !root.enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 1
|
||||
color: "#313244"
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: !root.enabled
|
||||
Layout.fillWidth: true
|
||||
color: "#6c7086"
|
||||
font.pixelSize: 13
|
||||
text: "Bluetooth is off"
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.enabled && (Bluetooth.devices?.values.length ?? 0) === 0
|
||||
Layout.fillWidth: true
|
||||
color: "#6c7086"
|
||||
font.pixelSize: 13
|
||||
text: "No devices found — tap Scan"
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.enabled ? (Bluetooth.devices?.values ?? []) : []
|
||||
|
||||
delegate: RowLayout {
|
||||
id: drow
|
||||
|
||||
required property var modelData
|
||||
readonly property bool busy: modelData.state === BluetoothDeviceState.Connecting
|
||||
|| modelData.state === BluetoothDeviceState.Disconnecting
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
font.family: "JetBrainsMono Nerd Font"
|
||||
font.pixelSize: 16
|
||||
color: drow.modelData.connected ? "#a6e3a1" : "#cdd6f4"
|
||||
text: "\u{F00AF}"
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
color: "#cdd6f4"
|
||||
font.pixelSize: 13
|
||||
text: drow.modelData.name || drow.modelData.address
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: drow.modelData.connected
|
||||
color: "#a6e3a1"
|
||||
font.pixelSize: 11
|
||||
text: drow.modelData.batteryAvailable
|
||||
? "Connected · " + Math.round(drow.modelData.battery * 100) + "%"
|
||||
: "Connected"
|
||||
}
|
||||
}
|
||||
|
||||
Pill {
|
||||
active: drow.modelData.connected
|
||||
text: drow.busy ? "…" : (drow.modelData.connected ? "Disconnect" : "Connect")
|
||||
onClicked: drow.modelData.connected = !drow.modelData.connected
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Pill: Rectangle {
|
||||
id: pill
|
||||
|
||||
property string text: ""
|
||||
property bool active: false
|
||||
signal clicked
|
||||
|
||||
implicitWidth: pillLabel.implicitWidth + 18
|
||||
implicitHeight: pillLabel.implicitHeight + 8
|
||||
radius: height / 2
|
||||
color: pill.active ? "#89b4fa" : "#313244"
|
||||
|
||||
Text {
|
||||
id: pillLabel
|
||||
anchors.centerIn: parent
|
||||
text: pill.text
|
||||
color: pill.active ? "#11111b" : "#cdd6f4"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: pill.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var popups: null
|
||||
readonly property bool expanded: popups ? popups.current === "clock" : false
|
||||
|
||||
property var now: new Date()
|
||||
property int viewYear: now.getFullYear()
|
||||
property int viewMonth: now.getMonth()
|
||||
|
||||
implicitWidth: label.implicitWidth
|
||||
implicitHeight: 30
|
||||
|
||||
function shiftMonth(delta: int): void {
|
||||
let m = root.viewMonth + delta;
|
||||
let y = root.viewYear;
|
||||
if (m < 0) {
|
||||
m = 11;
|
||||
y -= 1;
|
||||
} else if (m > 11) {
|
||||
m = 0;
|
||||
y += 1;
|
||||
}
|
||||
root.viewMonth = m;
|
||||
root.viewYear = y;
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 1000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: root.now = new Date()
|
||||
}
|
||||
|
||||
// Snap the calendar back to the current month each time it's opened.
|
||||
onExpandedChanged: {
|
||||
if (expanded) {
|
||||
root.viewYear = root.now.getFullYear();
|
||||
root.viewMonth = root.now.getMonth();
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: label
|
||||
anchors.centerIn: parent
|
||||
color: "#cdd6f4"
|
||||
text: Qt.formatDateTime(root.now, "hh:mm:ss dd MMM")
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: if (root.popups) root.popups.toggle("clock")
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
active: root.expanded
|
||||
|
||||
PanelWindow {
|
||||
id: win
|
||||
anchors.top: true
|
||||
anchors.right: true
|
||||
margins.top: 6
|
||||
margins.right: 8
|
||||
implicitWidth: 300
|
||||
implicitHeight: card.implicitHeight
|
||||
color: "transparent"
|
||||
exclusiveZone: 0
|
||||
|
||||
HyprlandFocusGrab {
|
||||
active: true
|
||||
windows: [win]
|
||||
onCleared: if (root.popups) root.popups.current = ""
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: card
|
||||
width: parent.width
|
||||
implicitHeight: col.implicitHeight + 24
|
||||
radius: 16
|
||||
color: "#f21e1e2e"
|
||||
border.color: "#45475a"
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
id: col
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: 12
|
||||
spacing: 10
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 0
|
||||
|
||||
Text {
|
||||
color: "#89b4fa"
|
||||
font.pixelSize: 26
|
||||
font.bold: true
|
||||
text: Qt.formatDateTime(root.now, "dddd")
|
||||
}
|
||||
|
||||
Text {
|
||||
color: "#a6adc8"
|
||||
font.pixelSize: 13
|
||||
text: Qt.formatDateTime(root.now, "dd MMMM yyyy \u00B7 hh:mm:ss")
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 1
|
||||
color: "#313244"
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
color: "#cdd6f4"
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
text: grid.title
|
||||
}
|
||||
|
||||
NavBtn {
|
||||
glyph: "\u{F0141}"
|
||||
onClicked: root.shiftMonth(-1)
|
||||
}
|
||||
|
||||
NavBtn {
|
||||
glyph: "\u{F0142}"
|
||||
onClicked: root.shiftMonth(1)
|
||||
}
|
||||
}
|
||||
|
||||
DayOfWeekRow {
|
||||
Layout.fillWidth: true
|
||||
locale: grid.locale
|
||||
|
||||
delegate: Text {
|
||||
required property string shortName
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
color: "#6c7086"
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
text: shortName
|
||||
}
|
||||
}
|
||||
|
||||
MonthGrid {
|
||||
id: grid
|
||||
|
||||
Layout.fillWidth: true
|
||||
month: root.viewMonth
|
||||
year: root.viewYear
|
||||
spacing: 2
|
||||
|
||||
delegate: Item {
|
||||
id: cell
|
||||
|
||||
required property var model
|
||||
|
||||
readonly property bool isToday: model.today
|
||||
readonly property bool inMonth: model.month === grid.month
|
||||
|
||||
implicitWidth: 36
|
||||
implicitHeight: 30
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 26
|
||||
height: 26
|
||||
radius: 13
|
||||
color: cell.isToday ? "#89b4fa" : "transparent"
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: cell.model.day
|
||||
font.pixelSize: 12
|
||||
color: cell.isToday ? "#11111b" : (cell.inMonth ? "#cdd6f4" : "#585b70")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component NavBtn: Rectangle {
|
||||
id: navBtn
|
||||
|
||||
property string glyph: ""
|
||||
signal clicked
|
||||
|
||||
implicitWidth: 26
|
||||
implicitHeight: 26
|
||||
radius: 13
|
||||
color: navMouse.containsMouse ? "#313244" : "transparent"
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
font.family: "JetBrainsMono Nerd Font"
|
||||
font.pixelSize: 16
|
||||
color: "#cdd6f4"
|
||||
text: navBtn.glyph
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: navMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: navBtn.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Hyprland
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
spacing: 4
|
||||
|
||||
readonly property int maxSlots: 20
|
||||
readonly property var focusedWs: Hyprland.focusedWorkspace
|
||||
|
||||
// Show every regular workspace up to the highest occupied/focused one, plus
|
||||
// one spare empty slot (the "+1"). e.g. 1 busy -> show 1,2 ; 3 busy -> 1..4.
|
||||
readonly property int slotCount: {
|
||||
let hi = 0;
|
||||
const vals = Hyprland.workspaces.values;
|
||||
for (let i = 0; i < vals.length; i++) {
|
||||
const w = vals[i];
|
||||
if (w.id > 0 && w.id > hi)
|
||||
hi = w.id;
|
||||
}
|
||||
const f = Hyprland.focusedWorkspace;
|
||||
if (f && f.id > hi)
|
||||
hi = f.id;
|
||||
return Math.min(root.maxSlots, hi + 1);
|
||||
}
|
||||
|
||||
// Existing special workspaces (id < 0).
|
||||
readonly property var specialList: {
|
||||
const out = [];
|
||||
const vals = Hyprland.workspaces.values;
|
||||
for (let i = 0; i < vals.length; i++) {
|
||||
if (vals[i].id < 0)
|
||||
out.push(vals[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function wsFor(id: int): var {
|
||||
const vals = Hyprland.workspaces.values;
|
||||
for (let i = 0; i < vals.length; i++) {
|
||||
if (vals[i].id === id)
|
||||
return vals[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Hyprland runs in Lua dispatch mode here, so dispatchers take a Lua
|
||||
// expression. Shell out to hyprctl with the raw Lua form.
|
||||
Process {
|
||||
id: dispatcher
|
||||
}
|
||||
|
||||
function focusWorkspace(id: int): void {
|
||||
dispatcher.exec(["hyprctl", "dispatch", "hl.dsp.focus({ workspace = " + id + " })"]);
|
||||
}
|
||||
|
||||
function toggleSpecial(name: string): void {
|
||||
dispatcher.exec(["hyprctl", "dispatch", "hl.dsp.workspace.toggle_special(\"" + name + "\")"]);
|
||||
}
|
||||
|
||||
// Quickshell's workspace `active`/`focused` don't reflect whether a special
|
||||
// is currently shown on a monitor, so we track it ourselves. Map of
|
||||
// monitorName -> shown special id (0 = none). A special chip is "active"
|
||||
// when its id appears here.
|
||||
property var shownSpecials: ({})
|
||||
|
||||
function isSpecialShown(id: int): bool {
|
||||
const m = root.shownSpecials;
|
||||
for (const k in m) {
|
||||
if (m[k] === id)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Hyprland
|
||||
|
||||
function onRawEvent(event: var): void {
|
||||
if (event.name !== "activespecialv2")
|
||||
return;
|
||||
// data: "<id>,<name>,<monitor>" (id/name empty when hidden)
|
||||
const parts = event.data.split(",");
|
||||
const mon = parts[parts.length - 1];
|
||||
const id = parts[0] === "" ? 0 : parseInt(parts[0]);
|
||||
const next = Object.assign({}, root.shownSpecials);
|
||||
next[mon] = id;
|
||||
root.shownSpecials = next;
|
||||
}
|
||||
}
|
||||
|
||||
// Seed the shown-special state at startup (no event has fired yet).
|
||||
Process {
|
||||
id: seedProc
|
||||
running: true
|
||||
command: ["hyprctl", "-j", "monitors"]
|
||||
stdout: StdioCollector {
|
||||
id: seedOut
|
||||
onStreamFinished: {
|
||||
try {
|
||||
const mons = JSON.parse(seedOut.text);
|
||||
const next = ({});
|
||||
for (const m of mons) {
|
||||
const sw = m.specialWorkspace;
|
||||
next[m.name] = sw ? sw.id : 0;
|
||||
}
|
||||
root.shownSpecials = next;
|
||||
} catch (e) {
|
||||
// ignore malformed output
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Regular workspaces ----
|
||||
Repeater {
|
||||
model: root.slotCount
|
||||
|
||||
delegate: Rectangle {
|
||||
id: chip
|
||||
|
||||
required property int index
|
||||
readonly property int wsId: index + 1
|
||||
readonly property var ws: root.wsFor(wsId)
|
||||
readonly property bool active: root.focusedWs ? root.focusedWs.id === wsId : false
|
||||
readonly property bool urgent: ws ? ws.urgent : false
|
||||
// A non-focused workspace only persists in Hyprland while occupied.
|
||||
readonly property bool occupied: ws !== null && !active
|
||||
|
||||
implicitWidth: Math.max(22, txt.implicitWidth + 12)
|
||||
implicitHeight: 22
|
||||
radius: 11
|
||||
color: chip.urgent ? "#f38ba8"
|
||||
: chip.active ? "#89b4fa"
|
||||
: (mouse.containsMouse ? "#313244" : "transparent")
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 120 }
|
||||
}
|
||||
|
||||
Text {
|
||||
id: txt
|
||||
anchors.centerIn: parent
|
||||
font.family: "JetBrainsMono Nerd Font"
|
||||
font.pixelSize: 13
|
||||
font.bold: chip.active
|
||||
text: chip.wsId
|
||||
color: (chip.active || chip.urgent) ? "#11111b"
|
||||
: chip.occupied ? "#cdd6f4"
|
||||
: "#585b70"
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.focusWorkspace(chip.wsId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Separator (only when specials exist) ----
|
||||
Rectangle {
|
||||
visible: root.specialList.length > 0
|
||||
implicitWidth: 1
|
||||
implicitHeight: 14
|
||||
color: "#45475a"
|
||||
Layout.leftMargin: 2
|
||||
Layout.rightMargin: 2
|
||||
}
|
||||
|
||||
// ---- Special workspaces ----
|
||||
Repeater {
|
||||
model: root.specialList
|
||||
|
||||
delegate: Rectangle {
|
||||
id: spc
|
||||
|
||||
required property var modelData
|
||||
readonly property bool active: root.isSpecialShown(modelData.id)
|
||||
readonly property bool urgent: modelData.urgent
|
||||
readonly property string shortName: {
|
||||
const n = modelData.name || "";
|
||||
return n.startsWith("special:") ? n.slice(8) : n;
|
||||
}
|
||||
|
||||
implicitWidth: Math.max(22, slabel.implicitWidth + 12)
|
||||
implicitHeight: 22
|
||||
radius: 11
|
||||
color: spc.urgent ? "#f38ba8"
|
||||
: spc.active ? "#cba6f7"
|
||||
: (smouse.containsMouse ? "#313244" : "transparent")
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 120 }
|
||||
}
|
||||
|
||||
Text {
|
||||
id: slabel
|
||||
anchors.centerIn: parent
|
||||
font.family: "JetBrainsMono Nerd Font"
|
||||
font.pixelSize: 13
|
||||
font.bold: spc.active
|
||||
text: "\u{F04CE}" + (spc.shortName ? " " + spc.shortName : "")
|
||||
color: (spc.active || spc.urgent) ? "#11111b" : "#cba6f7"
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: smouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.toggleSpecial(spc.shortName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var popups: null
|
||||
readonly property bool expanded: popups ? popups.current === "network" : false
|
||||
property bool wifiEnabled: true
|
||||
property var networks: []
|
||||
property string activeSsid: ""
|
||||
property int activeSignal: 0
|
||||
property var knownSsids: ({})
|
||||
property string statusMsg: ""
|
||||
property string pendingSsid: ""
|
||||
|
||||
implicitWidth: icon.implicitWidth
|
||||
implicitHeight: 30
|
||||
|
||||
function secured(sec) {
|
||||
return !!sec && sec.trim().length > 0;
|
||||
}
|
||||
|
||||
function signalGlyph(s) {
|
||||
if (s >= 80) return "\u{F0928}";
|
||||
if (s >= 55) return "\u{F0925}";
|
||||
if (s >= 30) return "\u{F0922}";
|
||||
return "\u{F091F}";
|
||||
}
|
||||
|
||||
function wifiGlyph() {
|
||||
if (!wifiEnabled) return "\u{F05AA}";
|
||||
if (activeSsid === "") return "\u{F05A9}";
|
||||
return signalGlyph(activeSignal);
|
||||
}
|
||||
|
||||
// nmcli -t escapes ':' as '\:' and '\' as '\\'; split on unescaped colons.
|
||||
function splitFields(line) {
|
||||
const out = [];
|
||||
let cur = "";
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const c = line[i];
|
||||
if (c === "\\" && i + 1 < line.length) {
|
||||
cur += line[i + 1];
|
||||
i++;
|
||||
} else if (c === ":") {
|
||||
out.push(cur);
|
||||
cur = "";
|
||||
} else {
|
||||
cur += c;
|
||||
}
|
||||
}
|
||||
out.push(cur);
|
||||
return out;
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
statusProc.running = true;
|
||||
listProc.running = true;
|
||||
knownProc.running = true;
|
||||
}
|
||||
|
||||
function setWifi(on) {
|
||||
radioProc.command = ["nmcli", "radio", "wifi", on ? "on" : "off"];
|
||||
radioProc.running = true;
|
||||
root.wifiEnabled = on;
|
||||
Qt.callLater(root.refresh);
|
||||
}
|
||||
|
||||
function rescan() {
|
||||
root.statusMsg = "Scanning…";
|
||||
rescanProc.running = true;
|
||||
}
|
||||
|
||||
function doConnect(ssid, password) {
|
||||
root.statusMsg = "Connecting to " + ssid + "…";
|
||||
const cmd = ["nmcli", "device", "wifi", "connect", ssid];
|
||||
if (password && password.length > 0)
|
||||
cmd.push("password", password);
|
||||
connectProc.command = cmd;
|
||||
connectProc.running = true;
|
||||
root.pendingSsid = "";
|
||||
}
|
||||
|
||||
function disconnect(ssid) {
|
||||
root.statusMsg = "Disconnecting…";
|
||||
actionProc.command = ["nmcli", "connection", "down", "id", ssid];
|
||||
actionProc.running = true;
|
||||
}
|
||||
|
||||
function onNetworkClicked(net) {
|
||||
if (net.active) {
|
||||
root.disconnect(net.ssid);
|
||||
return;
|
||||
}
|
||||
if (root.secured(net.security) && !root.knownSsids[net.ssid]) {
|
||||
root.pendingSsid = (root.pendingSsid === net.ssid) ? "" : net.ssid;
|
||||
return;
|
||||
}
|
||||
root.doConnect(net.ssid, "");
|
||||
}
|
||||
|
||||
Process {
|
||||
id: statusProc
|
||||
command: ["nmcli", "-t", "-f", "WIFI", "radio"]
|
||||
stdout: StdioCollector {
|
||||
id: statusOut
|
||||
onStreamFinished: root.wifiEnabled = statusOut.text.trim() === "enabled"
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: knownProc
|
||||
command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"]
|
||||
stdout: StdioCollector {
|
||||
id: knownOut
|
||||
onStreamFinished: {
|
||||
const map = ({});
|
||||
const lines = knownOut.text.split("\n");
|
||||
for (const line of lines) {
|
||||
if (!line) continue;
|
||||
const f = root.splitFields(line);
|
||||
if (f[1] && f[1].indexOf("wireless") >= 0)
|
||||
map[f[0]] = true;
|
||||
}
|
||||
root.knownSsids = map;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: listProc
|
||||
command: ["nmcli", "-t", "-f", "ACTIVE,SSID,SIGNAL,SECURITY", "device", "wifi", "list"]
|
||||
stdout: StdioCollector {
|
||||
id: listOut
|
||||
onStreamFinished: {
|
||||
const seen = ({});
|
||||
const list = [];
|
||||
let actSsid = "";
|
||||
let actSig = 0;
|
||||
const lines = listOut.text.split("\n");
|
||||
for (const line of lines) {
|
||||
if (!line) continue;
|
||||
const f = root.splitFields(line);
|
||||
const active = f[0] === "yes";
|
||||
const ssid = f[1] || "";
|
||||
const sig = parseInt(f[2] || "0") || 0;
|
||||
const sec = f[3] || "";
|
||||
if (ssid === "") continue;
|
||||
if (active) {
|
||||
actSsid = ssid;
|
||||
actSig = sig;
|
||||
}
|
||||
if (seen[ssid] !== undefined) {
|
||||
const ex = list[seen[ssid]];
|
||||
if (sig > ex.signal) ex.signal = sig;
|
||||
if (active) ex.active = true;
|
||||
continue;
|
||||
}
|
||||
seen[ssid] = list.length;
|
||||
list.push({ ssid: ssid, signal: sig, security: sec, active: active });
|
||||
}
|
||||
list.sort((a, b) => (b.active - a.active) || (b.signal - a.signal));
|
||||
root.networks = list;
|
||||
root.activeSsid = actSsid;
|
||||
root.activeSignal = actSig;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process { id: radioProc }
|
||||
|
||||
Process {
|
||||
id: rescanProc
|
||||
command: ["nmcli", "device", "wifi", "rescan"]
|
||||
onExited: {
|
||||
root.statusMsg = "";
|
||||
root.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: connectProc
|
||||
stdout: StdioCollector {
|
||||
id: connectOut
|
||||
onStreamFinished: {
|
||||
root.statusMsg = "";
|
||||
root.refresh();
|
||||
}
|
||||
}
|
||||
stderr: StdioCollector {
|
||||
id: connectErr
|
||||
onStreamFinished: {
|
||||
const t = connectErr.text.trim();
|
||||
if (t !== "")
|
||||
root.statusMsg = t.replace(/^Error:\s*/, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: actionProc
|
||||
onExited: root.refresh()
|
||||
}
|
||||
|
||||
onExpandedChanged: if (expanded) refresh()
|
||||
|
||||
Timer {
|
||||
interval: 8000
|
||||
running: root.expanded
|
||||
repeat: true
|
||||
onTriggered: root.refresh()
|
||||
}
|
||||
|
||||
Component.onCompleted: refresh()
|
||||
|
||||
Text {
|
||||
id: icon
|
||||
anchors.centerIn: parent
|
||||
font.family: "JetBrainsMono Nerd Font"
|
||||
font.pixelSize: 17
|
||||
color: root.wifiEnabled ? (root.activeSsid !== "" ? "#89b4fa" : "#cdd6f4") : "#6c7086"
|
||||
text: root.wifiGlyph()
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: if (root.popups) root.popups.toggle("network")
|
||||
}
|
||||
|
||||
LazyLoader {
|
||||
active: root.expanded
|
||||
|
||||
PanelWindow {
|
||||
id: win
|
||||
focusable: true
|
||||
anchors.top: true
|
||||
anchors.right: true
|
||||
margins.top: 6
|
||||
margins.right: 8
|
||||
implicitWidth: 360
|
||||
implicitHeight: card.implicitHeight
|
||||
color: "transparent"
|
||||
exclusiveZone: 0
|
||||
|
||||
HyprlandFocusGrab {
|
||||
active: true
|
||||
windows: [win]
|
||||
onCleared: if (root.popups) root.popups.current = ""
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: card
|
||||
width: parent.width
|
||||
implicitHeight: col.implicitHeight + 24
|
||||
radius: 16
|
||||
color: "#f21e1e2e"
|
||||
border.color: "#45475a"
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
id: col
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: 12
|
||||
spacing: 8
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
font.family: "JetBrainsMono Nerd Font"
|
||||
font.pixelSize: 16
|
||||
color: "#cdd6f4"
|
||||
text: "\u{F05A9} Wi-Fi"
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Pill {
|
||||
visible: root.wifiEnabled
|
||||
text: "Rescan"
|
||||
onClicked: root.rescan()
|
||||
}
|
||||
|
||||
Pill {
|
||||
active: root.wifiEnabled
|
||||
text: root.wifiEnabled ? "On" : "Off"
|
||||
onClicked: root.setWifi(!root.wifiEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 1
|
||||
color: "#313244"
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: !root.wifiEnabled
|
||||
color: "#6c7086"
|
||||
font.pixelSize: 13
|
||||
text: "Wi-Fi is off"
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.statusMsg !== ""
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
color: "#f9e2af"
|
||||
font.pixelSize: 11
|
||||
text: root.statusMsg
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.wifiEnabled ? root.networks : []
|
||||
|
||||
delegate: ColumnLayout {
|
||||
id: nrow
|
||||
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
font.family: "JetBrainsMono Nerd Font"
|
||||
font.pixelSize: 15
|
||||
color: nrow.modelData.active ? "#a6e3a1" : "#cdd6f4"
|
||||
text: root.signalGlyph(nrow.modelData.signal)
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
color: nrow.modelData.active ? "#a6e3a1" : "#cdd6f4"
|
||||
font.pixelSize: 13
|
||||
text: nrow.modelData.ssid
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.secured(nrow.modelData.security)
|
||||
font.family: "JetBrainsMono Nerd Font"
|
||||
font.pixelSize: 12
|
||||
color: "#6c7086"
|
||||
text: "\u{F033E}"
|
||||
}
|
||||
|
||||
Pill {
|
||||
active: nrow.modelData.active
|
||||
text: nrow.modelData.active ? "Disconnect" : "Connect"
|
||||
onClicked: root.onNetworkClicked(nrow.modelData)
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: pwRow
|
||||
visible: root.pendingSsid === nrow.modelData.ssid
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
|
||||
onVisibleChanged: if (visible) pwField.forceActiveFocus()
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 30
|
||||
radius: 8
|
||||
color: "#181825"
|
||||
border.color: pwField.activeFocus ? "#89b4fa" : "#45475a"
|
||||
border.width: 1
|
||||
|
||||
TextField {
|
||||
id: pwField
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
echoMode: TextInput.Password
|
||||
placeholderText: "Password"
|
||||
color: "#cdd6f4"
|
||||
placeholderTextColor: "#6c7086"
|
||||
font.pixelSize: 12
|
||||
background: null
|
||||
onAccepted: root.doConnect(nrow.modelData.ssid, text)
|
||||
}
|
||||
}
|
||||
|
||||
Pill {
|
||||
active: true
|
||||
text: "Join"
|
||||
onClicked: root.doConnect(nrow.modelData.ssid, pwField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Pill: Rectangle {
|
||||
id: pill
|
||||
|
||||
property string text: ""
|
||||
property bool active: false
|
||||
signal clicked
|
||||
|
||||
implicitWidth: pillLabel.implicitWidth + 18
|
||||
implicitHeight: pillLabel.implicitHeight + 8
|
||||
radius: height / 2
|
||||
color: pill.active ? "#89b4fa" : "#313244"
|
||||
|
||||
Text {
|
||||
id: pillLabel
|
||||
anchors.centerIn: parent
|
||||
text: pill.text
|
||||
color: pill.active ? "#11111b" : "#cdd6f4"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: pill.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Text {
|
||||
|
||||
required property var onClicked
|
||||
|
||||
function click() {
|
||||
if (mouseArea.containsMouse) {
|
||||
onClicked();
|
||||
}
|
||||
}
|
||||
|
||||
color: "#fff"
|
||||
text: "Sound"
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user