rewrite bar with quickshell
This commit is contained in:
143
quickshell/Services/AudioService.qml
Normal file
143
quickshell/Services/AudioService.qml
Normal file
@@ -0,0 +1,143 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
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 PwNode sink: Pipewire.defaultAudioSink
|
||||
readonly property PwNode source: Pipewire.defaultAudioSource
|
||||
readonly property list<PwNode> sinks: nodes.sinks
|
||||
readonly property list<PwNode> sources: nodes.sources
|
||||
|
||||
// Volume [0..1] is readonly from outside
|
||||
readonly property alias volume: root._volume
|
||||
property real _volume: sink?.audio?.volume ?? 0
|
||||
|
||||
readonly property alias muted: root._muted
|
||||
property bool _muted: !!sink?.audio?.muted
|
||||
|
||||
// Input volume [0..1] is readonly from outside
|
||||
readonly property alias inputVolume: root._inputVolume
|
||||
property real _inputVolume: source?.audio?.volume ?? 0
|
||||
|
||||
readonly property alias inputMuted: root._inputMuted
|
||||
property bool _inputMuted: !!source?.audio?.muted
|
||||
|
||||
readonly property real stepVolume: 5 / 100.0
|
||||
|
||||
PwObjectTracker {
|
||||
objects: [...root.sinks, ...root.sources]
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: sink?.audio ? sink?.audio : null
|
||||
|
||||
function onVolumeChanged() {
|
||||
var vol = (sink?.audio.volume ?? 0)
|
||||
if (isNaN(vol)) {
|
||||
vol = 0
|
||||
}
|
||||
root._volume = vol
|
||||
}
|
||||
|
||||
function onMutedChanged() {
|
||||
root._muted = (sink?.audio.muted ?? true)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: source?.audio ? source?.audio : null
|
||||
|
||||
function onVolumeChanged() {
|
||||
var vol = (source?.audio.volume ?? 0)
|
||||
if (isNaN(vol)) {
|
||||
vol = 0
|
||||
}
|
||||
root._inputVolume = vol
|
||||
}
|
||||
|
||||
function onMutedChanged() {
|
||||
root._inputMuted = (source?.audio.muted ?? true)
|
||||
}
|
||||
}
|
||||
|
||||
function increaseVolume() {
|
||||
setVolume(volume + stepVolume)
|
||||
}
|
||||
|
||||
function decreaseVolume() {
|
||||
setVolume(volume - stepVolume)
|
||||
}
|
||||
|
||||
function setVolume(newVolume: real) {
|
||||
if (sink?.ready && sink?.audio) {
|
||||
// Clamp it accordingly
|
||||
sink.audio.muted = false
|
||||
sink.audio.volume = Math.max(0, Math.min(1.0, newVolume))
|
||||
} else {
|
||||
console.warn("No sink available")
|
||||
}
|
||||
}
|
||||
|
||||
function setOutputMuted(muted: bool) {
|
||||
if (sink?.ready && sink?.audio) {
|
||||
sink.audio.muted = muted
|
||||
} else {
|
||||
console.warn("No sink available")
|
||||
}
|
||||
}
|
||||
|
||||
function setInputVolume(newVolume: real) {
|
||||
if (source?.ready && source?.audio) {
|
||||
// Clamp it accordingly
|
||||
source.audio.muted = false
|
||||
source.audio.volume = Math.max(0, Math.min(1.0, newVolume))
|
||||
} else {
|
||||
console.warn("No source available")
|
||||
}
|
||||
}
|
||||
|
||||
function setInputMuted(muted: bool) {
|
||||
if (source?.ready && source?.audio) {
|
||||
source.audio.muted = muted
|
||||
} else {
|
||||
console.warn("No source available")
|
||||
}
|
||||
}
|
||||
|
||||
function setAudioSink(newSink: PwNode): void {
|
||||
Pipewire.preferredDefaultAudioSink = newSink
|
||||
// Volume is changed by the sink change
|
||||
root._volume = newSink?.audio?.volume ?? 0
|
||||
root._muted = !!newSink?.audio?.muted
|
||||
}
|
||||
|
||||
function setAudioSource(newSource: PwNode): void {
|
||||
Pipewire.preferredDefaultAudioSource = newSource
|
||||
// Volume is changed by the source change
|
||||
root._inputVolume = newSource?.audio?.volume ?? 0
|
||||
root._inputMuted = !!newSource?.audio?.muted
|
||||
}
|
||||
|
||||
function toggleMute() {
|
||||
setOutputMuted(!muted)
|
||||
}
|
||||
}
|
||||
303
quickshell/Services/BrightnessService.qml
Normal file
303
quickshell/Services/BrightnessService.qml
Normal file
@@ -0,0 +1,303 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Signal emitted when a specific monitor's brightness changes, includes monitor context
|
||||
signal monitorBrightnessChanged(var monitor, real newBrightness)
|
||||
|
||||
function getAvailableMethods(): list<string> {
|
||||
var methods = []
|
||||
if (monitors.some(m => m.isDdc))
|
||||
methods.push("ddcutil")
|
||||
if (monitors.some(m => !m.isDdc))
|
||||
methods.push("internal")
|
||||
if (appleDisplayPresent)
|
||||
methods.push("apple")
|
||||
return methods
|
||||
}
|
||||
|
||||
// Global helpers for IPC and shortcuts
|
||||
function increaseBrightness(): void {
|
||||
monitors.forEach(m => m.increaseBrightness())
|
||||
}
|
||||
|
||||
function decreaseBrightness(): void {
|
||||
monitors.forEach(m => m.decreaseBrightness())
|
||||
}
|
||||
|
||||
function getDetectedDisplays(): list<var> {
|
||||
return detectedDisplays
|
||||
}
|
||||
|
||||
reloadableId: "brightness"
|
||||
|
||||
onMonitorsChanged: {
|
||||
ddcMonitors = []
|
||||
ddcProc.running = true
|
||||
}
|
||||
|
||||
Variants {
|
||||
id: variants
|
||||
model: Quickshell.screens
|
||||
Monitor {}
|
||||
}
|
||||
|
||||
// Check for Apple Display support
|
||||
Process {
|
||||
running: true
|
||||
command: ["sh", "-c", "which asdbctl >/dev/null 2>&1 && asdbctl get || echo ''"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root.appleDisplayPresent = text.trim().length > 0
|
||||
}
|
||||
}
|
||||
|
||||
// Detect DDC monitors
|
||||
Process {
|
||||
id: ddcProc
|
||||
property list<var> ddcMonitors: []
|
||||
command: ["ddcutil", "detect", "--sleep-multiplier=0.5"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var displays = text.trim().split("\n\n")
|
||||
ddcProc.ddcMonitors = displays.map(d => {
|
||||
|
||||
var ddcModelMatch = d.match(/(This monitor does not support DDC\/CI|Invalid display)/)
|
||||
var modelMatch = d.match(/Model:\s*(.*)/)
|
||||
var busMatch = d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/)
|
||||
var ddcModel = ddcModelMatch ? ddcModelMatch.length > 0 : false
|
||||
var model = modelMatch ? modelMatch[1] : "Unknown"
|
||||
var bus = busMatch ? busMatch[1] : "Unknown"
|
||||
console.log("Detected DDC Monitor:", model, "on bus", bus, "is DDC:", !ddcModel)
|
||||
return {
|
||||
"model": model,
|
||||
"busNum": bus,
|
||||
"isDdc": !ddcModel
|
||||
}
|
||||
})
|
||||
root.ddcMonitors = ddcProc.ddcMonitors.filter(m => m.isDdc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Monitor: QtObject {
|
||||
id: monitor
|
||||
|
||||
required property ShellScreen modelData
|
||||
readonly property bool isDdc: root.ddcMonitors.some(m => m.model === modelData.model)
|
||||
readonly property string busNum: root.ddcMonitors.find(m => m.model === modelData.model)?.busNum ?? ""
|
||||
readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay")
|
||||
readonly property string method: isAppleDisplay ? "apple" : (isDdc ? "ddcutil" : "internal")
|
||||
|
||||
property real brightness
|
||||
property real lastBrightness: 0
|
||||
property real queuedBrightness: NaN
|
||||
|
||||
// For internal displays - store the backlight device path
|
||||
property string backlightDevice: ""
|
||||
property string brightnessPath: ""
|
||||
property string maxBrightnessPath: ""
|
||||
property int maxBrightness: 100
|
||||
property bool ignoreNextChange: false
|
||||
|
||||
// Signal for brightness changes
|
||||
signal brightnessUpdated(real newBrightness)
|
||||
|
||||
// Execute a system command to get the current brightness value directly
|
||||
readonly property Process refreshProc: Process {
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var dataText = text.trim()
|
||||
if (dataText === "") {
|
||||
return
|
||||
}
|
||||
|
||||
var lines = dataText.split("\n")
|
||||
if (lines.length >= 2) {
|
||||
var current = parseInt(lines[0].trim())
|
||||
var max = parseInt(lines[1].trim())
|
||||
if (!isNaN(current) && !isNaN(max) && max > 0) {
|
||||
var newBrightness = current / max
|
||||
// Only update if it's actually different (avoid feedback loops)
|
||||
if (Math.abs(newBrightness - monitor.brightness) > 0.01) {
|
||||
// Update internal value to match system state
|
||||
monitor.brightness = newBrightness
|
||||
monitor.brightnessUpdated(monitor.brightness)
|
||||
root.monitorBrightnessChanged(monitor, monitor.brightness)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function to actively refresh the brightness from system
|
||||
function refreshBrightnessFromSystem() {
|
||||
if (!monitor.isDdc && !monitor.isAppleDisplay) {
|
||||
// For internal displays, query the system directly
|
||||
refreshProc.command = ["sh", "-c", "cat " + monitor.brightnessPath + " && " + "cat " + monitor.maxBrightnessPath]
|
||||
refreshProc.running = true
|
||||
} else if (monitor.isDdc) {
|
||||
// For DDC displays, get the current value
|
||||
refreshProc.command = ["ddcutil", "-b", monitor.busNum, "getvcp", "10", "--brief"]
|
||||
refreshProc.running = true
|
||||
} else if (monitor.isAppleDisplay) {
|
||||
// For Apple displays, get the current value
|
||||
refreshProc.command = ["asdbctl", "get"]
|
||||
refreshProc.running = true
|
||||
}
|
||||
}
|
||||
|
||||
// FileView to watch for external brightness changes (internal displays only)
|
||||
readonly property FileView brightnessWatcher: FileView {
|
||||
id: brightnessWatcher
|
||||
// Only set path for internal displays with a valid brightness path
|
||||
path: (!monitor.isDdc && !monitor.isAppleDisplay && monitor.brightnessPath !== "") ? monitor.brightnessPath : ""
|
||||
watchChanges: path !== ""
|
||||
onFileChanged: {
|
||||
// When a file change is detected, actively refresh from system
|
||||
// to ensure we get the most up-to-date value
|
||||
Qt.callLater(() => {
|
||||
monitor.refreshBrightnessFromSystem()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize brightness
|
||||
readonly property Process initProc: Process {
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var dataText = text.trim()
|
||||
if (dataText === "") {
|
||||
return
|
||||
}
|
||||
|
||||
if (monitor.isAppleDisplay) {
|
||||
var val = parseInt(dataText)
|
||||
if (!isNaN(val)) {
|
||||
monitor.brightness = val / 101
|
||||
console.log("Apple display brightness:", monitor.brightness)
|
||||
}
|
||||
} else if (monitor.isDdc) {
|
||||
var parts = dataText.split(" ")
|
||||
if (parts.length >= 4) {
|
||||
var current = parseInt(parts[3])
|
||||
var max = parseInt(parts[4])
|
||||
if (!isNaN(current) && !isNaN(max) && max > 0) {
|
||||
monitor.brightness = current / max
|
||||
console.log("DDC brightness:", monitor.brightness)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Internal backlight - parse the response which includes device path
|
||||
var lines = dataText.split("\n")
|
||||
if (lines.length >= 3) {
|
||||
monitor.backlightDevice = lines[0]
|
||||
monitor.brightnessPath = monitor.backlightDevice + "/brightness"
|
||||
monitor.maxBrightnessPath = monitor.backlightDevice + "/max_brightness"
|
||||
|
||||
var current = parseInt(lines[1])
|
||||
var max = parseInt(lines[2])
|
||||
if (!isNaN(current) && !isNaN(max) && max > 0) {
|
||||
monitor.maxBrightness = max
|
||||
monitor.brightness = current / max
|
||||
console.log("Internal brightness:", monitor.brightness)
|
||||
console.log("Using backlight device:", monitor.backlightDevice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Always update
|
||||
monitor.brightnessUpdated(monitor.brightness)
|
||||
root.monitorBrightnessChanged(monitor, monitor.brightness)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real stepSize: 5.0 / 100.0
|
||||
|
||||
// Timer for debouncing rapid changes
|
||||
readonly property Timer timer: Timer {
|
||||
interval: 100
|
||||
onTriggered: {
|
||||
if (!isNaN(monitor.queuedBrightness)) {
|
||||
monitor.setBrightness(monitor.queuedBrightness)
|
||||
monitor.queuedBrightness = NaN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setBrightnessDebounced(value: real): void {
|
||||
monitor.queuedBrightness = value
|
||||
timer.start()
|
||||
}
|
||||
|
||||
function increaseBrightness(): void {
|
||||
const value = !isNaN(monitor.queuedBrightness) ? monitor.queuedBrightness : monitor.brightness
|
||||
setBrightnessDebounced(value + stepSize)
|
||||
}
|
||||
|
||||
function decreaseBrightness(): void {
|
||||
const value = !isNaN(monitor.queuedBrightness) ? monitor.queuedBrightness : monitor.brightness
|
||||
setBrightnessDebounced(value - stepSize)
|
||||
}
|
||||
|
||||
function setBrightness(value: real): void {
|
||||
value = Math.max(0, Math.min(1, value))
|
||||
var rounded = Math.round(value * 100)
|
||||
|
||||
if (timer.running) {
|
||||
monitor.queuedBrightness = value
|
||||
return
|
||||
}
|
||||
|
||||
// Update internal value and trigger UI feedback
|
||||
monitor.brightness = value
|
||||
monitor.brightnessUpdated(value)
|
||||
root.monitorBrightnessChanged(monitor, monitor.brightness)
|
||||
|
||||
if (isAppleDisplay) {
|
||||
monitor.ignoreNextChange = true
|
||||
Quickshell.execDetached(["asdbctl", "set", rounded])
|
||||
} else if (isDdc) {
|
||||
monitor.ignoreNextChange = true
|
||||
Quickshell.execDetached(["ddcutil", "-b", busNum, "setvcp", "10", rounded])
|
||||
} else {
|
||||
monitor.ignoreNextChange = true
|
||||
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 {
|
||||
// Internal backlight - find the first available backlight device and get its info
|
||||
// This now returns: device_path, current_brightness, max_brightness (on separate lines)
|
||||
initProc.command = ["sh", "-c", "for dev in /sys/class/backlight/*; do " + " if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then " + " echo \"$dev\"; " + " cat \"$dev/brightness\"; " + " cat \"$dev/max_brightness\"; " + " break; " + " fi; " + "done"]
|
||||
}
|
||||
initProc.running = true
|
||||
}
|
||||
|
||||
onBusNumChanged: initBrightness()
|
||||
Component.onCompleted: initBrightness()
|
||||
}
|
||||
}
|
||||
146
quickshell/Services/Caffeine.qml
Normal file
146
quickshell/Services/Caffeine.qml
Normal file
@@ -0,0 +1,146 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Services
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
// "systemd", "wayland", or "auto"
|
||||
// systemd-inhibit not found, try Wayland tools
|
||||
// wayhibitor not found
|
||||
|
||||
id: root
|
||||
|
||||
property string reason: "Application request"
|
||||
property bool isInhibited: false
|
||||
property var activeInhibitors: []
|
||||
// Different inhibitor strategies
|
||||
property string strategy: "systemd"
|
||||
|
||||
// Auto-detect the best strategy
|
||||
function detectStrategy() {
|
||||
if (strategy === "auto") {
|
||||
// Check if systemd-inhibit is available
|
||||
try {
|
||||
var systemdResult = Quickshell.execDetached(["which", "systemd-inhibit"]);
|
||||
strategy = "systemd";
|
||||
return ;
|
||||
} catch (e) {
|
||||
}
|
||||
try {
|
||||
var waylandResult = Quickshell.execDetached(["which", "wayhibitor"]);
|
||||
strategy = "wayland";
|
||||
return ;
|
||||
} catch (e) {
|
||||
}
|
||||
strategy = "systemd"; // Fallback to systemd even if not detected
|
||||
}
|
||||
}
|
||||
|
||||
// Add an inhibitor
|
||||
function addInhibitor(id, reason = "Application request") {
|
||||
if (activeInhibitors.includes(id))
|
||||
return false;
|
||||
|
||||
activeInhibitors.push(id);
|
||||
updateInhibition(reason);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove an inhibitor
|
||||
function removeInhibitor(id) {
|
||||
const index = activeInhibitors.indexOf(id);
|
||||
if (index === -1) {
|
||||
console.log("Inhibitor not found:", id);
|
||||
return false;
|
||||
}
|
||||
activeInhibitors.splice(index, 1);
|
||||
updateInhibition();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Update the actual system inhibition
|
||||
function updateInhibition(newReason = reason) {
|
||||
const shouldInhibit = activeInhibitors.length > 0;
|
||||
if (shouldInhibit === isInhibited)
|
||||
return ;
|
||||
|
||||
// No change needed
|
||||
if (shouldInhibit)
|
||||
startInhibition(newReason);
|
||||
else
|
||||
stopInhibition();
|
||||
}
|
||||
|
||||
// Start system inhibition
|
||||
function startInhibition(newReason) {
|
||||
reason = newReason;
|
||||
if (strategy === "systemd")
|
||||
startSystemdInhibition();
|
||||
else if (strategy === "wayland")
|
||||
startWaylandInhibition();
|
||||
else
|
||||
return ;
|
||||
isInhibited = true;
|
||||
}
|
||||
|
||||
// Stop system inhibition
|
||||
function stopInhibition() {
|
||||
if (!isInhibited)
|
||||
return ;
|
||||
|
||||
if (inhibitorProcess.running)
|
||||
inhibitorProcess.signal(15);
|
||||
|
||||
// SIGTERM
|
||||
isInhibited = false;
|
||||
}
|
||||
|
||||
// Systemd inhibition using systemd-inhibit
|
||||
function startSystemdInhibition() {
|
||||
inhibitorProcess.command = ["systemd-inhibit", "--what=idle", "--why=" + reason, "--mode=block", "sleep", "infinity"];
|
||||
inhibitorProcess.running = true;
|
||||
}
|
||||
|
||||
// Wayland inhibition using wayhibitor or similar
|
||||
function startWaylandInhibition() {
|
||||
inhibitorProcess.command = ["wayhibitor"];
|
||||
inhibitorProcess.running = true;
|
||||
}
|
||||
|
||||
// Manual toggle for user control
|
||||
function manualToggle() {
|
||||
if (activeInhibitors.includes("manual")) {
|
||||
removeInhibitor("manual");
|
||||
return false;
|
||||
} else {
|
||||
addInhibitor("manual", "Manually activated by user");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
detectStrategy();
|
||||
}
|
||||
// Clean up on shutdown
|
||||
Component.onDestruction: {
|
||||
stopInhibition();
|
||||
}
|
||||
|
||||
// Process for maintaining the inhibition
|
||||
Process {
|
||||
id: inhibitorProcess
|
||||
|
||||
running: false
|
||||
onExited: function(exitCode, exitStatus) {
|
||||
if (isInhibited)
|
||||
isInhibited = false;
|
||||
|
||||
console.log("Inhibitor process exited with code:", exitCode, "status:", exitStatus);
|
||||
}
|
||||
onStarted: function() {
|
||||
console.log("Inhibitor process started with strategy:", root.strategy);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
131
quickshell/Services/IpService.qml
Normal file
131
quickshell/Services/IpService.qml
Normal file
@@ -0,0 +1,131 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
property string ip: "N/A"
|
||||
property string countryCode: "N/A"
|
||||
property real fetchInterval: 30 // in s
|
||||
property real fetchTimeout: 10 // in s
|
||||
property string ipURL: "https://api.uyanide.com/ip"
|
||||
property string geoURL: "curl https://api.ipinfo.io/lite/"
|
||||
property string geoURLToken: ""
|
||||
|
||||
function fetchIP() {
|
||||
if (fetchIPProcess.running) {
|
||||
console.warn("Fetch IP process is still running, skipping fetchIP");
|
||||
return ;
|
||||
}
|
||||
fetchIPProcess.running = true;
|
||||
}
|
||||
|
||||
function fetchGeoInfo() {
|
||||
if (fetchGeoProcess.running) {
|
||||
console.warn("Fetch geo process is still running, skipping fetchGeoInfo");
|
||||
return ;
|
||||
}
|
||||
if (!ip || ip === "N/A") {
|
||||
countryCode = "N/A";
|
||||
return ;
|
||||
}
|
||||
fetchGeoProcess.command = ["sh", "-c", `curl -L -m ${fetchTimeout.toString()} ${geoURL}${ip}${geoURLToken ? "?token=" + geoURLToken : ""}`];
|
||||
fetchGeoProcess.running = true;
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
fetchTimer.stop();
|
||||
ip = "N/A";
|
||||
fetchIP();
|
||||
fetchTimer.start();
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: tokenFile
|
||||
|
||||
path: Qt.resolvedUrl("../Assets/Ip/token.txt")
|
||||
onLoaded: {
|
||||
geoURLToken = tokenFile.text();
|
||||
if (!geoURLToken)
|
||||
console.warn("No token found for geoIP service, assuming none is required");
|
||||
|
||||
fetchIP();
|
||||
fetchTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: fetchTimer
|
||||
|
||||
interval: fetchInterval * 1000
|
||||
repeat: true
|
||||
running: false
|
||||
onTriggered: {
|
||||
fetchIP();
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: fetchIPProcess
|
||||
|
||||
command: ["sh", "-c", `curl -L -m ${fetchTimeout.toString()} ${ipURL}`]
|
||||
running: false
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: ""
|
||||
onRead: (data) => {
|
||||
let newIP = "";
|
||||
try {
|
||||
const response = JSON.parse(data);
|
||||
if (response && response.ip) {
|
||||
newIP = response.ip;
|
||||
console.log("Fetched IP: " + newIP);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to parse IP response: " + e);
|
||||
}
|
||||
if (newIP && newIP !== ip) {
|
||||
ip = newIP;
|
||||
fetchGeoInfo();
|
||||
} else if (!newIP) {
|
||||
ip = "N/A";
|
||||
countryCode = "N/A";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Process {
|
||||
id: fetchGeoProcess
|
||||
|
||||
command: []
|
||||
running: false
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: ""
|
||||
onRead: (data) => {
|
||||
let newCountryCode = "";
|
||||
try {
|
||||
const response = JSON.parse(data);
|
||||
if (response && response.country) {
|
||||
newCountryCode = response.country_code;
|
||||
console.log("Fetched country code: " + newCountryCode);
|
||||
SendNotification.show("New IP", `IP: ${ip}\nCountry: ${newCountryCode}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to parse geo response: " + e);
|
||||
}
|
||||
if (newCountryCode && newCountryCode !== countryCode)
|
||||
countryCode = newCountryCode;
|
||||
else if (!newCountryCode)
|
||||
countryCode = "N/A";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
164
quickshell/Services/MusicManager.qml
Normal file
164
quickshell/Services/MusicManager.qml
Normal file
@@ -0,0 +1,164 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Mpris
|
||||
import qs.Modules.Misc
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
id: manager
|
||||
|
||||
// Properties
|
||||
property var currentPlayer: null
|
||||
property real currentPosition: 0
|
||||
property bool isPlaying: currentPlayer ? currentPlayer.isPlaying : false
|
||||
property string trackTitle: currentPlayer ? (currentPlayer.trackTitle || "Unknown Track") : ""
|
||||
property string trackArtist: currentPlayer ? (currentPlayer.trackArtist || "Unknown Artist") : ""
|
||||
property string trackAlbum: currentPlayer ? (currentPlayer.trackAlbum || "Unknown Album") : ""
|
||||
property string trackArtUrl: currentPlayer ? (currentPlayer.trackArtUrl || "") : ""
|
||||
property real trackLength: currentPlayer ? currentPlayer.length : 0
|
||||
property bool canPlay: currentPlayer ? currentPlayer.canPlay : false
|
||||
property bool canPause: currentPlayer ? currentPlayer.canPause : false
|
||||
property bool canGoNext: currentPlayer ? currentPlayer.canGoNext : false
|
||||
property bool canGoPrevious: currentPlayer ? currentPlayer.canGoPrevious : false
|
||||
property bool canSeek: currentPlayer ? currentPlayer.canSeek : false
|
||||
property bool hasPlayer: getAvailablePlayers().length > 0
|
||||
// Expose cava values
|
||||
property alias cavaValues: cava.values
|
||||
|
||||
// Returns available MPRIS players
|
||||
function getAvailablePlayers() {
|
||||
if (!Mpris.players || !Mpris.players.values)
|
||||
return [];
|
||||
|
||||
let allPlayers = Mpris.players.values;
|
||||
let controllablePlayers = [];
|
||||
for (let i = 0; i < allPlayers.length; i++) {
|
||||
let player = allPlayers[i];
|
||||
if (player && player.canControl)
|
||||
controllablePlayers.push(player);
|
||||
|
||||
}
|
||||
return controllablePlayers;
|
||||
}
|
||||
|
||||
// Returns active player or first available
|
||||
function findActivePlayer() {
|
||||
let availablePlayers = getAvailablePlayers();
|
||||
if (availablePlayers.length === 0)
|
||||
return null;
|
||||
|
||||
// Get the first playing player
|
||||
for (let i = availablePlayers.length - 1; i >= 0; i--) {
|
||||
if (availablePlayers[i].isPlaying)
|
||||
return availablePlayers[i];
|
||||
|
||||
}
|
||||
// Fallback to last player
|
||||
return availablePlayers[availablePlayers.length - 1];
|
||||
}
|
||||
|
||||
// Updates currentPlayer and currentPosition
|
||||
function updateCurrentPlayer() {
|
||||
let newPlayer = findActivePlayer();
|
||||
if (newPlayer !== currentPlayer) {
|
||||
currentPlayer = newPlayer;
|
||||
currentPosition = currentPlayer ? currentPlayer.position : 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Player control functions
|
||||
function playPause() {
|
||||
if (currentPlayer) {
|
||||
if (currentPlayer.isPlaying)
|
||||
currentPlayer.pause();
|
||||
else
|
||||
currentPlayer.play();
|
||||
}
|
||||
}
|
||||
|
||||
function isAllPaused() {
|
||||
let availablePlayers = getAvailablePlayers();
|
||||
for (let i = 0; i < availablePlayers.length; i++) {
|
||||
if (availablePlayers[i].isPlaying)
|
||||
return false;
|
||||
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function play() {
|
||||
if (currentPlayer && currentPlayer.canPlay)
|
||||
currentPlayer.play();
|
||||
|
||||
}
|
||||
|
||||
function pause() {
|
||||
if (currentPlayer && currentPlayer.canPause)
|
||||
currentPlayer.pause();
|
||||
|
||||
}
|
||||
|
||||
function next() {
|
||||
if (currentPlayer && currentPlayer.canGoNext)
|
||||
currentPlayer.next();
|
||||
|
||||
}
|
||||
|
||||
function previous() {
|
||||
if (currentPlayer && currentPlayer.canGoPrevious)
|
||||
currentPlayer.previous();
|
||||
|
||||
}
|
||||
|
||||
function seek(position) {
|
||||
if (currentPlayer && currentPlayer.canSeek) {
|
||||
currentPlayer.position = position;
|
||||
currentPosition = position;
|
||||
}
|
||||
}
|
||||
|
||||
function seekByRatio(ratio) {
|
||||
if (currentPlayer && currentPlayer.canSeek && currentPlayer.length > 0) {
|
||||
let seekPosition = ratio * currentPlayer.length;
|
||||
currentPlayer.position = seekPosition;
|
||||
currentPosition = seekPosition;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
Item {
|
||||
Component.onCompleted: {
|
||||
updateCurrentPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
// Updates progress bar every second
|
||||
Timer {
|
||||
id: positionTimer
|
||||
|
||||
interval: 1000
|
||||
running: currentPlayer && currentPlayer.isPlaying && currentPlayer.length > 0
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (currentPlayer && currentPlayer.isPlaying)
|
||||
currentPosition = currentPlayer.position;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Reacts to player list changes
|
||||
Connections {
|
||||
function onValuesChanged() {
|
||||
updateCurrentPlayer();
|
||||
}
|
||||
|
||||
target: Mpris.players
|
||||
}
|
||||
|
||||
Cava {
|
||||
id: cava
|
||||
|
||||
count: 44
|
||||
}
|
||||
|
||||
}
|
||||
184
quickshell/Services/Niri.qml
Normal file
184
quickshell/Services/Niri.qml
Normal file
@@ -0,0 +1,184 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property var workspaces: []
|
||||
property var windows: {}
|
||||
property int focusedWindowId: -1
|
||||
property bool noFocus: focusedWindowId === -1
|
||||
property bool inOverview: false
|
||||
property string focusedWindowTitle: ""
|
||||
// property string focusedWindowAppId: ""
|
||||
|
||||
function updateFocusedWindowTitle() {
|
||||
if (windows && windows[focusedWindowId]) {
|
||||
focusedWindowTitle = windows[focusedWindowId].title || "";
|
||||
// focusedWindowAppId = windows[focusedWindowId].appId || "";
|
||||
} else {
|
||||
focusedWindowTitle = "";
|
||||
// focusedWindowAppId = "";
|
||||
}
|
||||
}
|
||||
|
||||
function getFocusedWindow() {
|
||||
return (windows && windows[focusedWindowId]) || null;
|
||||
}
|
||||
|
||||
onWindowsChanged: updateFocusedWindowTitle()
|
||||
onFocusedWindowIdChanged: updateFocusedWindowTitle()
|
||||
Component.onCompleted: {
|
||||
eventStream.running = true;
|
||||
}
|
||||
|
||||
Process {
|
||||
id: workspaceProcess
|
||||
|
||||
running: false
|
||||
command: ["niri", "msg", "--json", "workspaces"]
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: function(line) {
|
||||
try {
|
||||
const workspacesData = JSON.parse(line);
|
||||
const workspacesList = [];
|
||||
for (const ws of workspacesData) {
|
||||
workspacesList.push({
|
||||
"id": ws.id,
|
||||
"idx": ws.idx,
|
||||
"name": ws.name || "",
|
||||
"output": ws.output || "",
|
||||
"isFocused": ws.is_focused === true,
|
||||
"isActive": ws.is_active === true,
|
||||
"isUrgent": ws.is_urgent === true,
|
||||
"activeWindowId": ws.active_window_id
|
||||
});
|
||||
}
|
||||
workspacesList.sort((a, b) => {
|
||||
if (a.output !== b.output)
|
||||
return a.output.localeCompare(b.output);
|
||||
|
||||
return a.id - b.id;
|
||||
});
|
||||
root.workspaces = workspacesList;
|
||||
} catch (e) {
|
||||
console.error("Failed to parse workspaces:", e, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Process {
|
||||
id: eventStream
|
||||
|
||||
running: false
|
||||
command: ["niri", "msg", "--json", "event-stream"]
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: (data) => {
|
||||
try {
|
||||
const event = JSON.parse(data.trim());
|
||||
if (event.WorkspacesChanged) {
|
||||
workspaceProcess.running = true;
|
||||
} else if (event.WindowsChanged) {
|
||||
try {
|
||||
const windowsData = event.WindowsChanged.windows;
|
||||
const windowsMap = {};
|
||||
for (const win of windowsData) {
|
||||
if (win.is_focused === true) {
|
||||
root.focusedWindowId = win.id;
|
||||
}
|
||||
windowsMap[win.id] = {
|
||||
"title": win.title || "",
|
||||
"appId": win.app_id || "",
|
||||
"workspaceId": win.workspace_id || null,
|
||||
"isFocused": win.is_focused === true
|
||||
};
|
||||
}
|
||||
root.windows = windowsMap;
|
||||
} catch (e) {
|
||||
console.error("Error parsing windows event:", e);
|
||||
}
|
||||
} else if (event.WorkspaceActivated) {
|
||||
workspaceProcess.running = true;
|
||||
} else if (event.WindowFocusChanged) {
|
||||
try {
|
||||
const focusedId = event.WindowFocusChanged.id;
|
||||
if (focusedId) {
|
||||
if (root.windows[focusedId]) {
|
||||
root.focusedWindowId = focusedId;
|
||||
} else {
|
||||
root.focusedWindowId = -1;
|
||||
}
|
||||
|
||||
} else {
|
||||
root.focusedWindowId = -1;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing window focus event:", e);
|
||||
}
|
||||
} else if (event.OverviewOpenedOrClosed) {
|
||||
try {
|
||||
root.inOverview = event.OverviewOpenedOrClosed.is_open === true;
|
||||
} catch (e) {
|
||||
console.error("Error parsing overview state:", e);
|
||||
}
|
||||
} else if (event.WindowOpenedOrChanged) {
|
||||
try {
|
||||
const targetWin = event.WindowOpenedOrChanged.window;
|
||||
const id = targetWin.id;
|
||||
const isFocused = targetWin.is_focused === true;
|
||||
let needUpdateTitle = false;
|
||||
if (id) {
|
||||
if (root.windows && root.windows[id]) {
|
||||
const win = root.windows[id];
|
||||
// Update existing window
|
||||
needUpdateTitle = win.title !== targetWin.title;
|
||||
win.title = targetWin.title || win.title;
|
||||
win.appId = targetWin.app_id || win.appId;
|
||||
win.workspaceId = targetWin.workspace_id || win.workspaceId;
|
||||
win.isFocused = isFocused;
|
||||
} else {
|
||||
// New window
|
||||
const newWin = {
|
||||
"title": targetWin.title || "",
|
||||
"appId": targetWin.app_id || "",
|
||||
"workspaceId": targetWin.workspace_id || null,
|
||||
"isFocused": isFocused
|
||||
};
|
||||
root.windows[id] = targetWin;
|
||||
}
|
||||
if (isFocused) {
|
||||
root.focusedWindowId = id;
|
||||
if (needUpdateTitle)
|
||||
root.updateFocusedWindowTitle();
|
||||
}
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing window opened/changed event:", e);
|
||||
}
|
||||
} else if (event.windowClosed) {
|
||||
try {
|
||||
const closedId = event.windowClosed.id;
|
||||
if (closedId && (root.windows && root.windows[closedId])) {
|
||||
delete root.windows[closedId];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing window closed event:", e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing event stream:", e, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
67
quickshell/Services/PanelService.qml
Normal file
67
quickshell/Services/PanelService.qml
Normal file
@@ -0,0 +1,67 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// A ref. to the lockScreen, so it's accessible from anywhere
|
||||
// This is not a panel...
|
||||
property var lockScreen: null
|
||||
|
||||
// Panels
|
||||
property var registeredPanels: ({})
|
||||
property var openedPanel: null
|
||||
signal willOpen
|
||||
|
||||
// Currently opened popups, can have more than one.
|
||||
// ex: when opening an NIconPicker from a widget setting.
|
||||
property var openedPopups: []
|
||||
property bool hasOpenedPopup: false
|
||||
signal popupChanged
|
||||
|
||||
// Register this panel
|
||||
function registerPanel(panel) {
|
||||
registeredPanels[panel.objectName] = panel
|
||||
}
|
||||
|
||||
// Returns a panel
|
||||
function getPanel(name) {
|
||||
return registeredPanels[name] || null
|
||||
}
|
||||
|
||||
// Check if a panel exists
|
||||
function hasPanel(name) {
|
||||
return name in registeredPanels
|
||||
}
|
||||
|
||||
// Helper to keep only one panel open at any time
|
||||
function willOpenPanel(panel) {
|
||||
if (openedPanel && openedPanel !== panel) {
|
||||
openedPanel.close()
|
||||
}
|
||||
openedPanel = panel
|
||||
|
||||
// emit signal
|
||||
willOpen()
|
||||
}
|
||||
|
||||
function closedPanel(panel) {
|
||||
if (openedPanel && openedPanel === panel) {
|
||||
openedPanel = null
|
||||
}
|
||||
}
|
||||
|
||||
// Popups
|
||||
function willOpenPopup(popup) {
|
||||
openedPopups.push(popup)
|
||||
hasOpenedPopup = (openedPopups.length !== 0)
|
||||
popupChanged()
|
||||
}
|
||||
|
||||
function willClosePopup(popup) {
|
||||
openedPopups = openedPopups.filter(p => p !== popup)
|
||||
hasOpenedPopup = (openedPopups.length !== 0)
|
||||
popupChanged()
|
||||
}
|
||||
}
|
||||
23
quickshell/Services/SendNotification.qml
Normal file
23
quickshell/Services/SendNotification.qml
Normal file
@@ -0,0 +1,23 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function show(title, message, icon = "", urgency = "normal", timeout = 5000) {
|
||||
if (icon)
|
||||
action.command = ["notify-send", "-u", urgency, "-i", icon, "-t", timeout.toString(), title, message];
|
||||
else
|
||||
action.command = ["notify-send", "-u", urgency, "-t", timeout.toString(), title, message];
|
||||
action.startDetached();
|
||||
}
|
||||
|
||||
Process {
|
||||
id: action
|
||||
|
||||
running: false
|
||||
}
|
||||
|
||||
}
|
||||
392
quickshell/Services/SystemStatService.qml
Normal file
392
quickshell/Services/SystemStatService.qml
Normal file
@@ -0,0 +1,392 @@
|
||||
import Qt.labs.folderlistmodel
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
// For Intel coretemp, start averaging all available sensors/cores
|
||||
|
||||
id: root
|
||||
|
||||
// Public values
|
||||
property real cpuUsage: 0
|
||||
property real cpuTemp: 0
|
||||
property real memGb: 0
|
||||
property real memPercent: 0
|
||||
property real diskPercent: 0
|
||||
property real rxSpeed: 0
|
||||
property real txSpeed: 0
|
||||
// Configuration
|
||||
property int sleepDuration: 3000
|
||||
property int fasterSleepDuration: 1000
|
||||
// Internal state for CPU calculation
|
||||
property var prevCpuStats: null
|
||||
// Internal state for network speed calculation
|
||||
// Previous Bytes need to be stored as 'real' as they represent the total of bytes transfered
|
||||
// since the computer started, so their value will easily overlfow a 32bit int.
|
||||
property real prevRxBytes: 0
|
||||
property real prevTxBytes: 0
|
||||
property real prevTime: 0
|
||||
// Cpu temperature is the most complex
|
||||
readonly property var supportedTempCpuSensorNames: ["coretemp", "k10temp", "zenpower"]
|
||||
property string cpuTempSensorName: ""
|
||||
property string cpuTempHwmonPath: ""
|
||||
// For Intel coretemp averaging of all cores/sensors
|
||||
property var intelTempValues: []
|
||||
property int intelTempFilesChecked: 0
|
||||
property int intelTempMaxFiles: 20 // Will test up to temp20_input
|
||||
|
||||
// -------------------------------------------------------
|
||||
// -------------------------------------------------------
|
||||
// Parse memory info from /proc/meminfo
|
||||
function parseMemoryInfo(text) {
|
||||
if (!text)
|
||||
return ;
|
||||
|
||||
const lines = text.split('\n');
|
||||
let memTotal = 0;
|
||||
let memAvailable = 0;
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('MemTotal:'))
|
||||
memTotal = parseInt(line.split(/\s+/)[1]) || 0;
|
||||
else if (line.startsWith('MemAvailable:'))
|
||||
memAvailable = parseInt(line.split(/\s+/)[1]) || 0;
|
||||
}
|
||||
if (memTotal > 0) {
|
||||
const usageKb = memTotal - memAvailable;
|
||||
root.memGb = (usageKb / 1e+06).toFixed(1);
|
||||
root.memPercent = Math.round((usageKb / memTotal) * 100);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Calculate CPU usage from /proc/stat
|
||||
function calculateCpuUsage(text) {
|
||||
if (!text)
|
||||
return ;
|
||||
|
||||
const lines = text.split('\n');
|
||||
const cpuLine = lines[0];
|
||||
// First line is total CPU
|
||||
if (!cpuLine.startsWith('cpu '))
|
||||
return ;
|
||||
|
||||
const parts = cpuLine.split(/\s+/);
|
||||
const stats = {
|
||||
"user": parseInt(parts[1]) || 0,
|
||||
"nice": parseInt(parts[2]) || 0,
|
||||
"system": parseInt(parts[3]) || 0,
|
||||
"idle": parseInt(parts[4]) || 0,
|
||||
"iowait": parseInt(parts[5]) || 0,
|
||||
"irq": parseInt(parts[6]) || 0,
|
||||
"softirq": parseInt(parts[7]) || 0,
|
||||
"steal": parseInt(parts[8]) || 0,
|
||||
"guest": parseInt(parts[9]) || 0,
|
||||
"guestNice": parseInt(parts[10]) || 0
|
||||
};
|
||||
const totalIdle = stats.idle + stats.iowait;
|
||||
const total = Object.values(stats).reduce((sum, val) => {
|
||||
return sum + val;
|
||||
}, 0);
|
||||
if (root.prevCpuStats) {
|
||||
const prevTotalIdle = root.prevCpuStats.idle + root.prevCpuStats.iowait;
|
||||
const prevTotal = Object.values(root.prevCpuStats).reduce((sum, val) => {
|
||||
return sum + val;
|
||||
}, 0);
|
||||
const diffTotal = total - prevTotal;
|
||||
const diffIdle = totalIdle - prevTotalIdle;
|
||||
if (diffTotal > 0)
|
||||
root.cpuUsage = (((diffTotal - diffIdle) / diffTotal) * 100).toFixed(1);
|
||||
|
||||
}
|
||||
root.prevCpuStats = stats;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Calculate RX and TX speed from /proc/net/dev
|
||||
// Average speed of all interfaces excepted 'lo'
|
||||
function calculateNetworkSpeed(text) {
|
||||
if (!text)
|
||||
return ;
|
||||
|
||||
const currentTime = Date.now() / 1000;
|
||||
const lines = text.split('\n');
|
||||
let totalRx = 0;
|
||||
let totalTx = 0;
|
||||
for (var i = 2; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (!line)
|
||||
continue;
|
||||
|
||||
const colonIndex = line.indexOf(':');
|
||||
if (colonIndex === -1)
|
||||
continue;
|
||||
|
||||
const iface = line.substring(0, colonIndex).trim();
|
||||
if (iface === 'lo')
|
||||
continue;
|
||||
|
||||
const statsLine = line.substring(colonIndex + 1).trim();
|
||||
const stats = statsLine.split(/\s+/);
|
||||
const rxBytes = parseInt(stats[0], 10) || 0;
|
||||
const txBytes = parseInt(stats[8], 10) || 0;
|
||||
totalRx += rxBytes;
|
||||
totalTx += txBytes;
|
||||
}
|
||||
// Compute only if we have a previous run to compare to.
|
||||
if (root.prevTime > 0) {
|
||||
const timeDiff = currentTime - root.prevTime;
|
||||
// Avoid division by zero if time hasn't passed.
|
||||
if (timeDiff > 0) {
|
||||
let rxDiff = totalRx - root.prevRxBytes;
|
||||
let txDiff = totalTx - root.prevTxBytes;
|
||||
// Handle counter resets (e.g., WiFi reconnect), which would cause a negative value.
|
||||
if (rxDiff < 0)
|
||||
rxDiff = 0;
|
||||
|
||||
if (txDiff < 0)
|
||||
txDiff = 0;
|
||||
|
||||
root.rxSpeed = Math.round(rxDiff / timeDiff); // Speed in Bytes/s
|
||||
root.txSpeed = Math.round(txDiff / timeDiff);
|
||||
}
|
||||
}
|
||||
root.prevRxBytes = totalRx;
|
||||
root.prevTxBytes = totalTx;
|
||||
root.prevTime = currentTime;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Helper function to format network speeds
|
||||
function formatSpeed(bytesPerSecond) {
|
||||
if (bytesPerSecond < 1024 * 1024) {
|
||||
const kb = bytesPerSecond / 1024;
|
||||
if (kb < 10)
|
||||
return kb.toFixed(1) + "KB";
|
||||
else
|
||||
return Math.round(kb) + "KB";
|
||||
} else if (bytesPerSecond < 1024 * 1024 * 1024) {
|
||||
return (bytesPerSecond / (1024 * 1024)).toFixed(1) + "MB";
|
||||
} else {
|
||||
return (bytesPerSecond / (1024 * 1024 * 1024)).toFixed(1) + "GB";
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Compact speed formatter for vertical bar display
|
||||
function formatCompactSpeed(bytesPerSecond) {
|
||||
if (!bytesPerSecond || bytesPerSecond <= 0)
|
||||
return "0";
|
||||
|
||||
const units = ["", "K", "M", "G"];
|
||||
let value = bytesPerSecond;
|
||||
let unitIndex = 0;
|
||||
while (value >= 1024 && unitIndex < units.length - 1) {
|
||||
value = value / 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
// Promote at ~100 of current unit (e.g., 100k -> ~0.1M shown as 0.1M or 0M if rounded)
|
||||
if (unitIndex < units.length - 1 && value >= 100) {
|
||||
value = value / 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
const display = Math.round(value).toString();
|
||||
return display + units[unitIndex];
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Function to start fetching and computing the cpu temperature
|
||||
function updateCpuTemperature() {
|
||||
// For AMD sensors (k10temp and zenpower), only use Tctl sensor
|
||||
// temp1_input corresponds to Tctl (Temperature Control) on these sensors
|
||||
if (root.cpuTempSensorName === "k10temp" || root.cpuTempSensorName === "zenpower") {
|
||||
cpuTempReader.path = `${root.cpuTempHwmonPath}/temp1_input`;
|
||||
cpuTempReader.reload();
|
||||
} else if (root.cpuTempSensorName === "coretemp") {
|
||||
root.intelTempValues = [];
|
||||
root.intelTempFilesChecked = 0;
|
||||
checkNextIntelTemp();
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Function to check next Intel temperature sensor
|
||||
function checkNextIntelTemp() {
|
||||
if (root.intelTempFilesChecked >= root.intelTempMaxFiles) {
|
||||
// Calculate average of all found temperatures
|
||||
if (root.intelTempValues.length > 0) {
|
||||
let sum = 0;
|
||||
for (var i = 0; i < root.intelTempValues.length; i++) {
|
||||
sum += root.intelTempValues[i];
|
||||
}
|
||||
root.cpuTemp = Math.round(sum / root.intelTempValues.length);
|
||||
} else {
|
||||
console.warn("No temperature sensors found for coretemp");
|
||||
root.cpuTemp = 0;
|
||||
}
|
||||
return ;
|
||||
}
|
||||
// Check next temperature file
|
||||
root.intelTempFilesChecked++;
|
||||
cpuTempReader.path = `${root.cpuTempHwmonPath}/temp${root.intelTempFilesChecked}_input`;
|
||||
cpuTempReader.reload();
|
||||
}
|
||||
|
||||
// --------------------------------------------
|
||||
Component.onCompleted: {
|
||||
// Kickoff the cpu name detection for temperature
|
||||
cpuTempNameReader.checkNext();
|
||||
}
|
||||
|
||||
// --------------------------------------------
|
||||
// Timer for periodic updates
|
||||
Timer {
|
||||
id: updateTimer
|
||||
|
||||
interval: root.sleepDuration
|
||||
repeat: true
|
||||
running: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: {
|
||||
// Trigger all direct system files reads
|
||||
memInfoFile.reload();
|
||||
cpuStatFile.reload();
|
||||
// Run df (disk free) one time
|
||||
dfProcess.running = true;
|
||||
updateCpuTemperature();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: fasterUpdateTimer
|
||||
|
||||
interval: root.fasterSleepDuration
|
||||
repeat: true
|
||||
running: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: {
|
||||
netDevFile.reload();
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------
|
||||
// FileView components for reading system files
|
||||
FileView {
|
||||
id: memInfoFile
|
||||
|
||||
path: "/proc/meminfo"
|
||||
onLoaded: parseMemoryInfo(text())
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: cpuStatFile
|
||||
|
||||
path: "/proc/stat"
|
||||
onLoaded: calculateCpuUsage(text())
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: netDevFile
|
||||
|
||||
path: "/proc/net/dev"
|
||||
onLoaded: calculateNetworkSpeed(text())
|
||||
}
|
||||
|
||||
// --------------------------------------------
|
||||
// Process to fetch disk usage in percent
|
||||
// Uses 'df' aka 'disk free'
|
||||
Process {
|
||||
id: dfProcess
|
||||
|
||||
command: ["df", "--output=pcent", "/"]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const lines = text.trim().split('\n');
|
||||
if (lines.length >= 2) {
|
||||
const percent = lines[1].replace(/[^0-9]/g, '');
|
||||
root.diskPercent = parseInt(percent) || 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// --------------------------------------------
|
||||
// --------------------------------------------
|
||||
// CPU Temperature
|
||||
// It's more complex.
|
||||
// ----
|
||||
// #1 - Find a common cpu sensor name ie: "coretemp", "k10temp", "zenpower"
|
||||
FileView {
|
||||
id: cpuTempNameReader
|
||||
|
||||
property int currentIndex: 0
|
||||
|
||||
function checkNext() {
|
||||
if (currentIndex >= 16) {
|
||||
// Check up to hwmon10
|
||||
console.warn("No supported temperature sensor found");
|
||||
return ;
|
||||
}
|
||||
cpuTempNameReader.path = `/sys/class/hwmon/hwmon${currentIndex}/name`;
|
||||
cpuTempNameReader.reload();
|
||||
}
|
||||
|
||||
printErrors: false
|
||||
onLoaded: {
|
||||
const name = text().trim();
|
||||
if (root.supportedTempCpuSensorNames.includes(name)) {
|
||||
root.cpuTempSensorName = name;
|
||||
root.cpuTempHwmonPath = `/sys/class/hwmon/hwmon${currentIndex}`;
|
||||
console.log(`Found ${root.cpuTempSensorName} CPU thermal sensor at ${root.cpuTempHwmonPath}`);
|
||||
} else {
|
||||
currentIndex++;
|
||||
Qt.callLater(() => {
|
||||
// Qt.callLater is mandatory
|
||||
checkNext();
|
||||
});
|
||||
}
|
||||
}
|
||||
onLoadFailed: function(error) {
|
||||
currentIndex++;
|
||||
Qt.callLater(() => {
|
||||
// Qt.callLater is mandatory
|
||||
checkNext();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ----
|
||||
// #2 - Read sensor value
|
||||
FileView {
|
||||
id: cpuTempReader
|
||||
|
||||
printErrors: false
|
||||
onLoaded: {
|
||||
const data = text().trim();
|
||||
if (root.cpuTempSensorName === "coretemp") {
|
||||
// For Intel, collect all temperature values
|
||||
const temp = parseInt(data) / 1000;
|
||||
//console.log(temp, cpuTempReader.path)
|
||||
root.intelTempValues.push(temp);
|
||||
Qt.callLater(() => {
|
||||
// Qt.callLater is mandatory
|
||||
checkNextIntelTemp();
|
||||
});
|
||||
} else {
|
||||
// For AMD sensors (k10temp and zenpower), directly set the temperature
|
||||
root.cpuTemp = Math.round(parseInt(data) / 1000);
|
||||
}
|
||||
}
|
||||
onLoadFailed: function(error) {
|
||||
Qt.callLater(() => {
|
||||
// Qt.callLater is mandatory
|
||||
checkNextIntelTemp();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
46
quickshell/Services/ThemeIcons.qml
Normal file
46
quickshell/Services/ThemeIcons.qml
Normal file
@@ -0,0 +1,46 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
// ignore and fall back
|
||||
|
||||
id: root
|
||||
|
||||
function iconFromName(iconName, fallbackName) {
|
||||
const fallback = fallbackName || "application-x-executable";
|
||||
try {
|
||||
if (iconName && typeof Quickshell !== 'undefined' && Quickshell.iconPath) {
|
||||
const p = Quickshell.iconPath(iconName, fallback);
|
||||
if (p && p !== "")
|
||||
return p;
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
try {
|
||||
return Quickshell.iconPath ? (Quickshell.iconPath(fallback, true) || "") : "";
|
||||
} catch (e2) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve icon path for a DesktopEntries appId - safe on missing entries
|
||||
function iconForAppId(appId, fallbackName) {
|
||||
const fallback = fallbackName || "application-x-executable";
|
||||
if (!appId)
|
||||
return iconFromName(fallback, fallback);
|
||||
|
||||
try {
|
||||
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
|
||||
return iconFromName(fallback, fallback);
|
||||
|
||||
const entry = (DesktopEntries.heuristicLookup) ? DesktopEntries.heuristicLookup(appId) : DesktopEntries.byId(appId);
|
||||
const name = entry && entry.icon ? entry.icon : "";
|
||||
return iconFromName(name || fallback, fallback);
|
||||
} catch (e) {
|
||||
return iconFromName(fallback, fallback);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
44
quickshell/Services/TimeService.qml
Normal file
44
quickshell/Services/TimeService.qml
Normal file
@@ -0,0 +1,44 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property var date: new Date()
|
||||
property string time: Qt.formatDateTime(date, "HH:mm")
|
||||
property string dateString: {
|
||||
let now = date;
|
||||
let dayName = now.toLocaleDateString(Qt.locale(), "ddd");
|
||||
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1);
|
||||
let day = now.getDate();
|
||||
let suffix;
|
||||
if (day > 3 && day < 21)
|
||||
suffix = 'th';
|
||||
else
|
||||
switch (day % 10) {
|
||||
case 1:
|
||||
suffix = "st";
|
||||
break;
|
||||
case 2:
|
||||
suffix = "nd";
|
||||
break;
|
||||
case 3:
|
||||
suffix = "rd";
|
||||
break;
|
||||
default:
|
||||
suffix = "th";
|
||||
};
|
||||
let month = now.toLocaleDateString(Qt.locale(), "MMMM");
|
||||
let year = now.toLocaleDateString(Qt.locale(), "yyyy");
|
||||
return `${dayName}, ` + `${day}${suffix} ${month} ${year}`;
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 1000
|
||||
repeat: true
|
||||
running: true
|
||||
onTriggered: root.date = new Date()
|
||||
}
|
||||
|
||||
}
|
||||
55
quickshell/Services/WorkspaceManager.qml
Normal file
55
quickshell/Services/WorkspaceManager.qml
Normal file
@@ -0,0 +1,55 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property ListModel workspaces
|
||||
|
||||
workspaces: ListModel {
|
||||
}
|
||||
|
||||
function initNiri() {
|
||||
updateNiriWorkspaces();
|
||||
}
|
||||
|
||||
function updateNiriWorkspaces() {
|
||||
const niriWorkspaces = Niri.workspaces || [];
|
||||
workspaces.clear();
|
||||
for (let i = 0; i < niriWorkspaces.length; i++) {
|
||||
const ws = niriWorkspaces[i];
|
||||
workspaces.append({
|
||||
"id": ws.id,
|
||||
"idx": ws.idx || 1,
|
||||
"name": ws.name || "",
|
||||
"output": ws.output || "",
|
||||
"isFocused": ws.isFocused === true,
|
||||
"isActive": ws.isActive === true,
|
||||
"isUrgent": ws.isUrgent === true
|
||||
});
|
||||
}
|
||||
workspacesChanged();
|
||||
}
|
||||
|
||||
function switchToWorkspace(workspaceId) {
|
||||
try {
|
||||
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", workspaceId.toString()]);
|
||||
} catch (e) {
|
||||
console.error("Error switching Niri workspace:", e);
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onWorkspacesChanged() {
|
||||
updateNiriWorkspaces();
|
||||
}
|
||||
|
||||
target: Niri
|
||||
}
|
||||
|
||||
}
|
||||
20
quickshell/Services/WriteClipboard.qml
Normal file
20
quickshell/Services/WriteClipboard.qml
Normal file
@@ -0,0 +1,20 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
pragma Singleton
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function write(text) {
|
||||
action.command = ["sh", "-c", `echo ${text} | wl-copy -n`];
|
||||
action.startDetached();
|
||||
}
|
||||
|
||||
Process {
|
||||
id: action
|
||||
|
||||
running: false
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user