mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-07 05:55:37 -05:00
bluetooth: switch to native quickshell bluetooth service
removes dependency on bluetoothctl commands
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -11,16 +11,12 @@ Singleton {
|
||||
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 '%'"]
|
||||
@@ -36,7 +32,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Microphone level checker
|
||||
Process {
|
||||
id: micLevelChecker
|
||||
command: ["bash", "-c", "pactl get-source-volume @DEFAULT_SOURCE@ | grep -o '[0-9]*%' | head -1 | tr -d '%'"]
|
||||
@@ -68,7 +63,6 @@ Singleton {
|
||||
for (let line of lines) {
|
||||
line = line.trim()
|
||||
|
||||
// New sink starts
|
||||
if (line.startsWith('Sink #')) {
|
||||
if (currentSink && currentSink.name && currentSink.id) {
|
||||
sinks.push(currentSink)
|
||||
@@ -84,39 +78,31 @@ Singleton {
|
||||
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"
|
||||
@@ -136,7 +122,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Audio source (microphone) lister
|
||||
Process {
|
||||
id: audioSourceLister
|
||||
command: ["pactl", "list", "sources"]
|
||||
@@ -153,7 +138,6 @@ Singleton {
|
||||
for (let line of lines) {
|
||||
line = line.trim()
|
||||
|
||||
// New source starts
|
||||
if (line.startsWith('Source #')) {
|
||||
if (currentSource && currentSource.name && currentSource.id) {
|
||||
sources.push(currentSource)
|
||||
@@ -165,23 +149,19 @@ Singleton {
|
||||
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
|
||||
@@ -202,7 +182,6 @@ Singleton {
|
||||
if (data.trim()) {
|
||||
root.currentAudioSink = data.trim()
|
||||
|
||||
// Update active status in audioSinks
|
||||
let updatedSinks = []
|
||||
for (let sink of root.audioSinks) {
|
||||
updatedSinks.push({
|
||||
@@ -218,7 +197,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Default source (microphone) checker
|
||||
Process {
|
||||
id: defaultSourceChecker
|
||||
command: ["pactl", "get-default-source"]
|
||||
@@ -230,7 +208,6 @@ Singleton {
|
||||
if (data.trim()) {
|
||||
root.currentAudioSource = data.trim()
|
||||
|
||||
// Update active status in audioSources
|
||||
let updatedSources = []
|
||||
for (let source of root.audioSources) {
|
||||
updatedSources.push({
|
||||
@@ -271,12 +248,10 @@ Singleton {
|
||||
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
|
||||
@@ -285,7 +260,6 @@ Singleton {
|
||||
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
|
||||
@@ -303,7 +277,6 @@ Singleton {
|
||||
sourceSetProcess.running = true
|
||||
}
|
||||
|
||||
// Dedicated process for setting audio source
|
||||
Process {
|
||||
id: sourceSetProcess
|
||||
running: false
|
||||
@@ -312,7 +285,6 @@ Singleton {
|
||||
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
|
||||
@@ -337,25 +309,21 @@ Singleton {
|
||||
|
||||
Component.onCompleted: {
|
||||
console.log("AudioService: Starting initialization...")
|
||||
// Do initial device scan
|
||||
audioSinkLister.running = true
|
||||
audioSourceLister.running = true
|
||||
initialScanComplete = true
|
||||
console.log("AudioService: Initialization complete")
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
// Manual refresh function for when user opens audio settings
|
||||
function refreshDevices() {
|
||||
console.log("AudioService: Manual device refresh triggered")
|
||||
audioSinkLister.running = true
|
||||
|
||||
@@ -7,7 +7,6 @@ pragma ComponentBehavior: Bound
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Battery properties
|
||||
property bool batteryAvailable: false
|
||||
property int batteryLevel: 0
|
||||
property string batteryStatus: "Unknown"
|
||||
@@ -20,8 +19,6 @@ Singleton {
|
||||
property int batteryCapacity: 0
|
||||
property var powerProfiles: []
|
||||
property string activePowerProfile: ""
|
||||
|
||||
// Check if battery is available
|
||||
Process {
|
||||
id: batteryAvailabilityChecker
|
||||
command: ["bash", "-c", "ls /sys/class/power_supply/ | grep -E '^BAT' | head -1"]
|
||||
@@ -58,7 +55,6 @@ Singleton {
|
||||
if (text.trim() && text.trim() !== "fallback") {
|
||||
parseBatteryInfo(text.trim())
|
||||
} else {
|
||||
// Fallback to simple methods
|
||||
fallbackBatteryChecker.running = true
|
||||
}
|
||||
}
|
||||
@@ -72,7 +68,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// 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"]
|
||||
@@ -87,7 +82,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// 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"]
|
||||
@@ -170,7 +164,6 @@ Singleton {
|
||||
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()
|
||||
@@ -248,7 +241,6 @@ Singleton {
|
||||
}
|
||||
|
||||
|
||||
// Update timer
|
||||
Timer {
|
||||
interval: 30000
|
||||
running: root.batteryAvailable
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Bluetooth
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
@@ -9,410 +9,466 @@ 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
|
||||
if (deviceObj.connected !== nativeDevice.connected) {
|
||||
deviceObj.connecting = false
|
||||
deviceObj.connectionFailed = false
|
||||
}
|
||||
|
||||
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 {}
|
||||
}
|
||||
}
|
||||
@@ -7,15 +7,12 @@ pragma ComponentBehavior: Bound
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Process list properties
|
||||
property var processes: []
|
||||
property bool isUpdating: false
|
||||
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,28 +21,23 @@ Singleton {
|
||||
property real totalCpuUsage: 0.0
|
||||
property bool systemInfoAvailable: false
|
||||
|
||||
// Performance history for charts
|
||||
property var cpuHistory: []
|
||||
property var memoryHistory: []
|
||||
property var networkHistory: ({rx: [], tx: []})
|
||||
property var diskHistory: ({read: [], write: []})
|
||||
property int historySize: 60 // Keep 60 data points
|
||||
property int historySize: 60
|
||||
|
||||
// Per-core CPU usage
|
||||
property var perCoreCpuUsage: []
|
||||
|
||||
// Network stats
|
||||
property real networkRxRate: 0 // bytes/sec
|
||||
property real networkTxRate: 0 // bytes/sec
|
||||
property real networkRxRate: 0
|
||||
property real networkTxRate: 0
|
||||
property var lastNetworkStats: null
|
||||
|
||||
// Disk I/O stats
|
||||
property real diskReadRate: 0 // bytes/sec
|
||||
property real diskWriteRate: 0 // bytes/sec
|
||||
property real diskReadRate: 0
|
||||
property real diskWriteRate: 0
|
||||
property var lastDiskStats: null
|
||||
|
||||
// Sorting options
|
||||
property string sortBy: "cpu" // "cpu", "memory", "name", "pid"
|
||||
property string sortBy: "cpu"
|
||||
property bool sortDescending: true
|
||||
property int maxProcesses: 20
|
||||
|
||||
@@ -53,9 +45,6 @@ Singleton {
|
||||
console.log("ProcessMonitorService: Starting initialization...")
|
||||
updateProcessList()
|
||||
console.log("ProcessMonitorService: Initialization complete")
|
||||
|
||||
// Test monitoring disabled - only monitor when explicitly enabled
|
||||
// testTimer.start()
|
||||
}
|
||||
|
||||
Timer {
|
||||
@@ -66,7 +55,6 @@ Singleton {
|
||||
onTriggered: {
|
||||
console.log("ProcessMonitorService: Starting test monitoring...")
|
||||
enableMonitoring(true)
|
||||
// Stop after 8 seconds
|
||||
stopTestTimer.start()
|
||||
}
|
||||
}
|
||||
@@ -82,7 +70,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// System information monitoring
|
||||
Process {
|
||||
id: systemInfoProcess
|
||||
command: ["bash", "-c", "cat /proc/meminfo; echo '---CPU---'; nproc; echo '---CPUSTAT---'; grep '^cpu' /proc/stat | head -" + (root.cpuCount + 1)]
|
||||
@@ -104,7 +91,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Network monitoring process
|
||||
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/:/ /'"]
|
||||
@@ -119,7 +105,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Disk I/O monitoring process
|
||||
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}'"]
|
||||
@@ -134,7 +119,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Process monitoring with ps command
|
||||
Process {
|
||||
id: processListProcess
|
||||
command: ["bash", "-c", "ps axo pid,ppid,pcpu,pmem,rss,comm,cmd --sort=-pcpu | head -" + (root.maxProcesses + 1)]
|
||||
@@ -146,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])
|
||||
@@ -189,11 +171,10 @@ 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: {
|
||||
@@ -206,29 +187,24 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// Clear history when starting
|
||||
root.cpuHistory = []
|
||||
root.memoryHistory = []
|
||||
root.networkHistory = ({rx: [], tx: []})
|
||||
root.diskHistory = ({read: [], write: []})
|
||||
// Immediately update when enabled
|
||||
updateSystemInfo()
|
||||
updateProcessList()
|
||||
updateNetworkStats()
|
||||
updateDiskStats()
|
||||
// console.log("ProcessMonitorService: Initial data collection started")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,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":
|
||||
@@ -307,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"
|
||||
@@ -315,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) {
|
||||
@@ -444,7 +418,6 @@ Singleton {
|
||||
root.networkRxRate = Math.max(0, (totalRx - root.lastNetworkStats.rx) / timeDiff)
|
||||
root.networkTxRate = Math.max(0, (totalTx - root.lastNetworkStats.tx) / timeDiff)
|
||||
|
||||
// Convert to KB/s for history
|
||||
addToHistory(root.networkHistory.rx, root.networkRxRate / 1024)
|
||||
addToHistory(root.networkHistory.tx, root.networkTxRate / 1024)
|
||||
}
|
||||
@@ -463,7 +436,7 @@ Singleton {
|
||||
const readSectors = parseInt(parts[1])
|
||||
const writeSectors = parseInt(parts[2])
|
||||
if (!isNaN(readSectors) && !isNaN(writeSectors)) {
|
||||
totalRead += readSectors * 512 // Convert sectors to bytes
|
||||
totalRead += readSectors * 512
|
||||
totalWrite += writeSectors * 512
|
||||
}
|
||||
}
|
||||
@@ -474,7 +447,6 @@ Singleton {
|
||||
root.diskReadRate = Math.max(0, (totalRead - root.lastDiskStats.read) / timeDiff)
|
||||
root.diskWriteRate = Math.max(0, (totalWrite - root.lastDiskStats.write) / timeDiff)
|
||||
|
||||
// Convert to MB/s for history
|
||||
addToHistory(root.diskHistory.read, root.diskReadRate / (1024 * 1024))
|
||||
addToHistory(root.diskHistory.write, root.diskWriteRate / (1024 * 1024))
|
||||
}
|
||||
|
||||
@@ -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,12 @@ Singleton {
|
||||
property real bufferMemory: 0.0
|
||||
property real cacheMemory: 0.0
|
||||
|
||||
// Temperature properties
|
||||
property real cpuTemperature: 0.0
|
||||
|
||||
property int cpuUpdateInterval: 3000
|
||||
property int memoryUpdateInterval: 5000
|
||||
property int temperatureUpdateInterval: 10000
|
||||
|
||||
// Performance control
|
||||
property bool enabledForTopBar: true
|
||||
property bool enabledForDetailedView: false
|
||||
|
||||
@@ -43,7 +38,6 @@ Singleton {
|
||||
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 +63,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 +73,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 +97,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 +122,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// CPU frequency monitoring
|
||||
Process {
|
||||
id: cpuFrequencyProcess
|
||||
command: ["bash", "-c", "cat /proc/cpuinfo | grep 'cpu MHz' | head -1 | awk '{print $4}'"]
|
||||
@@ -154,7 +142,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 +162,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// CPU monitoring timer
|
||||
Timer {
|
||||
id: cpuTimer
|
||||
interval: root.cpuUpdateInterval
|
||||
@@ -190,7 +176,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Memory monitoring timer
|
||||
Timer {
|
||||
id: memoryTimer
|
||||
interval: root.memoryUpdateInterval
|
||||
@@ -204,7 +189,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Temperature monitoring timer
|
||||
Timer {
|
||||
id: temperatureTimer
|
||||
interval: root.temperatureUpdateInterval
|
||||
@@ -218,7 +202,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Public functions
|
||||
function getCpuInfo() {
|
||||
cpuInfoProcess.running = true
|
||||
}
|
||||
@@ -243,15 +226,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 +245,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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user