This commit is contained in:
pika 2025-06-10 15:34:15 +02:00
parent 32edcff102
commit 0296117901
110 changed files with 9713 additions and 5 deletions

37
services/Apps.qml Normal file
View file

@ -0,0 +1,37 @@
pragma Singleton
import "root:/utils/scripts/fuzzysort.js" as Fuzzy
import Quickshell
import Quickshell.Io
Singleton {
id: root
readonly property list<DesktopEntry> list: DesktopEntries.applications.values.filter(a => !a.noDisplay).sort((a, b) => a.name.localeCompare(b.name))
readonly property list<var> preppedApps: list.map(a => ({
name: Fuzzy.prepare(a.name),
comment: Fuzzy.prepare(a.comment),
entry: a
}))
function fuzzyQuery(search: string): var { // Idk why list<DesktopEntry> doesn't work
return Fuzzy.go(search, preppedApps, {
all: true,
keys: ["name", "comment"],
scoreFn: r => r[0].score > 0 ? r[0].score * 0.9 + r[1].score * 0.1 : 0
}).map(r => r.obj.entry);
}
function launch(entry: DesktopEntry): void {
launchProc.entry = entry;
launchProc.startDetached();
}
Process {
id: launchProc
property DesktopEntry entry
command: ["app2unit", "--", `${entry?.id}.desktop`]
}
}

25
services/Audio.qml Normal file
View file

@ -0,0 +1,25 @@
pragma Singleton
import Quickshell
import Quickshell.Services.Pipewire
Singleton {
id: root
readonly property PwNode sink: Pipewire.defaultAudioSink
readonly property PwNode source: Pipewire.defaultAudioSource
readonly property bool muted: sink?.audio?.muted ?? false
readonly property real volume: sink?.audio?.volume ?? 0
function setVolume(volume: real): void {
if (sink?.ready && sink?.audio) {
sink.audio.muted = false;
sink.audio.volume = volume;
}
}
PwObjectTracker {
objects: [Pipewire.defaultAudioSink, Pipewire.defaultAudioSource]
}
}

18
services/BeatDetector.qml Normal file
View file

@ -0,0 +1,18 @@
pragma Singleton
import Quickshell
import Quickshell.Io
Singleton {
id: root
property real bpm
Process {
running: true
command: [`${Quickshell.shellRoot}/assets/realtime-beat-detector.py`]
stdout: SplitParser {
onRead: data => root.bpm = parseFloat(data)
}
}
}

80
services/Bluetooth.qml Normal file
View file

@ -0,0 +1,80 @@
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: ["sh", "-c", "bluetoothctl show | paste -s"]
stdout: SplitParser {
onRead: data => {
root.powered = data.includes("Powered: yes");
root.discovering = data.includes("Discovering: yes");
}
}
}
Process {
id: getDevices
running: true
command: ["fish", "-c", `for a in (bluetoothctl devices | cut -d ' ' -f 2); bluetoothctl info $a | jq -R 'reduce (inputs / ":") as [$key, $value] ({}; .[$key | ltrimstr("\t")] = ($value | ltrimstr(" ")))' | jq -c --arg addr $a '.Address = $addr'; end | jq -sc`]
stdout: SplitParser {
onRead: data => {
const devices = JSON.parse(data).filter(d => d.Name);
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 === "yes"
readonly property bool paired: lastIpcObject.Paired === "yes"
readonly property bool trusted: lastIpcObject.Trusted === "yes"
}
Component {
id: deviceComp
Device {}
}
}

118
services/Brightness.qml Normal file
View file

@ -0,0 +1,118 @@
pragma Singleton
pragma ComponentBehavior: Bound
import "root:/widgets"
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
property var ddcMonitors: []
readonly property list<Monitor> monitors: variants.instances
function getMonitorForScreen(screen: ShellScreen): var {
return monitors.find(m => m.modelData === screen);
}
function increaseBrightness(): void {
const focusedName = Hyprland.focusedMonitor.name;
const monitor = monitors.find(m => focusedName === m.modelData.name);
if (monitor)
monitor.setBrightness(monitor.brightness + 0.1);
}
function decreaseBrightness(): void {
const focusedName = Hyprland.focusedMonitor.name;
const monitor = monitors.find(m => focusedName === m.modelData.name);
if (monitor)
monitor.setBrightness(monitor.brightness - 0.1);
}
reloadableId: "brightness"
onMonitorsChanged: {
ddcMonitors = [];
ddcProc.running = true;
}
Variants {
id: variants
model: Quickshell.screens
Monitor {}
}
Process {
id: ddcProc
command: ["ddcutil", "detect", "--brief"]
stdout: SplitParser {
splitMarker: "\n\n"
onRead: data => {
if (data.startsWith("Display ")) {
const lines = data.split("\n").map(l => l.trim());
root.ddcMonitors.push({
model: lines.find(l => l.startsWith("Monitor:")).split(":")[2],
busNum: lines.find(l => l.startsWith("I2C bus:")).split("/dev/i2c-")[1]
});
}
}
}
onExited: root.ddcMonitorsChanged()
}
Process {
id: setProc
}
CustomShortcut {
name: "brightnessUp"
onPressed: root.increaseBrightness()
}
CustomShortcut {
name: "brightnessDown"
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 ?? ""
property real brightness
readonly property Process initProc: Process {
stdout: SplitParser {
onRead: data => {
const [, , , current, max] = data.split(" ");
monitor.brightness = parseInt(current) / parseInt(max);
}
}
}
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;
brightness = value;
setProc.command = isDdc ? ["ddcutil", "-b", busNum, "setvcp", "10", rounded] : ["brightnessctl", "s", `${rounded}%`];
setProc.startDetached();
}
onBusNumChanged: {
initProc.command = isDdc ? ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"] : ["sh", "-c", `echo "a b c $(brightnessctl g) $(brightnessctl m)"`];
initProc.running = true;
}
Component.onCompleted: {
initProc.command = isDdc ? ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"] : ["sh", "-c", `echo "a b c $(brightnessctl g) $(brightnessctl m)"`];
initProc.running = true;
}
}
}

19
services/Cava.qml Normal file
View file

@ -0,0 +1,19 @@
pragma Singleton
import "root:/config"
import Quickshell
import Quickshell.Io
Singleton {
id: root
property list<int> values
Process {
running: true
command: ["sh", "-c", `printf '[general]\nframerate=60\nbars=${DashboardConfig.visualiserBars}\n[output]\nchannels=mono\nmethod=raw\nraw_target=/dev/stdout\ndata_format=ascii\nascii_max_range=100' | cava -p /dev/stdin`]
stdout: SplitParser {
onRead: data => root.values = data.slice(0, -1).split(";").map(v => parseInt(v, 10))
}
}
}

153
services/Colours.qml Normal file
View file

@ -0,0 +1,153 @@
pragma Singleton
import "root:/config"
import "root:/utils"
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
readonly property list<string> colourNames: ["rosewater", "flamingo", "pink", "mauve", "red", "maroon", "peach", "yellow", "green", "teal", "sky", "sapphire", "blue", "lavender"]
property bool showPreview
property bool endPreviewOnNextChange
property bool light
readonly property Colours palette: showPreview ? preview : current
readonly property Colours current: Colours {}
readonly property Colours preview: Colours {}
readonly property Transparency transparency: Transparency {}
function alpha(c: color, layer: bool): color {
if (!transparency.enabled)
return c;
c = Qt.rgba(c.r, c.g, c.b, layer ? transparency.layers : transparency.base);
if (layer)
c.hsvValue = Math.max(0, Math.min(1, c.hslLightness + (light ? -0.2 : 0.2))); // TODO: edit based on colours (hue or smth)
return c;
}
function on(c: color): color {
if (c.hslLightness < 0.5)
return Qt.hsla(c.hslHue, c.hslSaturation, 0.9, 1);
return Qt.hsla(c.hslHue, c.hslSaturation, 0.1, 1);
}
function load(data: string, isPreview: bool): void {
const colours = isPreview ? preview : current;
for (const line of data.trim().split("\n")) {
let [name, colour] = line.split(" ");
name = name.trim();
name = colourNames.includes(name) ? name : `m3${name}`;
if (colours.hasOwnProperty(name))
colours[name] = `#${colour.trim()}`;
}
if (!isPreview || (isPreview && endPreviewOnNextChange)) {
showPreview = false;
endPreviewOnNextChange = false;
}
}
function setMode(mode: string): void {
setModeProc.command = ["caelestia", "scheme", "dynamic", "default", mode];
setModeProc.startDetached();
}
Process {
id: setModeProc
}
FileView {
path: `${Paths.state}/scheme/current-mode.txt`
watchChanges: true
onFileChanged: reload()
onLoaded: root.light = text() === "light"
}
FileView {
path: `${Paths.state}/scheme/current.txt`
watchChanges: true
onFileChanged: reload()
onLoaded: root.load(text(), false)
}
component Transparency: QtObject {
readonly property bool enabled: false
readonly property real base: 0.78
readonly property real layers: 0.58
}
component Colours: QtObject {
property color m3primary_paletteKeyColor: "#7870AB"
property color m3secondary_paletteKeyColor: "#78748A"
property color m3tertiary_paletteKeyColor: "#976A7D"
property color m3neutral_paletteKeyColor: "#79767D"
property color m3neutral_variant_paletteKeyColor: "#797680"
property color m3background: "#141318"
property color m3onBackground: "#E5E1E9"
property color m3surface: "#141318"
property color m3surfaceDim: "#141318"
property color m3surfaceBright: "#3A383E"
property color m3surfaceContainerLowest: "#0E0D13"
property color m3surfaceContainerLow: "#1C1B20"
property color m3surfaceContainer: "#201F25"
property color m3surfaceContainerHigh: "#2B292F"
property color m3surfaceContainerHighest: "#35343A"
property color m3onSurface: "#E5E1E9"
property color m3surfaceVariant: "#48454E"
property color m3onSurfaceVariant: "#C9C5D0"
property color m3inverseSurface: "#E5E1E9"
property color m3inverseOnSurface: "#312F36"
property color m3outline: "#938F99"
property color m3outlineVariant: "#48454E"
property color m3shadow: "#000000"
property color m3scrim: "#000000"
property color m3surfaceTint: "#C8BFFF"
property color m3primary: "#C8BFFF"
property color m3onPrimary: "#30285F"
property color m3primaryContainer: "#473F77"
property color m3onPrimaryContainer: "#E5DEFF"
property color m3inversePrimary: "#5F5791"
property color m3secondary: "#C9C3DC"
property color m3onSecondary: "#312E41"
property color m3secondaryContainer: "#484459"
property color m3onSecondaryContainer: "#E5DFF9"
property color m3tertiary: "#ECB8CD"
property color m3onTertiary: "#482536"
property color m3tertiaryContainer: "#B38397"
property color m3onTertiaryContainer: "#000000"
property color m3error: "#EA8DC1"
property color m3onError: "#690005"
property color m3errorContainer: "#93000A"
property color m3onErrorContainer: "#FFDAD6"
property color m3primaryFixed: "#E5DEFF"
property color m3primaryFixedDim: "#C8BFFF"
property color m3onPrimaryFixed: "#1B1149"
property color m3onPrimaryFixedVariant: "#473F77"
property color m3secondaryFixed: "#E5DFF9"
property color m3secondaryFixedDim: "#C9C3DC"
property color m3onSecondaryFixed: "#1C192B"
property color m3onSecondaryFixedVariant: "#484459"
property color m3tertiaryFixed: "#FFD8E7"
property color m3tertiaryFixedDim: "#ECB8CD"
property color m3onTertiaryFixed: "#301121"
property color m3onTertiaryFixedVariant: "#613B4C"
property color rosewater: "#B8C4FF"
property color flamingo: "#DBB9F8"
property color pink: "#F3B3E3"
property color mauve: "#D0BDFE"
property color red: "#F8B3D1"
property color maroon: "#F6B2DA"
property color peach: "#E4B7F4"
property color yellow: "#C3C0FF"
property color green: "#ADC6FF"
property color teal: "#D4BBFC"
property color sky: "#CBBEFF"
property color sapphire: "#BDC2FF"
property color blue: "#C7BFFF"
property color lavender: "#EAB5ED"
}
}

114
services/Hyprland.qml Normal file
View file

@ -0,0 +1,114 @@
pragma Singleton
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
import QtQuick
Singleton {
id: root
readonly property list<Client> clients: []
readonly property var workspaces: Hyprland.workspaces
readonly property var monitors: Hyprland.monitors
property Client activeClient: null
readonly property HyprlandWorkspace activeWorkspace: focusedMonitor?.activeWorkspace ?? null
readonly property HyprlandMonitor focusedMonitor: Hyprland.focusedMonitor
readonly property int activeWsId: activeWorkspace?.id ?? 1
property point cursorPos
function reload() {
Hyprland.refreshWorkspaces();
Hyprland.refreshMonitors();
getClients.running = true;
getActiveClient.running = true;
}
function dispatch(request: string): void {
Hyprland.dispatch(request);
}
Component.onCompleted: reload()
Connections {
target: Hyprland
function onRawEvent(event: HyprlandEvent): void {
if (!event.name.endsWith("v2"))
root.reload();
}
}
Process {
id: getClients
command: ["sh", "-c", "hyprctl -j clients | jq -c"]
stdout: SplitParser {
onRead: data => {
const clients = JSON.parse(data);
const rClients = root.clients;
const destroyed = rClients.filter(rc => !clients.find(c => c.address === rc.address));
for (const client of destroyed)
rClients.splice(rClients.indexOf(client), 1).forEach(c => c.destroy());
for (const client of clients) {
const match = rClients.find(c => c.address === client.address);
if (match) {
match.lastIpcObject = client;
} else {
rClients.push(clientComp.createObject(root, {
lastIpcObject: client
}));
}
}
}
}
}
Process {
id: getActiveClient
command: ["hyprctl", "-j", "activewindow"]
stdout: SplitParser {
splitMarker: ""
onRead: data => {
const client = JSON.parse(data);
const rClient = root.activeClient;
if (client.address) {
if (rClient)
rClient.lastIpcObject = client;
else
root.activeClient = clientComp.createObject(root, {
lastIpcObject: client
});
} else if (rClient) {
rClient.destroy();
root.activeClient = null;
}
}
}
}
component Client: QtObject {
required property var lastIpcObject
readonly property string address: lastIpcObject.address
readonly property string wmClass: lastIpcObject.class
readonly property string title: lastIpcObject.title
readonly property string initialClass: lastIpcObject.initialClass
readonly property string initialTitle: lastIpcObject.initialTitle
readonly property int x: lastIpcObject.at[0]
readonly property int y: lastIpcObject.at[1]
readonly property int width: lastIpcObject.size[0]
readonly property int height: lastIpcObject.size[1]
readonly property HyprlandWorkspace workspace: Hyprland.workspaces.values.find(w => w.id === lastIpcObject.workspace.id) ?? null
readonly property bool floating: lastIpcObject.floating
readonly property bool fullscreen: lastIpcObject.fullscreen
readonly property int pid: lastIpcObject.pid
readonly property int focusHistoryId: lastIpcObject.focusHistoryID
}
Component {
id: clientComp
Client {}
}
}

68
services/Network.qml Normal file
View file

@ -0,0 +1,68 @@
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
reloadableId: "network"
Process {
running: true
command: ["nmcli", "m"]
stdout: SplitParser {
onRead: getNetworks.running = true
}
}
Process {
id: getNetworks
running: true
command: ["sh", "-c", `nmcli -g ACTIVE,SIGNAL,FREQ,SSID d w | jq -cR '[(inputs / ":") | select(.[3] | length >= 4)]'`]
stdout: SplitParser {
onRead: data => {
const networks = JSON.parse(data).map(n => [n[0] === "yes", parseInt(n[1]), parseInt(n[2]), n[3]]);
const rNetworks = root.networks;
const destroyed = rNetworks.filter(rn => !networks.find(n => n[2] === rn.frequency && n[3] === rn.ssid));
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[2] && n.ssid === network[3]);
if (match) {
match.active = network[0];
match.strength = network[1];
match.frequency = network[2];
match.ssid = network[3];
} else {
rNetworks.push(apComp.createObject(root, {
active: network[0],
strength: network[1],
frequency: network[2],
ssid: network[3]
}));
}
}
}
}
}
component AccessPoint: QtObject {
required property string ssid
required property int strength
required property int frequency
required property bool active
}
Component {
id: apComp
AccessPoint {}
}
}

98
services/Notifs.qml Normal file
View file

@ -0,0 +1,98 @@
pragma Singleton
pragma ComponentBehavior: Bound
import "root:/widgets"
import "root:/config"
import Quickshell
import Quickshell.Services.Notifications
import QtQuick
Singleton {
id: root
readonly property list<Notif> list: []
readonly property list<Notif> popups: list.filter(n => n.popup)
NotificationServer {
id: server
keepOnReload: false
actionsSupported: true
bodyHyperlinksSupported: true
bodyImagesSupported: true
bodyMarkupSupported: true
imageSupported: true
onNotification: notif => {
notif.tracked = true;
root.list.push(notifComp.createObject(root, {
popup: true,
notification: notif
}));
}
}
CustomShortcut {
name: "clearNotifs"
description: "Clear all notifications"
onPressed: {
for (const notif of root.list)
notif.popup = false;
}
}
component Notif: QtObject {
id: notif
property bool popup
readonly property date time: new Date()
readonly property string timeStr: {
const diff = Time.date.getTime() - time.getTime();
const m = Math.floor(diff / 60000);
const h = Math.floor(m / 60);
if (h < 1 && m < 1)
return "now";
if (h < 1)
return `${m}m`;
return `${h}h`;
}
required property Notification notification
readonly property string summary: notification.summary
readonly property string body: notification.body
readonly property string appIcon: notification.appIcon
readonly property string appName: notification.appName
readonly property string image: notification.image
readonly property var urgency: notification.urgency // Idk why NotificationUrgency doesn't work
readonly property list<NotificationAction> actions: notification.actions
readonly property Timer timer: Timer {
running: true
interval: notif.notification.expireTimeout > 0 ? notif.notification.expireTimeout : NotifsConfig.defaultExpireTimeout
onTriggered: {
if (NotifsConfig.expire)
notif.popup = false;
}
}
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 {}
}
}

59
services/Players.qml Normal file
View file

@ -0,0 +1,59 @@
pragma Singleton
import "root:/widgets"
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 => p.identity === "Spotify") ?? list[0] ?? null
property MprisPlayer manualActive
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";
}
}
}

173
services/SystemUsage.qml Normal file
View file

@ -0,0 +1,173 @@
pragma Singleton
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
property real cpuPerc
property real cpuTemp
property real gpuPerc
property real gpuTemp
property int memUsed
property int memTotal
readonly property real memPerc: memTotal > 0 ? memUsed / memTotal : 0
property int storageUsed
property int storageTotal
property real storagePerc: storageTotal > 0 ? storageUsed / storageTotal : 0
property int lastCpuIdle
property int lastCpuTotal
function formatKib(kib: int): 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: true
interval: 3000
repeat: true
onTriggered: {
stat.reload();
meminfo.reload();
storage.running = true;
cpuTemp.running = true;
gpuUsage.running = true;
gpuTemp.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];
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
running: true
command: ["sh", "-c", "df | grep '^/dev/' | awk '{print $3, $4}'"]
stdout: SplitParser {
splitMarker: ""
onRead: data => {
let used = 0;
let avail = 0;
for (const line of data.trim().split("\n")) {
const [u, a] = line.split(" ");
used += parseInt(u, 10);
avail += parseInt(a, 10);
}
root.storageUsed = used;
root.storageTotal = used + avail;
}
}
}
Process {
id: cpuTemp
running: true
command: ["fish", "-c", "cat /sys/class/thermal/thermal_zone*/temp | string join ' '"]
stdout: SplitParser {
onRead: data => {
const temps = data.trim().split(" ");
const sum = temps.reduce((acc, d) => acc + parseInt(d, 10), 0);
root.cpuTemp = sum / temps.length / 1000;
}
}
}
Process {
id: gpuUsage
running: true
command: ["sh", "-c", "cat /sys/class/drm/card*/device/gpu_busy_percent"]
stdout: SplitParser {
splitMarker: ""
onRead: data => {
const percs = data.trim().split("\n");
const sum = percs.reduce((acc, d) => acc + parseInt(d, 10), 0);
root.gpuPerc = sum / percs.length / 100;
}
}
}
Process {
id: gpuTemp
running: true
command: ["sh", "-c", "sensors | jq -nRc '[inputs]'"]
stdout: SplitParser {
onRead: data => {
let eligible = false;
let sum = 0;
let count = 0;
for (const line of JSON.parse(data)) {
if (line === "Adapter: PCI adapter")
eligible = true;
else if (line === "")
eligible = false;
else if (eligible) {
const match = line.match(/^(temp[0-9]+|GPU core|edge)+:\s+\+([0-9]+\.[0-9]+)°C/);
if (match) {
sum += parseFloat(match[2]);
count++;
}
}
}
root.gpuTemp = count > 0 ? sum / count : 0;
}
}
}
}

72
services/Thumbnailer.qml Normal file
View file

@ -0,0 +1,72 @@
pragma Singleton
pragma ComponentBehavior: Bound
import "root:/utils"
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
readonly property string thumbDir: `${Paths.cache}/thumbnails`.slice(7)
function go(obj: var): var {
return thumbComp.createObject(obj, {
originalPath: obj.path,
width: obj.width,
height: obj.height,
loadOriginal: obj.loadOriginal
});
}
component Thumbnail: QtObject {
id: obj
required property string originalPath
required property int width
required property int height
required property bool loadOriginal
property string path
readonly property Process proc: Process {
running: true
command: ["fish", "-c", `
set -l path "${root.thumbDir}/$(sha1sum ${obj.originalPath} | cut -d ' ' -f 1)@${obj.width}x${obj.height}-exact.png"
if test -f $path
echo $path
else
echo 'start'
set -l size (identify -ping -format '%w\n%h' ${obj.originalPath})
if test $size[1] -gt ${obj.width} -o $size[2] -gt ${obj.height}
magick ${obj.originalPath} -${obj.width > 1024 || obj.height > 1024 ? "resize" : "thumbnail"} ${obj.width}x${obj.height}^ -background none -gravity center -extent ${obj.width}x${obj.height} -unsharp 0x.5 $path
else
cp ${obj.originalPath} $path
end
echo $path
end`]
stdout: SplitParser {
onRead: data => {
if (data === "start") {
if (obj.loadOriginal)
obj.path = obj.originalPath;
} else {
obj.path = data;
}
}
}
}
function reload(): void {
proc.signal(9);
proc.running = true;
}
}
Component {
id: thumbComp
Thumbnail {}
}
}

20
services/Time.qml Normal file
View file

@ -0,0 +1,20 @@
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
function format(fmt: string): string {
return Qt.formatDateTime(clock.date, fmt);
}
SystemClock {
id: clock
precision: SystemClock.Seconds
}
}

12
services/Visibilities.qml Normal file
View file

@ -0,0 +1,12 @@
pragma Singleton
import Quickshell
Singleton {
property var screens: ({})
property var panels: ({})
function getForActive(): PersistentProperties {
return Object.entries(screens).find(s => s[0].slice(s[0].indexOf('"') + 1, s[0].lastIndexOf('"')) === Hyprland.focusedMonitor.name)[1];
}
}

102
services/Wallpapers.qml Normal file
View file

@ -0,0 +1,102 @@
pragma Singleton
import "root:/utils/scripts/fuzzysort.js" as Fuzzy
import "root:/utils"
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
readonly property string currentNamePath: `${Paths.state}/wallpaper/last.txt`.slice(7)
readonly property string path: `${Paths.pictures}/Wallpapers`.slice(7)
readonly property list<Wallpaper> list: wallpapers.instances
property bool showPreview: false
readonly property string current: showPreview ? previewPath : actualCurrent
property string previewPath
property string actualCurrent
readonly property list<var> preppedWalls: list.map(w => ({
name: Fuzzy.prepare(w.name),
path: Fuzzy.prepare(w.path),
wall: w
}))
function fuzzyQuery(search: string): var {
return Fuzzy.go(search, preppedWalls, {
all: true,
keys: ["name", "path"],
scoreFn: r => r[0].score * 0.9 + r[1].score * 0.1
}).map(r => r.obj.wall);
}
function setWallpaper(path: string): void {
actualCurrent = path;
setWall.path = path;
setWall.startDetached();
}
function preview(path: string): void {
previewPath = path;
showPreview = true;
getPreviewColoursProc.running = true;
}
function stopPreview(): void {
showPreview = false;
Colours.endPreviewOnNextChange = true;
}
reloadableId: "wallpapers"
FileView {
path: root.currentNamePath
watchChanges: true
onFileChanged: reload()
onLoaded: root.actualCurrent = text().trim()
}
Process {
id: getPreviewColoursProc
command: ["caelestia", "scheme", "print", root.previewPath]
stdout: SplitParser {
splitMarker: ""
onRead: data => {
Colours.load(data, true);
Colours.showPreview = true;
}
}
}
Process {
id: setWall
property string path
command: ["caelestia", "wallpaper", "-f", path]
}
Process {
running: true
command: ["fd", ".", root.path, "-t", "f", "-e", "jpg", "-e", "jpeg", "-e", "png", "-e", "svg"]
stdout: SplitParser {
splitMarker: ""
onRead: data => wallpapers.model = data.trim().split("\n")
}
}
Variants {
id: wallpapers
Wallpaper {}
}
component Wallpaper: QtObject {
required property string modelData
readonly property string path: modelData
readonly property string name: path.slice(path.lastIndexOf("/") + 1, path.lastIndexOf("."))
}
}

32
services/Weather.qml Normal file
View file

@ -0,0 +1,32 @@
pragma Singleton
import "root:/utils"
import Quickshell
import Quickshell.Io
Singleton {
id: root
property string icon
property string description
property real temperature
function reload(): void {
wttrProc.running = true;
}
Process {
id: wttrProc
running: true
command: ["fish", "-c", `curl "https://wttr.in/$(curl ipinfo.io | jq -r '.city' | string replace -a ' ' '%20')?format=j1" | jq -c '.current_condition[0] | {code: .weatherCode, desc: .weatherDesc[0].value, temp: .temp_C}'`]
stdout: SplitParser {
onRead: data => {
const json = JSON.parse(data);
root.icon = Icons.getWeatherIcon(json.code);
root.description = json.desc;
root.temperature = parseFloat(json.temp);
}
}
}
}