This commit is contained in:
2026-07-05 23:16:16 +05:30
commit 8abef18b68
28 changed files with 3212 additions and 0 deletions
+211
View File
@@ -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()
}
}
}
+231
View File
@@ -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()
}
}
}
+224
View File
@@ -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()
}
}
}
+23
View File
@@ -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
}
}