Evernight & Caelestia
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
pragma Singleton
|
||||
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property string previousSinkName: ""
|
||||
property string previousSourceName: ""
|
||||
|
||||
readonly property var nodes: Pipewire.nodes.values.reduce((acc, node) => {
|
||||
if (!node.isStream) {
|
||||
if (node.isSink)
|
||||
acc.sinks.push(node);
|
||||
else if (node.audio)
|
||||
acc.sources.push(node);
|
||||
}
|
||||
return acc;
|
||||
}, {
|
||||
sources: [],
|
||||
sinks: []
|
||||
})
|
||||
|
||||
readonly property list<PwNode> sinks: nodes.sinks
|
||||
readonly property list<PwNode> sources: nodes.sources
|
||||
|
||||
readonly property PwNode sink: Pipewire.defaultAudioSink
|
||||
readonly property PwNode source: Pipewire.defaultAudioSource
|
||||
|
||||
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
|
||||
|
||||
readonly property alias cava: cava
|
||||
readonly property alias beatTracker: beatTracker
|
||||
|
||||
function setVolume(newVolume: real): void {
|
||||
if (sink?.ready && sink?.audio) {
|
||||
sink.audio.muted = false;
|
||||
sink.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume));
|
||||
}
|
||||
}
|
||||
|
||||
function incrementVolume(amount: real): void {
|
||||
setVolume(volume + (amount || Config.services.audioIncrement));
|
||||
}
|
||||
|
||||
function decrementVolume(amount: real): void {
|
||||
setVolume(volume - (amount || Config.services.audioIncrement));
|
||||
}
|
||||
|
||||
function setSourceVolume(newVolume: real): void {
|
||||
if (source?.ready && source?.audio) {
|
||||
source.audio.muted = false;
|
||||
source.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume));
|
||||
}
|
||||
}
|
||||
|
||||
function incrementSourceVolume(amount: real): void {
|
||||
setSourceVolume(sourceVolume + (amount || Config.services.audioIncrement));
|
||||
}
|
||||
|
||||
function decrementSourceVolume(amount: real): void {
|
||||
setSourceVolume(sourceVolume - (amount || Config.services.audioIncrement));
|
||||
}
|
||||
|
||||
function setAudioSink(newSink: PwNode): void {
|
||||
Pipewire.preferredDefaultAudioSink = newSink;
|
||||
}
|
||||
|
||||
function setAudioSource(newSource: PwNode): void {
|
||||
Pipewire.preferredDefaultAudioSource = newSource;
|
||||
}
|
||||
|
||||
onSinkChanged: {
|
||||
if (!sink?.ready)
|
||||
return;
|
||||
|
||||
const newSinkName = sink.description || sink.name || qsTr("Unknown Device");
|
||||
|
||||
if (previousSinkName && previousSinkName !== newSinkName && Config.utilities.toasts.audioOutputChanged)
|
||||
Toaster.toast(qsTr("Audio output changed"), qsTr("Now using: %1").arg(newSinkName), "volume_up");
|
||||
|
||||
previousSinkName = newSinkName;
|
||||
}
|
||||
|
||||
onSourceChanged: {
|
||||
if (!source?.ready)
|
||||
return;
|
||||
|
||||
const newSourceName = source.description || source.name || qsTr("Unknown Device");
|
||||
|
||||
if (previousSourceName && previousSourceName !== newSourceName && Config.utilities.toasts.audioInputChanged)
|
||||
Toaster.toast(qsTr("Audio input changed"), qsTr("Now using: %1").arg(newSourceName), "mic");
|
||||
|
||||
previousSourceName = newSourceName;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
previousSinkName = sink?.description || sink?.name || qsTr("Unknown Device");
|
||||
previousSourceName = source?.description || source?.name || qsTr("Unknown Device");
|
||||
}
|
||||
|
||||
PwObjectTracker {
|
||||
objects: [...root.sinks, ...root.sources]
|
||||
}
|
||||
|
||||
CavaProvider {
|
||||
id: cava
|
||||
|
||||
bars: Config.services.visualiserBars
|
||||
}
|
||||
|
||||
BeatTracker {
|
||||
id: beatTracker
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components.misc
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
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 getMonitor(query: string): var {
|
||||
if (query === "active") {
|
||||
return monitors.find(m => Hypr.monitorFor(m.modelData)?.focused);
|
||||
}
|
||||
|
||||
if (query.startsWith("model:")) {
|
||||
const model = query.slice(6);
|
||||
return monitors.find(m => m.modelData.model === model);
|
||||
}
|
||||
|
||||
if (query.startsWith("serial:")) {
|
||||
const serial = query.slice(7);
|
||||
return monitors.find(m => m.modelData.serialNumber === serial);
|
||||
}
|
||||
|
||||
if (query.startsWith("id:")) {
|
||||
const id = parseInt(query.slice(3), 10);
|
||||
return monitors.find(m => Hypr.monitorFor(m.modelData)?.id === id);
|
||||
}
|
||||
|
||||
return monitors.find(m => m.modelData.name === query);
|
||||
}
|
||||
|
||||
function increaseBrightness(): void {
|
||||
const monitor = getMonitor("active");
|
||||
if (monitor)
|
||||
monitor.setBrightness(monitor.brightness + 0.1);
|
||||
}
|
||||
|
||||
function decreaseBrightness(): void {
|
||||
const monitor = getMonitor("active");
|
||||
if (monitor)
|
||||
monitor.setBrightness(monitor.brightness - 0.1);
|
||||
}
|
||||
|
||||
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 => ({
|
||||
busNum: d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/)[1],
|
||||
connector: d.match(/DRM connector:\s+(.*)/)[1].replace(/^card\d+-/, "") // strip "card1-"
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
name: "brightnessUp"
|
||||
description: "Increase brightness"
|
||||
onPressed: root.increaseBrightness()
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
name: "brightnessDown"
|
||||
description: "Decrease brightness"
|
||||
onPressed: root.decreaseBrightness()
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "brightness"
|
||||
|
||||
function get(): real {
|
||||
return getFor("active");
|
||||
}
|
||||
|
||||
// Allows searching by active/model/serial/id/name
|
||||
function getFor(query: string): real {
|
||||
return root.getMonitor(query)?.brightness ?? -1;
|
||||
}
|
||||
|
||||
function set(value: string): string {
|
||||
return setFor("active", value);
|
||||
}
|
||||
|
||||
// Handles brightness value like brightnessctl: 0.1, +0.1, 0.1-, 10%, +10%, 10%-
|
||||
function setFor(query: string, value: string): string {
|
||||
const monitor = root.getMonitor(query);
|
||||
if (!monitor)
|
||||
return "Invalid monitor: " + query;
|
||||
|
||||
let targetBrightness;
|
||||
if (value.endsWith("%-")) {
|
||||
const percent = parseFloat(value.slice(0, -2));
|
||||
targetBrightness = monitor.brightness - (percent / 100);
|
||||
} else if (value.startsWith("+") && value.endsWith("%")) {
|
||||
const percent = parseFloat(value.slice(1, -1));
|
||||
targetBrightness = monitor.brightness + (percent / 100);
|
||||
} else if (value.endsWith("%")) {
|
||||
const percent = parseFloat(value.slice(0, -1));
|
||||
targetBrightness = percent / 100;
|
||||
} else if (value.startsWith("+")) {
|
||||
const increment = parseFloat(value.slice(1));
|
||||
targetBrightness = monitor.brightness + increment;
|
||||
} else if (value.endsWith("-")) {
|
||||
const decrement = parseFloat(value.slice(0, -1));
|
||||
targetBrightness = monitor.brightness - decrement;
|
||||
} else if (value.includes("%") || value.includes("-") || value.includes("+")) {
|
||||
return `Invalid brightness format: ${value}\nExpected: 0.1, +0.1, 0.1-, 10%, +10%, 10%-`;
|
||||
} else {
|
||||
targetBrightness = parseFloat(value);
|
||||
}
|
||||
|
||||
if (isNaN(targetBrightness))
|
||||
return `Failed to parse value: ${value}\nExpected: 0.1, +0.1, 0.1-, 10%, +10%, 10%-`;
|
||||
|
||||
monitor.setBrightness(targetBrightness);
|
||||
|
||||
return `Set monitor ${monitor.modelData.name} brightness to ${+monitor.brightness.toFixed(2)}`;
|
||||
}
|
||||
}
|
||||
|
||||
component Monitor: QtObject {
|
||||
id: monitor
|
||||
|
||||
required property ShellScreen modelData
|
||||
readonly property bool isDdc: root.ddcMonitors.some(m => m.connector === modelData.name)
|
||||
readonly property string busNum: root.ddcMonitors.find(m => m.connector === modelData.name)?.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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Caelestia
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool showPreview
|
||||
property string scheme
|
||||
property string flavour
|
||||
readonly property bool light: showPreview ? previewLight : currentLight
|
||||
property bool currentLight
|
||||
property bool previewLight
|
||||
readonly property M3Palette palette: showPreview ? preview : current
|
||||
readonly property M3TPalette tPalette: M3TPalette {}
|
||||
readonly property M3Palette current: M3Palette {}
|
||||
readonly property M3Palette preview: M3Palette {}
|
||||
readonly property Transparency transparency: Transparency {}
|
||||
readonly property alias wallLuminance: analyser.luminance
|
||||
|
||||
function getLuminance(c: color): real {
|
||||
if (c.r == 0 && c.g == 0 && c.b == 0)
|
||||
return 0;
|
||||
return Math.sqrt(0.299 * (c.r ** 2) + 0.587 * (c.g ** 2) + 0.114 * (c.b ** 2));
|
||||
}
|
||||
|
||||
function alterColour(c: color, a: real, layer: int): color {
|
||||
const luminance = getLuminance(c);
|
||||
|
||||
const offset = (!light || layer == 1 ? 1 : -layer / 2) * (light ? 0.2 : 0.3) * (1 - transparency.base) * (1 + wallLuminance * (light ? (layer == 1 ? 3 : 1) : 2.5));
|
||||
const scale = (luminance + offset) / luminance;
|
||||
const r = Math.max(0, Math.min(1, c.r * scale));
|
||||
const g = Math.max(0, Math.min(1, c.g * scale));
|
||||
const b = Math.max(0, Math.min(1, c.b * scale));
|
||||
|
||||
return Qt.rgba(r, g, b, a);
|
||||
}
|
||||
|
||||
function layer(c: color, layer: var): color {
|
||||
if (!transparency.enabled)
|
||||
return c;
|
||||
|
||||
return layer === 0 ? Qt.alpha(c, transparency.base) : alterColour(c, transparency.layers, layer ?? 1);
|
||||
}
|
||||
|
||||
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;
|
||||
const scheme = JSON.parse(data);
|
||||
|
||||
if (!isPreview) {
|
||||
root.scheme = scheme.name;
|
||||
flavour = scheme.flavour;
|
||||
currentLight = scheme.mode === "light";
|
||||
} else {
|
||||
previewLight = scheme.mode === "light";
|
||||
}
|
||||
|
||||
for (const [name, colour] of Object.entries(scheme.colours)) {
|
||||
const propName = name.startsWith("term") ? name : `m3${name}`;
|
||||
if (colours.hasOwnProperty(propName))
|
||||
colours[propName] = `#${colour}`;
|
||||
}
|
||||
}
|
||||
|
||||
function setMode(mode: string): void {
|
||||
Quickshell.execDetached(["caelestia", "scheme", "set", "--notify", "-m", mode]);
|
||||
}
|
||||
|
||||
FileView {
|
||||
path: `${Paths.state}/scheme.json`
|
||||
watchChanges: true
|
||||
onFileChanged: reload()
|
||||
onLoaded: root.load(text(), false)
|
||||
}
|
||||
|
||||
ImageAnalyser {
|
||||
id: analyser
|
||||
|
||||
source: Wallpapers.current
|
||||
}
|
||||
|
||||
component Transparency: QtObject {
|
||||
readonly property bool enabled: Appearance.transparency.enabled
|
||||
readonly property real base: Appearance.transparency.base - (root.light ? 0.1 : 0)
|
||||
readonly property real layers: Appearance.transparency.layers
|
||||
}
|
||||
|
||||
component M3TPalette: QtObject {
|
||||
readonly property color m3primary_paletteKeyColor: root.layer(root.palette.m3primary_paletteKeyColor)
|
||||
readonly property color m3secondary_paletteKeyColor: root.layer(root.palette.m3secondary_paletteKeyColor)
|
||||
readonly property color m3tertiary_paletteKeyColor: root.layer(root.palette.m3tertiary_paletteKeyColor)
|
||||
readonly property color m3neutral_paletteKeyColor: root.layer(root.palette.m3neutral_paletteKeyColor)
|
||||
readonly property color m3neutral_variant_paletteKeyColor: root.layer(root.palette.m3neutral_variant_paletteKeyColor)
|
||||
readonly property color m3background: root.layer(root.palette.m3background, 0)
|
||||
readonly property color m3onBackground: root.layer(root.palette.m3onBackground)
|
||||
readonly property color m3surface: root.layer(root.palette.m3surface, 0)
|
||||
readonly property color m3surfaceDim: root.layer(root.palette.m3surfaceDim, 0)
|
||||
readonly property color m3surfaceBright: root.layer(root.palette.m3surfaceBright, 0)
|
||||
readonly property color m3surfaceContainerLowest: root.layer(root.palette.m3surfaceContainerLowest)
|
||||
readonly property color m3surfaceContainerLow: root.layer(root.palette.m3surfaceContainerLow)
|
||||
readonly property color m3surfaceContainer: root.layer(root.palette.m3surfaceContainer)
|
||||
readonly property color m3surfaceContainerHigh: root.layer(root.palette.m3surfaceContainerHigh)
|
||||
readonly property color m3surfaceContainerHighest: root.layer(root.palette.m3surfaceContainerHighest)
|
||||
readonly property color m3onSurface: root.layer(root.palette.m3onSurface)
|
||||
readonly property color m3surfaceVariant: root.layer(root.palette.m3surfaceVariant, 0)
|
||||
readonly property color m3onSurfaceVariant: root.layer(root.palette.m3onSurfaceVariant)
|
||||
readonly property color m3inverseSurface: root.layer(root.palette.m3inverseSurface, 0)
|
||||
readonly property color m3inverseOnSurface: root.layer(root.palette.m3inverseOnSurface)
|
||||
readonly property color m3outline: root.layer(root.palette.m3outline)
|
||||
readonly property color m3outlineVariant: root.layer(root.palette.m3outlineVariant)
|
||||
readonly property color m3shadow: root.layer(root.palette.m3shadow)
|
||||
readonly property color m3scrim: root.layer(root.palette.m3scrim)
|
||||
readonly property color m3surfaceTint: root.layer(root.palette.m3surfaceTint)
|
||||
readonly property color m3primary: root.layer(root.palette.m3primary)
|
||||
readonly property color m3onPrimary: root.layer(root.palette.m3onPrimary)
|
||||
readonly property color m3primaryContainer: root.layer(root.palette.m3primaryContainer)
|
||||
readonly property color m3onPrimaryContainer: root.layer(root.palette.m3onPrimaryContainer)
|
||||
readonly property color m3inversePrimary: root.layer(root.palette.m3inversePrimary)
|
||||
readonly property color m3secondary: root.layer(root.palette.m3secondary)
|
||||
readonly property color m3onSecondary: root.layer(root.palette.m3onSecondary)
|
||||
readonly property color m3secondaryContainer: root.layer(root.palette.m3secondaryContainer)
|
||||
readonly property color m3onSecondaryContainer: root.layer(root.palette.m3onSecondaryContainer)
|
||||
readonly property color m3tertiary: root.layer(root.palette.m3tertiary)
|
||||
readonly property color m3onTertiary: root.layer(root.palette.m3onTertiary)
|
||||
readonly property color m3tertiaryContainer: root.layer(root.palette.m3tertiaryContainer)
|
||||
readonly property color m3onTertiaryContainer: root.layer(root.palette.m3onTertiaryContainer)
|
||||
readonly property color m3error: root.layer(root.palette.m3error)
|
||||
readonly property color m3onError: root.layer(root.palette.m3onError)
|
||||
readonly property color m3errorContainer: root.layer(root.palette.m3errorContainer)
|
||||
readonly property color m3onErrorContainer: root.layer(root.palette.m3onErrorContainer)
|
||||
readonly property color m3success: root.layer(root.palette.m3success)
|
||||
readonly property color m3onSuccess: root.layer(root.palette.m3onSuccess)
|
||||
readonly property color m3successContainer: root.layer(root.palette.m3successContainer)
|
||||
readonly property color m3onSuccessContainer: root.layer(root.palette.m3onSuccessContainer)
|
||||
readonly property color m3primaryFixed: root.layer(root.palette.m3primaryFixed)
|
||||
readonly property color m3primaryFixedDim: root.layer(root.palette.m3primaryFixedDim)
|
||||
readonly property color m3onPrimaryFixed: root.layer(root.palette.m3onPrimaryFixed)
|
||||
readonly property color m3onPrimaryFixedVariant: root.layer(root.palette.m3onPrimaryFixedVariant)
|
||||
readonly property color m3secondaryFixed: root.layer(root.palette.m3secondaryFixed)
|
||||
readonly property color m3secondaryFixedDim: root.layer(root.palette.m3secondaryFixedDim)
|
||||
readonly property color m3onSecondaryFixed: root.layer(root.palette.m3onSecondaryFixed)
|
||||
readonly property color m3onSecondaryFixedVariant: root.layer(root.palette.m3onSecondaryFixedVariant)
|
||||
readonly property color m3tertiaryFixed: root.layer(root.palette.m3tertiaryFixed)
|
||||
readonly property color m3tertiaryFixedDim: root.layer(root.palette.m3tertiaryFixedDim)
|
||||
readonly property color m3onTertiaryFixed: root.layer(root.palette.m3onTertiaryFixed)
|
||||
readonly property color m3onTertiaryFixedVariant: root.layer(root.palette.m3onTertiaryFixedVariant)
|
||||
}
|
||||
|
||||
component M3Palette: QtObject {
|
||||
property color m3primary_paletteKeyColor: "#a8627b"
|
||||
property color m3secondary_paletteKeyColor: "#8e6f78"
|
||||
property color m3tertiary_paletteKeyColor: "#986e4c"
|
||||
property color m3neutral_paletteKeyColor: "#807477"
|
||||
property color m3neutral_variant_paletteKeyColor: "#837377"
|
||||
property color m3background: "#191114"
|
||||
property color m3onBackground: "#efdfe2"
|
||||
property color m3surface: "#191114"
|
||||
property color m3surfaceDim: "#191114"
|
||||
property color m3surfaceBright: "#403739"
|
||||
property color m3surfaceContainerLowest: "#130c0e"
|
||||
property color m3surfaceContainerLow: "#22191c"
|
||||
property color m3surfaceContainer: "#261d20"
|
||||
property color m3surfaceContainerHigh: "#31282a"
|
||||
property color m3surfaceContainerHighest: "#3c3235"
|
||||
property color m3onSurface: "#efdfe2"
|
||||
property color m3surfaceVariant: "#514347"
|
||||
property color m3onSurfaceVariant: "#d5c2c6"
|
||||
property color m3inverseSurface: "#efdfe2"
|
||||
property color m3inverseOnSurface: "#372e30"
|
||||
property color m3outline: "#9e8c91"
|
||||
property color m3outlineVariant: "#514347"
|
||||
property color m3shadow: "#000000"
|
||||
property color m3scrim: "#000000"
|
||||
property color m3surfaceTint: "#ffb0ca"
|
||||
property color m3primary: "#ffb0ca"
|
||||
property color m3onPrimary: "#541d34"
|
||||
property color m3primaryContainer: "#6f334a"
|
||||
property color m3onPrimaryContainer: "#ffd9e3"
|
||||
property color m3inversePrimary: "#8b4a62"
|
||||
property color m3secondary: "#e2bdc7"
|
||||
property color m3onSecondary: "#422932"
|
||||
property color m3secondaryContainer: "#5a3f48"
|
||||
property color m3onSecondaryContainer: "#ffd9e3"
|
||||
property color m3tertiary: "#f0bc95"
|
||||
property color m3onTertiary: "#48290c"
|
||||
property color m3tertiaryContainer: "#b58763"
|
||||
property color m3onTertiaryContainer: "#000000"
|
||||
property color m3error: "#ffb4ab"
|
||||
property color m3onError: "#690005"
|
||||
property color m3errorContainer: "#93000a"
|
||||
property color m3onErrorContainer: "#ffdad6"
|
||||
property color m3success: "#B5CCBA"
|
||||
property color m3onSuccess: "#213528"
|
||||
property color m3successContainer: "#374B3E"
|
||||
property color m3onSuccessContainer: "#D1E9D6"
|
||||
property color m3primaryFixed: "#ffd9e3"
|
||||
property color m3primaryFixedDim: "#ffb0ca"
|
||||
property color m3onPrimaryFixed: "#39071f"
|
||||
property color m3onPrimaryFixedVariant: "#6f334a"
|
||||
property color m3secondaryFixed: "#ffd9e3"
|
||||
property color m3secondaryFixedDim: "#e2bdc7"
|
||||
property color m3onSecondaryFixed: "#2b151d"
|
||||
property color m3onSecondaryFixedVariant: "#5a3f48"
|
||||
property color m3tertiaryFixed: "#ffdcc3"
|
||||
property color m3tertiaryFixedDim: "#f0bc95"
|
||||
property color m3onTertiaryFixed: "#2f1500"
|
||||
property color m3onTertiaryFixedVariant: "#623f21"
|
||||
property color term0: "#353434"
|
||||
property color term1: "#ff4c8a"
|
||||
property color term2: "#ffbbb7"
|
||||
property color term3: "#ffdedf"
|
||||
property color term4: "#b3a2d5"
|
||||
property color term5: "#e98fb0"
|
||||
property color term6: "#ffba93"
|
||||
property color term7: "#eed1d2"
|
||||
property color term8: "#b39e9e"
|
||||
property color term9: "#ff80a3"
|
||||
property color term10: "#ffd3d0"
|
||||
property color term11: "#fff1f0"
|
||||
property color term12: "#dcbc93"
|
||||
property color term13: "#f9a8c2"
|
||||
property color term14: "#ffd1c0"
|
||||
property color term15: "#ffffff"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
pragma Singleton
|
||||
|
||||
import qs.services
|
||||
import qs.config
|
||||
import Caelestia
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property alias enabled: props.enabled
|
||||
|
||||
function setDynamicConfs(): void {
|
||||
Hypr.extras.applyOptions({
|
||||
"animations:enabled": 0,
|
||||
"decoration:shadow:enabled": 0,
|
||||
"decoration:blur:enabled": 0,
|
||||
"general:gaps_in": 0,
|
||||
"general:gaps_out": 0,
|
||||
"general:border_size": 1,
|
||||
"decoration:rounding": 0,
|
||||
"general:allow_tearing": 1
|
||||
});
|
||||
}
|
||||
|
||||
onEnabledChanged: {
|
||||
if (enabled) {
|
||||
setDynamicConfs();
|
||||
if (Config.utilities.toasts.gameModeChanged)
|
||||
Toaster.toast(qsTr("Game mode enabled"), qsTr("Disabled Hyprland animations, blur, gaps and shadows"), "gamepad");
|
||||
} else {
|
||||
Hypr.extras.message("reload");
|
||||
if (Config.utilities.toasts.gameModeChanged)
|
||||
Toaster.toast(qsTr("Game mode disabled"), qsTr("Hyprland settings restored"), "gamepad");
|
||||
}
|
||||
}
|
||||
|
||||
PersistentProperties {
|
||||
id: props
|
||||
|
||||
property bool enabled: Hypr.options["animations:enabled"] === 0
|
||||
|
||||
reloadableId: "gameMode"
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Hypr
|
||||
|
||||
function onConfigReloaded(): void {
|
||||
if (props.enabled)
|
||||
root.setDynamicConfs();
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "gameMode"
|
||||
|
||||
function isEnabled(): bool {
|
||||
return props.enabled;
|
||||
}
|
||||
|
||||
function toggle(): void {
|
||||
props.enabled = !props.enabled;
|
||||
}
|
||||
|
||||
function enable(): void {
|
||||
props.enabled = true;
|
||||
}
|
||||
|
||||
function disable(): void {
|
||||
props.enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
pragma Singleton
|
||||
|
||||
import qs.components.misc
|
||||
import qs.config
|
||||
import Caelestia
|
||||
import Caelestia.Internal
|
||||
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
|
||||
|
||||
readonly property HyprlandToplevel activeToplevel: Hyprland.activeToplevel?.wayland?.activated ? Hyprland.activeToplevel : null
|
||||
readonly property HyprlandWorkspace focusedWorkspace: Hyprland.focusedWorkspace
|
||||
readonly property HyprlandMonitor focusedMonitor: Hyprland.focusedMonitor
|
||||
readonly property int activeWsId: focusedWorkspace?.id ?? 1
|
||||
|
||||
readonly property HyprKeyboard keyboard: extras.devices.keyboards.find(kb => kb.main) ?? null
|
||||
readonly property bool capsLock: keyboard?.capsLock ?? false
|
||||
readonly property bool numLock: keyboard?.numLock ?? false
|
||||
readonly property string defaultKbLayout: keyboard?.layout.split(",")[0] ?? "??"
|
||||
readonly property string kbLayoutFull: keyboard?.activeKeymap ?? "Unknown"
|
||||
readonly property string kbLayout: kbMap.get(kbLayoutFull) ?? "??"
|
||||
readonly property var kbMap: new Map()
|
||||
|
||||
readonly property alias extras: extras
|
||||
readonly property alias options: extras.options
|
||||
readonly property alias devices: extras.devices
|
||||
|
||||
property bool hadKeyboard
|
||||
|
||||
signal configReloaded
|
||||
|
||||
function dispatch(request: string): void {
|
||||
Hyprland.dispatch(request);
|
||||
}
|
||||
|
||||
function monitorFor(screen: ShellScreen): HyprlandMonitor {
|
||||
return Hyprland.monitorFor(screen);
|
||||
}
|
||||
|
||||
function reloadDynamicConfs(): void {
|
||||
extras.batchMessage(["keyword bindlni ,Caps_Lock,global,caelestia:refreshDevices", "keyword bindlni ,Num_Lock,global,caelestia:refreshDevices"]);
|
||||
}
|
||||
|
||||
Component.onCompleted: reloadDynamicConfs()
|
||||
|
||||
onCapsLockChanged: {
|
||||
if (!Config.utilities.toasts.capsLockChanged)
|
||||
return;
|
||||
|
||||
if (capsLock)
|
||||
Toaster.toast(qsTr("Caps lock enabled"), qsTr("Caps lock is currently enabled"), "keyboard_capslock_badge");
|
||||
else
|
||||
Toaster.toast(qsTr("Caps lock disabled"), qsTr("Caps lock is currently disabled"), "keyboard_capslock");
|
||||
}
|
||||
|
||||
onNumLockChanged: {
|
||||
if (!Config.utilities.toasts.numLockChanged)
|
||||
return;
|
||||
|
||||
if (numLock)
|
||||
Toaster.toast(qsTr("Num lock enabled"), qsTr("Num lock is currently enabled"), "looks_one");
|
||||
else
|
||||
Toaster.toast(qsTr("Num lock disabled"), qsTr("Num lock is currently disabled"), "timer_1");
|
||||
}
|
||||
|
||||
onKbLayoutFullChanged: {
|
||||
if (hadKeyboard && Config.utilities.toasts.kbLayoutChanged)
|
||||
Toaster.toast(qsTr("Keyboard layout changed"), qsTr("Layout changed to: %1").arg(kbLayoutFull), "keyboard");
|
||||
|
||||
hadKeyboard = !!keyboard;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Hyprland
|
||||
|
||||
function onRawEvent(event: HyprlandEvent): void {
|
||||
const n = event.name;
|
||||
if (n.endsWith("v2"))
|
||||
return;
|
||||
|
||||
if (n === "configreloaded") {
|
||||
root.configReloaded();
|
||||
root.reloadDynamicConfs();
|
||||
} 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: kbLayoutFile
|
||||
|
||||
path: Quickshell.env("CAELESTIA_XKB_RULES_PATH") || "/usr/share/X11/xkb/rules/base.lst"
|
||||
onLoaded: {
|
||||
const lines = text().match(/! layout\n([\s\S]*?)\n\n/)[1].split("\n");
|
||||
for (const line of lines) {
|
||||
if (!line.trim() || line.trim().startsWith("!"))
|
||||
continue;
|
||||
|
||||
const match = line.match(/^\s*([a-z]{2,})\s+([a-zA-Z() ]+)$/);
|
||||
if (match)
|
||||
root.kbMap.set(match[2], match[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "hypr"
|
||||
|
||||
function refreshDevices(): void {
|
||||
extras.refreshDevices();
|
||||
}
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
name: "refreshDevices"
|
||||
description: "Reload devices"
|
||||
onPressed: extras.refreshDevices()
|
||||
onReleased: extras.refreshDevices()
|
||||
}
|
||||
|
||||
HyprExtras {
|
||||
id: extras
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property alias enabled: props.enabled
|
||||
readonly property alias enabledSince: props.enabledSince
|
||||
|
||||
onEnabledChanged: {
|
||||
if (enabled)
|
||||
props.enabledSince = new Date();
|
||||
}
|
||||
|
||||
PersistentProperties {
|
||||
id: props
|
||||
|
||||
property bool enabled
|
||||
property date enabledSince
|
||||
|
||||
reloadableId: "idleInhibitor"
|
||||
}
|
||||
|
||||
IdleInhibitor {
|
||||
enabled: props.enabled
|
||||
window: PanelWindow {
|
||||
implicitWidth: 0
|
||||
implicitHeight: 0
|
||||
color: "transparent"
|
||||
mask: Region {}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "idleInhibitor"
|
||||
|
||||
function isEnabled(): bool {
|
||||
return props.enabled;
|
||||
}
|
||||
|
||||
function toggle(): void {
|
||||
props.enabled = !props.enabled;
|
||||
}
|
||||
|
||||
function enable(): void {
|
||||
props.enabled = true;
|
||||
}
|
||||
|
||||
function disable(): void {
|
||||
props.enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
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
|
||||
|
||||
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.UTF-8",
|
||||
LC_ALL: "C.UTF-8"
|
||||
})
|
||||
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.UTF-8",
|
||||
LC_ALL: "C.UTF-8"
|
||||
})
|
||||
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]?.replace(rep2, ":") ?? "",
|
||||
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 {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.components.misc
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Caelestia
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Notifications
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property list<Notif> list: []
|
||||
readonly property list<Notif> notClosed: list.filter(n => !n.closed)
|
||||
readonly property list<Notif> popups: list.filter(n => n.popup)
|
||||
property alias dnd: props.dnd
|
||||
|
||||
property bool loaded
|
||||
|
||||
onDndChanged: {
|
||||
if (!Config.utilities.toasts.dndChanged)
|
||||
return;
|
||||
|
||||
if (dnd)
|
||||
Toaster.toast(qsTr("Do not disturb enabled"), qsTr("Popup notifications are now disabled"), "do_not_disturb_on");
|
||||
else
|
||||
Toaster.toast(qsTr("Do not disturb disabled"), qsTr("Popup notifications are now enabled"), "do_not_disturb_off");
|
||||
}
|
||||
|
||||
onListChanged: {
|
||||
if (loaded)
|
||||
saveTimer.restart();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: saveTimer
|
||||
|
||||
interval: 1000
|
||||
onTriggered: storage.setText(JSON.stringify(root.notClosed.map(n => ({
|
||||
time: n.time,
|
||||
id: n.id,
|
||||
summary: n.summary,
|
||||
body: n.body,
|
||||
appIcon: n.appIcon,
|
||||
appName: n.appName,
|
||||
image: n.image,
|
||||
expireTimeout: n.expireTimeout,
|
||||
urgency: n.urgency,
|
||||
resident: n.resident,
|
||||
hasActionIcons: n.hasActionIcons,
|
||||
actions: n.actions
|
||||
}))))
|
||||
}
|
||||
|
||||
PersistentProperties {
|
||||
id: props
|
||||
|
||||
property bool dnd
|
||||
|
||||
reloadableId: "notifs"
|
||||
}
|
||||
|
||||
NotificationServer {
|
||||
id: server
|
||||
|
||||
keepOnReload: false
|
||||
actionsSupported: true
|
||||
bodyHyperlinksSupported: true
|
||||
bodyImagesSupported: true
|
||||
bodyMarkupSupported: true
|
||||
imageSupported: true
|
||||
persistenceSupported: true
|
||||
|
||||
onNotification: notif => {
|
||||
notif.tracked = true;
|
||||
|
||||
const comp = notifComp.createObject(root, {
|
||||
popup: !props.dnd && ![...Visibilities.screens.values()].some(v => v.sidebar),
|
||||
notification: notif
|
||||
});
|
||||
root.list = [comp, ...root.list];
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: storage
|
||||
|
||||
path: `${Paths.state}/notifs.json`
|
||||
onLoaded: {
|
||||
const data = JSON.parse(text());
|
||||
for (const notif of data)
|
||||
root.list.push(notifComp.createObject(root, notif));
|
||||
root.list.sort((a, b) => b.time - a.time);
|
||||
root.loaded = true;
|
||||
}
|
||||
onLoadFailed: err => {
|
||||
if (err === FileViewError.FileNotFound) {
|
||||
root.loaded = true;
|
||||
setText("[]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
name: "clearNotifs"
|
||||
description: "Clear all notifications"
|
||||
onPressed: {
|
||||
for (const notif of root.list.slice())
|
||||
notif.close();
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "notifs"
|
||||
|
||||
function clear(): void {
|
||||
for (const notif of root.list.slice())
|
||||
notif.close();
|
||||
}
|
||||
|
||||
function isDndEnabled(): bool {
|
||||
return props.dnd;
|
||||
}
|
||||
|
||||
function toggleDnd(): void {
|
||||
props.dnd = !props.dnd;
|
||||
}
|
||||
|
||||
function enableDnd(): void {
|
||||
props.dnd = true;
|
||||
}
|
||||
|
||||
function disableDnd(): void {
|
||||
props.dnd = false;
|
||||
}
|
||||
}
|
||||
|
||||
component Notif: QtObject {
|
||||
id: notif
|
||||
|
||||
property bool popup
|
||||
property bool closed
|
||||
property var locks: new Set()
|
||||
|
||||
property date time: new Date()
|
||||
readonly property string timeStr: {
|
||||
const diff = Time.date.getTime() - time.getTime();
|
||||
const m = Math.floor(diff / 60000);
|
||||
|
||||
if (m < 1)
|
||||
return qsTr("now");
|
||||
|
||||
const h = Math.floor(m / 60);
|
||||
const d = Math.floor(h / 24);
|
||||
|
||||
if (d > 0)
|
||||
return `${d}d`;
|
||||
if (h > 0)
|
||||
return `${h}h`;
|
||||
return `${m}m`;
|
||||
}
|
||||
|
||||
property Notification notification
|
||||
property string id
|
||||
property string summary
|
||||
property string body
|
||||
property string appIcon
|
||||
property string appName
|
||||
property string image
|
||||
property real expireTimeout: Config.notifs.defaultExpireTimeout
|
||||
property int urgency: NotificationUrgency.Normal
|
||||
property bool resident
|
||||
property bool hasActionIcons
|
||||
property list<var> actions
|
||||
|
||||
readonly property Timer timer: Timer {
|
||||
running: true
|
||||
interval: notif.expireTimeout > 0 ? notif.expireTimeout : Config.notifs.defaultExpireTimeout
|
||||
onTriggered: {
|
||||
if (Config.notifs.expire)
|
||||
notif.popup = false;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property LazyLoader dummyImageLoader: LazyLoader {
|
||||
active: false
|
||||
|
||||
PanelWindow {
|
||||
implicitWidth: Config.notifs.sizes.image
|
||||
implicitHeight: Config.notifs.sizes.image
|
||||
color: "transparent"
|
||||
mask: Region {}
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: Qt.resolvedUrl(notif.image)
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
cache: false
|
||||
asynchronous: true
|
||||
opacity: 0
|
||||
|
||||
onStatusChanged: {
|
||||
if (status !== Image.Ready)
|
||||
return;
|
||||
|
||||
const cacheKey = notif.appName + notif.summary + notif.id;
|
||||
let h1 = 0xdeadbeef, h2 = 0x41c6ce57, ch;
|
||||
for (let i = 0; i < cacheKey.length; i++) {
|
||||
ch = cacheKey.charCodeAt(i);
|
||||
h1 = Math.imul(h1 ^ ch, 2654435761);
|
||||
h2 = Math.imul(h2 ^ ch, 1597334677);
|
||||
}
|
||||
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
|
||||
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
||||
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
|
||||
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
||||
const hash = (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0);
|
||||
|
||||
const cache = `${Paths.notifimagecache}/${hash}.png`;
|
||||
CUtils.saveItem(this, Qt.resolvedUrl(cache), () => {
|
||||
notif.image = cache;
|
||||
notif.dummyImageLoader.active = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property Connections conn: Connections {
|
||||
target: notif.notification
|
||||
|
||||
function onClosed(): void {
|
||||
notif.close();
|
||||
}
|
||||
|
||||
function onSummaryChanged(): void {
|
||||
notif.summary = notif.notification.summary;
|
||||
}
|
||||
|
||||
function onBodyChanged(): void {
|
||||
notif.body = notif.notification.body;
|
||||
}
|
||||
|
||||
function onAppIconChanged(): void {
|
||||
notif.appIcon = notif.notification.appIcon;
|
||||
}
|
||||
|
||||
function onAppNameChanged(): void {
|
||||
notif.appName = notif.notification.appName;
|
||||
}
|
||||
|
||||
function onImageChanged(): void {
|
||||
notif.image = notif.notification.image;
|
||||
if (notif.notification?.image)
|
||||
notif.dummyImageLoader.active = true;
|
||||
}
|
||||
|
||||
function onExpireTimeoutChanged(): void {
|
||||
notif.expireTimeout = notif.notification.expireTimeout;
|
||||
}
|
||||
|
||||
function onUrgencyChanged(): void {
|
||||
notif.urgency = notif.notification.urgency;
|
||||
}
|
||||
|
||||
function onResidentChanged(): void {
|
||||
notif.resident = notif.notification.resident;
|
||||
}
|
||||
|
||||
function onHasActionIconsChanged(): void {
|
||||
notif.hasActionIcons = notif.notification.hasActionIcons;
|
||||
}
|
||||
|
||||
function onActionsChanged(): void {
|
||||
notif.actions = notif.notification.actions.map(a => ({
|
||||
identifier: a.identifier,
|
||||
text: a.text,
|
||||
invoke: () => a.invoke()
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function lock(item: Item): void {
|
||||
locks.add(item);
|
||||
}
|
||||
|
||||
function unlock(item: Item): void {
|
||||
locks.delete(item);
|
||||
if (closed)
|
||||
close();
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
closed = true;
|
||||
if (locks.size === 0 && root.list.includes(this)) {
|
||||
root.list = root.list.filter(n => n !== this);
|
||||
notification?.dismiss();
|
||||
destroy();
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!notification)
|
||||
return;
|
||||
|
||||
id = notification.id;
|
||||
summary = notification.summary;
|
||||
body = notification.body;
|
||||
appIcon = notification.appIcon;
|
||||
appName = notification.appName;
|
||||
image = notification.image;
|
||||
if (notification?.image)
|
||||
dummyImageLoader.active = true;
|
||||
expireTimeout = notification.expireTimeout;
|
||||
urgency = notification.urgency;
|
||||
resident = notification.resident;
|
||||
hasActionIcons = notification.hasActionIcons;
|
||||
actions = notification.actions.map(a => ({
|
||||
identifier: a.identifier,
|
||||
text: a.text,
|
||||
invoke: () => a.invoke()
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: notifComp
|
||||
|
||||
Notif {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
pragma Singleton
|
||||
|
||||
import qs.components.misc
|
||||
import qs.config
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Mpris
|
||||
import QtQml
|
||||
import Caelestia
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property list<MprisPlayer> list: Mpris.players.values
|
||||
readonly property MprisPlayer active: props.manualActive ?? list.find(p => getIdentity(p) === Config.services.defaultPlayer) ?? list[0] ?? null
|
||||
property alias manualActive: props.manualActive
|
||||
|
||||
function getIdentity(player: MprisPlayer): string {
|
||||
const alias = Config.services.playerAliases.find(a => a.from === player.identity);
|
||||
return alias?.to ?? player.identity;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: active
|
||||
|
||||
function onPostTrackChanged() {
|
||||
if (!Config.utilities.toasts.nowPlaying) {
|
||||
return;
|
||||
}
|
||||
if (active.trackArtist != "" && active.trackTitle != "") {
|
||||
Toaster.toast(qsTr("Now Playing"), qsTr("%1 - %2").arg(active.trackArtist).arg(active.trackTitle), "music_note");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PersistentProperties {
|
||||
id: props
|
||||
|
||||
property MprisPlayer manualActive
|
||||
|
||||
reloadableId: "players"
|
||||
}
|
||||
|
||||
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.getIdentity(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property alias running: props.running
|
||||
readonly property alias paused: props.paused
|
||||
readonly property alias elapsed: props.elapsed
|
||||
property bool needsStart
|
||||
property list<string> startArgs
|
||||
property bool needsStop
|
||||
property bool needsPause
|
||||
|
||||
function start(extraArgs: list<string>): void {
|
||||
needsStart = true;
|
||||
startArgs = extraArgs;
|
||||
checkProc.running = true;
|
||||
}
|
||||
|
||||
function stop(): void {
|
||||
needsStop = true;
|
||||
checkProc.running = true;
|
||||
}
|
||||
|
||||
function togglePause(): void {
|
||||
needsPause = true;
|
||||
checkProc.running = true;
|
||||
}
|
||||
|
||||
PersistentProperties {
|
||||
id: props
|
||||
|
||||
property bool running: false
|
||||
property bool paused: false
|
||||
property real elapsed: 0 // Might get too large for int
|
||||
|
||||
reloadableId: "recorder"
|
||||
}
|
||||
|
||||
Process {
|
||||
id: checkProc
|
||||
|
||||
running: true
|
||||
command: ["pidof", "gpu-screen-recorder"]
|
||||
onExited: code => {
|
||||
props.running = code === 0;
|
||||
|
||||
if (code === 0) {
|
||||
if (root.needsStop) {
|
||||
Quickshell.execDetached(["caelestia", "record"]);
|
||||
props.running = false;
|
||||
props.paused = false;
|
||||
} else if (root.needsPause) {
|
||||
Quickshell.execDetached(["caelestia", "record", "-p"]);
|
||||
props.paused = !props.paused;
|
||||
}
|
||||
} else if (root.needsStart) {
|
||||
Quickshell.execDetached(["caelestia", "record", ...root.startArgs]);
|
||||
props.running = true;
|
||||
props.paused = false;
|
||||
props.elapsed = 0;
|
||||
}
|
||||
|
||||
root.needsStart = false;
|
||||
root.needsStop = false;
|
||||
root.needsPause = false;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Time
|
||||
// enabled: props.running && !props.paused
|
||||
|
||||
function onSecondsChanged(): void {
|
||||
props.elapsed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import Caelestia
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool connected: false
|
||||
|
||||
readonly property bool connecting: connectProc.running || disconnectProc.running
|
||||
readonly property bool enabled: Config.utilities.vpn.enabled
|
||||
readonly property var providerInput: (Config.utilities.vpn.provider && Config.utilities.vpn.provider.length > 0) ? Config.utilities.vpn.provider[0] : "wireguard"
|
||||
readonly property bool isCustomProvider: typeof providerInput === "object"
|
||||
readonly property string providerName: isCustomProvider ? (providerInput.name || "custom") : String(providerInput)
|
||||
readonly property string interfaceName: isCustomProvider ? (providerInput.interface || "") : ""
|
||||
readonly property var currentConfig: {
|
||||
const name = providerName;
|
||||
const iface = interfaceName;
|
||||
const defaults = getBuiltinDefaults(name, iface);
|
||||
|
||||
if (isCustomProvider) {
|
||||
const custom = providerInput;
|
||||
return {
|
||||
connectCmd: custom.connectCmd || defaults.connectCmd,
|
||||
disconnectCmd: custom.disconnectCmd || defaults.disconnectCmd,
|
||||
interface: custom.interface || defaults.interface,
|
||||
displayName: custom.displayName || defaults.displayName
|
||||
};
|
||||
}
|
||||
|
||||
return defaults;
|
||||
}
|
||||
|
||||
function getBuiltinDefaults(name, iface) {
|
||||
const builtins = {
|
||||
"wireguard": {
|
||||
connectCmd: ["pkexec", "wg-quick", "up", iface],
|
||||
disconnectCmd: ["pkexec", "wg-quick", "down", iface],
|
||||
interface: iface,
|
||||
displayName: iface
|
||||
},
|
||||
"warp": {
|
||||
connectCmd: ["warp-cli", "connect"],
|
||||
disconnectCmd: ["warp-cli", "disconnect"],
|
||||
interface: "CloudflareWARP",
|
||||
displayName: "Warp"
|
||||
},
|
||||
"netbird": {
|
||||
connectCmd: ["netbird", "up"],
|
||||
disconnectCmd: ["netbird", "down"],
|
||||
interface: "wt0",
|
||||
displayName: "NetBird"
|
||||
},
|
||||
"tailscale": {
|
||||
connectCmd: ["tailscale", "up"],
|
||||
disconnectCmd: ["tailscale", "down"],
|
||||
interface: "tailscale0",
|
||||
displayName: "Tailscale"
|
||||
}
|
||||
};
|
||||
|
||||
return builtins[name] || {
|
||||
connectCmd: [name, "up"],
|
||||
disconnectCmd: [name, "down"],
|
||||
interface: iface || name,
|
||||
displayName: name
|
||||
};
|
||||
}
|
||||
|
||||
function connect(): void {
|
||||
if (!connected && !connecting && root.currentConfig && root.currentConfig.connectCmd) {
|
||||
connectProc.exec(root.currentConfig.connectCmd);
|
||||
}
|
||||
}
|
||||
|
||||
function disconnect(): void {
|
||||
if (connected && !connecting && root.currentConfig && root.currentConfig.disconnectCmd) {
|
||||
disconnectProc.exec(root.currentConfig.disconnectCmd);
|
||||
}
|
||||
}
|
||||
|
||||
function toggle(): void {
|
||||
if (connected) {
|
||||
disconnect();
|
||||
} else {
|
||||
connect();
|
||||
}
|
||||
}
|
||||
|
||||
function checkStatus(): void {
|
||||
if (root.enabled) {
|
||||
statusProc.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
onConnectedChanged: {
|
||||
if (!Config.utilities.toasts.vpnChanged)
|
||||
return;
|
||||
|
||||
const displayName = root.currentConfig ? (root.currentConfig.displayName || "VPN") : "VPN";
|
||||
if (connected) {
|
||||
Toaster.toast(qsTr("VPN connected"), qsTr("Connected to %1").arg(displayName), "vpn_key");
|
||||
} else {
|
||||
Toaster.toast(qsTr("VPN disconnected"), qsTr("Disconnected from %1").arg(displayName), "vpn_key_off");
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: root.enabled && statusCheckTimer.start()
|
||||
|
||||
Process {
|
||||
id: nmMonitor
|
||||
|
||||
running: root.enabled
|
||||
command: ["nmcli", "monitor"]
|
||||
stdout: SplitParser {
|
||||
onRead: statusCheckTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: statusProc
|
||||
|
||||
command: ["ip", "link", "show"]
|
||||
environment: ({
|
||||
LANG: "C.UTF-8",
|
||||
LC_ALL: "C.UTF-8"
|
||||
})
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const iface = root.currentConfig ? root.currentConfig.interface : "";
|
||||
root.connected = iface && text.includes(iface + ":");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: connectProc
|
||||
|
||||
onExited: statusCheckTimer.start()
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const error = text.trim();
|
||||
if (error && !error.includes("[#]") && !error.includes("already exists")) {
|
||||
console.warn("VPN connection error:", error);
|
||||
} else if (error.includes("already exists")) {
|
||||
root.connected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: disconnectProc
|
||||
|
||||
onExited: statusCheckTimer.start()
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const error = text.trim();
|
||||
if (error && !error.includes("[#]")) {
|
||||
console.warn("VPN disconnection error:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: statusCheckTimer
|
||||
|
||||
interval: 500
|
||||
onTriggered: root.checkStatus()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
property var screens: new Map()
|
||||
property var bars: new Map()
|
||||
|
||||
function load(screen: ShellScreen, visibilities: var): void {
|
||||
screens.set(Hypr.monitorFor(screen), visibilities);
|
||||
}
|
||||
|
||||
function getForActive(): PersistentProperties {
|
||||
return screens.get(Hypr.focusedMonitor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
pragma Singleton
|
||||
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Caelestia.Models
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
Searcher {
|
||||
id: root
|
||||
|
||||
readonly property string currentNamePath: `${Paths.state}/wallpaper/path.txt`
|
||||
readonly property list<string> smartArg: Config.services.smartScheme ? [] : ["--no-smart"]
|
||||
|
||||
property bool showPreview: false
|
||||
readonly property string current: showPreview ? previewPath : actualCurrent
|
||||
property string previewPath
|
||||
property string actualCurrent
|
||||
property bool previewColourLock
|
||||
|
||||
function setWallpaper(path: string): void {
|
||||
actualCurrent = path;
|
||||
Quickshell.execDetached(["caelestia", "wallpaper", "-f", path, ...smartArg]);
|
||||
}
|
||||
|
||||
function preview(path: string): void {
|
||||
previewPath = path;
|
||||
showPreview = true;
|
||||
|
||||
if (Colours.scheme === "dynamic")
|
||||
getPreviewColoursProc.running = true;
|
||||
}
|
||||
|
||||
function stopPreview(): void {
|
||||
showPreview = false;
|
||||
if (!previewColourLock)
|
||||
Colours.showPreview = false;
|
||||
}
|
||||
|
||||
list: wallpapers.entries
|
||||
key: "relativePath"
|
||||
useFuzzy: Config.launcher.useFuzzy.wallpapers
|
||||
extraOpts: useFuzzy ? ({}) : ({
|
||||
forward: false
|
||||
})
|
||||
|
||||
IpcHandler {
|
||||
target: "wallpaper"
|
||||
|
||||
function get(): string {
|
||||
return root.actualCurrent;
|
||||
}
|
||||
|
||||
function set(path: string): void {
|
||||
root.setWallpaper(path);
|
||||
}
|
||||
|
||||
function list(): string {
|
||||
return root.list.map(w => w.path).join("\n");
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
path: root.currentNamePath
|
||||
watchChanges: true
|
||||
onFileChanged: reload()
|
||||
onLoaded: {
|
||||
root.actualCurrent = text().trim();
|
||||
root.previewColourLock = false;
|
||||
}
|
||||
}
|
||||
|
||||
FileSystemModel {
|
||||
id: wallpapers
|
||||
|
||||
recursive: true
|
||||
path: Paths.wallsdir
|
||||
filter: FileSystemModel.Images
|
||||
}
|
||||
|
||||
Process {
|
||||
id: getPreviewColoursProc
|
||||
|
||||
command: ["caelestia", "wallpaper", "-p", root.previewPath, ...root.smartArg]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
Colours.load(text, true);
|
||||
Colours.showPreview = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
pragma Singleton
|
||||
|
||||
import qs.config
|
||||
import qs.utils
|
||||
import Caelestia
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property string city
|
||||
property var cc
|
||||
property var forecast
|
||||
readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert"
|
||||
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 int humidity: cc?.humidity ?? 0
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user