1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-08 22:45:38 -05:00

bluetooth: switch to native quickshell bluetooth service

removes dependency on bluetoothctl commands
This commit is contained in:
bbedward
2025-07-15 12:43:22 -04:00
parent 1396b0b582
commit b7da76147f
8 changed files with 1030 additions and 761 deletions

View File

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

View File

@@ -11,16 +11,12 @@ Singleton {
property var audioSinks: [] property var audioSinks: []
property string currentAudioSink: "" property string currentAudioSink: ""
// Microphone properties
property int micLevel: 50 property int micLevel: 50
property var audioSources: [] property var audioSources: []
property string currentAudioSource: "" property string currentAudioSource: ""
// Device scanning control
property bool deviceScanningEnabled: false property bool deviceScanningEnabled: false
property bool initialScanComplete: false property bool initialScanComplete: false
// Real Audio Control
Process { Process {
id: volumeChecker id: volumeChecker
command: ["bash", "-c", "pactl get-sink-volume @DEFAULT_SINK@ | grep -o '[0-9]*%' | head -1 | tr -d '%'"] 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 { Process {
id: micLevelChecker id: micLevelChecker
command: ["bash", "-c", "pactl get-source-volume @DEFAULT_SOURCE@ | grep -o '[0-9]*%' | head -1 | tr -d '%'"] 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) { for (let line of lines) {
line = line.trim() line = line.trim()
// New sink starts
if (line.startsWith('Sink #')) { if (line.startsWith('Sink #')) {
if (currentSink && currentSink.name && currentSink.id) { if (currentSink && currentSink.name && currentSink.id) {
sinks.push(currentSink) sinks.push(currentSink)
@@ -84,39 +78,31 @@ Singleton {
active: false active: false
} }
} }
// Get the Name field
else if (line.startsWith('Name: ') && currentSink) { else if (line.startsWith('Name: ') && currentSink) {
currentSink.name = line.replace('Name: ', '').trim() currentSink.name = line.replace('Name: ', '').trim()
} }
// Get the Description field (main display name)
else if (line.startsWith('Description: ') && currentSink) { else if (line.startsWith('Description: ') && currentSink) {
currentSink.description = line.replace('Description: ', '').trim() currentSink.description = line.replace('Description: ', '').trim()
} }
// Get device.description as fallback
else if (line.includes('device.description = ') && currentSink && !currentSink.description) { else if (line.includes('device.description = ') && currentSink && !currentSink.description) {
currentSink.description = line.replace('device.description = ', '').replace(/"/g, '').trim() 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) { else if (line.includes('node.nick = ') && currentSink && !currentSink.description) {
currentSink.nick = line.replace('node.nick = ', '').replace(/"/g, '').trim() currentSink.nick = line.replace('node.nick = ', '').replace(/"/g, '').trim()
} }
} }
// Add the last sink
if (currentSink && currentSink.name && currentSink.id) { if (currentSink && currentSink.name && currentSink.id) {
sinks.push(currentSink) sinks.push(currentSink)
} }
// Process display names
for (let sink of sinks) { for (let sink of sinks) {
let displayName = sink.description let displayName = sink.description
// If no good description, try nick
if (!displayName || displayName === sink.name) { if (!displayName || displayName === sink.name) {
displayName = sink.nick displayName = sink.nick
} }
// Still no good name? Fall back to smart defaults
if (!displayName || displayName === sink.name) { if (!displayName || displayName === sink.name) {
if (sink.name.includes("analog-stereo")) displayName = "Built-in Speakers" if (sink.name.includes("analog-stereo")) displayName = "Built-in Speakers"
else if (sink.name.includes("bluez")) displayName = "Bluetooth Audio" else if (sink.name.includes("bluez")) displayName = "Bluetooth Audio"
@@ -136,7 +122,6 @@ Singleton {
} }
} }
// Audio source (microphone) lister
Process { Process {
id: audioSourceLister id: audioSourceLister
command: ["pactl", "list", "sources"] command: ["pactl", "list", "sources"]
@@ -153,7 +138,6 @@ Singleton {
for (let line of lines) { for (let line of lines) {
line = line.trim() line = line.trim()
// New source starts
if (line.startsWith('Source #')) { if (line.startsWith('Source #')) {
if (currentSource && currentSource.name && currentSource.id) { if (currentSource && currentSource.name && currentSource.id) {
sources.push(currentSource) sources.push(currentSource)
@@ -165,23 +149,19 @@ Singleton {
active: false active: false
} }
} }
// Source name
else if (line.startsWith('Name: ') && currentSource) { else if (line.startsWith('Name: ') && currentSource) {
currentSource.name = line.replace('Name: ', '') currentSource.name = line.replace('Name: ', '')
} }
// Description (display name)
else if (line.startsWith('Description: ') && currentSource) { else if (line.startsWith('Description: ') && currentSource) {
let desc = line.replace('Description: ', '') let desc = line.replace('Description: ', '')
currentSource.displayName = desc currentSource.displayName = desc
} }
} }
// Add the last source
if (currentSource && currentSource.name && currentSource.id) { if (currentSource && currentSource.name && currentSource.id) {
sources.push(currentSource) sources.push(currentSource)
} }
// Filter out monitor sources (we want actual input devices)
sources = sources.filter(source => !source.name.includes('.monitor')) sources = sources.filter(source => !source.name.includes('.monitor'))
root.audioSources = sources root.audioSources = sources
@@ -202,7 +182,6 @@ Singleton {
if (data.trim()) { if (data.trim()) {
root.currentAudioSink = data.trim() root.currentAudioSink = data.trim()
// Update active status in audioSinks
let updatedSinks = [] let updatedSinks = []
for (let sink of root.audioSinks) { for (let sink of root.audioSinks) {
updatedSinks.push({ updatedSinks.push({
@@ -218,7 +197,6 @@ Singleton {
} }
} }
// Default source (microphone) checker
Process { Process {
id: defaultSourceChecker id: defaultSourceChecker
command: ["pactl", "get-default-source"] command: ["pactl", "get-default-source"]
@@ -230,7 +208,6 @@ Singleton {
if (data.trim()) { if (data.trim()) {
root.currentAudioSource = data.trim() root.currentAudioSource = data.trim()
// Update active status in audioSources
let updatedSources = [] let updatedSources = []
for (let source of root.audioSources) { for (let source of root.audioSources) {
updatedSources.push({ updatedSources.push({
@@ -271,12 +248,10 @@ Singleton {
function setAudioSink(sinkName) { function setAudioSink(sinkName) {
console.log("Setting audio sink to:", 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.command = ["pactl", "set-default-sink", sinkName]
sinkSetProcess.running = true sinkSetProcess.running = true
} }
// Dedicated process for setting audio sink
Process { Process {
id: sinkSetProcess id: sinkSetProcess
running: false running: false
@@ -285,7 +260,6 @@ Singleton {
console.log("Audio sink change exit code:", exitCode) console.log("Audio sink change exit code:", exitCode)
if (exitCode === 0) { if (exitCode === 0) {
console.log("Audio sink changed successfully") console.log("Audio sink changed successfully")
// Refresh current sink and list
defaultSinkChecker.running = true defaultSinkChecker.running = true
if (root.deviceScanningEnabled) { if (root.deviceScanningEnabled) {
audioSinkLister.running = true audioSinkLister.running = true
@@ -303,7 +277,6 @@ Singleton {
sourceSetProcess.running = true sourceSetProcess.running = true
} }
// Dedicated process for setting audio source
Process { Process {
id: sourceSetProcess id: sourceSetProcess
running: false running: false
@@ -312,7 +285,6 @@ Singleton {
console.log("Audio source change exit code:", exitCode) console.log("Audio source change exit code:", exitCode)
if (exitCode === 0) { if (exitCode === 0) {
console.log("Audio source changed successfully") console.log("Audio source changed successfully")
// Refresh current source and list
defaultSourceChecker.running = true defaultSourceChecker.running = true
if (root.deviceScanningEnabled) { if (root.deviceScanningEnabled) {
audioSourceLister.running = true audioSourceLister.running = true
@@ -337,25 +309,21 @@ Singleton {
Component.onCompleted: { Component.onCompleted: {
console.log("AudioService: Starting initialization...") console.log("AudioService: Starting initialization...")
// Do initial device scan
audioSinkLister.running = true audioSinkLister.running = true
audioSourceLister.running = true audioSourceLister.running = true
initialScanComplete = true initialScanComplete = true
console.log("AudioService: Initialization complete") console.log("AudioService: Initialization complete")
} }
// Control functions for managing device scanning
function enableDeviceScanning(enabled) { function enableDeviceScanning(enabled) {
console.log("AudioService: Device scanning", enabled ? "enabled" : "disabled") console.log("AudioService: Device scanning", enabled ? "enabled" : "disabled")
root.deviceScanningEnabled = enabled root.deviceScanningEnabled = enabled
if (enabled && root.initialScanComplete) { if (enabled && root.initialScanComplete) {
// Immediately scan when enabled
audioSinkLister.running = true audioSinkLister.running = true
audioSourceLister.running = true audioSourceLister.running = true
} }
} }
// Manual refresh function for when user opens audio settings
function refreshDevices() { function refreshDevices() {
console.log("AudioService: Manual device refresh triggered") console.log("AudioService: Manual device refresh triggered")
audioSinkLister.running = true audioSinkLister.running = true

View File

@@ -7,7 +7,6 @@ pragma ComponentBehavior: Bound
Singleton { Singleton {
id: root id: root
// Battery properties
property bool batteryAvailable: false property bool batteryAvailable: false
property int batteryLevel: 0 property int batteryLevel: 0
property string batteryStatus: "Unknown" property string batteryStatus: "Unknown"
@@ -20,8 +19,6 @@ Singleton {
property int batteryCapacity: 0 property int batteryCapacity: 0
property var powerProfiles: [] property var powerProfiles: []
property string activePowerProfile: "" property string activePowerProfile: ""
// Check if battery is available
Process { Process {
id: batteryAvailabilityChecker id: batteryAvailabilityChecker
command: ["bash", "-c", "ls /sys/class/power_supply/ | grep -E '^BAT' | head -1"] command: ["bash", "-c", "ls /sys/class/power_supply/ | grep -E '^BAT' | head -1"]
@@ -58,7 +55,6 @@ Singleton {
if (text.trim() && text.trim() !== "fallback") { if (text.trim() && text.trim() !== "fallback") {
parseBatteryInfo(text.trim()) parseBatteryInfo(text.trim())
} else { } else {
// Fallback to simple methods
fallbackBatteryChecker.running = true fallbackBatteryChecker.running = true
} }
} }
@@ -72,7 +68,6 @@ Singleton {
} }
} }
// Fallback battery checker using /sys files
Process { Process {
id: fallbackBatteryChecker 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"] 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 { Process {
id: powerProfilesChecker id: powerProfilesChecker
command: ["bash", "-c", "if command -v powerprofilesctl > /dev/null; then powerprofilesctl list 2>/dev/null; else echo 'not-available'; fi"] 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) { for (let line of lines) {
line = line.trim() line = line.trim()
if (line.includes('*')) { if (line.includes('*')) {
// Active profile
let profileName = line.replace('*', '').trim() let profileName = line.replace('*', '').trim()
if (profileName.includes(':')) { if (profileName.includes(':')) {
profileName = profileName.split(':')[0].trim() profileName = profileName.split(':')[0].trim()
@@ -248,7 +241,6 @@ Singleton {
} }
// Update timer
Timer { Timer {
interval: 30000 interval: 30000
running: root.batteryAvailable running: root.batteryAvailable

View File

@@ -1,6 +1,6 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Bluetooth
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
@@ -9,410 +9,466 @@ Singleton {
property bool bluetoothEnabled: false property bool bluetoothEnabled: false
property bool bluetoothAvailable: false property bool bluetoothAvailable: false
property var bluetoothDevices: [] readonly property list<BluetoothDevice> bluetoothDevices: []
property var availableDevices: [] readonly property list<BluetoothDevice> availableDevices: []
property bool scanning: false property bool scanning: false
property bool discoverable: false property bool discoverable: false
// Real Bluetooth Management property var connectingDevices: ({})
Process {
id: bluetoothStatusChecker
command: ["bluetoothctl", "show"] // Use default controller
running: true
stdout: StdioCollector { Component.onCompleted: {
onStreamFinished: { refreshBluetoothState()
root.bluetoothAvailable = text.trim() !== "" && !text.includes("No default controller") updateDevices()
root.bluetoothEnabled = text.includes("Powered: yes") }
if (root.bluetoothEnabled && root.bluetoothAvailable) { Connections {
bluetoothDeviceScanner.running = true target: Bluetooth
} else {
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.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)
} }
} }
} }
Process { for (let device of allNativeDevices) {
id: bluetoothDeviceScanner if (!device) continue
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 { let deviceType = getDeviceType(device.name || device.deviceName, device.icon)
onStreamFinished: { let displayName = device.name || device.deviceName
if (text.trim()) {
let devices = []
let lines = text.trim().split('\n')
for (let line of lines) { if (!displayName || displayName.startsWith('/org/bluez') || displayName.includes('hci0') || displayName.length < 2) {
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 continue
} }
// Determine device type from name if (device.paired) {
let type = "bluetooth" let existingDevice = findDeviceInList(root.bluetoothDevices, device.address)
let nameLower = name.toLowerCase() if (existingDevice) {
if (nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || nameLower.includes("arctis")) type = "headset" updateDeviceData(existingDevice, device, deviceType, displayName)
else if (nameLower.includes("mouse")) type = "mouse" newPairedDevices.push(existingDevice)
else if (nameLower.includes("keyboard")) type = "keyboard" } else {
else if (nameLower.includes("phone") || nameLower.includes("iphone") || nameLower.includes("samsung")) type = "phone" let newDevice = createBluetoothDevice(device, deviceType, displayName)
else if (nameLower.includes("watch")) type = "watch" newPairedDevices.push(newDevice)
else if (nameLower.includes("speaker")) type = "speaker" }
} 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)
}
}
}
}
devices.push({ cleanupOldDevices(root.bluetoothDevices, newPairedDevices)
mac: mac, cleanupOldDevices(root.availableDevices, newAvailableDevices)
name: name,
type: type, console.log("BluetoothService: Found", newPairedDevices.length, "paired devices and", newAvailableDevices.length, "available devices")
connected: connected,
battery: battery 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
} }
root.bluetoothDevices = devices 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() { function clearDeviceList(deviceList) {
if (root.bluetoothEnabled && root.bluetoothAvailable) { for (let device of deviceList) {
bluetoothDeviceScanner.running = true 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() { function startDiscovery() {
root.scanning = true if (Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.enabled) {
// Run comprehensive scan that gets all devices Bluetooth.defaultAdapter.discovering = true
discoveryScanner.running = true updateDevices()
}
} }
function stopDiscovery() { function stopDiscovery() {
let stopDiscoveryProcess = Qt.createQmlObject(' if (Bluetooth.defaultAdapter) {
import Quickshell.Io Bluetooth.defaultAdapter.discovering = false
Process { updateDevices()
command: ["bluetoothctl", "scan", "off"]
running: true
onExited: {
root.scanning = false
} }
} }
', root)
}
function pairDevice(mac) { function pairDevice(mac) {
console.log("Pairing device:", mac) console.log("Pairing device:", mac)
let pairProcess = Qt.createQmlObject(' let device = findDeviceByMac(mac)
import Quickshell.Io if (device) {
Process { device.pair()
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)
} }
function connectDevice(mac) { function connectDevice(mac) {
console.log("Connecting to device:", mac) console.log("Connecting to device:", mac)
let connectProcess = Qt.createQmlObject(' let device = findDeviceByMac(mac)
import Quickshell.Io if (device) {
Process { device.connect()
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)
} }
function removeDevice(mac) { function removeDevice(mac) {
console.log("Removing device:", mac) console.log("Removing device:", mac)
let removeProcess = Qt.createQmlObject(' let device = findDeviceByMac(mac)
import Quickshell.Io if (device) {
Process { device.forget()
command: ["bluetoothctl", "remove", "' + mac + '"]
running: true
onExited: {
bluetoothDeviceScanner.running = true
availableDeviceScanner.running = true
} }
} }
', root)
}
function toggleBluetoothDevice(mac) { function toggleBluetoothDevice(mac) {
let device = root.bluetoothDevices.find(d => d.mac === mac) let typedDevice = findDeviceInList(root.bluetoothDevices, mac)
if (device) { if (!typedDevice) {
let action = device.connected ? "disconnect" : "connect" typedDevice = findDeviceInList(root.availableDevices, mac)
let toggleProcess = Qt.createQmlObject(' }
import Quickshell.Io
Process { if (typedDevice && typedDevice.nativeDevice) {
command: ["bluetoothctl", "' + action + '", "' + mac + '"] if (typedDevice.connected) {
running: true console.log("Disconnecting device:", mac)
onExited: bluetoothDeviceScanner.running = true 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
} }
', root)
} }
} }
function toggleBluetooth() { function toggleBluetooth() {
let action = root.bluetoothEnabled ? "off" : "on" if (Bluetooth.defaultAdapter) {
let toggleProcess = Qt.createQmlObject(' Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter.enabled
import Quickshell.Io
Process {
command: ["bluetoothctl", "power", "' + action + '"]
running: true
onExited: bluetoothStatusChecker.running = true
} }
', root)
} }
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 { Timer {
id: bluetoothMonitorTimer id: bluetoothMonitorTimer
interval: 5000 interval: 2000
running: false; repeat: true running: false
repeat: true
onTriggered: { onTriggered: {
bluetoothStatusChecker.running = true updateDevices()
if (root.bluetoothEnabled) {
bluetoothDeviceScanner.running = true
// Also refresh paired devices to get current connection status
pairedDeviceChecker.discoveredToMerge = []
pairedDeviceChecker.running = true
}
} }
} }
function enableMonitoring(enabled) { function enableMonitoring(enabled) {
bluetoothMonitorTimer.running = enabled bluetoothMonitorTimer.running = enabled
if (enabled) { if (enabled) {
// Immediately update when enabled refreshBluetoothState()
bluetoothStatusChecker.running = true updateDevices()
} }
} }
property var discoveredDevices: [] Timer {
id: bluetoothStateRefreshTimer
// Handle discovered devices interval: 5000
function _handleDiscovered(found) { running: true
repeat: true
let discoveredDevices = [] onTriggered: {
for (let device of found) { refreshBluetoothState()
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, ")")
} }
// Get paired devices first, then merge with discovered Timer {
pairedDeviceChecker.discoveredToMerge = discoveredDevices id: connectionTimeout
pairedDeviceChecker.running = true interval: 10000 // 10 second timeout
}
// 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"]
running: false running: false
repeat: false
stdout: StdioCollector { property string deviceMac: ""
onStreamFinished: {
let devices = [] onTriggered: {
if (text.trim()) { if (deviceMac) {
let lines = text.trim().split('\n') let typedDevice = findDeviceInList(root.bluetoothDevices, deviceMac)
if (!typedDevice) {
for (let line of lines) { typedDevice = findDeviceInList(root.availableDevices, deviceMac)
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 if (typedDevice && typedDevice.connecting && !typedDevice.connected) {
let type = "bluetooth" console.log("Connection timeout for device:", deviceMac)
let nameLower = name.toLowerCase() typedDevice.connecting = false
if (nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || nameLower.includes("arctis") || nameLower.includes("audio")) type = "headset" typedDevice.connectionFailed = true
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({ // Clear failure state after 3 seconds
mac: mac, Qt.callLater(() => {
name: name, clearFailureTimer.deviceMac = deviceMac
type: type, clearFailureTimer.start()
paired: paired,
connected: connected,
rssi: 0,
signalStrength: "unknown",
canPair: false // Already paired
}) })
} }
deviceMac = ""
} }
} }
} }
root.availableDevices = devices Timer {
} id: clearFailureTimer
} interval: 3000
}
// 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 running: false
repeat: false
stdout: StdioCollector { property string deviceMac: ""
onStreamFinished: {
/* onTriggered: {
* bluetoothctl prints lines like: if (deviceMac) {
* [NEW] Device 12:34:56:78:9A:BC My-Headphones let typedDevice = findDeviceInList(root.bluetoothDevices, deviceMac)
*/ if (!typedDevice) {
const rx = /^\[NEW\] Device ([0-9A-F:]+)\s+(.+)$/i; typedDevice = findDeviceInList(root.availableDevices, deviceMac)
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: { if (typedDevice) {
root.scanning = false typedDevice.connectionFailed = 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')
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
})
} }
deviceMac = ""
} }
} }
} }
root.availableDevices = allDevices component BluetoothDevice: QtObject {
root.scanning = false 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,15 +7,12 @@ pragma ComponentBehavior: Bound
Singleton { Singleton {
id: root id: root
// Process list properties
property var processes: [] property var processes: []
property bool isUpdating: false property bool isUpdating: false
property int processUpdateInterval: 3000 property int processUpdateInterval: 3000
// Performance control - only run when process monitor is actually visible
property bool monitoringEnabled: false property bool monitoringEnabled: false
// System information properties
property int totalMemoryKB: 0 property int totalMemoryKB: 0
property int usedMemoryKB: 0 property int usedMemoryKB: 0
property int totalSwapKB: 0 property int totalSwapKB: 0
@@ -24,28 +21,23 @@ Singleton {
property real totalCpuUsage: 0.0 property real totalCpuUsage: 0.0
property bool systemInfoAvailable: false property bool systemInfoAvailable: false
// Performance history for charts
property var cpuHistory: [] property var cpuHistory: []
property var memoryHistory: [] property var memoryHistory: []
property var networkHistory: ({rx: [], tx: []}) property var networkHistory: ({rx: [], tx: []})
property var diskHistory: ({read: [], write: []}) property var diskHistory: ({read: [], write: []})
property int historySize: 60 // Keep 60 data points property int historySize: 60
// Per-core CPU usage
property var perCoreCpuUsage: [] property var perCoreCpuUsage: []
// Network stats property real networkRxRate: 0
property real networkRxRate: 0 // bytes/sec property real networkTxRate: 0
property real networkTxRate: 0 // bytes/sec
property var lastNetworkStats: null property var lastNetworkStats: null
// Disk I/O stats property real diskReadRate: 0
property real diskReadRate: 0 // bytes/sec property real diskWriteRate: 0
property real diskWriteRate: 0 // bytes/sec
property var lastDiskStats: null property var lastDiskStats: null
// Sorting options property string sortBy: "cpu"
property string sortBy: "cpu" // "cpu", "memory", "name", "pid"
property bool sortDescending: true property bool sortDescending: true
property int maxProcesses: 20 property int maxProcesses: 20
@@ -53,9 +45,6 @@ Singleton {
console.log("ProcessMonitorService: Starting initialization...") console.log("ProcessMonitorService: Starting initialization...")
updateProcessList() updateProcessList()
console.log("ProcessMonitorService: Initialization complete") console.log("ProcessMonitorService: Initialization complete")
// Test monitoring disabled - only monitor when explicitly enabled
// testTimer.start()
} }
Timer { Timer {
@@ -66,7 +55,6 @@ Singleton {
onTriggered: { onTriggered: {
console.log("ProcessMonitorService: Starting test monitoring...") console.log("ProcessMonitorService: Starting test monitoring...")
enableMonitoring(true) enableMonitoring(true)
// Stop after 8 seconds
stopTestTimer.start() stopTestTimer.start()
} }
} }
@@ -82,7 +70,6 @@ Singleton {
} }
} }
// System information monitoring
Process { Process {
id: systemInfoProcess id: systemInfoProcess
command: ["bash", "-c", "cat /proc/meminfo; echo '---CPU---'; nproc; echo '---CPUSTAT---'; grep '^cpu' /proc/stat | head -" + (root.cpuCount + 1)] 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 { Process {
id: networkStatsProcess id: networkStatsProcess
command: ["bash", "-c", "cat /proc/net/dev | grep -E '(wlan|eth|enp|wlp|ens|eno)' | awk '{print $1,$2,$10}' | sed 's/:/ /'"] 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 { Process {
id: diskStatsProcess 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}'"] 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 { Process {
id: processListProcess id: processListProcess
command: ["bash", "-c", "ps axo pid,ppid,pcpu,pmem,rss,comm,cmd --sort=-pcpu | head -" + (root.maxProcesses + 1)] 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 lines = text.trim().split('\n')
const newProcesses = [] const newProcesses = []
// Skip header line
for (let i = 1; i < lines.length; i++) { for (let i = 1; i < lines.length; i++) {
const line = lines[i].trim() const line = lines[i].trim()
if (!line) continue if (!line) continue
// Parse ps output: PID PPID %CPU %MEM RSS COMMAND CMD
const parts = line.split(/\s+/) const parts = line.split(/\s+/)
if (parts.length >= 7) { if (parts.length >= 7) {
const pid = parseInt(parts[0]) const pid = parseInt(parts[0])
@@ -189,11 +171,10 @@ Singleton {
} }
} }
// System and process monitoring timer - now conditional
Timer { Timer {
id: processTimer id: processTimer
interval: root.processUpdateInterval interval: root.processUpdateInterval
running: root.monitoringEnabled // Only run when monitoring is enabled running: root.monitoringEnabled
repeat: true repeat: true
onTriggered: { onTriggered: {
@@ -206,29 +187,24 @@ Singleton {
} }
} }
// Public functions
function updateSystemInfo() { function updateSystemInfo() {
if (!systemInfoProcess.running && root.monitoringEnabled) { if (!systemInfoProcess.running && root.monitoringEnabled) {
systemInfoProcess.running = true systemInfoProcess.running = true
} }
} }
// Control functions for enabling/disabling monitoring
function enableMonitoring(enabled) { function enableMonitoring(enabled) {
console.log("ProcessMonitorService: Monitoring", enabled ? "enabled" : "disabled") console.log("ProcessMonitorService: Monitoring", enabled ? "enabled" : "disabled")
root.monitoringEnabled = enabled root.monitoringEnabled = enabled
if (enabled) { if (enabled) {
// Clear history when starting
root.cpuHistory = [] root.cpuHistory = []
root.memoryHistory = [] root.memoryHistory = []
root.networkHistory = ({rx: [], tx: []}) root.networkHistory = ({rx: [], tx: []})
root.diskHistory = ({read: [], write: []}) root.diskHistory = ({read: [], write: []})
// Immediately update when enabled
updateSystemInfo() updateSystemInfo()
updateProcessList() updateProcessList()
updateNetworkStats() updateNetworkStats()
updateDiskStats() updateDiskStats()
// console.log("ProcessMonitorService: Initial data collection started")
} }
} }
@@ -248,7 +224,6 @@ Singleton {
if (!root.isUpdating && root.monitoringEnabled) { if (!root.isUpdating && root.monitoringEnabled) {
root.isUpdating = true root.isUpdating = true
// Update sort command based on current sort option
let sortOption = "" let sortOption = ""
switch (root.sortBy) { switch (root.sortBy) {
case "cpu": case "cpu":
@@ -307,7 +282,6 @@ Singleton {
} }
function getProcessIcon(command) { function getProcessIcon(command) {
// Return appropriate Material Design icon for common processes
const cmd = command.toLowerCase() const cmd = command.toLowerCase()
if (cmd.includes("firefox") || cmd.includes("chrome") || cmd.includes("browser")) return "web" if (cmd.includes("firefox") || cmd.includes("chrome") || cmd.includes("browser")) return "web"
if (cmd.includes("code") || cmd.includes("editor") || cmd.includes("vim")) return "code" 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("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("video") || cmd.includes("vlc") || cmd.includes("mpv")) return "play_circle"
if (cmd.includes("systemd") || cmd.includes("kernel") || cmd.includes("kthread")) return "settings" if (cmd.includes("systemd") || cmd.includes("kernel") || cmd.includes("kthread")) return "settings"
return "memory" // Default process icon return "memory"
} }
function formatCpuUsage(cpu) { function formatCpuUsage(cpu) {
@@ -444,7 +418,6 @@ Singleton {
root.networkRxRate = Math.max(0, (totalRx - root.lastNetworkStats.rx) / timeDiff) root.networkRxRate = Math.max(0, (totalRx - root.lastNetworkStats.rx) / timeDiff)
root.networkTxRate = Math.max(0, (totalTx - root.lastNetworkStats.tx) / 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.rx, root.networkRxRate / 1024)
addToHistory(root.networkHistory.tx, root.networkTxRate / 1024) addToHistory(root.networkHistory.tx, root.networkTxRate / 1024)
} }
@@ -463,7 +436,7 @@ Singleton {
const readSectors = parseInt(parts[1]) const readSectors = parseInt(parts[1])
const writeSectors = parseInt(parts[2]) const writeSectors = parseInt(parts[2])
if (!isNaN(readSectors) && !isNaN(writeSectors)) { if (!isNaN(readSectors) && !isNaN(writeSectors)) {
totalRead += readSectors * 512 // Convert sectors to bytes totalRead += readSectors * 512
totalWrite += writeSectors * 512 totalWrite += writeSectors * 512
} }
} }
@@ -474,7 +447,6 @@ Singleton {
root.diskReadRate = Math.max(0, (totalRead - root.lastDiskStats.read) / timeDiff) root.diskReadRate = Math.max(0, (totalRead - root.lastDiskStats.read) / timeDiff)
root.diskWriteRate = Math.max(0, (totalWrite - root.lastDiskStats.write) / 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.read, root.diskReadRate / (1024 * 1024))
addToHistory(root.diskHistory.write, root.diskWriteRate / (1024 * 1024)) addToHistory(root.diskHistory.write, root.diskWriteRate / (1024 * 1024))
} }

View File

@@ -7,16 +7,13 @@ pragma ComponentBehavior: Bound
Singleton { Singleton {
id: root id: root
// CPU properties
property real cpuUsage: 0.0 property real cpuUsage: 0.0
property int cpuCores: 1 property int cpuCores: 1
property string cpuModel: "" property string cpuModel: ""
property real cpuFrequency: 0.0 property real cpuFrequency: 0.0
// Previous CPU stats for accurate calculation
property var prevCpuStats: [0, 0, 0, 0, 0, 0, 0, 0] property var prevCpuStats: [0, 0, 0, 0, 0, 0, 0, 0]
// Memory properties
property real memoryUsage: 0.0 property real memoryUsage: 0.0
property real totalMemory: 0.0 property real totalMemory: 0.0
property real usedMemory: 0.0 property real usedMemory: 0.0
@@ -25,14 +22,12 @@ Singleton {
property real bufferMemory: 0.0 property real bufferMemory: 0.0
property real cacheMemory: 0.0 property real cacheMemory: 0.0
// Temperature properties
property real cpuTemperature: 0.0 property real cpuTemperature: 0.0
property int cpuUpdateInterval: 3000 property int cpuUpdateInterval: 3000
property int memoryUpdateInterval: 5000 property int memoryUpdateInterval: 5000
property int temperatureUpdateInterval: 10000 property int temperatureUpdateInterval: 10000
// Performance control
property bool enabledForTopBar: true property bool enabledForTopBar: true
property bool enabledForDetailedView: false property bool enabledForDetailedView: false
@@ -43,7 +38,6 @@ Singleton {
console.log("SystemMonitorService: Initialization complete") console.log("SystemMonitorService: Initialization complete")
} }
// Get CPU information (static)
Process { Process {
id: cpuInfoProcess id: cpuInfoProcess
command: ["bash", "-c", "lscpu | grep -E 'Model name|CPU\\(s\\):' | head -2"] command: ["bash", "-c", "lscpu | grep -E 'Model name|CPU\\(s\\):' | head -2"]
@@ -69,7 +63,6 @@ Singleton {
} }
} }
// CPU usage monitoring with accurate calculation
Process { Process {
id: cpuUsageProcess id: cpuUsageProcess
command: ["bash", "-c", "head -1 /proc/stat | awk '{print $2,$3,$4,$5,$6,$7,$8,$9}'"] 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()) { if (text.trim()) {
const stats = text.trim().split(" ").map(x => parseInt(x)) const stats = text.trim().split(" ").map(x => parseInt(x))
if (root.prevCpuStats[0] > 0) { if (root.prevCpuStats[0] > 0) {
// Calculate differences
let diffs = [] let diffs = []
for (let i = 0; i < 8; i++) { for (let i = 0; i < 8; i++) {
diffs[i] = stats[i] - root.prevCpuStats[i] diffs[i] = stats[i] - root.prevCpuStats[i]
} }
// Calculate total and idle time
const totalTime = diffs.reduce((a, b) => a + b, 0) 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) { if (totalTime > 0) {
root.cpuUsage = Math.max(0, Math.min(100, ((totalTime - idleTime) / totalTime) * 100)) root.cpuUsage = Math.max(0, Math.min(100, ((totalTime - idleTime) / totalTime) * 100))
} }
@@ -107,7 +97,6 @@ Singleton {
} }
} }
// Memory usage monitoring
Process { Process {
id: memoryUsageProcess id: memoryUsageProcess
command: ["bash", "-c", "free -m | awk 'NR==2{printf \"%.1f %.1f %.1f %.1f\", $3*100/$2, $2, $3, $7}'"] 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 { Process {
id: cpuFrequencyProcess id: cpuFrequencyProcess
command: ["bash", "-c", "cat /proc/cpuinfo | grep 'cpu MHz' | head -1 | awk '{print $4}'"] command: ["bash", "-c", "cat /proc/cpuinfo | grep 'cpu MHz' | head -1 | awk '{print $4}'"]
@@ -154,7 +142,6 @@ Singleton {
} }
} }
// CPU temperature monitoring
Process { Process {
id: temperatureProcess 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"] 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 { Timer {
id: cpuTimer id: cpuTimer
interval: root.cpuUpdateInterval interval: root.cpuUpdateInterval
@@ -190,7 +176,6 @@ Singleton {
} }
} }
// Memory monitoring timer
Timer { Timer {
id: memoryTimer id: memoryTimer
interval: root.memoryUpdateInterval interval: root.memoryUpdateInterval
@@ -204,7 +189,6 @@ Singleton {
} }
} }
// Temperature monitoring timer
Timer { Timer {
id: temperatureTimer id: temperatureTimer
interval: root.temperatureUpdateInterval interval: root.temperatureUpdateInterval
@@ -218,7 +202,6 @@ Singleton {
} }
} }
// Public functions
function getCpuInfo() { function getCpuInfo() {
cpuInfoProcess.running = true cpuInfoProcess.running = true
} }
@@ -243,15 +226,15 @@ Singleton {
} }
function getCpuUsageColor() { function getCpuUsageColor() {
if (cpuUsage > 80) return "#e74c3c" // Red if (cpuUsage > 80) return "#e74c3c"
if (cpuUsage > 60) return "#f39c12" // Orange if (cpuUsage > 60) return "#f39c12"
return "#27ae60" // Green return "#27ae60"
} }
function getMemoryUsageColor() { function getMemoryUsageColor() {
if (memoryUsage > 90) return "#e74c3c" // Red if (memoryUsage > 90) return "#e74c3c"
if (memoryUsage > 75) return "#f39c12" // Orange if (memoryUsage > 75) return "#f39c12"
return "#3498db" // Blue return "#3498db"
} }
function formatMemory(mb) { function formatMemory(mb) {
@@ -262,8 +245,8 @@ Singleton {
} }
function getTemperatureColor() { function getTemperatureColor() {
if (cpuTemperature > 80) return "#e74c3c" // Red if (cpuTemperature > 80) return "#e74c3c"
if (cpuTemperature > 65) return "#f39c12" // Orange if (cpuTemperature > 65) return "#f39c12"
return "#27ae60" // Green return "#27ae60"
} }
} }

View File

@@ -6,19 +6,23 @@ import Quickshell.Io
import "../../Common" import "../../Common"
import "../../Services" import "../../Services"
ScrollView { Item {
id: bluetoothTab id: bluetoothTab
clip: true
// These should be bound from parent
property bool bluetoothEnabled: false property bool bluetoothEnabled: false
property var bluetoothDevices: [] property var bluetoothDevices: []
ScrollView {
anchors.fill: parent
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
Column { Column {
width: parent.width width: parent.width
spacing: Theme.spacingL spacing: Theme.spacingL
// Bluetooth toggle
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 60 height: 60
@@ -73,7 +77,6 @@ ScrollView {
} }
} }
// Bluetooth devices (when enabled)
Column { Column {
width: parent.width width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingM
@@ -86,7 +89,6 @@ ScrollView {
font.weight: Font.Medium font.weight: Font.Medium
} }
// Real Bluetooth devices
Repeater { Repeater {
model: bluetoothTab.bluetoothDevices model: bluetoothTab.bluetoothDevices
@@ -117,8 +119,22 @@ ScrollView {
} }
font.family: Theme.iconFont font.family: Theme.iconFont
font.pixelSize: Theme.iconSize font.pixelSize: Theme.iconSize
color: modelData.connected ? Theme.primary : Theme.surfaceText color: {
if (modelData.connecting) return Theme.primary
if (modelData.connected) return Theme.primary
return Theme.surfaceText
}
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
opacity: modelData.connecting ? 0.6 : 1.0
Behavior on opacity {
SequentialAnimation {
running: modelData.connecting
loops: Animation.Infinite
NumberAnimation { from: 1.0; to: 0.3; duration: 800 }
NumberAnimation { from: 0.3; to: 1.0; duration: 800 }
}
}
} }
Column { Column {
@@ -136,9 +152,13 @@ ScrollView {
spacing: Theme.spacingXS spacing: Theme.spacingXS
Text { Text {
text: modelData.connected ? "Connected" : "Disconnected" text: modelData.connectionStatus
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) color: {
if (modelData.connecting) return Theme.primary
if (modelData.connectionFailed) return Theme.error
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
} }
Text { Text {
@@ -151,21 +171,67 @@ ScrollView {
} }
} }
Rectangle {
id: btMenuButton
width: 32
height: 32
radius: Theme.cornerRadius
color: btMenuButtonArea.containsMouse ?
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) :
"transparent"
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
Text {
text: "more_vert"
font.family: Theme.iconFont
font.weight: Theme.iconFontWeight
font.pixelSize: Theme.iconSize
color: Theme.surfaceText
opacity: 0.6
anchors.centerIn: parent
}
MouseArea {
id: btMenuButtonArea
anchors.fill: parent
hoverEnabled: !modelData.connecting
enabled: !modelData.connecting
cursorShape: modelData.connecting ? Qt.ArrowCursor : Qt.PointingHandCursor
onClicked: {
if (!modelData.connecting) {
bluetoothContextMenuWindow.deviceData = modelData
let localPos = btMenuButtonArea.mapToItem(bluetoothTab, btMenuButtonArea.width / 2, btMenuButtonArea.height)
bluetoothContextMenuWindow.show(localPos.x, localPos.y)
}
}
}
Behavior on color {
ColorAnimation { duration: Theme.shortDuration }
}
}
MouseArea { MouseArea {
id: btDeviceArea id: btDeviceArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true anchors.rightMargin: 40 // Don't overlap with menu button
cursorShape: Qt.PointingHandCursor hoverEnabled: !modelData.connecting
enabled: !modelData.connecting
cursorShape: modelData.connecting ? Qt.ArrowCursor : Qt.PointingHandCursor
onClicked: { onClicked: {
if (!modelData.connecting) {
BluetoothService.toggleBluetoothDevice(modelData.mac) BluetoothService.toggleBluetoothDevice(modelData.mac)
} }
} }
} }
} }
} }
}
// Available devices for pairing (when enabled)
Column { Column {
width: parent.width width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingM
@@ -186,8 +252,8 @@ ScrollView {
Item { width: 1; height: 1 } Item { width: 1; height: 1 }
Rectangle { Rectangle {
width: Math.max(100, scanText.contentWidth + Theme.spacingM * 2) width: Math.max(140, scanText.contentWidth + Theme.spacingL * 2)
height: 32 height: 36
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: scanArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) color: scanArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
border.color: Theme.primary border.color: Theme.primary
@@ -198,24 +264,16 @@ ScrollView {
spacing: Theme.spacingXS spacing: Theme.spacingXS
Text { Text {
text: BluetoothService.scanning ? "search" : "bluetooth_searching" text: BluetoothService.scanning ? "stop" : "bluetooth_searching"
font.family: Theme.iconFont font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 4 font.pixelSize: Theme.iconSize - 4
color: Theme.primary color: Theme.primary
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
RotationAnimation on rotation {
running: BluetoothService.scanning
loops: Animation.Infinite
from: 0
to: 360
duration: 2000
}
} }
Text { Text {
id: scanText id: scanText
text: BluetoothService.scanning ? "Scanning..." : "Scan" text: BluetoothService.scanning ? "Stop Scanning" : "Start Scanning"
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.primary color: Theme.primary
font.weight: Font.Medium font.weight: Font.Medium
@@ -228,16 +286,18 @@ ScrollView {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
enabled: !BluetoothService.scanning
onClicked: { onClicked: {
if (BluetoothService.scanning) {
BluetoothService.stopDiscovery()
} else {
BluetoothService.startDiscovery() BluetoothService.startDiscovery()
} }
} }
} }
} }
}
// Available devices list
Repeater { Repeater {
model: BluetoothService.availableDevices model: BluetoothService.availableDevices
@@ -309,7 +369,6 @@ ScrollView {
} }
} }
// Action button on the right
Rectangle { Rectangle {
width: 80 width: 80
height: 28 height: 28
@@ -368,9 +427,50 @@ ScrollView {
} }
} }
// No devices message Column {
width: parent.width
spacing: Theme.spacingM
visible: BluetoothService.scanning && BluetoothService.availableDevices.length === 0
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Text { Text {
text: "No devices found. Put your device in pairing mode and click Scan." text: "sync"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSizeLarge
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
RotationAnimation on rotation {
running: true
loops: Animation.Infinite
from: 0
to: 360
duration: 2000
}
}
Text {
text: "Scanning for devices..."
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
Text {
text: "Make sure your device is in pairing mode"
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
Text {
text: "No devices found. Put your device in pairing mode and click Start Scanning."
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: BluetoothService.availableDevices.length === 0 && !BluetoothService.scanning visible: BluetoothService.availableDevices.length === 0 && !BluetoothService.scanning
@@ -380,4 +480,211 @@ ScrollView {
} }
} }
} }
}
Rectangle {
id: bluetoothContextMenuWindow
property var deviceData: null
property bool menuVisible: false
visible: false
width: 160
height: menuColumn.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadiusLarge
color: Theme.popupBackground()
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
z: 1000
Rectangle {
anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
color: Qt.rgba(0, 0, 0, 0.15)
z: parent.z - 1
}
opacity: menuVisible ? 1.0 : 0.0
scale: menuVisible ? 1.0 : 0.85
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadiusSmall
color: connectArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Text {
text: bluetoothContextMenuWindow.deviceData && bluetoothContextMenuWindow.deviceData.connected ? "link_off" : "link"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: bluetoothContextMenuWindow.deviceData && bluetoothContextMenuWindow.deviceData.connected ? "Disconnect" : "Connect"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: connectArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (bluetoothContextMenuWindow.deviceData) {
BluetoothService.toggleBluetoothDevice(bluetoothContextMenuWindow.deviceData.mac)
}
bluetoothContextMenuWindow.hide()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
width: parent.width - Theme.spacingS * 2
height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadiusSmall
color: forgetArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Text {
text: "delete"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 2
color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Forget Device"
font.pixelSize: Theme.fontSizeSmall
color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: forgetArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (bluetoothContextMenuWindow.deviceData) {
BluetoothService.removeDevice(bluetoothContextMenuWindow.deviceData.mac)
}
bluetoothContextMenuWindow.hide()
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
function show(x, y) {
const menuWidth = 160
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2
let finalX = x - menuWidth / 2
let finalY = y
finalX = Math.max(0, Math.min(finalX, bluetoothTab.width - menuWidth))
finalY = Math.max(0, Math.min(finalY, bluetoothTab.height - menuHeight))
bluetoothContextMenuWindow.x = finalX
bluetoothContextMenuWindow.y = finalY
bluetoothContextMenuWindow.visible = true
bluetoothContextMenuWindow.menuVisible = true
}
function hide() {
bluetoothContextMenuWindow.menuVisible = false
Qt.callLater(() => { bluetoothContextMenuWindow.visible = false })
}
}
MouseArea {
anchors.fill: parent
visible: bluetoothContextMenuWindow.visible
onClicked: {
bluetoothContextMenuWindow.hide()
}
MouseArea {
x: bluetoothContextMenuWindow.x
y: bluetoothContextMenuWindow.y
width: bluetoothContextMenuWindow.width
height: bluetoothContextMenuWindow.height
onClicked: {
}
}
}
} }

View File

@@ -334,7 +334,7 @@ PanelWindow {
topBar.shellRoot.controlCenterVisible = !topBar.shellRoot.controlCenterVisible topBar.shellRoot.controlCenterVisible = !topBar.shellRoot.controlCenterVisible
if (topBar.shellRoot.controlCenterVisible) { if (topBar.shellRoot.controlCenterVisible) {
WifiService.scanWifi() WifiService.scanWifi()
BluetoothService.scanDevices() // Bluetooth devices are automatically updated via signals
} }
} }
} }