wip
This commit is contained in:
parent
32edcff102
commit
0296117901
110 changed files with 9713 additions and 5 deletions
37
services/Apps.qml
Normal file
37
services/Apps.qml
Normal 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
25
services/Audio.qml
Normal 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
18
services/BeatDetector.qml
Normal 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
80
services/Bluetooth.qml
Normal 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
118
services/Brightness.qml
Normal 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
19
services/Cava.qml
Normal 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
153
services/Colours.qml
Normal 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
114
services/Hyprland.qml
Normal 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
68
services/Network.qml
Normal 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
98
services/Notifs.qml
Normal 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
59
services/Players.qml
Normal 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
173
services/SystemUsage.qml
Normal 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
72
services/Thumbnailer.qml
Normal 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
20
services/Time.qml
Normal 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
12
services/Visibilities.qml
Normal 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
102
services/Wallpapers.qml
Normal 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
32
services/Weather.qml
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue