1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-09 15:05:39 -05:00
This commit is contained in:
purian23
2025-07-15 16:41:47 -04:00
22 changed files with 3255 additions and 1633 deletions

View File

@@ -14,7 +14,6 @@ Singleton {
property var applicationsByExec: ({})
property bool ready: false
// Pre-prepared fuzzy search data
property var preppedApps: []
@@ -33,7 +32,6 @@ Singleton {
function loadApplications() {
// Trigger rescan on next frame to avoid blocking
Qt.callLater(function() {
var allApps = Array.from(DesktopEntries.applications.values)
@@ -41,7 +39,6 @@ Singleton {
.filter(app => !app.noDisplay)
.sort((a, b) => a.name.localeCompare(b.name))
// Build lookup maps
var byName = {}
var byExec = {}
@@ -49,7 +46,6 @@ Singleton {
var app = applications[i]
byName[app.name.toLowerCase()] = app
// Clean exec string for lookup
var execProp = app.execString || ""
var cleanExec = execProp ? execProp.replace(/%[fFuU]/g, "").trim() : ""
if (cleanExec) {
@@ -60,7 +56,6 @@ Singleton {
applicationsByName = byName
applicationsByExec = byExec
// Prepare fuzzy search data
preppedApps = applications.map(app => ({
name: Fuzzy.prepare(app.name || ""),
comment: Fuzzy.prepare(app.comment || ""),
@@ -84,12 +79,10 @@ Singleton {
return []
}
// Use fuzzy search with both name and comment fields
var results = Fuzzy.go(query, preppedApps, {
all: false,
keys: ["name", "comment"],
scoreFn: r => {
// Prioritize name matches over comment matches
var nameScore = r[0] ? r[0].score : 0
var commentScore = r[1] ? r[1].score : 0
return nameScore > 0 ? nameScore * 0.9 + commentScore * 0.1 : commentScore * 0.5
@@ -97,7 +90,6 @@ Singleton {
limit: 50
})
// Extract the desktop entries from results
return results.map(r => r.obj.entry)
}
@@ -180,7 +172,6 @@ Singleton {
return false
}
// DesktopEntry objects have an execute() method
if (typeof app.execute === "function") {
app.execute()
return true

View File

@@ -1,364 +1,264 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
pragma Singleton
pragma ComponentBehavior: Bound
Singleton {
id: root
readonly property PwNode sink: Pipewire.defaultAudioSink
readonly property PwNode source: Pipewire.defaultAudioSource
readonly property bool sinkMuted: sink?.audio?.muted ?? false
readonly property bool sourceMuted: source?.audio?.muted ?? false
readonly property real volumeLevel: (sink?.audio?.volume ?? 0) * 100
readonly property real micLevel: (source?.audio?.volume ?? 0) * 100
signal audioVolumeChanged(real volume)
signal audioMicLevelChanged(real level)
signal audioMuteChanged(bool muted)
signal audioMicMuteChanged(bool muted)
signal audioDeviceChanged()
property int volumeLevel: 50
onVolumeLevelChanged: audioVolumeChanged(volumeLevel)
onMicLevelChanged: audioMicLevelChanged(micLevel)
onSinkMutedChanged: audioMuteChanged(sinkMuted)
onSourceMutedChanged: audioMicMuteChanged(sourceMuted)
onSinkChanged: {
audioDeviceChanged()
}
onSourceChanged: {
audioDeviceChanged()
}
property var audioSinks: []
property string currentAudioSink: ""
// Microphone properties
property int micLevel: 50
property var audioSources: []
property string currentAudioSource: ""
// Device scanning control
property bool deviceScanningEnabled: false
property bool initialScanComplete: false
// Real Audio Control
Process {
id: volumeChecker
command: ["bash", "-c", "pactl get-sink-volume @DEFAULT_SINK@ | grep -o '[0-9]*%' | head -1 | tr -d '%'"]
running: true
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
if (data.trim()) {
root.volumeLevel = Math.min(100, parseInt(data.trim()) || 50)
}
Component.onCompleted: {
Qt.callLater(updateDevices)
}
function updateDevices() {
updateAudioSinks()
updateAudioSources()
}
Connections {
target: Pipewire
function onReadyChanged() {
if (Pipewire.ready) {
updateAudioSinks()
updateAudioSources()
}
}
}
// Microphone level checker
Process {
id: micLevelChecker
command: ["bash", "-c", "pactl get-source-volume @DEFAULT_SOURCE@ | grep -o '[0-9]*%' | head -1 | tr -d '%'"]
running: true
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
if (data.trim()) {
root.micLevel = Math.min(100, parseInt(data.trim()) || 50)
}
}
function onDefaultAudioSinkChanged() {
updateAudioSinks()
}
function onDefaultAudioSourceChanged() {
updateAudioSources()
}
}
Process {
id: audioSinkLister
command: ["pactl", "list", "sinks"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
let sinks = []
let lines = text.trim().split('\n')
let currentSink = null
for (let line of lines) {
line = line.trim()
// New sink starts
if (line.startsWith('Sink #')) {
if (currentSink && currentSink.name && currentSink.id) {
sinks.push(currentSink)
}
let sinkId = line.replace('Sink #', '').trim()
currentSink = {
id: sinkId,
name: "",
displayName: "",
description: "",
nick: "",
active: false
}
}
// Get the Name field
else if (line.startsWith('Name: ') && currentSink) {
currentSink.name = line.replace('Name: ', '').trim()
}
// Get the Description field (main display name)
else if (line.startsWith('Description: ') && currentSink) {
currentSink.description = line.replace('Description: ', '').trim()
}
// Get device.description as fallback
else if (line.includes('device.description = ') && currentSink && !currentSink.description) {
currentSink.description = line.replace('device.description = ', '').replace(/"/g, '').trim()
}
// Get node.nick as another fallback option
else if (line.includes('node.nick = ') && currentSink && !currentSink.description) {
currentSink.nick = line.replace('node.nick = ', '').replace(/"/g, '').trim()
}
}
// Add the last sink
if (currentSink && currentSink.name && currentSink.id) {
sinks.push(currentSink)
}
// Process display names
for (let sink of sinks) {
let displayName = sink.description
// If no good description, try nick
if (!displayName || displayName === sink.name) {
displayName = sink.nick
}
// Still no good name? Fall back to smart defaults
if (!displayName || displayName === sink.name) {
if (sink.name.includes("analog-stereo")) displayName = "Built-in Speakers"
else if (sink.name.includes("bluez")) displayName = "Bluetooth Audio"
else if (sink.name.includes("usb")) displayName = "USB Audio"
else if (sink.name.includes("hdmi")) displayName = "HDMI Audio"
else if (sink.name.includes("easyeffects")) displayName = "EasyEffects"
else displayName = sink.name
}
sink.displayName = displayName
}
root.audioSinks = sinks
defaultSinkChecker.running = true
}
}
}
}
// Audio source (microphone) lister
Process {
id: audioSourceLister
command: ["pactl", "list", "sources"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
let sources = []
let lines = text.trim().split('\n')
let currentSource = null
for (let line of lines) {
line = line.trim()
// New source starts
if (line.startsWith('Source #')) {
if (currentSource && currentSource.name && currentSource.id) {
sources.push(currentSource)
}
currentSource = {
id: line.replace('Source #', '').replace(':', ''),
name: '',
displayName: '',
active: false
}
}
// Source name
else if (line.startsWith('Name: ') && currentSource) {
currentSource.name = line.replace('Name: ', '')
}
// Description (display name)
else if (line.startsWith('Description: ') && currentSource) {
let desc = line.replace('Description: ', '')
currentSource.displayName = desc
}
}
// Add the last source
if (currentSource && currentSource.name && currentSource.id) {
sources.push(currentSource)
}
// Filter out monitor sources (we want actual input devices)
sources = sources.filter(source => !source.name.includes('.monitor'))
root.audioSources = sources
defaultSourceChecker.running = true
}
}
}
}
Process {
id: defaultSinkChecker
command: ["pactl", "get-default-sink"]
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
if (data.trim()) {
root.currentAudioSink = data.trim()
// Update active status in audioSinks
let updatedSinks = []
for (let sink of root.audioSinks) {
updatedSinks.push({
id: sink.id,
name: sink.name,
displayName: sink.displayName,
active: sink.name === root.currentAudioSink
})
}
root.audioSinks = updatedSinks
}
}
}
}
// Default source (microphone) checker
Process {
id: defaultSourceChecker
command: ["pactl", "get-default-source"]
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
if (data.trim()) {
root.currentAudioSource = data.trim()
// Update active status in audioSources
let updatedSources = []
for (let source of root.audioSources) {
updatedSources.push({
id: source.id,
name: source.name,
displayName: source.displayName,
active: source.name === root.currentAudioSource
})
}
root.audioSources = updatedSources
}
}
}
}
function setVolume(percentage) {
let volumeSetProcess = Qt.createQmlObject('
import Quickshell.Io
Process {
command: ["pactl", "set-sink-volume", "@DEFAULT_SINK@", "' + percentage + '%"]
running: true
onExited: volumeChecker.running = true
}
', root)
}
function setMicLevel(percentage) {
let micSetProcess = Qt.createQmlObject('
import Quickshell.Io
Process {
command: ["pactl", "set-source-volume", "@DEFAULT_SOURCE@", "' + percentage + '%"]
running: true
onExited: micLevelChecker.running = true
}
', root)
}
function setAudioSink(sinkName) {
console.log("Setting audio sink to:", sinkName)
// Use a more reliable approach instead of Qt.createQmlObject
sinkSetProcess.command = ["pactl", "set-default-sink", sinkName]
sinkSetProcess.running = true
}
// Dedicated process for setting audio sink
Process {
id: sinkSetProcess
running: false
onExited: (exitCode) => {
console.log("Audio sink change exit code:", exitCode)
if (exitCode === 0) {
console.log("Audio sink changed successfully")
// Refresh current sink and list
defaultSinkChecker.running = true
if (root.deviceScanningEnabled) {
audioSinkLister.running = true
}
} else {
console.error("Failed to change audio sink")
}
}
}
function setAudioSource(sourceName) {
console.log("Setting audio source to:", sourceName)
sourceSetProcess.command = ["pactl", "set-default-source", sourceName]
sourceSetProcess.running = true
}
// Dedicated process for setting audio source
Process {
id: sourceSetProcess
running: false
onExited: (exitCode) => {
console.log("Audio source change exit code:", exitCode)
if (exitCode === 0) {
console.log("Audio source changed successfully")
// Refresh current source and list
defaultSourceChecker.running = true
if (root.deviceScanningEnabled) {
audioSourceLister.running = true
}
} else {
console.error("Failed to change audio source")
}
}
}
// Timer to check for node changes since ObjectModel doesn't expose change signals
Timer {
interval: 5000
running: root.deviceScanningEnabled && root.initialScanComplete
interval: 2000
running: Pipewire.ready
repeat: true
onTriggered: {
if (root.deviceScanningEnabled) {
audioSinkLister.running = true
audioSourceLister.running = true
if (Pipewire.nodes && Pipewire.nodes.values) {
let currentCount = Pipewire.nodes.values.length
if (currentCount !== lastNodeCount) {
lastNodeCount = currentCount
updateAudioSinks()
updateAudioSources()
}
}
}
}
property int lastNodeCount: 0
function updateAudioSinks() {
if (!Pipewire.ready || !Pipewire.nodes) return
let sinks = []
if (Pipewire.nodes.values) {
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
let node = Pipewire.nodes.values[i]
if (!node) continue
if ((node.type & PwNodeType.AudioSink) === PwNodeType.AudioSink && !node.isStream) {
let displayName = getDisplayName(node)
sinks.push({
id: node.id.toString(),
name: node.name,
displayName: displayName,
subtitle: getDeviceSubtitle(node.name),
active: node === root.sink,
node: node
})
}
}
}
audioSinks = sinks
}
function updateAudioSources() {
if (!Pipewire.ready || !Pipewire.nodes) return
let sources = []
if (Pipewire.nodes.values) {
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
let node = Pipewire.nodes.values[i]
if (!node) continue
if ((node.type & PwNodeType.AudioSource) === PwNodeType.AudioSource && !node.isStream && !node.name.includes('.monitor')) {
sources.push({
id: node.id.toString(),
name: node.name,
displayName: getDisplayName(node),
subtitle: getDeviceSubtitle(node.name),
active: node === root.source,
node: node
})
}
}
}
audioSources = sources
}
function getDisplayName(node) {
// Check properties first (this is key for Bluetooth devices!)
if (node.properties && node.properties["device.description"]) {
return node.properties["device.description"]
}
if (node.description && node.description !== node.name) {
return node.description
}
if (node.nickname && node.nickname !== node.name) {
return node.nickname
}
// Fallback to name processing
if (node.name.includes("analog-stereo")) return "Built-in Speakers"
else if (node.name.includes("bluez")) return "Bluetooth Audio"
else if (node.name.includes("usb")) return "USB Audio"
else if (node.name.includes("hdmi")) return "HDMI Audio"
return node.name
}
function getDeviceSubtitle(nodeName) {
if (!nodeName) return ""
// Simple subtitle based on node name patterns
if (nodeName.includes('usb-')) {
if (nodeName.includes('SteelSeries')) {
return "USB Gaming Headset"
} else if (nodeName.includes('Generic')) {
return "USB Audio Device"
}
return "USB Audio"
} else if (nodeName.includes('pci-')) {
if (nodeName.includes('01_00.1') || nodeName.includes('01:00.1')) {
return "NVIDIA GPU Audio"
}
return "PCI Audio"
} else if (nodeName.includes('bluez')) {
return "Bluetooth Audio"
} else if (nodeName.includes('analog')) {
return "Built-in Audio"
}
return ""
}
readonly property string currentAudioSink: sink?.name ?? ""
readonly property string currentAudioSource: source?.name ?? ""
Component.onCompleted: {
console.log("AudioService: Starting initialization...")
// Do initial device scan
audioSinkLister.running = true
audioSourceLister.running = true
initialScanComplete = true
console.log("AudioService: Initialization complete")
readonly property string currentSinkDisplayName: {
if (!sink) return ""
for (let sinkInfo of audioSinks) {
if (sinkInfo.node === sink) {
return sinkInfo.displayName
}
}
return sink.description || sink.name
}
// Control functions for managing device scanning
function enableDeviceScanning(enabled) {
console.log("AudioService: Device scanning", enabled ? "enabled" : "disabled")
root.deviceScanningEnabled = enabled
if (enabled && root.initialScanComplete) {
// Immediately scan when enabled
audioSinkLister.running = true
audioSourceLister.running = true
readonly property string currentSourceDisplayName: {
if (!source) return ""
for (let sourceInfo of audioSources) {
if (sourceInfo.node === source) {
return sourceInfo.displayName
}
}
return source.description || source.name
}
function setVolume(percentage) {
if (sink?.ready && sink?.audio) {
sink.audio.muted = false
sink.audio.volume = percentage / 100
}
}
// Manual refresh function for when user opens audio settings
function refreshDevices() {
console.log("AudioService: Manual device refresh triggered")
audioSinkLister.running = true
audioSourceLister.running = true
function setMicLevel(percentage) {
if (source?.ready && source?.audio) {
source.audio.muted = false
source.audio.volume = percentage / 100
}
}
function toggleMute() {
if (sink?.ready && sink?.audio) {
sink.audio.muted = !sink.audio.muted
}
}
function toggleMicMute() {
if (source?.ready && source?.audio) {
source.audio.muted = !source.audio.muted
}
}
function setAudioSink(sinkName) {
if (Pipewire.nodes.values) {
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
let node = Pipewire.nodes.values[i]
if (node && node.name === sinkName && (node.type & PwNodeType.AudioSink) === PwNodeType.AudioSink && !node.isStream) {
Pipewire.preferredDefaultAudioSink = node
break
}
}
}
}
function setAudioSource(sourceName) {
if (Pipewire.nodes.values) {
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
let node = Pipewire.nodes.values[i]
if (node && node.name === sourceName && (node.type & PwNodeType.AudioSource) === PwNodeType.AudioSource && !node.isStream) {
Pipewire.preferredDefaultAudioSource = node
break
}
}
}
}
PwObjectTracker {
id: nodeTracker
objects: [Pipewire.defaultAudioSink, Pipewire.defaultAudioSource]
}
}

View File

@@ -1,213 +1,120 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.UPower
pragma Singleton
pragma ComponentBehavior: Bound
Singleton {
id: root
// Battery properties
property bool batteryAvailable: false
property int batteryLevel: 0
property string batteryStatus: "Unknown"
property int timeRemaining: 0
property bool isCharging: false
property bool isLowBattery: false
property int batteryHealth: 100
property string batteryTechnology: "Unknown"
property int cycleCount: 0
property int batteryCapacity: 0
property var powerProfiles: []
property string activePowerProfile: ""
// Debug mode for testing on desktop systems without batteries
property bool debugMode: false // Set to true to enable fake battery for testing
// Check if battery is available
Process {
id: batteryAvailabilityChecker
command: ["bash", "-c", "ls /sys/class/power_supply/ | grep -E '^BAT' | head -1"]
running: true
// Debug fake battery data
property int debugBatteryLevel: 65
property string debugBatteryStatus: "Discharging"
property int debugTimeRemaining: 7200 // 2 hours in seconds
property bool debugIsCharging: false
property int debugBatteryHealth: 88
property string debugBatteryTechnology: "Li-ion"
property int debugBatteryCapacity: 45000 // 45 Wh in mWh
property bool batteryAvailable: debugMode || (battery.ready && battery.isLaptopBattery)
property int batteryLevel: debugMode ? debugBatteryLevel : Math.round(battery.percentage)
property string batteryStatus: debugMode ? debugBatteryStatus : UPowerDeviceState.toString(battery.state)
property int timeRemaining: debugMode ? debugTimeRemaining : (battery.timeToEmpty || battery.timeToFull)
property bool isCharging: debugMode ? debugIsCharging : (battery.state === UPowerDeviceState.Charging)
property bool isLowBattery: debugMode ? (debugBatteryLevel <= 20) : (battery.percentage <= 20)
property int batteryHealth: debugMode ? debugBatteryHealth : (battery.healthSupported ? Math.round(battery.healthPercentage) : 100)
property string batteryTechnology: {
if (debugMode) return debugBatteryTechnology
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
if (data.trim()) {
root.batteryAvailable = true
console.log("Battery found:", data.trim())
batteryStatusChecker.running = true
} else {
root.batteryAvailable = false
console.log("No battery found - this appears to be a desktop system")
// Try to get technology from any available laptop battery
for (let i = 0; i < UPower.devices.length; i++) {
let device = UPower.devices[i]
if (device.isLaptopBattery && device.ready) {
// UPower doesn't expose technology directly, but we can get it from the model
let model = device.model || ""
if (model.toLowerCase().includes("li-ion") || model.toLowerCase().includes("lithium")) {
return "Li-ion"
} else if (model.toLowerCase().includes("li-po") || model.toLowerCase().includes("polymer")) {
return "Li-polymer"
} else if (model.toLowerCase().includes("nimh")) {
return "NiMH"
}
}
}
return "Unknown"
}
property int cycleCount: 0 // UPower doesn't expose cycle count
property int batteryCapacity: debugMode ? debugBatteryCapacity : Math.round(battery.energyCapacity * 1000)
property var powerProfiles: availableProfiles
property string activePowerProfile: PowerProfile.toString(PowerProfiles.profile)
// Battery status checker
Process {
id: batteryStatusChecker
command: ["bash", "-c", "if [ -d /sys/class/power_supply/BAT0 ] || [ -d /sys/class/power_supply/BAT1 ]; then upower -i $(upower -e | grep 'BAT') | grep -E 'state|percentage|time to|energy|technology|cycle-count' || acpi -b 2>/dev/null || echo 'fallback'; else echo 'no-battery'; fi"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim() === "no-battery") {
root.batteryAvailable = false
return
}
if (text.trim() && text.trim() !== "fallback") {
parseBatteryInfo(text.trim())
} else {
// Fallback to simple methods
fallbackBatteryChecker.running = true
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Battery status check failed, trying fallback methods")
fallbackBatteryChecker.running = true
}
}
}
property var battery: UPower.displayDevice
// Fallback battery checker using /sys files
Process {
id: fallbackBatteryChecker
command: ["bash", "-c", "if [ -f /sys/class/power_supply/BAT0/capacity ]; then BAT=BAT0; elif [ -f /sys/class/power_supply/BAT1/capacity ]; then BAT=BAT1; else echo 'no-battery'; exit 1; fi; echo \"percentage: $(cat /sys/class/power_supply/$BAT/capacity)%\"; echo \"state: $(cat /sys/class/power_supply/$BAT/status 2>/dev/null || echo Unknown)\"; if [ -f /sys/class/power_supply/$BAT/technology ]; then echo \"technology: $(cat /sys/class/power_supply/$BAT/technology)\"; fi; if [ -f /sys/class/power_supply/$BAT/cycle_count ]; then echo \"cycle-count: $(cat /sys/class/power_supply/$BAT/cycle_count)\"; fi"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim() !== "no-battery") {
parseBatteryInfo(text.trim())
}
}
}
}
// Power profiles checker (for systems with power-profiles-daemon)
Process {
id: powerProfilesChecker
command: ["bash", "-c", "if command -v powerprofilesctl > /dev/null; then powerprofilesctl list 2>/dev/null; else echo 'not-available'; fi"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim() !== "not-available") {
parsePowerProfiles(text.trim())
}
}
}
}
function parseBatteryInfo(batteryText) {
let lines = batteryText.split('\n')
for (let line of lines) {
line = line.trim().toLowerCase()
if (line.includes('percentage:') || line.includes('capacity:')) {
let match = line.match(/(\d+)%?/)
if (match) {
root.batteryLevel = parseInt(match[1])
root.isLowBattery = root.batteryLevel <= 20
}
} else if (line.includes('state:') || line.includes('status:')) {
let statusPart = line.split(':')[1]?.trim().toLowerCase() || line
console.log("Raw battery status line:", line, "extracted status:", statusPart)
if (statusPart === 'charging') {
root.batteryStatus = "Charging"
root.isCharging = true
console.log("Battery is charging")
} else if (statusPart === 'discharging') {
root.batteryStatus = "Discharging"
root.isCharging = false
console.log("Battery is discharging")
} else if (statusPart === 'full') {
root.batteryStatus = "Full"
root.isCharging = false
console.log("Battery is full")
} else if (statusPart === 'not charging') {
root.batteryStatus = "Not charging"
root.isCharging = false
console.log("Battery is not charging")
} else {
root.batteryStatus = statusPart.charAt(0).toUpperCase() + statusPart.slice(1) || "Unknown"
root.isCharging = false
console.log("Battery status unknown:", statusPart)
}
} else if (line.includes('time to')) {
let match = line.match(/(\d+):(\d+)/)
if (match) {
root.timeRemaining = parseInt(match[1]) * 60 + parseInt(match[2])
}
} else if (line.includes('technology:')) {
let tech = line.split(':')[1]?.trim() || "Unknown"
root.batteryTechnology = tech.charAt(0).toUpperCase() + tech.slice(1)
} else if (line.includes('cycle-count:')) {
let match = line.match(/(\d+)/)
if (match) {
root.cycleCount = parseInt(match[1])
}
} else if (line.includes('energy-full:') || line.includes('capacity:')) {
let match = line.match(/([\d.]+)\s*wh/i)
if (match) {
root.batteryCapacity = Math.round(parseFloat(match[1]) * 1000) // Convert to mWh
}
}
}
console.log("Battery status updated:", root.batteryLevel + "%", root.batteryStatus)
}
function parsePowerProfiles(profileText) {
let lines = profileText.split('\n')
property var availableProfiles: {
let profiles = []
for (let line of lines) {
line = line.trim()
if (line.includes('*')) {
// Active profile
let profileName = line.replace('*', '').trim()
if (profileName.includes(':')) {
profileName = profileName.split(':')[0].trim()
}
root.activePowerProfile = profileName
profiles.push(profileName)
} else if (line && !line.includes(':') && line.length > 0) {
profiles.push(line)
if (PowerProfiles.profile !== undefined) {
profiles.push("power-saver")
profiles.push("balanced")
if (PowerProfiles.hasPerformanceProfile) {
profiles.push("performance")
}
}
root.powerProfiles = profiles
console.log("Power profiles available:", profiles, "Active:", root.activePowerProfile)
return profiles
}
// Timer to simulate battery changes in debug mode
Timer {
id: debugTimer
interval: 5000 // Update every 5 seconds
running: debugMode
repeat: true
onTriggered: {
// Simulate battery discharge/charge
if (debugIsCharging) {
debugBatteryLevel = Math.min(100, debugBatteryLevel + 1)
if (debugBatteryLevel >= 100) {
debugBatteryStatus = "Full"
debugIsCharging = false
}
} else {
debugBatteryLevel = Math.max(0, debugBatteryLevel - 1)
if (debugBatteryLevel <= 15) {
debugBatteryStatus = "Charging"
debugIsCharging = true
}
}
// Update time remaining
debugTimeRemaining = debugIsCharging ?
Math.max(0, debugTimeRemaining - 300) : // 5 minutes less to full
Math.max(0, debugTimeRemaining - 300) // 5 minutes less remaining
}
}
function setBatteryProfile(profileName) {
if (!root.powerProfiles.includes(profileName)) {
let profile = PowerProfile.Balanced
if (profileName === "power-saver") {
profile = PowerProfile.PowerSaver
} else if (profileName === "balanced") {
profile = PowerProfile.Balanced
} else if (profileName === "performance") {
if (PowerProfiles.hasPerformanceProfile) {
profile = PowerProfile.Performance
} else {
console.warn("Performance profile not available")
return
}
} else {
console.warn("Invalid power profile:", profileName)
return
}
console.log("Setting power profile to:", profileName)
let profileProcess = Qt.createQmlObject(`
import Quickshell.Io
Process {
command: ["powerprofilesctl", "set", "${profileName}"]
running: true
onExited: (exitCode) => {
if (exitCode === 0) {
console.log("Power profile changed to:", "${profileName}")
root.activePowerProfile = "${profileName}"
} else {
console.warn("Failed to change power profile")
}
}
}
`, root)
PowerProfiles.profile = profile
}
function getBatteryIcon() {
@@ -237,8 +144,8 @@ Singleton {
function formatTimeRemaining() {
if (root.timeRemaining <= 0) return "Unknown"
let hours = Math.floor(root.timeRemaining / 60)
let minutes = root.timeRemaining % 60
let hours = Math.floor(root.timeRemaining / 3600)
let minutes = Math.floor((root.timeRemaining % 3600) / 60)
if (hours > 0) {
return hours + "h " + minutes + "m"
@@ -246,17 +153,4 @@ Singleton {
return minutes + "m"
}
}
// Update timer
Timer {
interval: 30000
running: root.batteryAvailable
repeat: true
triggeredOnStart: true
onTriggered: {
batteryStatusChecker.running = true
powerProfilesChecker.running = true
}
}
}

View File

@@ -1,6 +1,6 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Bluetooth
pragma Singleton
pragma ComponentBehavior: Bound
@@ -9,410 +9,475 @@ Singleton {
property bool bluetoothEnabled: false
property bool bluetoothAvailable: false
property var bluetoothDevices: []
property var availableDevices: []
readonly property list<BluetoothDevice> bluetoothDevices: []
readonly property list<BluetoothDevice> availableDevices: []
property bool scanning: false
property bool discoverable: false
// Real Bluetooth Management
Process {
id: bluetoothStatusChecker
command: ["bluetoothctl", "show"] // Use default controller
running: true
property var connectingDevices: ({})
Component.onCompleted: {
refreshBluetoothState()
updateDevices()
}
Connections {
target: Bluetooth
stdout: StdioCollector {
onStreamFinished: {
root.bluetoothAvailable = text.trim() !== "" && !text.includes("No default controller")
root.bluetoothEnabled = text.includes("Powered: yes")
if (root.bluetoothEnabled && root.bluetoothAvailable) {
bluetoothDeviceScanner.running = true
function onDefaultAdapterChanged() {
console.log("BluetoothService: Default adapter changed")
refreshBluetoothState()
updateDevices()
}
}
Connections {
target: Bluetooth.defaultAdapter
function onEnabledChanged() {
refreshBluetoothState()
updateDevices()
}
function onDiscoveringChanged() {
refreshBluetoothState()
updateDevices()
}
}
Connections {
target: Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.devices : null
function onModelReset() {
updateDevices()
}
function onItemAdded() {
updateDevices()
}
function onItemRemoved() {
updateDevices()
}
}
Connections {
target: Bluetooth.devices
function onModelReset() {
updateDevices()
}
function onItemAdded() {
updateDevices()
}
function onItemRemoved() {
updateDevices()
}
}
function refreshBluetoothState() {
root.bluetoothAvailable = Bluetooth.defaultAdapter !== null
root.bluetoothEnabled = Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.enabled : false
root.scanning = Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.discovering : false
root.discoverable = Bluetooth.defaultAdapter ? Bluetooth.defaultAdapter.discoverable : false
}
function updateDevices() {
if (!Bluetooth.defaultAdapter) {
clearDeviceList(root.bluetoothDevices)
clearDeviceList(root.availableDevices)
root.bluetoothDevices = []
root.availableDevices = []
return
}
let newPairedDevices = []
let newAvailableDevices = []
let allNativeDevices = []
let adapterDevices = Bluetooth.defaultAdapter.devices
if (adapterDevices.values) {
allNativeDevices = allNativeDevices.concat(adapterDevices.values)
}
if (Bluetooth.devices.values) {
for (let device of Bluetooth.devices.values) {
if (!allNativeDevices.some(d => d.address === device.address)) {
allNativeDevices.push(device)
}
}
}
for (let device of allNativeDevices) {
if (!device) continue
let deviceType = getDeviceType(device.name || device.deviceName, device.icon)
let displayName = device.name || device.deviceName
if (!displayName || displayName.startsWith('/org/bluez') || displayName.includes('hci0') || displayName.length < 2) {
continue
}
if (device.paired) {
let existingDevice = findDeviceInList(root.bluetoothDevices, device.address)
if (existingDevice) {
updateDeviceData(existingDevice, device, deviceType, displayName)
newPairedDevices.push(existingDevice)
} else {
root.bluetoothDevices = []
let newDevice = createBluetoothDevice(device, deviceType, displayName)
newPairedDevices.push(newDevice)
}
}
}
}
Process {
id: bluetoothDeviceScanner
command: ["bash", "-c", "bluetoothctl devices | while read -r line; do if [[ $line =~ Device\\ ([0-9A-F:]+)\\ (.+) ]]; then mac=\"${BASH_REMATCH[1]}\"; name=\"${BASH_REMATCH[2]}\"; if [[ ! $name =~ ^/org/bluez ]]; then info=$(bluetoothctl info $mac); connected=$(echo \"$info\" | grep -m1 'Connected:' | awk '{print $2}'); battery=$(echo \"$info\" | grep -m1 'Battery Percentage:' | grep -o '[0-9]\\+'); echo \"$mac|$name|$connected|${battery:-}\"; fi; fi; done"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
let devices = []
let lines = text.trim().split('\n')
for (let line of lines) {
if (line.trim()) {
let parts = line.split('|')
if (parts.length >= 3) {
let mac = parts[0].trim()
let name = parts[1].trim()
let connected = parts[2].trim() === 'yes'
let battery = parts[3] ? parseInt(parts[3]) : -1
// Skip if name is still a technical path
if (name.startsWith('/org/bluez') || name.includes('hci0')) {
continue
}
// Determine device type from name
let type = "bluetooth"
let nameLower = name.toLowerCase()
if (nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || nameLower.includes("arctis")) type = "headset"
else if (nameLower.includes("mouse")) type = "mouse"
else if (nameLower.includes("keyboard")) type = "keyboard"
else if (nameLower.includes("phone") || nameLower.includes("iphone") || nameLower.includes("samsung")) type = "phone"
else if (nameLower.includes("watch")) type = "watch"
else if (nameLower.includes("speaker")) type = "speaker"
devices.push({
mac: mac,
name: name,
type: type,
connected: connected,
battery: battery
})
}
}
} else {
if (Bluetooth.defaultAdapter.discovering && isDeviceDiscoverable(device)) {
let existingDevice = findDeviceInList(root.availableDevices, device.address)
if (existingDevice) {
updateDeviceData(existingDevice, device, deviceType, displayName)
newAvailableDevices.push(existingDevice)
} else {
let newDevice = createBluetoothDevice(device, deviceType, displayName)
newAvailableDevices.push(newDevice)
}
root.bluetoothDevices = devices
}
}
}
cleanupOldDevices(root.bluetoothDevices, newPairedDevices)
cleanupOldDevices(root.availableDevices, newAvailableDevices)
console.log("BluetoothService: Found", newPairedDevices.length, "paired devices and", newAvailableDevices.length, "available devices")
root.bluetoothDevices = newPairedDevices
root.availableDevices = newAvailableDevices
}
function createBluetoothDevice(nativeDevice, deviceType, displayName) {
return deviceComponent.createObject(root, {
mac: nativeDevice.address,
name: displayName,
type: deviceType,
paired: nativeDevice.paired,
connected: nativeDevice.connected,
battery: nativeDevice.batteryAvailable ? Math.round(nativeDevice.battery * 100) : -1,
signalStrength: nativeDevice.connected ? "excellent" : "unknown",
canPair: !nativeDevice.paired,
nativeDevice: nativeDevice,
connecting: false,
connectionFailed: false
})
}
function updateDeviceData(deviceObj, nativeDevice, deviceType, displayName) {
deviceObj.name = displayName
deviceObj.type = deviceType
deviceObj.paired = nativeDevice.paired
// If device connected state changed, clear connecting/failed states and refresh audio
if (deviceObj.connected !== nativeDevice.connected) {
deviceObj.connecting = false
deviceObj.connectionFailed = false
// Refresh audio devices when bluetooth audio device connects/disconnects
if (deviceType === "headset" || deviceType === "speaker") {
Qt.callLater(() => {
if (typeof AudioService !== 'undefined') {
AudioService.updateDevices()
}
})
}
}
deviceObj.connected = nativeDevice.connected
deviceObj.battery = nativeDevice.batteryAvailable ? Math.round(nativeDevice.battery * 100) : -1
deviceObj.signalStrength = nativeDevice.connected ? "excellent" : "unknown"
deviceObj.canPair = !nativeDevice.paired
deviceObj.nativeDevice = nativeDevice
}
function findDeviceInList(deviceList, address) {
for (let device of deviceList) {
if (device.mac === address) {
return device
}
}
return null
}
function cleanupOldDevices(oldList, newList) {
for (let oldDevice of oldList) {
if (!newList.includes(oldDevice)) {
oldDevice.destroy()
}
}
}
function scanDevices() {
if (root.bluetoothEnabled && root.bluetoothAvailable) {
bluetoothDeviceScanner.running = true
function clearDeviceList(deviceList) {
for (let device of deviceList) {
device.destroy()
}
}
function isDeviceDiscoverable(device) {
let displayName = device.name || device.deviceName
if (!displayName || displayName.length < 2) return false
if (displayName.startsWith('/org/bluez') || displayName.includes('hci0')) return false
let nameLower = displayName.toLowerCase()
if (nameLower.match(/^[0-9a-f]{2}[:-][0-9a-f]{2}[:-][0-9a-f]{2}/)) {
return false
}
if (displayName.length < 3) return false
if (nameLower.includes('iphone') || nameLower.includes('ipad') ||
nameLower.includes('airpods') || nameLower.includes('samsung') ||
nameLower.includes('galaxy') || nameLower.includes('pixel') ||
nameLower.includes('headphone') || nameLower.includes('speaker') ||
nameLower.includes('mouse') || nameLower.includes('keyboard') ||
nameLower.includes('watch') || nameLower.includes('buds') ||
nameLower.includes('android')) {
return true
}
return displayName.length >= 4 && !displayName.match(/^[A-Z0-9_-]+$/)
}
function getDeviceType(name, icon) {
if (!name && !icon) return "bluetooth"
let nameLower = (name || "").toLowerCase()
let iconLower = (icon || "").toLowerCase()
if (iconLower.includes("audio") || iconLower.includes("headset") || iconLower.includes("headphone") ||
nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") ||
nameLower.includes("arctis") || nameLower.includes("audio")) return "headset"
else if (iconLower.includes("input-mouse") || nameLower.includes("mouse")) return "mouse"
else if (iconLower.includes("input-keyboard") || nameLower.includes("keyboard")) return "keyboard"
else if (iconLower.includes("phone") || nameLower.includes("phone") || nameLower.includes("iphone") ||
nameLower.includes("samsung") || nameLower.includes("android")) return "phone"
else if (iconLower.includes("watch") || nameLower.includes("watch")) return "watch"
else if (iconLower.includes("audio-speakers") || nameLower.includes("speaker")) return "speaker"
else if (iconLower.includes("video-display") || nameLower.includes("tv") || nameLower.includes("display")) return "tv"
return "bluetooth"
}
function startDiscovery() {
root.scanning = true
// Run comprehensive scan that gets all devices
discoveryScanner.running = true
if (Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.enabled) {
Bluetooth.defaultAdapter.discovering = true
updateDevices()
}
}
function stopDiscovery() {
let stopDiscoveryProcess = Qt.createQmlObject('
import Quickshell.Io
Process {
command: ["bluetoothctl", "scan", "off"]
running: true
onExited: {
root.scanning = false
}
}
', root)
if (Bluetooth.defaultAdapter) {
Bluetooth.defaultAdapter.discovering = false
updateDevices()
}
}
function pairDevice(mac) {
console.log("Pairing device:", mac)
let pairProcess = Qt.createQmlObject('
import Quickshell.Io
Process {
command: ["bluetoothctl", "pair", "' + mac + '"]
running: true
onExited: (exitCode) => {
if (exitCode === 0) {
console.log("Pairing successful")
connectDevice("' + mac + '")
} else {
console.warn("Pairing failed with exit code:", exitCode)
}
availableDeviceScanner.running = true
bluetoothDeviceScanner.running = true
}
}
', root)
let device = findDeviceByMac(mac)
if (device) {
device.pair()
}
}
function connectDevice(mac) {
console.log("Connecting to device:", mac)
let connectProcess = Qt.createQmlObject('
import Quickshell.Io
Process {
command: ["bluetoothctl", "connect", "' + mac + '"]
running: true
onExited: (exitCode) => {
if (exitCode === 0) {
console.log("Connection successful")
} else {
console.warn("Connection failed with exit code:", exitCode)
}
bluetoothDeviceScanner.running = true
}
}
', root)
let device = findDeviceByMac(mac)
if (device) {
device.connect()
}
}
function removeDevice(mac) {
console.log("Removing device:", mac)
let removeProcess = Qt.createQmlObject('
import Quickshell.Io
Process {
command: ["bluetoothctl", "remove", "' + mac + '"]
running: true
onExited: {
bluetoothDeviceScanner.running = true
availableDeviceScanner.running = true
}
}
', root)
let device = findDeviceByMac(mac)
if (device) {
device.forget()
}
}
function toggleBluetoothDevice(mac) {
let device = root.bluetoothDevices.find(d => d.mac === mac)
if (device) {
let action = device.connected ? "disconnect" : "connect"
let toggleProcess = Qt.createQmlObject('
import Quickshell.Io
Process {
command: ["bluetoothctl", "' + action + '", "' + mac + '"]
running: true
onExited: bluetoothDeviceScanner.running = true
}
', root)
let typedDevice = findDeviceInList(root.bluetoothDevices, mac)
if (!typedDevice) {
typedDevice = findDeviceInList(root.availableDevices, mac)
}
if (typedDevice && typedDevice.nativeDevice) {
if (typedDevice.connected) {
console.log("Disconnecting device:", mac)
typedDevice.connecting = false
typedDevice.connectionFailed = false
typedDevice.nativeDevice.connected = false
} else {
console.log("Connecting to device:", mac)
typedDevice.connecting = true
typedDevice.connectionFailed = false
// Set a timeout to handle connection failure
Qt.callLater(() => {
connectionTimeout.deviceMac = mac
connectionTimeout.start()
})
typedDevice.nativeDevice.connected = true
}
}
}
function toggleBluetooth() {
let action = root.bluetoothEnabled ? "off" : "on"
let toggleProcess = Qt.createQmlObject('
import Quickshell.Io
Process {
command: ["bluetoothctl", "power", "' + action + '"]
running: true
onExited: bluetoothStatusChecker.running = true
}
', root)
if (Bluetooth.defaultAdapter) {
Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter.enabled
}
}
function findDeviceByMac(mac) {
let typedDevice = findDeviceInList(root.bluetoothDevices, mac)
if (typedDevice && typedDevice.nativeDevice) {
return typedDevice.nativeDevice
}
typedDevice = findDeviceInList(root.availableDevices, mac)
if (typedDevice && typedDevice.nativeDevice) {
return typedDevice.nativeDevice
}
if (Bluetooth.defaultAdapter) {
let adapterDevices = Bluetooth.defaultAdapter.devices
if (adapterDevices.values) {
for (let device of adapterDevices.values) {
if (device && device.address === mac) {
return device
}
}
}
}
if (Bluetooth.devices.values) {
for (let device of Bluetooth.devices.values) {
if (device && device.address === mac) {
return device
}
}
}
return null
}
Timer {
id: bluetoothMonitorTimer
interval: 5000
running: false; repeat: true
interval: 2000
running: false
repeat: true
onTriggered: {
bluetoothStatusChecker.running = true
if (root.bluetoothEnabled) {
bluetoothDeviceScanner.running = true
// Also refresh paired devices to get current connection status
pairedDeviceChecker.discoveredToMerge = []
pairedDeviceChecker.running = true
}
updateDevices()
}
}
function enableMonitoring(enabled) {
bluetoothMonitorTimer.running = enabled
if (enabled) {
// Immediately update when enabled
bluetoothStatusChecker.running = true
refreshBluetoothState()
updateDevices()
}
}
property var discoveredDevices: []
// Handle discovered devices
function _handleDiscovered(found) {
let discoveredDevices = []
for (let device of found) {
let type = "bluetooth"
let nameLower = device.name.toLowerCase()
if (nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || nameLower.includes("arctis") || nameLower.includes("audio")) type = "headset"
else if (nameLower.includes("mouse")) type = "mouse"
else if (nameLower.includes("keyboard")) type = "keyboard"
else if (nameLower.includes("phone") || nameLower.includes("iphone") || nameLower.includes("samsung") || nameLower.includes("android")) type = "phone"
else if (nameLower.includes("watch")) type = "watch"
else if (nameLower.includes("speaker")) type = "speaker"
else if (nameLower.includes("tv") || nameLower.includes("display")) type = "tv"
discoveredDevices.push({
mac: device.mac,
name: device.name,
type: type,
paired: false,
connected: false,
rssi: -70,
signalStrength: "fair",
canPair: true
})
console.log(" -", device.name, "(", device.mac, ")")
Timer {
id: bluetoothStateRefreshTimer
interval: 5000
running: true
repeat: true
onTriggered: {
refreshBluetoothState()
}
// Get paired devices first, then merge with discovered
pairedDeviceChecker.discoveredToMerge = discoveredDevices
pairedDeviceChecker.running = true
}
// Get only currently connected/paired devices that matter
Process {
id: availableDeviceScanner
command: ["bash", "-c", "bluetoothctl devices | while read -r line; do if [[ $line =~ Device\\ ([A-F0-9:]+)\\ (.+) ]]; then mac=\"${BASH_REMATCH[1]}\"; name=\"${BASH_REMATCH[2]}\"; info=$(bluetoothctl info \"$mac\" 2>/dev/null); paired=$(echo \"$info\" | grep -m1 'Paired:' | awk '{print $2}'); connected=$(echo \"$info\" | grep -m1 'Connected:' | awk '{print $2}'); if [[ \"$paired\" == \"yes\" ]] || [[ \"$connected\" == \"yes\" ]]; then echo \"$mac|$name|$paired|$connected\"; fi; fi; done"]
Timer {
id: connectionTimeout
interval: 10000 // 10 second timeout
running: false
repeat: false
stdout: StdioCollector {
onStreamFinished: {
let devices = []
if (text.trim()) {
let lines = text.trim().split('\n')
for (let line of lines) {
if (line.trim()) {
let parts = line.split('|')
if (parts.length >= 4) {
let mac = parts[0].trim()
let name = parts[1].trim()
let paired = parts[2].trim() === 'yes'
let connected = parts[3].trim() === 'yes'
// Skip technical names
if (name.startsWith('/org/bluez') || name.includes('hci0') || name.length < 3) {
continue
}
// Determine device type
let type = "bluetooth"
let nameLower = name.toLowerCase()
if (nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || nameLower.includes("arctis") || nameLower.includes("audio")) type = "headset"
else if (nameLower.includes("mouse")) type = "mouse"
else if (nameLower.includes("keyboard")) type = "keyboard"
else if (nameLower.includes("phone") || nameLower.includes("iphone") || nameLower.includes("samsung") || nameLower.includes("android")) type = "phone"
else if (nameLower.includes("watch")) type = "watch"
else if (nameLower.includes("speaker")) type = "speaker"
else if (nameLower.includes("tv") || nameLower.includes("display")) type = "tv"
devices.push({
mac: mac,
name: name,
type: type,
paired: paired,
connected: connected,
rssi: 0,
signalStrength: "unknown",
canPair: false // Already paired
})
}
}
}
property string deviceMac: ""
onTriggered: {
if (deviceMac) {
let typedDevice = findDeviceInList(root.bluetoothDevices, deviceMac)
if (!typedDevice) {
typedDevice = findDeviceInList(root.availableDevices, deviceMac)
}
root.availableDevices = devices
}
}
}
// Discovery scanner using bluetoothctl --timeout
Process {
id: discoveryScanner
// Discover for 8 s in non-interactive mode, then auto-exit
command: ["bluetoothctl",
"--timeout", "8",
"--monitor", // keeps stdout unbuffered
"scan", "on"]
running: false
stdout: StdioCollector {
onStreamFinished: {
/*
* bluetoothctl prints lines like:
* [NEW] Device 12:34:56:78:9A:BC My-Headphones
*/
const rx = /^\[NEW\] Device ([0-9A-F:]+)\s+(.+)$/i;
const found = text.split('\n')
.filter(l => rx.test(l))
.map(l => {
const [,mac,name] = l.match(rx);
return { mac, name };
});
root._handleDiscovered(found);
}
}
onExited: {
root.scanning = false
}
}
// Get paired devices and merge with discovered ones
Process {
id: pairedDeviceChecker
command: ["bash", "-c", "bluetoothctl devices | while read -r line; do if [[ $line =~ Device\\ ([A-F0-9:]+)\\ (.+) ]]; then mac=\"${BASH_REMATCH[1]}\"; name=\"${BASH_REMATCH[2]}\"; if [[ ${#name} -gt 3 ]] && [[ ! $name =~ ^/org/bluez ]] && [[ ! $name =~ hci0 ]]; then info=$(bluetoothctl info \"$mac\" 2>/dev/null); paired=$(echo \"$info\" | grep -m1 'Paired:' | awk '{print $2}'); connected=$(echo \"$info\" | grep -m1 'Connected:' | awk '{print $2}'); echo \"$mac|$name|$paired|$connected\"; fi; fi; done"]
running: false
property var discoveredToMerge: []
stdout: StdioCollector {
onStreamFinished: {
// Start with discovered devices (unpaired, available to pair)
let allDevices = [...pairedDeviceChecker.discoveredToMerge]
let seenMacs = new Set(allDevices.map(d => d.mac))
// Add only actually paired devices from bluetoothctl
if (text.trim()) {
let lines = text.trim().split('\n')
if (typedDevice && typedDevice.connecting && !typedDevice.connected) {
console.log("Connection timeout for device:", deviceMac)
typedDevice.connecting = false
typedDevice.connectionFailed = true
for (let line of lines) {
if (line.trim()) {
let parts = line.split('|')
if (parts.length >= 4) {
let mac = parts[0].trim()
let name = parts[1].trim()
let paired = parts[2].trim() === 'yes'
let connected = parts[3].trim() === 'yes'
// Only include if actually paired
if (!paired) continue
// Check if already in discovered list
if (seenMacs.has(mac)) {
// Update existing device to show it's paired
let existing = allDevices.find(d => d.mac === mac)
if (existing) {
existing.paired = true
existing.connected = connected
existing.canPair = false
}
continue
}
// Add paired device not found during scan
let type = "bluetooth"
let nameLower = name.toLowerCase()
if (nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || nameLower.includes("arctis") || nameLower.includes("audio")) type = "headset"
else if (nameLower.includes("mouse")) type = "mouse"
else if (nameLower.includes("keyboard")) type = "keyboard"
else if (nameLower.includes("phone") || nameLower.includes("iphone") || nameLower.includes("samsung") || nameLower.includes("android")) type = "phone"
else if (nameLower.includes("watch")) type = "watch"
else if (nameLower.includes("speaker")) type = "speaker"
else if (nameLower.includes("tv") || nameLower.includes("display")) type = "tv"
allDevices.push({
mac: mac,
name: name,
type: type,
paired: true,
connected: connected,
rssi: -100,
signalStrength: "unknown",
canPair: false
})
}
}
}
// Clear failure state after 3 seconds
Qt.callLater(() => {
clearFailureTimer.deviceMac = deviceMac
clearFailureTimer.start()
})
}
deviceMac = ""
}
}
}
Timer {
id: clearFailureTimer
interval: 3000
running: false
repeat: false
property string deviceMac: ""
onTriggered: {
if (deviceMac) {
let typedDevice = findDeviceInList(root.bluetoothDevices, deviceMac)
if (!typedDevice) {
typedDevice = findDeviceInList(root.availableDevices, deviceMac)
}
root.availableDevices = allDevices
root.scanning = false
if (typedDevice) {
typedDevice.connectionFailed = false
}
deviceMac = ""
}
}
}
component BluetoothDevice: QtObject {
required property string mac
required property string name
required property string type
required property bool paired
required property bool connected
required property int battery
required property string signalStrength
required property bool canPair
required property var nativeDevice // Reference to native Quickshell device
property bool connecting: false
property bool connectionFailed: false
readonly property string displayName: name
readonly property bool batteryAvailable: battery >= 0
readonly property string connectionStatus: {
if (connecting) return "Connecting..."
if (connectionFailed) return "Connection Failed"
if (connected) return "Connected"
return "Disconnected"
}
}
Component {
id: deviceComponent
BluetoothDevice {}
}
}

View File

@@ -7,101 +7,160 @@ pragma ComponentBehavior: Bound
Singleton {
id: root
property int brightnessLevel: 75
property list<var> ddcMonitors: []
readonly property list<Monitor> monitors: variants.instances
property bool brightnessAvailable: false
property int brightnessLevel: 75
// Check if brightness control is available
Process {
id: brightnessAvailabilityChecker
command: ["bash", "-c", "if command -v brightnessctl > /dev/null; then echo 'brightnessctl'; elif command -v xbacklight > /dev/null; then echo 'xbacklight'; else echo 'none'; fi"]
running: true
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
if (data.trim()) {
let method = data.trim()
if (method === "brightnessctl" || method === "xbacklight") {
root.brightnessAvailable = true
brightnessChecker.running = true
} else {
root.brightnessAvailable = false
console.log("Brightness control not available - no brightnessctl or xbacklight found")
}
}
}
}
function getMonitorForScreen(screen: ShellScreen): var {
return monitors.find(function(m) { return m.modelData === screen; });
}
// Brightness Control
Process {
id: brightnessChecker
command: ["bash", "-c", "if command -v brightnessctl > /dev/null; then brightnessctl get; elif command -v xbacklight > /dev/null; then xbacklight -get | cut -d. -f1; else echo 75; fi"]
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
if (data.trim()) {
let brightness = parseInt(data.trim()) || 75
// brightnessctl returns absolute value, need to convert to percentage
if (brightness > 100) {
brightnessMaxChecker.running = true
} else {
root.brightnessLevel = brightness
}
}
}
}
}
Process {
id: brightnessMaxChecker
command: ["brightnessctl", "max"]
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
if (data.trim()) {
let maxBrightness = parseInt(data.trim()) || 100
brightnessCurrentChecker.property("maxBrightness", maxBrightness)
brightnessCurrentChecker.running = true
}
}
}
}
Process {
id: brightnessCurrentChecker
property int maxBrightness: 100
command: ["brightnessctl", "get"]
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
if (data.trim()) {
let currentBrightness = parseInt(data.trim()) || 75
root.brightnessLevel = Math.round((currentBrightness / maxBrightness) * 100)
}
property var debounceTimer: Timer {
id: debounceTimer
interval: 50
repeat: false
property int pendingValue: 0
onTriggered: {
const focusedMonitor = monitors.find(function(m) { return m.modelData === Quickshell.screens[0]; });
if (focusedMonitor) {
focusedMonitor.setBrightness(pendingValue / 100);
}
}
}
function setBrightness(percentage) {
if (!root.brightnessAvailable) {
console.warn("Brightness control not available")
return
root.brightnessLevel = percentage;
debounceTimer.pendingValue = percentage;
debounceTimer.restart();
}
function increaseBrightness(): void {
const focusedMonitor = monitors.find(function(m) { return m.modelData === Quickshell.screens[0]; });
if (focusedMonitor)
focusedMonitor.setBrightness(focusedMonitor.brightness + 0.1);
}
function decreaseBrightness(): void {
const focusedMonitor = monitors.find(function(m) { return m.modelData === Quickshell.screens[0]; });
if (focusedMonitor)
focusedMonitor.setBrightness(focusedMonitor.brightness - 0.1);
}
onMonitorsChanged: {
ddcMonitors = [];
if (ddcAvailable) {
ddcProc.running = true;
}
let brightnessSetProcess = Qt.createQmlObject('
import Quickshell.Io
Process {
command: ["bash", "-c", "if command -v brightnessctl > /dev/null; then brightnessctl set ' + percentage + '%; elif command -v xbacklight > /dev/null; then xbacklight -set ' + percentage + '; fi"]
running: true
onExited: brightnessChecker.running = true
// Update brightness level from first monitor
if (monitors.length > 0) {
root.brightnessLevel = Math.round(monitors[0].brightness * 100);
}
}
Component.onCompleted: {
ddcAvailabilityChecker.running = true;
}
Variants {
id: variants
model: Quickshell.screens
Monitor {}
}
Process {
id: ddcAvailabilityChecker
command: ["which", "ddcutil"]
onExited: function(exitCode) {
root.brightnessAvailable = (exitCode === 0);
if (root.brightnessAvailable) {
ddcProc.running = true;
}
', root)
}
}
Process {
id: ddcProc
command: ["ddcutil", "detect", "--brief"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.ddcMonitors = text.trim().split("\n\n").filter(function(d) { return d.startsWith("Display "); }).map(function(d) { return ({
model: d.match(/Monitor:.*:(.*):.*/)?.[1] || "Unknown",
busNum: d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/)?.[1] || "0"
}); });
} else {
root.ddcMonitors = [];
}
}
}
onExited: function(exitCode) {
if (exitCode !== 0) {
root.ddcMonitors = [];
}
}
}
component Monitor: QtObject {
id: monitor
required property ShellScreen modelData
readonly property bool isDdc: root.ddcMonitors.some(function(m) { return m.model === modelData.model; })
readonly property string busNum: root.ddcMonitors.find(function(m) { return m.model === modelData.model; })?.busNum ?? ""
property real brightness: 0.75
readonly property Process initProc: Process {
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
const parts = text.trim().split(" ");
if (parts.length >= 5) {
const current = parseInt(parts[3]) || 75;
const max = parseInt(parts[4]) || 100;
monitor.brightness = current / max;
root.brightnessLevel = Math.round(monitor.brightness * 100);
}
}
}
}
onExited: function(exitCode) {
if (exitCode !== 0) {
monitor.brightness = 0.75;
root.brightnessLevel = 75;
}
}
}
function setBrightness(value: real): void {
value = Math.max(0, Math.min(1, value));
const rounded = Math.round(value * 100);
if (Math.round(brightness * 100) === rounded)
return;
brightness = value;
root.brightnessLevel = rounded;
if (isDdc && busNum) {
Quickshell.execDetached(["ddcutil", "-b", busNum, "setvcp", "10", rounded.toString()]);
}
}
onBusNumChanged: {
if (isDdc && busNum) {
initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"];
initProc.running = true;
}
}
Component.onCompleted: {
Qt.callLater(function() {
if (isDdc && busNum) {
initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"];
initProc.running = true;
}
});
}
}
}

View File

@@ -7,15 +7,12 @@ pragma ComponentBehavior: Bound
Singleton {
id: root
// Process list properties
property var processes: []
property bool isUpdating: false
property int processUpdateInterval: 1500
property int processUpdateInterval: 3000
// Performance control - only run when process monitor is actually visible
property bool monitoringEnabled: false
// System information properties
property int totalMemoryKB: 0
property int usedMemoryKB: 0
property int totalSwapKB: 0
@@ -24,8 +21,23 @@ Singleton {
property real totalCpuUsage: 0.0
property bool systemInfoAvailable: false
// Sorting options
property string sortBy: "cpu" // "cpu", "memory", "name", "pid"
property var cpuHistory: []
property var memoryHistory: []
property var networkHistory: ({rx: [], tx: []})
property var diskHistory: ({read: [], write: []})
property int historySize: 60
property var perCoreCpuUsage: []
property real networkRxRate: 0
property real networkTxRate: 0
property var lastNetworkStats: null
property real diskReadRate: 0
property real diskWriteRate: 0
property var lastDiskStats: null
property string sortBy: "cpu"
property bool sortDescending: true
property int maxProcesses: 20
@@ -35,10 +47,32 @@ Singleton {
console.log("ProcessMonitorService: Initialization complete")
}
// System information monitoring
Timer {
id: testTimer
interval: 3000
running: false
repeat: false
onTriggered: {
console.log("ProcessMonitorService: Starting test monitoring...")
enableMonitoring(true)
stopTestTimer.start()
}
}
Timer {
id: stopTestTimer
interval: 8000
running: false
repeat: false
onTriggered: {
console.log("ProcessMonitorService: Stopping test monitoring...")
enableMonitoring(false)
}
}
Process {
id: systemInfoProcess
command: ["bash", "-c", "cat /proc/meminfo; echo '---CPU---'; nproc; echo '---CPUSTAT---'; grep '^cpu ' /proc/stat"]
command: ["bash", "-c", "cat /proc/meminfo; echo '---CPU---'; nproc; echo '---CPUSTAT---'; grep '^cpu' /proc/stat | head -" + (root.cpuCount + 1)]
running: false
stdout: StdioCollector {
@@ -57,7 +91,34 @@ Singleton {
}
}
// Process monitoring with ps command
Process {
id: networkStatsProcess
command: ["bash", "-c", "cat /proc/net/dev | grep -E '(wlan|eth|enp|wlp|ens|eno)' | awk '{print $1,$2,$10}' | sed 's/:/ /'"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
parseNetworkStats(text.trim())
}
}
}
}
Process {
id: diskStatsProcess
command: ["bash", "-c", "cat /proc/diskstats | grep -E ' (sd[a-z]+|nvme[0-9]+n[0-9]+|vd[a-z]+) ' | grep -v 'p[0-9]' | awk '{print $3,$6,$10}'"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
parseDiskStats(text.trim())
}
}
}
}
Process {
id: processListProcess
command: ["bash", "-c", "ps axo pid,ppid,pcpu,pmem,rss,comm,cmd --sort=-pcpu | head -" + (root.maxProcesses + 1)]
@@ -69,12 +130,10 @@ Singleton {
const lines = text.trim().split('\n')
const newProcesses = []
// Skip header line
for (let i = 1; i < lines.length; i++) {
const line = lines[i].trim()
if (!line) continue
// Parse ps output: PID PPID %CPU %MEM RSS COMMAND CMD
const parts = line.split(/\s+/)
if (parts.length >= 7) {
const pid = parseInt(parts[0])
@@ -112,36 +171,52 @@ Singleton {
}
}
// System and process monitoring timer - now conditional
Timer {
id: processTimer
interval: root.processUpdateInterval
running: root.monitoringEnabled // Only run when monitoring is enabled
running: root.monitoringEnabled
repeat: true
onTriggered: {
if (root.monitoringEnabled) {
updateSystemInfo()
updateProcessList()
updateNetworkStats()
updateDiskStats()
}
}
}
// Public functions
function updateSystemInfo() {
if (!systemInfoProcess.running && root.monitoringEnabled) {
systemInfoProcess.running = true
}
}
// Control functions for enabling/disabling monitoring
function enableMonitoring(enabled) {
console.log("ProcessMonitorService: Monitoring", enabled ? "enabled" : "disabled")
root.monitoringEnabled = enabled
if (enabled) {
// Immediately update when enabled
root.cpuHistory = []
root.memoryHistory = []
root.networkHistory = ({rx: [], tx: []})
root.diskHistory = ({read: [], write: []})
updateSystemInfo()
updateProcessList()
updateNetworkStats()
updateDiskStats()
}
}
function updateNetworkStats() {
if (!networkStatsProcess.running && root.monitoringEnabled) {
networkStatsProcess.running = true
}
}
function updateDiskStats() {
if (!diskStatsProcess.running && root.monitoringEnabled) {
diskStatsProcess.running = true
}
}
@@ -149,7 +224,6 @@ Singleton {
if (!root.isUpdating && root.monitoringEnabled) {
root.isUpdating = true
// Update sort command based on current sort option
let sortOption = ""
switch (root.sortBy) {
case "cpu":
@@ -208,7 +282,6 @@ Singleton {
}
function getProcessIcon(command) {
// Return appropriate Material Design icon for common processes
const cmd = command.toLowerCase()
if (cmd.includes("firefox") || cmd.includes("chrome") || cmd.includes("browser")) return "web"
if (cmd.includes("code") || cmd.includes("editor") || cmd.includes("vim")) return "code"
@@ -216,7 +289,7 @@ Singleton {
if (cmd.includes("music") || cmd.includes("audio") || cmd.includes("spotify")) return "music_note"
if (cmd.includes("video") || cmd.includes("vlc") || cmd.includes("mpv")) return "play_circle"
if (cmd.includes("systemd") || cmd.includes("kernel") || cmd.includes("kthread")) return "settings"
return "memory" // Default process icon
return "memory"
}
function formatCpuUsage(cpu) {
@@ -244,6 +317,10 @@ Singleton {
function parseSystemInfo(text) {
const lines = text.split('\n')
let section = 'memory'
const coreUsages = []
let memFree = 0
let memBuffers = 0
let memCached = 0
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim()
@@ -259,9 +336,12 @@ Singleton {
if (section === 'memory') {
if (line.startsWith('MemTotal:')) {
root.totalMemoryKB = parseInt(line.split(/\s+/)[1])
} else if (line.startsWith('MemAvailable:')) {
const availableKB = parseInt(line.split(/\s+/)[1])
root.usedMemoryKB = root.totalMemoryKB - availableKB
} else if (line.startsWith('MemFree:')) {
memFree = parseInt(line.split(/\s+/)[1])
} else if (line.startsWith('Buffers:')) {
memBuffers = parseInt(line.split(/\s+/)[1])
} else if (line.startsWith('Cached:')) {
memCached = parseInt(line.split(/\s+/)[1])
} else if (line.startsWith('SwapTotal:')) {
root.totalSwapKB = parseInt(line.split(/\s+/)[1])
} else if (line.startsWith('SwapFree:')) {
@@ -289,10 +369,104 @@ Singleton {
const used = total - idle - iowait
root.totalCpuUsage = total > 0 ? (used / total) * 100 : 0
}
} else if (line.match(/^cpu\d+/)) {
const parts = line.split(/\s+/)
if (parts.length >= 8) {
const user = parseInt(parts[1])
const nice = parseInt(parts[2])
const system = parseInt(parts[3])
const idle = parseInt(parts[4])
const iowait = parseInt(parts[5])
const irq = parseInt(parts[6])
const softirq = parseInt(parts[7])
const total = user + nice + system + idle + iowait + irq + softirq
const used = total - idle - iowait
const usage = total > 0 ? (used / total) * 100 : 0
coreUsages.push(usage)
}
}
}
}
// Calculate used memory as total minus free minus buffers minus cached
root.usedMemoryKB = root.totalMemoryKB - memFree - memBuffers - memCached
// Update per-core usage
root.perCoreCpuUsage = coreUsages
// Update history
addToHistory(root.cpuHistory, root.totalCpuUsage)
const memoryPercent = root.totalMemoryKB > 0 ? (root.usedMemoryKB / root.totalMemoryKB) * 100 : 0
addToHistory(root.memoryHistory, memoryPercent)
// console.log("ProcessMonitorService: Updated - CPU:", root.totalCpuUsage.toFixed(1) + "%", "Memory:", memoryPercent.toFixed(1) + "%", "History length:", root.cpuHistory.length)
root.systemInfoAvailable = true
}
function parseNetworkStats(text) {
const lines = text.split('\n')
let totalRx = 0
let totalTx = 0
for (const line of lines) {
const parts = line.trim().split(/\s+/)
if (parts.length >= 3) {
const rx = parseInt(parts[1])
const tx = parseInt(parts[2])
if (!isNaN(rx) && !isNaN(tx)) {
totalRx += rx
totalTx += tx
}
}
}
if (root.lastNetworkStats) {
const timeDiff = root.processUpdateInterval / 1000
root.networkRxRate = Math.max(0, (totalRx - root.lastNetworkStats.rx) / timeDiff)
root.networkTxRate = Math.max(0, (totalTx - root.lastNetworkStats.tx) / timeDiff)
addToHistory(root.networkHistory.rx, root.networkRxRate / 1024)
addToHistory(root.networkHistory.tx, root.networkTxRate / 1024)
}
root.lastNetworkStats = { rx: totalRx, tx: totalTx }
}
function parseDiskStats(text) {
const lines = text.split('\n')
let totalRead = 0
let totalWrite = 0
for (const line of lines) {
const parts = line.trim().split(/\s+/)
if (parts.length >= 3) {
const readSectors = parseInt(parts[1])
const writeSectors = parseInt(parts[2])
if (!isNaN(readSectors) && !isNaN(writeSectors)) {
totalRead += readSectors * 512
totalWrite += writeSectors * 512
}
}
}
if (root.lastDiskStats) {
const timeDiff = root.processUpdateInterval / 1000
root.diskReadRate = Math.max(0, (totalRead - root.lastDiskStats.read) / timeDiff)
root.diskWriteRate = Math.max(0, (totalWrite - root.lastDiskStats.write) / timeDiff)
addToHistory(root.diskHistory.read, root.diskReadRate / (1024 * 1024))
addToHistory(root.diskHistory.write, root.diskWriteRate / (1024 * 1024))
}
root.lastDiskStats = { read: totalRead, write: totalWrite }
}
function addToHistory(array, value) {
array.push(value)
if (array.length > root.historySize) {
array.shift()
}
}
}

View File

@@ -7,16 +7,13 @@ pragma ComponentBehavior: Bound
Singleton {
id: root
// CPU properties
property real cpuUsage: 0.0
property int cpuCores: 1
property string cpuModel: ""
property real cpuFrequency: 0.0
// Previous CPU stats for accurate calculation
property var prevCpuStats: [0, 0, 0, 0, 0, 0, 0, 0]
// Memory properties
property real memoryUsage: 0.0
property real totalMemory: 0.0
property real usedMemory: 0.0
@@ -25,14 +22,28 @@ Singleton {
property real bufferMemory: 0.0
property real cacheMemory: 0.0
// Temperature properties
property real cpuTemperature: 0.0
property string kernelVersion: ""
property string distribution: ""
property string hostname: ""
property string uptime: ""
property string scheduler: ""
property string architecture: ""
property string loadAverage: ""
property int processCount: 0
property int threadCount: 0
property string bootTime: ""
property string motherboard: ""
property string biosVersion: ""
property var diskMounts: []
property string diskUsage: ""
property int cpuUpdateInterval: 3000
property int memoryUpdateInterval: 5000
property int temperatureUpdateInterval: 10000
property int systemInfoUpdateInterval: 30000
// Performance control
property bool enabledForTopBar: true
property bool enabledForDetailedView: false
@@ -40,10 +51,10 @@ Singleton {
console.log("SystemMonitorService: Starting initialization...")
getCpuInfo()
updateSystemStats()
updateSystemInfo()
console.log("SystemMonitorService: Initialization complete")
}
// Get CPU information (static)
Process {
id: cpuInfoProcess
command: ["bash", "-c", "lscpu | grep -E 'Model name|CPU\\(s\\):' | head -2"]
@@ -69,7 +80,6 @@ Singleton {
}
}
// CPU usage monitoring with accurate calculation
Process {
id: cpuUsageProcess
command: ["bash", "-c", "head -1 /proc/stat | awk '{print $2,$3,$4,$5,$6,$7,$8,$9}'"]
@@ -80,17 +90,14 @@ Singleton {
if (text.trim()) {
const stats = text.trim().split(" ").map(x => parseInt(x))
if (root.prevCpuStats[0] > 0) {
// Calculate differences
let diffs = []
for (let i = 0; i < 8; i++) {
diffs[i] = stats[i] - root.prevCpuStats[i]
}
// Calculate total and idle time
const totalTime = diffs.reduce((a, b) => a + b, 0)
const idleTime = diffs[3] + diffs[4] // idle + iowait
const idleTime = diffs[3] + diffs[4]
// CPU usage percentage
if (totalTime > 0) {
root.cpuUsage = Math.max(0, Math.min(100, ((totalTime - idleTime) / totalTime) * 100))
}
@@ -107,7 +114,6 @@ Singleton {
}
}
// Memory usage monitoring
Process {
id: memoryUsageProcess
command: ["bash", "-c", "free -m | awk 'NR==2{printf \"%.1f %.1f %.1f %.1f\", $3*100/$2, $2, $3, $7}'"]
@@ -133,7 +139,6 @@ Singleton {
}
}
// CPU frequency monitoring
Process {
id: cpuFrequencyProcess
command: ["bash", "-c", "cat /proc/cpuinfo | grep 'cpu MHz' | head -1 | awk '{print $4}'"]
@@ -154,7 +159,6 @@ Singleton {
}
}
// CPU temperature monitoring
Process {
id: temperatureProcess
command: ["bash", "-c", "if [ -f /sys/class/thermal/thermal_zone0/temp ]; then cat /sys/class/thermal/thermal_zone0/temp | awk '{print $1/1000}'; else sensors 2>/dev/null | grep 'Core 0' | awk '{print $3}' | sed 's/+//g;s/°C//g' | head -1; fi"]
@@ -175,7 +179,282 @@ Singleton {
}
}
// CPU monitoring timer
Process {
id: kernelInfoProcess
command: ["bash", "-c", "uname -r"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.kernelVersion = text.trim()
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Kernel info check failed with exit code:", exitCode)
}
}
}
Process {
id: distributionProcess
command: ["bash", "-c", "grep PRETTY_NAME /etc/os-release 2>/dev/null | cut -d'=' -f2 | tr -d '\"' || echo 'Unknown'"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.distribution = text.trim()
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Distribution check failed with exit code:", exitCode)
}
}
}
Process {
id: hostnameProcess
command: ["bash", "-c", "hostname"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.hostname = text.trim()
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Hostname check failed with exit code:", exitCode)
}
}
}
Process {
id: uptimeProcess
command: ["bash", "-c", "uptime -p | sed 's/up //'"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.uptime = text.trim()
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Uptime check failed with exit code:", exitCode)
}
}
}
Process {
id: schedulerProcess
command: ["bash", "-c", "cat /sys/block/sda/queue/scheduler 2>/dev/null | grep -o '\\[.*\\]' | tr -d '[]' || echo 'Unknown'"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.scheduler = text.trim()
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Scheduler check failed with exit code:", exitCode)
}
}
}
Process {
id: architectureProcess
command: ["bash", "-c", "uname -m"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.architecture = text.trim()
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Architecture check failed with exit code:", exitCode)
}
}
}
Process {
id: loadAverageProcess
command: ["bash", "-c", "cat /proc/loadavg | cut -d' ' -f1,2,3"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.loadAverage = text.trim()
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Load average check failed with exit code:", exitCode)
}
}
}
Process {
id: processCountProcess
command: ["bash", "-c", "ps aux | wc -l"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.processCount = parseInt(text.trim()) - 1
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Process count check failed with exit code:", exitCode)
}
}
}
Process {
id: threadCountProcess
command: ["bash", "-c", "cat /proc/stat | grep processes | awk '{print $2}'"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.threadCount = parseInt(text.trim())
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Thread count check failed with exit code:", exitCode)
}
}
}
Process {
id: bootTimeProcess
command: ["bash", "-c", "who -b | awk '{print $3, $4}' || stat -c %w /proc/1 2>/dev/null | cut -d' ' -f1,2 || echo 'Unknown'"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.bootTime = text.trim()
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Boot time check failed with exit code:", exitCode)
}
}
}
Process {
id: motherboardProcess
command: ["bash", "-c", "if [ -r /sys/devices/virtual/dmi/id/board_vendor ] && [ -r /sys/devices/virtual/dmi/id/board_name ]; then echo \"$(cat /sys/devices/virtual/dmi/id/board_vendor 2>/dev/null) $(cat /sys/devices/virtual/dmi/id/board_name 2>/dev/null)\"; else echo 'Unknown'; fi"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.motherboard = text.trim()
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Motherboard check failed with exit code:", exitCode)
}
}
}
Process {
id: biosProcess
command: ["bash", "-c", "if [ -r /sys/devices/virtual/dmi/id/bios_version ] && [ -r /sys/devices/virtual/dmi/id/bios_date ]; then echo \"$(cat /sys/devices/virtual/dmi/id/bios_version 2>/dev/null) $(cat /sys/devices/virtual/dmi/id/bios_date 2>/dev/null)\"; else echo 'Unknown'; fi"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.biosVersion = text.trim()
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("BIOS check failed with exit code:", exitCode)
}
}
}
Process {
id: diskMountsProcess
command: ["bash", "-c", "df -h --output=source,target,fstype,size,used,avail,pcent | tail -n +2 | grep -v tmpfs | grep -v devtmpfs | head -10"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
let mounts = []
const lines = text.trim().split('\n')
for (const line of lines) {
const parts = line.split(/\s+/)
if (parts.length >= 7) {
mounts.push({
device: parts[0],
mount: parts[1],
fstype: parts[2],
size: parts[3],
used: parts[4],
avail: parts[5],
percent: parts[6]
})
}
}
root.diskMounts = mounts
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Disk mounts check failed with exit code:", exitCode)
}
}
}
Timer {
id: cpuTimer
interval: root.cpuUpdateInterval
@@ -190,7 +469,6 @@ Singleton {
}
}
// Memory monitoring timer
Timer {
id: memoryTimer
interval: root.memoryUpdateInterval
@@ -204,7 +482,6 @@ Singleton {
}
}
// Temperature monitoring timer
Timer {
id: temperatureTimer
interval: root.temperatureUpdateInterval
@@ -218,7 +495,19 @@ Singleton {
}
}
// Public functions
Timer {
id: systemInfoTimer
interval: root.systemInfoUpdateInterval
running: root.enabledForDetailedView
repeat: true
onTriggered: {
if (root.enabledForDetailedView) {
updateSystemInfo()
}
}
}
function getCpuInfo() {
cpuInfoProcess.running = true
}
@@ -234,6 +523,22 @@ Singleton {
}
}
function updateSystemInfo() {
kernelInfoProcess.running = true
distributionProcess.running = true
hostnameProcess.running = true
uptimeProcess.running = true
schedulerProcess.running = true
architectureProcess.running = true
loadAverageProcess.running = true
processCountProcess.running = true
threadCountProcess.running = true
bootTimeProcess.running = true
motherboardProcess.running = true
biosProcess.running = true
diskMountsProcess.running = true
}
function enableTopBarMonitoring(enabled) {
root.enabledForTopBar = enabled
}
@@ -243,15 +548,15 @@ Singleton {
}
function getCpuUsageColor() {
if (cpuUsage > 80) return "#e74c3c" // Red
if (cpuUsage > 60) return "#f39c12" // Orange
return "#27ae60" // Green
if (cpuUsage > 80) return "#e74c3c"
if (cpuUsage > 60) return "#f39c12"
return "#27ae60"
}
function getMemoryUsageColor() {
if (memoryUsage > 90) return "#e74c3c" // Red
if (memoryUsage > 75) return "#f39c12" // Orange
return "#3498db" // Blue
if (memoryUsage > 90) return "#e74c3c"
if (memoryUsage > 75) return "#f39c12"
return "#3498db"
}
function formatMemory(mb) {
@@ -262,8 +567,8 @@ Singleton {
}
function getTemperatureColor() {
if (cpuTemperature > 80) return "#e74c3c" // Red
if (cpuTemperature > 65) return "#f39c12" // Orange
return "#27ae60" // Green
if (cpuTemperature > 80) return "#e74c3c"
if (cpuTemperature > 65) return "#f39c12"
return "#27ae60"
}
}