1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-07 14:05: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 Component.onCompleted: {
command: ["bluetoothctl", "show"] // Use default controller refreshBluetoothState()
running: true updateDevices()
}
Connections {
target: Bluetooth
stdout: StdioCollector { function onDefaultAdapterChanged() {
onStreamFinished: { console.log("BluetoothService: Default adapter changed")
root.bluetoothAvailable = text.trim() !== "" && !text.includes("No default controller") refreshBluetoothState()
root.bluetoothEnabled = text.includes("Powered: yes") updateDevices()
}
if (root.bluetoothEnabled && root.bluetoothAvailable) { }
bluetoothDeviceScanner.running = true
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 { } else {
root.bluetoothDevices = [] let newDevice = createBluetoothDevice(device, deviceType, displayName)
newPairedDevices.push(newDevice)
} }
} } else {
} if (Bluetooth.defaultAdapter.discovering && isDeviceDiscoverable(device)) {
} let existingDevice = findDeviceInList(root.availableDevices, device.address)
if (existingDevice) {
Process { updateDeviceData(existingDevice, device, deviceType, displayName)
id: bluetoothDeviceScanner newAvailableDevices.push(existingDevice)
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"] } else {
running: false let newDevice = createBluetoothDevice(device, deviceType, displayName)
newAvailableDevices.push(newDevice)
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
})
}
}
} }
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() { 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
', root) 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() { 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
pairedDeviceChecker.discoveredToMerge = discoveredDevices
pairedDeviceChecker.running = true
} }
// Get only currently connected/paired devices that matter Timer {
Process { id: connectionTimeout
id: availableDeviceScanner interval: 10000 // 10 second timeout
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: {
onTriggered: {
let devices = [] if (deviceMac) {
if (text.trim()) { let typedDevice = findDeviceInList(root.bluetoothDevices, deviceMac)
let lines = text.trim().split('\n') if (!typedDevice) {
typedDevice = findDeviceInList(root.availableDevices, deviceMac)
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
})
}
}
}
} }
root.availableDevices = devices if (typedDevice && typedDevice.connecting && !typedDevice.connected) {
} console.log("Connection timeout for device:", deviceMac)
} typedDevice.connecting = false
} typedDevice.connectionFailed = true
// 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')
for (let line of lines) { // Clear failure state after 3 seconds
if (line.trim()) { Qt.callLater(() => {
let parts = line.split('|') clearFailureTimer.deviceMac = deviceMac
if (parts.length >= 4) { clearFailureTimer.start()
let mac = parts[0].trim() })
let name = parts[1].trim() }
let paired = parts[2].trim() === 'yes' deviceMac = ""
let connected = parts[3].trim() === 'yes' }
}
// Only include if actually paired }
if (!paired) continue
Timer {
// Check if already in discovered list id: clearFailureTimer
if (seenMacs.has(mac)) { interval: 3000
// Update existing device to show it's paired running: false
let existing = allDevices.find(d => d.mac === mac) repeat: false
if (existing) {
existing.paired = true property string deviceMac: ""
existing.connected = connected
existing.canPair = false onTriggered: {
} if (deviceMac) {
continue let typedDevice = findDeviceInList(root.bluetoothDevices, deviceMac)
} if (!typedDevice) {
typedDevice = findDeviceInList(root.availableDevices, deviceMac)
// 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
})
}
}
}
} }
root.availableDevices = allDevices if (typedDevice) {
root.scanning = false typedDevice.connectionFailed = false
}
deviceMac = ""
} }
} }
} }
component BluetoothDevice: QtObject {
required property string mac
required property string name
required property string type
required property bool paired
required property bool connected
required property int battery
required property string signalStrength
required property bool canPair
required property var nativeDevice // Reference to native Quickshell device
property bool connecting: false
property bool connectionFailed: false
readonly property string displayName: name
readonly property bool batteryAvailable: battery >= 0
readonly property string connectionStatus: {
if (connecting) return "Connecting..."
if (connectionFailed) return "Connection Failed"
if (connected) return "Connected"
return "Disconnected"
}
}
Component {
id: deviceComponent
BluetoothDevice {}
}
} }

View File

@@ -7,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,206 +6,445 @@ 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: []
Column { ScrollView {
width: parent.width anchors.fill: parent
spacing: Theme.spacingL clip: true
// Bluetooth toggle ScrollBar.vertical.policy: ScrollBar.AsNeeded
Rectangle { ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
width: parent.width
height: 60
radius: Theme.cornerRadius
color: bluetoothToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
(bluetoothTab.bluetoothEnabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12))
border.color: bluetoothTab.bluetoothEnabled ? Theme.primary : "transparent"
border.width: 2
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingL
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Text {
text: "bluetooth"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSizeLarge
color: bluetoothTab.bluetoothEnabled ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
Text {
text: "Bluetooth"
font.pixelSize: Theme.fontSizeLarge
color: bluetoothTab.bluetoothEnabled ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
}
Text {
text: bluetoothTab.bluetoothEnabled ? "Enabled" : "Disabled"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
}
}
MouseArea {
id: bluetoothToggle
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
BluetoothService.toggleBluetooth()
}
}
}
// Bluetooth devices (when enabled)
Column { Column {
width: parent.width width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingL
visible: bluetoothTab.bluetoothEnabled
Text { Rectangle {
text: "Paired Devices" width: parent.width
font.pixelSize: Theme.fontSizeLarge height: 60
color: Theme.surfaceText radius: Theme.cornerRadius
font.weight: Font.Medium color: bluetoothToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) :
} (bluetoothTab.bluetoothEnabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12))
border.color: bluetoothTab.bluetoothEnabled ? Theme.primary : "transparent"
// Real Bluetooth devices border.width: 2
Repeater {
model: bluetoothTab.bluetoothDevices
Rectangle { Row {
width: parent.width anchors.left: parent.left
height: 60 anchors.leftMargin: Theme.spacingL
radius: Theme.cornerRadius anchors.verticalCenter: parent.verticalCenter
color: btDeviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : spacing: Theme.spacingM
(modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: modelData.connected ? Theme.primary : "transparent"
border.width: 1
Row { Text {
anchors.left: parent.left text: "bluetooth"
anchors.leftMargin: Theme.spacingM font.family: Theme.iconFont
font.pixelSize: Theme.iconSizeLarge
color: bluetoothTab.bluetoothEnabled ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Text { Text {
text: { text: "Bluetooth"
switch (modelData.type) { font.pixelSize: Theme.fontSizeLarge
case "headset": return "headset" color: bluetoothTab.bluetoothEnabled ? Theme.primary : Theme.surfaceText
case "mouse": return "mouse" font.weight: Font.Medium
case "keyboard": return "keyboard"
case "phone": return "smartphone"
default: return "bluetooth"
}
}
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: modelData.connected ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
} }
Column { Text {
spacing: 2 text: bluetoothTab.bluetoothEnabled ? "Enabled" : "Disabled"
anchors.verticalCenter: parent.verticalCenter font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
Text {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
color: modelData.connected ? Theme.primary : Theme.surfaceText
font.weight: modelData.connected ? Font.Medium : Font.Normal
}
Row {
spacing: Theme.spacingXS
Text {
text: modelData.connected ? "Connected" : "Disconnected"
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
Text {
text: modelData.battery >= 0 ? "• " + modelData.battery + "%" : ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: modelData.battery >= 0
}
}
}
}
MouseArea {
id: btDeviceArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
BluetoothService.toggleBluetoothDevice(modelData.mac)
} }
} }
} }
MouseArea {
id: bluetoothToggle
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
BluetoothService.toggleBluetooth()
}
}
} }
}
// Available devices for pairing (when enabled)
Column {
width: parent.width
spacing: Theme.spacingM
visible: bluetoothTab.bluetoothEnabled
Row { Column {
width: parent.width width: parent.width
spacing: Theme.spacingM spacing: Theme.spacingM
visible: bluetoothTab.bluetoothEnabled
Text { Text {
text: "Available Devices" text: "Paired Devices"
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText color: Theme.surfaceText
font.weight: Font.Medium font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
} }
Item { width: 1; height: 1 } Repeater {
model: bluetoothTab.bluetoothDevices
Rectangle {
width: parent.width
height: 60
radius: Theme.cornerRadius
color: btDeviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
(modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: modelData.connected ? Theme.primary : "transparent"
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Text {
text: {
switch (modelData.type) {
case "headset": return "headset"
case "mouse": return "mouse"
case "keyboard": return "keyboard"
case "phone": return "smartphone"
default: return "bluetooth"
}
}
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: {
if (modelData.connecting) return Theme.primary
if (modelData.connected) return Theme.primary
return Theme.surfaceText
}
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 {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
Text {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
color: modelData.connected ? Theme.primary : Theme.surfaceText
font.weight: modelData.connected ? Font.Medium : Font.Normal
}
Row {
spacing: Theme.spacingXS
Text {
text: modelData.connectionStatus
font.pixelSize: Theme.fontSizeSmall
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: modelData.battery >= 0 ? "• " + modelData.battery + "%" : ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: modelData.battery >= 0
}
}
}
}
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 {
id: btDeviceArea
anchors.fill: parent
anchors.rightMargin: 40 // Don't overlap with menu button
hoverEnabled: !modelData.connecting
enabled: !modelData.connecting
cursorShape: modelData.connecting ? Qt.ArrowCursor : Qt.PointingHandCursor
onClicked: {
if (!modelData.connecting) {
BluetoothService.toggleBluetoothDevice(modelData.mac)
}
}
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: bluetoothTab.bluetoothEnabled
Rectangle { Row {
width: Math.max(100, scanText.contentWidth + Theme.spacingM * 2) width: parent.width
height: 32 spacing: Theme.spacingM
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) Text {
border.color: Theme.primary text: "Available Devices"
border.width: 1 font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item { width: 1; height: 1 }
Rectangle {
width: Math.max(140, scanText.contentWidth + Theme.spacingL * 2)
height: 36
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)
border.color: Theme.primary
border.width: 1
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
Text {
text: BluetoothService.scanning ? "stop" : "bluetooth_searching"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 4
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Text {
id: scanText
text: BluetoothService.scanning ? "Stop Scanning" : "Start Scanning"
font.pixelSize: Theme.fontSizeMedium
color: Theme.primary
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: scanArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (BluetoothService.scanning) {
BluetoothService.stopDiscovery()
} else {
BluetoothService.startDiscovery()
}
}
}
}
}
Repeater {
model: BluetoothService.availableDevices
Rectangle {
width: parent.width
height: 70
radius: Theme.cornerRadius
color: availableDeviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
(modelData.paired ? Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: modelData.paired ? Theme.secondary : (modelData.canPair ? Theme.primary : "transparent")
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Text {
text: {
switch (modelData.type) {
case "headset": return "headset"
case "mouse": return "mouse"
case "keyboard": return "keyboard"
case "phone": return "smartphone"
case "watch": return "watch"
case "speaker": return "speaker"
case "tv": return "tv"
default: return "bluetooth"
}
}
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: modelData.paired ? Theme.secondary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
Text {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
color: modelData.paired ? Theme.secondary : Theme.surfaceText
font.weight: modelData.paired ? Font.Medium : Font.Normal
}
Row {
spacing: Theme.spacingXS
Text {
text: {
if (modelData.paired && modelData.connected) return "Connected"
if (modelData.paired) return "Paired"
return "Signal: " + modelData.signalStrength
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
Text {
text: modelData.rssi !== 0 ? "• " + modelData.rssi + " dBm" : ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
visible: modelData.rssi !== 0
}
}
}
}
Rectangle {
width: 80
height: 28
radius: Theme.cornerRadiusSmall
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
color: actionButtonArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
border.color: Theme.primary
border.width: 1
visible: modelData.canPair || modelData.paired
Text {
anchors.centerIn: parent
text: modelData.paired ? (modelData.connected ? "Disconnect" : "Connect") : "Pair"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
}
MouseArea {
id: actionButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData.paired) {
if (modelData.connected) {
BluetoothService.toggleBluetoothDevice(modelData.mac)
} else {
BluetoothService.connectDevice(modelData.mac)
}
} else {
BluetoothService.pairDevice(modelData.mac)
}
}
}
}
MouseArea {
id: availableDeviceArea
anchors.fill: parent
anchors.rightMargin: 90 // Don't overlap with action button
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData.paired) {
BluetoothService.toggleBluetoothDevice(modelData.mac)
} else {
BluetoothService.pairDevice(modelData.mac)
}
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: BluetoothService.scanning && BluetoothService.availableDevices.length === 0
Row { Row {
anchors.centerIn: parent anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXS spacing: Theme.spacingM
Text { Text {
text: BluetoothService.scanning ? "search" : "bluetooth_searching" text: "sync"
font.family: Theme.iconFont font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 4 font.pixelSize: Theme.iconSizeLarge
color: Theme.primary color: Theme.primary
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
RotationAnimation on rotation { RotationAnimation on rotation {
running: BluetoothService.scanning running: true
loops: Animation.Infinite loops: Animation.Infinite
from: 0 from: 0
to: 360 to: 360
@@ -214,169 +453,237 @@ ScrollView {
} }
Text { Text {
id: scanText text: "Scanning for devices..."
text: BluetoothService.scanning ? "Scanning..." : "Scan" font.pixelSize: Theme.fontSizeLarge
font.pixelSize: Theme.fontSizeMedium color: Theme.surfaceText
color: Theme.primary
font.weight: Font.Medium font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
MouseArea { Text {
id: scanArea text: "Make sure your device is in pairing mode"
anchors.fill: parent font.pixelSize: Theme.fontSizeMedium
hoverEnabled: true color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
cursorShape: Qt.PointingHandCursor anchors.horizontalCenter: parent.horizontalCenter
enabled: !BluetoothService.scanning }
}
onClicked: {
BluetoothService.startDiscovery() Text {
text: "No devices found. Put your device in pairing mode and click Start Scanning."
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: BluetoothService.availableDevices.length === 0 && !BluetoothService.scanning
wrapMode: Text.WordWrap
width: parent.width
horizontalAlignment: Text.AlignHCenter
}
}
}
}
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
} }
} }
} }
// Available devices list Rectangle {
Repeater { width: parent.width - Theme.spacingS * 2
model: BluetoothService.availableDevices height: 5
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle { Rectangle {
anchors.centerIn: parent
width: parent.width width: parent.width
height: 70 height: 1
radius: Theme.cornerRadius color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
color: availableDeviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
(modelData.paired ? Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
border.color: modelData.paired ? Theme.secondary : (modelData.canPair ? Theme.primary : "transparent")
border.width: 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
Text {
text: {
switch (modelData.type) {
case "headset": return "headset"
case "mouse": return "mouse"
case "keyboard": return "keyboard"
case "phone": return "smartphone"
case "watch": return "watch"
case "speaker": return "speaker"
case "tv": return "tv"
default: return "bluetooth"
}
}
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: modelData.paired ? Theme.secondary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
Text {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
color: modelData.paired ? Theme.secondary : Theme.surfaceText
font.weight: modelData.paired ? Font.Medium : Font.Normal
}
Row {
spacing: Theme.spacingXS
Text {
text: {
if (modelData.paired && modelData.connected) return "Connected"
if (modelData.paired) return "Paired"
return "Signal: " + modelData.signalStrength
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
}
Text {
text: modelData.rssi !== 0 ? "• " + modelData.rssi + " dBm" : ""
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
visible: modelData.rssi !== 0
}
}
}
}
// Action button on the right
Rectangle {
width: 80
height: 28
radius: Theme.cornerRadiusSmall
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
color: actionButtonArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
border.color: Theme.primary
border.width: 1
visible: modelData.canPair || modelData.paired
Text {
anchors.centerIn: parent
text: modelData.paired ? (modelData.connected ? "Disconnect" : "Connect") : "Pair"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
font.weight: Font.Medium
}
MouseArea {
id: actionButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData.paired) {
if (modelData.connected) {
BluetoothService.toggleBluetoothDevice(modelData.mac)
} else {
BluetoothService.connectDevice(modelData.mac)
}
} else {
BluetoothService.pairDevice(modelData.mac)
}
}
}
}
MouseArea {
id: availableDeviceArea
anchors.fill: parent
anchors.rightMargin: 90 // Don't overlap with action button
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData.paired) {
BluetoothService.toggleBluetoothDevice(modelData.mac)
} else {
BluetoothService.pairDevice(modelData.mac)
}
}
}
} }
} }
// No devices message Rectangle {
Text {
text: "No devices found. Put your device in pairing mode and click Scan."
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: BluetoothService.availableDevices.length === 0 && !BluetoothService.scanning
wrapMode: Text.WordWrap
width: parent.width width: parent.width
horizontalAlignment: Text.AlignHCenter 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
} }
} }
} }