init: working version
This commit is contained in:
commit
7d8d7dacae
109 changed files with 15066 additions and 0 deletions
85
services/Audio.qml
Normal file
85
services/Audio.qml
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
pragma Singleton
|
||||
|
||||
import qs.config
|
||||
import qs.custom
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property PwNode sink: Pipewire.defaultAudioSink
|
||||
readonly property PwNode source: Pipewire.defaultAudioSource
|
||||
|
||||
PwObjectTracker {
|
||||
objects: [root.sink, root.source]
|
||||
}
|
||||
|
||||
readonly property bool muted: !!sink?.audio?.muted
|
||||
readonly property real volume: sink?.audio?.volume ?? 0
|
||||
|
||||
readonly property bool sourceMuted: !!source?.audio?.muted
|
||||
readonly property real sourceVolume: source?.audio?.volume ?? 0
|
||||
|
||||
function setVolume(newVolume: real, s: PwNode): void {
|
||||
if (!s) s = sink;
|
||||
if (s?.ready && s?.audio) {
|
||||
s.audio.volume = Math.max(0, Math.min(1, newVolume));
|
||||
}
|
||||
}
|
||||
|
||||
function increaseVolume(amount: real, s: PwNode): void {
|
||||
setVolume(volume + (amount || Config.osd.volumeIncrement), s);
|
||||
}
|
||||
|
||||
function decreaseVolume(amount: real, s: PwNode): void {
|
||||
setVolume(volume - (amount || Config.osd.volumeIncrement), s);
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
name: "volumeUp"
|
||||
description: "Increase volume"
|
||||
onPressed: root.increaseVolume()
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
name: "volumeDown"
|
||||
description: "Decrease volume"
|
||||
onPressed: root.decreaseVolume()
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
name: "mute"
|
||||
description: "Toggle muting of output (Pipewire default audio sink)"
|
||||
onPressed: root.sink.audio.muted = !root.muted
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
name: "muteMic"
|
||||
description: "Toggle muting of input (Pipewire default audio source)"
|
||||
onPressed: root.source.audio.muted = !root.sourceMuted
|
||||
}
|
||||
|
||||
function setSourceVolume(newVolume: real, s: PwNode): void {
|
||||
if (!s) s = source;
|
||||
if (s?.ready && s?.audio) {
|
||||
s.audio.volume = Math.max(0, Math.min(1, newVolume));
|
||||
}
|
||||
}
|
||||
|
||||
function incrementSourceVolume(amount: real, s: PwNode): void {
|
||||
setSourceVolume(sourceVolume + (amount || Config.osd.micIncrement), s);
|
||||
}
|
||||
|
||||
function decrementSourceVolume(amount: real, s: PwNode): void {
|
||||
setSourceVolume(sourceVolume - (amount || Config.osd.micIncrement), s);
|
||||
}
|
||||
|
||||
function setAudioSink(newSink: PwNode): void {
|
||||
Pipewire.preferredDefaultAudioSink = newSink;
|
||||
}
|
||||
|
||||
function setAudioSource(newSource: PwNode): void {
|
||||
Pipewire.preferredDefaultAudioSource = newSource;
|
||||
}
|
||||
}
|
||||
104
services/Bluetooth.qml
Normal file
104
services/Bluetooth.qml
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool powered
|
||||
property bool discovering
|
||||
readonly property list<Device> devices: []
|
||||
|
||||
Process {
|
||||
running: true
|
||||
command: ["bluetoothctl"]
|
||||
stdout: SplitParser {
|
||||
onRead: {
|
||||
getInfo.running = true;
|
||||
getDevices.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: getInfo
|
||||
|
||||
running: true
|
||||
command: ["bluetoothctl", "show"]
|
||||
environment: ({
|
||||
LANG: "C",
|
||||
LC_ALL: "C"
|
||||
})
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.powered = text.includes("Powered: yes");
|
||||
root.discovering = text.includes("Discovering: yes");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: getDevices
|
||||
|
||||
running: true
|
||||
command: ["fish", "-c", `
|
||||
for a in (bluetoothctl devices)
|
||||
if string match -q 'Device *' $a
|
||||
bluetoothctl info $addr (string split ' ' $a)[2]
|
||||
echo
|
||||
end
|
||||
end`]
|
||||
environment: ({
|
||||
LANG: "C",
|
||||
LC_ALL: "C"
|
||||
})
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const devices = text.trim().split("\n\n").map(d => ({
|
||||
name: d.match(/Name: (.*)/)[1],
|
||||
alias: d.match(/Alias: (.*)/)[1],
|
||||
address: d.match(/Device ([0-9A-Z:]{17})/)[1],
|
||||
icon: d.match(/Icon: (.*)/)[1],
|
||||
connected: d.includes("Connected: yes"),
|
||||
paired: d.includes("Paired: yes"),
|
||||
trusted: d.includes("Trusted: yes")
|
||||
}));
|
||||
const rDevices = root.devices;
|
||||
|
||||
const destroyed = rDevices.filter(rd => !devices.find(d => d.address === rd.address));
|
||||
for (const device of destroyed)
|
||||
rDevices.splice(rDevices.indexOf(device), 1).forEach(d => d.destroy());
|
||||
|
||||
for (const device of devices) {
|
||||
const match = rDevices.find(d => d.address === device.address);
|
||||
if (match) {
|
||||
match.lastIpcObject = device;
|
||||
} else {
|
||||
rDevices.push(deviceComp.createObject(root, {
|
||||
lastIpcObject: device
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Device: QtObject {
|
||||
required property var lastIpcObject
|
||||
readonly property string name: lastIpcObject.name
|
||||
readonly property string alias: lastIpcObject.alias
|
||||
readonly property string address: lastIpcObject.address
|
||||
readonly property string icon: lastIpcObject.icon
|
||||
readonly property bool connected: lastIpcObject.connected
|
||||
readonly property bool paired: lastIpcObject.paired
|
||||
readonly property bool trusted: lastIpcObject.trusted
|
||||
}
|
||||
|
||||
Component {
|
||||
id: deviceComp
|
||||
|
||||
Device {}
|
||||
}
|
||||
}
|
||||
153
services/Brightness.qml
Normal file
153
services/Brightness.qml
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.custom
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
reloadableId: "brightness"
|
||||
|
||||
property list<var> ddcMonitors: []
|
||||
readonly property list<Monitor> monitors: variants.instances
|
||||
property bool appleDisplayPresent: false
|
||||
|
||||
function getMonitorForScreen(screen: ShellScreen): var {
|
||||
return monitors.find(m => m.modelData === screen);
|
||||
}
|
||||
|
||||
function increaseBrightness(): void {
|
||||
const focusedName = Hypr.focusedMonitor.name;
|
||||
const monitor = monitors.find(m => focusedName === m.modelData.name);
|
||||
if (monitor)
|
||||
monitor.setBrightness(monitor.brightness + Config.osd.brightnessIncrement);
|
||||
}
|
||||
|
||||
function decreaseBrightness(): void {
|
||||
const focusedName = Hypr.focusedMonitor.name;
|
||||
const monitor = monitors.find(m => focusedName === m.modelData.name);
|
||||
if (monitor)
|
||||
monitor.setBrightness(monitor.brightness - Config.osd.brightnessIncrement);
|
||||
}
|
||||
|
||||
onMonitorsChanged: {
|
||||
ddcMonitors = [];
|
||||
ddcProc.running = true;
|
||||
}
|
||||
|
||||
Variants {
|
||||
id: variants
|
||||
|
||||
model: Quickshell.screens
|
||||
|
||||
Monitor {}
|
||||
}
|
||||
|
||||
Process {
|
||||
running: true
|
||||
command: ["sh", "-c", "asdbctl get"] // To avoid warnings if asdbctl is not installed
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root.appleDisplayPresent = text.trim().length > 0
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: ddcProc
|
||||
|
||||
command: ["ddcutil", "detect", "--brief"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root.ddcMonitors = text.trim().split("\n\n").filter(d => d.startsWith("Display ")).map(d => ({
|
||||
model: d.match(/Monitor:.*:(.*):.*/)[1],
|
||||
busNum: d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/)[1]
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
name: "brightnessUp"
|
||||
description: "Increase brightness"
|
||||
onPressed: root.increaseBrightness()
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
name: "brightnessDown"
|
||||
description: "Decrease brightness"
|
||||
onPressed: root.decreaseBrightness()
|
||||
}
|
||||
|
||||
component Monitor: QtObject {
|
||||
id: monitor
|
||||
|
||||
required property ShellScreen modelData
|
||||
readonly property bool isDdc: root.ddcMonitors.some(m => m.model === modelData.model)
|
||||
readonly property string busNum: root.ddcMonitors.find(m => m.model === modelData.model)?.busNum ?? ""
|
||||
readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay")
|
||||
property real brightness
|
||||
property real queuedBrightness: NaN
|
||||
|
||||
readonly property Process initProc: Process {
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (monitor.isAppleDisplay) {
|
||||
const val = parseInt(text.trim());
|
||||
monitor.brightness = val / 101;
|
||||
} else {
|
||||
const [, , , cur, max] = text.split(" ");
|
||||
monitor.brightness = parseInt(cur) / parseInt(max);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property Timer timer: Timer {
|
||||
interval: 500
|
||||
onTriggered: {
|
||||
if (!isNaN(monitor.queuedBrightness)) {
|
||||
monitor.setBrightness(monitor.queuedBrightness);
|
||||
monitor.queuedBrightness = NaN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setBrightness(value: real): void {
|
||||
value = Math.max(0, Math.min(1, value));
|
||||
const rounded = Math.round(value * 100);
|
||||
if (Math.round(brightness * 100) === rounded)
|
||||
return;
|
||||
|
||||
if (isDdc && timer.running) {
|
||||
queuedBrightness = value;
|
||||
return;
|
||||
}
|
||||
|
||||
brightness = value;
|
||||
|
||||
if (isAppleDisplay)
|
||||
Quickshell.execDetached(["asdbctl", "set", rounded]);
|
||||
else if (isDdc)
|
||||
Quickshell.execDetached(["ddcutil", "-b", busNum, "setvcp", "10", rounded]);
|
||||
else
|
||||
Quickshell.execDetached(["brightnessctl", "s", `${rounded}%`]);
|
||||
|
||||
if (isDdc)
|
||||
timer.restart();
|
||||
}
|
||||
|
||||
function initBrightness(): void {
|
||||
if (isAppleDisplay)
|
||||
initProc.command = ["asdbctl", "get"];
|
||||
else if (isDdc)
|
||||
initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"];
|
||||
else
|
||||
initProc.command = ["sh", "-c", "echo a b c $(brightnessctl g) $(brightnessctl m)"];
|
||||
|
||||
initProc.running = true;
|
||||
}
|
||||
|
||||
onBusNumChanged: initBrightness()
|
||||
Component.onCompleted: initBrightness()
|
||||
}
|
||||
}
|
||||
86
services/Hypr.qml
Normal file
86
services/Hypr.qml
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property var toplevels: Hyprland.toplevels
|
||||
readonly property var workspaces: Hyprland.workspaces
|
||||
readonly property var monitors: Hyprland.monitors
|
||||
property HyprlandToplevel activeToplevel: null
|
||||
readonly property HyprlandWorkspace focusedWorkspace: Hyprland.focusedWorkspace
|
||||
readonly property HyprlandMonitor focusedMonitor: Hyprland.focusedMonitor
|
||||
property string kbLayout: "?"
|
||||
|
||||
readonly property int arbitraryRaceConditionDelay: 50
|
||||
|
||||
function dispatch(request: string): void {
|
||||
Hyprland.dispatch(request);
|
||||
}
|
||||
|
||||
function monitorFor(screen: ShellScreen): HyprlandMonitor {
|
||||
return Hyprland.monitorFor(screen);
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Hyprland
|
||||
|
||||
function onRawEvent(event: HyprlandEvent): void {
|
||||
const n = event.name;
|
||||
if (n.endsWith("v2"))
|
||||
return;
|
||||
|
||||
if (n === "activelayout") {
|
||||
root.kbLayout = event.parse(2)[1].slice(0, 2).toLowerCase();
|
||||
} else if (["workspace", "moveworkspace", "activespecial", "focusedmon"].includes(n)) {
|
||||
Hyprland.refreshWorkspaces();
|
||||
Hyprland.refreshMonitors();
|
||||
} else if (["openwindow", "closewindow", "movewindow"].includes(n)) {
|
||||
Hyprland.refreshToplevels();
|
||||
Hyprland.refreshWorkspaces();
|
||||
} else if (n.includes("mon")) {
|
||||
Hyprland.refreshMonitors();
|
||||
} else if (n.includes("workspace")) {
|
||||
Hyprland.refreshWorkspaces();
|
||||
} else if (n.includes("window") || n.includes("group") || ["pin", "fullscreen", "changefloatingmode", "minimize"].includes(n)) {
|
||||
Hyprland.refreshToplevels();
|
||||
}
|
||||
}
|
||||
|
||||
function onActiveToplevelChanged() {
|
||||
toplevelTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
onFocusedWorkspaceChanged: toplevelTimer.start()
|
||||
|
||||
// Delay update to account for Hyprland's processing delay
|
||||
// (Prevent false null reports)
|
||||
Timer {
|
||||
id: toplevelTimer
|
||||
interval: root.arbitraryRaceConditionDelay
|
||||
repeat: false
|
||||
|
||||
onTriggered: {
|
||||
const toplevel = Hyprland.activeToplevel;
|
||||
// Invalidate active toplevel if in different workspace
|
||||
if (toplevel && toplevel?.workspace === focusedWorkspace) {
|
||||
root.activeToplevel = toplevel;
|
||||
} else {
|
||||
root.activeToplevel = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
running: true
|
||||
command: ["hyprctl", "-j", "devices"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root.kbLayout = JSON.parse(text).keyboards.find(k => k.main).active_keymap.slice(0, 2).toLowerCase()
|
||||
}
|
||||
}
|
||||
}
|
||||
103
services/Hyprsunset.qml
Normal file
103
services/Hyprsunset.qml
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
pragma Singleton
|
||||
|
||||
import qs.config
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
property var manualActive
|
||||
property string from: Config.services.sunsetFrom
|
||||
property string to: Config.services.sunsetTo
|
||||
property bool automatic: from !== to
|
||||
property bool shouldBeOn
|
||||
property bool firstEvaluation: true
|
||||
property bool active: false
|
||||
|
||||
property int fromHour: Number(from.split(":")[0])
|
||||
property int fromMinute: Number(from.split(":")[1])
|
||||
property int toHour: Number(to.split(":")[0])
|
||||
property int toMinute: Number(to.split(":")[1])
|
||||
|
||||
property int clockHour: Time.hours
|
||||
property int clockMinute: Time.minutes
|
||||
|
||||
onClockMinuteChanged: reEvaluate()
|
||||
onAutomaticChanged: {
|
||||
root.manualActive = undefined;
|
||||
root.firstEvaluation = true;
|
||||
reEvaluate();
|
||||
}
|
||||
function reEvaluate() {
|
||||
const t = clockHour * 60 + clockMinute;
|
||||
const from = fromHour * 60 + fromMinute;
|
||||
const to = toHour * 60 + toMinute;
|
||||
|
||||
if (from < to) {
|
||||
root.shouldBeOn = t >= from && t <= to;
|
||||
} else {
|
||||
// Wrapped around midnight
|
||||
root.shouldBeOn = t >= from || t <= to;
|
||||
}
|
||||
if (firstEvaluation) {
|
||||
firstEvaluation = false;
|
||||
root.ensureState();
|
||||
}
|
||||
}
|
||||
|
||||
onShouldBeOnChanged: ensureState()
|
||||
function ensureState() {
|
||||
if (!root.automatic || root.manualActive !== undefined)
|
||||
return;
|
||||
if (root.shouldBeOn) {
|
||||
root.enable();
|
||||
} else {
|
||||
root.disable();
|
||||
}
|
||||
}
|
||||
|
||||
function load() { } // Dummy to force init
|
||||
|
||||
function enable() {
|
||||
root.active = true;
|
||||
Quickshell.execDetached(["hyprsunset", "--temperature", Config.services.sunsetTemperature]);
|
||||
}
|
||||
|
||||
function disable() {
|
||||
root.active = false;
|
||||
Quickshell.execDetached(["pkill", "hyprsunset"]);
|
||||
}
|
||||
|
||||
function fetchState() {
|
||||
fetchProc.running = true;
|
||||
}
|
||||
|
||||
Process {
|
||||
id: fetchProc
|
||||
running: true
|
||||
command: ["hyprctl", "hyprsunset", "temperature"]
|
||||
stdout: StdioCollector {
|
||||
id: stateCollector
|
||||
onStreamFinished: {
|
||||
const output = stateCollector.text.trim();
|
||||
if (output.length == 0 || output.startsWith("Couldn't"))
|
||||
root.active = false;
|
||||
else
|
||||
root.active = (output != "6500");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (root.manualActive === undefined)
|
||||
root.manualActive = root.active;
|
||||
|
||||
root.manualActive = !root.manualActive;
|
||||
if (root.manualActive) {
|
||||
root.enable();
|
||||
} else {
|
||||
root.disable();
|
||||
}
|
||||
}
|
||||
}
|
||||
47
services/Idle.qml
Normal file
47
services/Idle.qml
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property alias inhibit: inhibitor.running
|
||||
property bool inhibitPipewire
|
||||
|
||||
function toggleInhibitPipewire() {
|
||||
root.inhibitPipewire = !root.inhibitPipewire;
|
||||
pipewireInhibitor.running = true;
|
||||
}
|
||||
|
||||
// Idle Inhibitor
|
||||
|
||||
Process {
|
||||
id: inhibitor
|
||||
command: ["wayland-idle-inhibitor"]
|
||||
}
|
||||
|
||||
// Idle Inhibit on Pipewire
|
||||
|
||||
readonly property string pipewireInhibitorService: "wayland-pipewire-idle-inhibit.service"
|
||||
|
||||
Timer {
|
||||
id: pipewireInhibitorTimer
|
||||
running: true
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: pipewireInhibitorCheck.running = true
|
||||
}
|
||||
|
||||
Process {
|
||||
id: pipewireInhibitorCheck
|
||||
command: ["systemctl", "status", "--user", root.pipewireInhibitorService]
|
||||
onExited: (code, _) => root.inhibitPipewire = (code === 0)
|
||||
}
|
||||
|
||||
Process {
|
||||
id: pipewireInhibitor
|
||||
command: ["systemctl", root.inhibitPipewire ? "start" : "stop", "--user", root.pipewireInhibitorService]
|
||||
}
|
||||
}
|
||||
192
services/Network.qml
Normal file
192
services/Network.qml
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property list<AccessPoint> networks: []
|
||||
readonly property AccessPoint active: networks.find(n => n.active) ?? null
|
||||
property bool wifiEnabled: true
|
||||
readonly property bool scanning: rescanProc.running
|
||||
|
||||
reloadableId: "network"
|
||||
|
||||
function enableWifi(enabled: bool): void {
|
||||
const cmd = enabled ? "on" : "off";
|
||||
enableWifiProc.exec(["nmcli", "radio", "wifi", cmd]);
|
||||
}
|
||||
|
||||
function toggleWifi(): void {
|
||||
const cmd = wifiEnabled ? "off" : "on";
|
||||
enableWifiProc.exec(["nmcli", "radio", "wifi", cmd]);
|
||||
}
|
||||
|
||||
function rescanWifi(): void {
|
||||
rescanProc.running = true;
|
||||
}
|
||||
|
||||
function connectToNetwork(ssid: string, password: string): void {
|
||||
// TODO: Implement password
|
||||
connectProc.exec(["nmcli", "conn", "up", ssid]);
|
||||
}
|
||||
|
||||
function disconnectFromNetwork(): void {
|
||||
if (active) {
|
||||
disconnectProc.exec(["nmcli", "connection", "down", active.ssid]);
|
||||
}
|
||||
}
|
||||
|
||||
function getWifiStatus(): void {
|
||||
wifiStatusProc.running = true;
|
||||
}
|
||||
|
||||
Process {
|
||||
running: true
|
||||
command: ["nmcli", "m"]
|
||||
stdout: SplitParser {
|
||||
onRead: getNetworks.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: wifiStatusProc
|
||||
|
||||
running: true
|
||||
command: ["nmcli", "radio", "wifi"]
|
||||
environment: ({
|
||||
LANG: "C",
|
||||
LC_ALL: "C"
|
||||
})
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.wifiEnabled = text.trim() === "enabled";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: enableWifiProc
|
||||
|
||||
onExited: {
|
||||
root.getWifiStatus();
|
||||
getNetworks.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: rescanProc
|
||||
|
||||
command: ["nmcli", "dev", "wifi", "list", "--rescan", "yes"]
|
||||
onExited: {
|
||||
getNetworks.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: connectProc
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: getNetworks.running = true
|
||||
}
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: console.warn("Network connection error:", text)
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: disconnectProc
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: getNetworks.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: getNetworks
|
||||
|
||||
running: true
|
||||
command: ["nmcli", "-g", "ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY", "d", "w"]
|
||||
environment: ({
|
||||
LANG: "C",
|
||||
LC_ALL: "C"
|
||||
})
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED";
|
||||
const rep = new RegExp("\\\\:", "g");
|
||||
const rep2 = new RegExp(PLACEHOLDER, "g");
|
||||
|
||||
const allNetworks = text.trim().split("\n").map(n => {
|
||||
const net = n.replace(rep, PLACEHOLDER).split(":");
|
||||
return {
|
||||
active: net[0] === "yes",
|
||||
strength: parseInt(net[1]),
|
||||
frequency: parseInt(net[2]),
|
||||
ssid: net[3],
|
||||
bssid: net[4]?.replace(rep2, ":") ?? "",
|
||||
security: net[5] || ""
|
||||
};
|
||||
}).filter(n => n.ssid && n.ssid.length > 0);
|
||||
|
||||
// Group networks by SSID and prioritize connected ones
|
||||
const networkMap = new Map();
|
||||
for (const network of allNetworks) {
|
||||
const existing = networkMap.get(network.ssid);
|
||||
if (!existing) {
|
||||
networkMap.set(network.ssid, network);
|
||||
} else {
|
||||
// Prioritize active/connected networks
|
||||
if (network.active && !existing.active) {
|
||||
networkMap.set(network.ssid, network);
|
||||
} else if (!network.active && !existing.active) {
|
||||
// If both are inactive, keep the one with better signal
|
||||
if (network.strength > existing.strength) {
|
||||
networkMap.set(network.ssid, network);
|
||||
}
|
||||
}
|
||||
// If existing is active and new is not, keep existing
|
||||
}
|
||||
}
|
||||
|
||||
const networks = Array.from(networkMap.values());
|
||||
|
||||
const rNetworks = root.networks;
|
||||
|
||||
const destroyed = rNetworks.filter(rn => !networks.find(n => n.frequency === rn.frequency && n.ssid === rn.ssid && n.bssid === rn.bssid));
|
||||
for (const network of destroyed)
|
||||
rNetworks.splice(rNetworks.indexOf(network), 1).forEach(n => n.destroy());
|
||||
|
||||
for (const network of networks) {
|
||||
const match = rNetworks.find(n => n.frequency === network.frequency && n.ssid === network.ssid && n.bssid === network.bssid);
|
||||
if (match) {
|
||||
match.lastIpcObject = network;
|
||||
} else {
|
||||
rNetworks.push(apComp.createObject(root, {
|
||||
lastIpcObject: network
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component AccessPoint: QtObject {
|
||||
required property var lastIpcObject
|
||||
readonly property string ssid: lastIpcObject.ssid
|
||||
readonly property string bssid: lastIpcObject.bssid
|
||||
readonly property int strength: lastIpcObject.strength
|
||||
readonly property int frequency: lastIpcObject.frequency
|
||||
readonly property bool active: lastIpcObject.active
|
||||
readonly property string security: lastIpcObject.security
|
||||
readonly property bool isSecure: security.length > 0
|
||||
}
|
||||
|
||||
Component {
|
||||
id: apComp
|
||||
|
||||
AccessPoint {}
|
||||
}
|
||||
}
|
||||
60
services/NixOS.qml
Normal file
60
services/NixOS.qml
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property string nixVersion: nixVersionProc.nixVersion
|
||||
readonly property list<Generation> generations: nixosGenerationsProc.generations
|
||||
readonly property Generation currentGen: generations.find(g => g.current) ?? null
|
||||
|
||||
Timer {
|
||||
running: true
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
interval: 60000
|
||||
onTriggered: {
|
||||
nixVersionProc.running = true;
|
||||
nixosGenerationsProc.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: nixVersionProc
|
||||
command: ["nix", "--version"]
|
||||
property string nixVersion: ""
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: nixVersionProc.nixVersion = this.text.split(" ")[2].trim()
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: nixosGenerationsProc
|
||||
command: ["nixos-rebuild", "list-generations", "--json"]
|
||||
property list<Generation> generations: []
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const json = JSON.parse(this.text);
|
||||
nixosGenerationsProc.generations = json.map(o => genComp.createObject(root, { ipcObject: o }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Generation: QtObject {
|
||||
required property var ipcObject
|
||||
readonly property int id: ipcObject.generation
|
||||
readonly property date date: ipcObject.date
|
||||
readonly property string nixosVersion: ipcObject.nixosVersion
|
||||
readonly property string kernelVersion: ipcObject.kernelVersion
|
||||
readonly property string revision: ipcObject.configurationRevision
|
||||
readonly property bool current: ipcObject.current
|
||||
}
|
||||
|
||||
Component {
|
||||
id: genComp
|
||||
Generation {}
|
||||
}
|
||||
}
|
||||
132
services/Notifs.qml
Normal file
132
services/Notifs.qml
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.config
|
||||
import qs.custom
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Services.Notifications
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property list<Notif> list: []
|
||||
|
||||
property bool dnd: false
|
||||
function toggleDnd(): void {
|
||||
dnd = !dnd
|
||||
}
|
||||
|
||||
NotificationServer {
|
||||
id: server
|
||||
|
||||
keepOnReload: false
|
||||
actionsSupported: true
|
||||
bodyHyperlinksSupported: true
|
||||
bodyImagesSupported: true
|
||||
bodyMarkupSupported: true
|
||||
imageSupported: true
|
||||
persistenceSupported: true
|
||||
|
||||
onNotification: notif => {
|
||||
notif.tracked = true;
|
||||
|
||||
root.list.push(notifComp.createObject(root, {
|
||||
popup: root.dnd ? null : Hypr.focusedMonitor,
|
||||
notification: notif
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
name: "clearNotifs"
|
||||
description: "Clear all notifications"
|
||||
onPressed: {
|
||||
for (const notif of root.list)
|
||||
notif.popup = null;
|
||||
}
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
name: "dismissNotifs"
|
||||
description: "Dismiss all notifications"
|
||||
onPressed: {
|
||||
for (const notif of root.list)
|
||||
notif.popup = null;
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "notifs"
|
||||
|
||||
function clear(): void {
|
||||
for (const notif of root.list)
|
||||
notif.popup = null;
|
||||
}
|
||||
}
|
||||
|
||||
component Notif: QtObject {
|
||||
id: notif
|
||||
|
||||
// Monitor to display popup in (typically the focused monitor when received)
|
||||
property HyprlandMonitor popup: null
|
||||
readonly property date time: new Date()
|
||||
readonly property string timeStr: Qt.formatTime(time, "hh:mm:ss")
|
||||
readonly property string timeSince: {
|
||||
const diff = Time.date.getTime() - time.getTime();
|
||||
const m = Math.floor(diff / 60000);
|
||||
const h = Math.floor(m / 60);
|
||||
const d = Math.floor(h / 24);
|
||||
|
||||
if (m < 1)
|
||||
return "now";
|
||||
if (h < 1)
|
||||
return `${m}m`;
|
||||
if (d < 1)
|
||||
return `${h}h`;
|
||||
return `${d}d`;
|
||||
}
|
||||
|
||||
required property Notification notification
|
||||
readonly property string summary: notification.summary
|
||||
readonly property string appIcon: notification.appIcon
|
||||
readonly property string appName: notification.appName
|
||||
readonly property string image: notification.image
|
||||
readonly property int urgency: notification.urgency
|
||||
readonly property list<NotificationAction> actions: notification.actions
|
||||
|
||||
// Split body text into lines parseable by StyledText format
|
||||
readonly property string body: notification.body.replace("\n", "<br/>")
|
||||
// One-line version (for non-expanded notifications)
|
||||
readonly property string bodyOneLine: notification.body.replace("\n", " ")
|
||||
|
||||
readonly property Timer timer: Timer {
|
||||
running: true
|
||||
interval: notif.notification.expireTimeout > 0 ? notif.notification.expireTimeout : Config.notifs.defaultExpireTimeout
|
||||
onTriggered: {
|
||||
if (Config.notifs.expire)
|
||||
notif.popup = null;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property Connections conn: Connections {
|
||||
target: notif.notification.Retainable
|
||||
|
||||
function onDropped(): void {
|
||||
root.list.splice(root.list.indexOf(notif), 1);
|
||||
}
|
||||
|
||||
function onAboutToDestroy(): void {
|
||||
notif.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: notifComp
|
||||
|
||||
Notif {}
|
||||
}
|
||||
}
|
||||
123
services/Players.qml
Normal file
123
services/Players.qml
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
pragma Singleton
|
||||
|
||||
import qs.config
|
||||
import qs.custom
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Mpris
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property list<MprisPlayer> list: Mpris.players.values
|
||||
readonly property MprisPlayer active: manualActive ?? list.find(p => getIdentity(p) === Config.services.defaultPlayer) ?? list[0] ?? null
|
||||
property MprisPlayer manualActive
|
||||
|
||||
// Alias IDs (for looking up icons)
|
||||
readonly property list<var> idAliases: [
|
||||
{
|
||||
"from": "Mozilla firefox",
|
||||
"to": "Firefox"
|
||||
}
|
||||
]
|
||||
// Alias names (for displaying)
|
||||
readonly property list<var> nameAliases: [
|
||||
{
|
||||
"from": "com.github.th_ch.youtube_music",
|
||||
"to": "YT Music"
|
||||
}
|
||||
]
|
||||
|
||||
function getIdentity(player: MprisPlayer): string {
|
||||
const alias = idAliases.find(a => a.from === player.identity);
|
||||
return alias?.to ?? player.identity;
|
||||
}
|
||||
|
||||
function getName(player: MprisPlayer): string {
|
||||
const alias = nameAliases.find(a => a.from === player.identity);
|
||||
return alias?.to ?? getIdentity(player);
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
name: "mediaToggle"
|
||||
description: "Toggle media playback"
|
||||
onPressed: {
|
||||
const active = root.active;
|
||||
if (active && active.canTogglePlaying)
|
||||
active.togglePlaying();
|
||||
}
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
name: "mediaPrev"
|
||||
description: "Previous track"
|
||||
onPressed: {
|
||||
const active = root.active;
|
||||
if (active && active.canGoPrevious)
|
||||
active.previous();
|
||||
}
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
name: "mediaNext"
|
||||
description: "Next track"
|
||||
onPressed: {
|
||||
const active = root.active;
|
||||
if (active && active.canGoNext)
|
||||
active.next();
|
||||
}
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
name: "mediaStop"
|
||||
description: "Stop media playback"
|
||||
onPressed: root.active?.stop()
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "mpris"
|
||||
|
||||
function getActive(prop: string): string {
|
||||
const active = root.active;
|
||||
return active ? active[prop] ?? "Invalid property" : "No active player";
|
||||
}
|
||||
|
||||
function list(): string {
|
||||
return root.list.map(p => root.get(p)).join("\n");
|
||||
}
|
||||
|
||||
function play(): void {
|
||||
const active = root.active;
|
||||
if (active?.canPlay)
|
||||
active.play();
|
||||
}
|
||||
|
||||
function pause(): void {
|
||||
const active = root.active;
|
||||
if (active?.canPause)
|
||||
active.pause();
|
||||
}
|
||||
|
||||
function playPause(): void {
|
||||
const active = root.active;
|
||||
if (active?.canTogglePlaying)
|
||||
active.togglePlaying();
|
||||
}
|
||||
|
||||
function previous(): void {
|
||||
const active = root.active;
|
||||
if (active?.canGoPrevious)
|
||||
active.previous();
|
||||
}
|
||||
|
||||
function next(): void {
|
||||
const active = root.active;
|
||||
if (active?.canGoNext)
|
||||
active.next();
|
||||
}
|
||||
|
||||
function stop(): void {
|
||||
root.active?.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
36
services/Requests.qml
Normal file
36
services/Requests.qml
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
pragma Singleton
|
||||
|
||||
import qs.config
|
||||
import qs.util
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function get(url: string, callback: var): void {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
const cleanup = () => {
|
||||
xhr.abort();
|
||||
xhr.onreadystatechange = null;
|
||||
xhr.onerror = null;
|
||||
};
|
||||
|
||||
xhr.open("GET", url, true);
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE) {
|
||||
if (xhr.status === 200)
|
||||
callback(xhr.responseText);
|
||||
else
|
||||
console.warn(`[REQUESTS] GET request to ${url} failed with status ${xhr.status}`);
|
||||
cleanup();
|
||||
}
|
||||
};
|
||||
xhr.onerror = () => {
|
||||
console.warn(`[REQUESTS] GET request to ${url} failed`);
|
||||
cleanup();
|
||||
};
|
||||
|
||||
xhr.send();
|
||||
}
|
||||
}
|
||||
222
services/SystemUsage.qml
Normal file
222
services/SystemUsage.qml
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
pragma Singleton
|
||||
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property real cpuPerc
|
||||
property real cpuTemp
|
||||
readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType
|
||||
property string autoGpuType: "NONE"
|
||||
property real gpuPerc
|
||||
property real gpuTemp
|
||||
property real memUsed
|
||||
property real memTotal
|
||||
readonly property real memPerc: memTotal > 0 ? memUsed / memTotal : 0
|
||||
property real storageUsed
|
||||
property real storageTotal
|
||||
property real storagePerc: storageTotal > 0 ? storageUsed / storageTotal : 0
|
||||
|
||||
property real lastCpuIdle
|
||||
property real lastCpuTotal
|
||||
|
||||
property int refCount
|
||||
|
||||
function formatKib(kib: real): var {
|
||||
const mib = 1024;
|
||||
const gib = 1024 ** 2;
|
||||
const tib = 1024 ** 3;
|
||||
|
||||
if (kib >= tib)
|
||||
return {
|
||||
value: kib / tib,
|
||||
unit: "TiB"
|
||||
};
|
||||
if (kib >= gib)
|
||||
return {
|
||||
value: kib / gib,
|
||||
unit: "GiB"
|
||||
};
|
||||
if (kib >= mib)
|
||||
return {
|
||||
value: kib / mib,
|
||||
unit: "MiB"
|
||||
};
|
||||
return {
|
||||
value: kib,
|
||||
unit: "KiB"
|
||||
};
|
||||
}
|
||||
|
||||
Timer {
|
||||
running: root.refCount > 0
|
||||
interval: 3000
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: {
|
||||
stat.reload();
|
||||
meminfo.reload();
|
||||
storage.running = true;
|
||||
gpuUsage.running = true;
|
||||
sensors.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: stat
|
||||
|
||||
path: "/proc/stat"
|
||||
onLoaded: {
|
||||
const data = text().match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/);
|
||||
if (data) {
|
||||
const stats = data.slice(1).map(n => parseInt(n, 10));
|
||||
const total = stats.reduce((a, b) => a + b, 0);
|
||||
const idle = stats[3] + (stats[4] ?? 0);
|
||||
|
||||
const totalDiff = total - root.lastCpuTotal;
|
||||
const idleDiff = idle - root.lastCpuIdle;
|
||||
root.cpuPerc = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0;
|
||||
|
||||
root.lastCpuTotal = total;
|
||||
root.lastCpuIdle = idle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: meminfo
|
||||
|
||||
path: "/proc/meminfo"
|
||||
onLoaded: {
|
||||
const data = text();
|
||||
root.memTotal = parseInt(data.match(/MemTotal: *(\d+)/)[1], 10) || 1;
|
||||
root.memUsed = (root.memTotal - parseInt(data.match(/MemAvailable: *(\d+)/)[1], 10)) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: storage
|
||||
|
||||
command: ["sh", "-c", "df | grep '^/dev/' | awk '{print $1, $3, $4}'"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const deviceMap = new Map();
|
||||
|
||||
for (const line of text.trim().split("\n")) {
|
||||
if (line.trim() === "")
|
||||
continue;
|
||||
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length >= 3) {
|
||||
const device = parts[0];
|
||||
const used = parseInt(parts[1], 10) || 0;
|
||||
const avail = parseInt(parts[2], 10) || 0;
|
||||
|
||||
// Only keep the entry with the largest total space for each device
|
||||
if (!deviceMap.has(device) || (used + avail) > (deviceMap.get(device).used + deviceMap.get(device).avail)) {
|
||||
deviceMap.set(device, {
|
||||
used: used,
|
||||
avail: avail
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let totalUsed = 0;
|
||||
let totalAvail = 0;
|
||||
|
||||
for (const [device, stats] of deviceMap) {
|
||||
totalUsed += stats.used;
|
||||
totalAvail += stats.avail;
|
||||
}
|
||||
|
||||
root.storageUsed = totalUsed;
|
||||
root.storageTotal = totalUsed + totalAvail;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: gpuTypeCheck
|
||||
|
||||
running: !Config.services.gpuType
|
||||
command: ["sh", "-c", "if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then echo NVIDIA; elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC; else echo NONE; fi"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root.autoGpuType = text.trim()
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: gpuUsage
|
||||
|
||||
command: root.gpuType === "GENERIC" ? ["sh", "-c", "cat /sys/class/drm/card*/device/gpu_busy_percent"] : root.gpuType === "NVIDIA" ? ["nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu", "--format=csv,noheader,nounits"] : ["echo"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (root.gpuType === "GENERIC") {
|
||||
const percs = text.trim().split("\n");
|
||||
const sum = percs.reduce((acc, d) => acc + parseInt(d, 10), 0);
|
||||
root.gpuPerc = sum / percs.length / 100;
|
||||
} else if (root.gpuType === "NVIDIA") {
|
||||
const [usage, temp] = text.trim().split(",");
|
||||
root.gpuPerc = parseInt(usage, 10) / 100;
|
||||
root.gpuTemp = parseInt(temp, 10);
|
||||
} else {
|
||||
root.gpuPerc = 0;
|
||||
root.gpuTemp = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: sensors
|
||||
|
||||
command: ["sensors"]
|
||||
environment: ({
|
||||
LANG: "C.UTF-8",
|
||||
LC_ALL: "C.UTF-8"
|
||||
})
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
let cpuTemp = text.match(/(?:Package id [0-9]+|Tdie):\s+((\+|-)[0-9.]+)(°| )C/);
|
||||
if (!cpuTemp)
|
||||
// If AMD Tdie pattern failed, try fallback on Tctl
|
||||
cpuTemp = text.match(/Tctl:\s+((\+|-)[0-9.]+)(°| )C/);
|
||||
|
||||
if (cpuTemp)
|
||||
root.cpuTemp = parseFloat(cpuTemp[1]);
|
||||
|
||||
if (root.gpuType !== "GENERIC")
|
||||
return;
|
||||
|
||||
let eligible = false;
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
|
||||
for (const line of text.trim().split("\n")) {
|
||||
if (line === "Adapter: PCI adapter")
|
||||
eligible = true;
|
||||
else if (line === "")
|
||||
eligible = false;
|
||||
else if (eligible) {
|
||||
let match = line.match(/^(temp[0-9]+|GPU core|edge)+:\s+\+([0-9]+\.[0-9]+)(°| )C/);
|
||||
if (!match)
|
||||
// Fall back to junction/mem if GPU doesn't have edge temp (for AMD GPUs)
|
||||
match = line.match(/^(junction|mem)+:\s+\+([0-9]+\.[0-9]+)(°| )C/);
|
||||
|
||||
if (match) {
|
||||
sum += parseFloat(match[2]);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
root.gpuTemp = count > 0 ? sum / count : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
52
services/Time.qml
Normal file
52
services/Time.qml
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
property alias enabled: clock.enabled
|
||||
readonly property date date: clock.date
|
||||
readonly property int hours: clock.hours
|
||||
readonly property int minutes: clock.minutes
|
||||
readonly property int seconds: clock.seconds
|
||||
|
||||
SystemClock {
|
||||
id: clock
|
||||
precision: SystemClock.Seconds
|
||||
}
|
||||
|
||||
function format(fmt: string): string {
|
||||
return Qt.formatDateTime(clock.date, fmt);
|
||||
}
|
||||
|
||||
function formatSeconds(s: int, includeSecs = false): string {
|
||||
let min = Math.floor(s / 60);
|
||||
let hr = Math.floor(min / 60);
|
||||
let day = Math.floor(hr / 24);
|
||||
let week = Math.floor(day / 7);
|
||||
let year = Math.floor(day / 365);
|
||||
s = s % 60;
|
||||
min = min % 60;
|
||||
hr = hr % 24;
|
||||
day = day % 7;
|
||||
week = week % 52;
|
||||
|
||||
let comps = [];
|
||||
if (year > 0)
|
||||
comps.push(`${year}y`);
|
||||
if (week > 0)
|
||||
comps.push(`${week}w`);
|
||||
if (day > 0)
|
||||
comps.push(`${day}d`);
|
||||
if (hr > 0)
|
||||
comps.push(`${hr}h`);
|
||||
if (min > 0)
|
||||
comps.push(`${min}m`);
|
||||
if (includeSecs && s > 0)
|
||||
comps.push(`${s}s`);
|
||||
|
||||
if (comps.length === 0)
|
||||
return "";
|
||||
else
|
||||
return comps.join(" ");
|
||||
}
|
||||
}
|
||||
36
services/User.qml
Normal file
36
services/User.qml
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
|
||||
readonly property string user: userProcess.user
|
||||
readonly property string uptime: fileUptime.uptime
|
||||
|
||||
Timer {
|
||||
running: true
|
||||
repeat: true
|
||||
interval: 15000
|
||||
triggeredOnStart: true
|
||||
onTriggered: fileUptime.reload()
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: fileUptime
|
||||
property string uptime: ""
|
||||
path: "/proc/uptime"
|
||||
onLoaded: uptime = Time.formatSeconds(parseInt(text().split(" ")[0] ?? 0));
|
||||
}
|
||||
|
||||
Process {
|
||||
id: userProcess
|
||||
property string user: ""
|
||||
running: true
|
||||
command: ["whoami"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: userProcess.user = this.text.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
42
services/Weather.qml
Normal file
42
services/Weather.qml
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
pragma Singleton
|
||||
|
||||
import qs.config
|
||||
import qs.util
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property string city
|
||||
property var cc
|
||||
property var forecast
|
||||
readonly property bool available: !!cc
|
||||
readonly property string icon: available ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert"
|
||||
readonly property color iconColor: available ? Icons.weatherIconColors[icon] : Config.colors.inactive
|
||||
readonly property string description: cc?.weatherDesc[0].value ?? qsTr("No weather")
|
||||
readonly property string temp: Config.services.useFahrenheit ? `${cc?.temp_F ?? 0}°F` : `${cc?.temp_C ?? 0}°C`
|
||||
readonly property string feelsLike: Config.services.useFahrenheit ? `${cc?.FeelsLikeF ?? 0}°F` : `${cc?.FeelsLikeC ?? 0}°C`
|
||||
readonly property real humidity: (cc?.humidity ?? 0) / 100
|
||||
readonly property string humidityIcon: available ? Icons.getHumidityIcon(humidity) : "humidity_low"
|
||||
|
||||
function reload(): void {
|
||||
if (Config.services.weatherLocation)
|
||||
city = Config.services.weatherLocation;
|
||||
else if (!city || timer.elapsed() > 900)
|
||||
Requests.get("https://ipinfo.io/json", text => {
|
||||
city = JSON.parse(text).city ?? "";
|
||||
timer.restart();
|
||||
});
|
||||
}
|
||||
|
||||
onCityChanged: Requests.get(`https://wttr.in/${city}?format=j1`, text => {
|
||||
const json = JSON.parse(text);
|
||||
cc = json.current_condition[0];
|
||||
forecast = json.weather;
|
||||
})
|
||||
|
||||
ElapsedTimer {
|
||||
id: timer
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue