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

View File

@@ -68,7 +68,6 @@ Singleton {
function parseSettings(content) { function parseSettings(content) {
try { try {
console.log("Settings file content:", content)
if (content && content.trim()) { if (content && content.trim()) {
var settings = JSON.parse(content) var settings = JSON.parse(content)
themeIndex = settings.themeIndex !== undefined ? settings.themeIndex : 0 themeIndex = settings.themeIndex !== undefined ? settings.themeIndex : 0

View File

@@ -12,12 +12,13 @@ Specifically created for [Niri](https://github.com/YaLTeR/niri).
```bash ```bash
# Arch # Arch
paru -S quickshell-git nerd-fonts ttf-material-symbols-variable-git matugen cliphist cava wl-clipboard paru -S quickshell-git nerd-fonts ttf-material-symbols-variable-git matugen cliphist cava wl-clipboard ddcutil
# Some dependencies are optional # Some dependencies are optional
# - cava for audio visualizer, without it music will just randomly visualize # - cava for audio visualizer, without it music will just randomly visualize
# - cliphist for clipboard history # - cliphist for clipboard history
# - matugen for dynamic themes based on wallpaper # - matugen for dynamic themes based on wallpaper
# - ddcutil for brightness changing
``` ```
2. Configure SwayBG (Optional) 2. Configure SwayBG (Optional)

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

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

View File

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

View File

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

View File

@@ -7,15 +7,12 @@ pragma ComponentBehavior: Bound
Singleton { 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: 1500 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,8 +21,23 @@ Singleton {
property real totalCpuUsage: 0.0 property real totalCpuUsage: 0.0
property bool systemInfoAvailable: false property bool systemInfoAvailable: false
// Sorting options property var cpuHistory: []
property string sortBy: "cpu" // "cpu", "memory", "name", "pid" property var memoryHistory: []
property var networkHistory: ({rx: [], tx: []})
property var diskHistory: ({read: [], write: []})
property int historySize: 60
property var perCoreCpuUsage: []
property real networkRxRate: 0
property real networkTxRate: 0
property var lastNetworkStats: null
property real diskReadRate: 0
property real diskWriteRate: 0
property var lastDiskStats: null
property string sortBy: "cpu"
property bool sortDescending: true property bool sortDescending: true
property int maxProcesses: 20 property int maxProcesses: 20
@@ -35,10 +47,32 @@ Singleton {
console.log("ProcessMonitorService: Initialization complete") console.log("ProcessMonitorService: Initialization complete")
} }
// System information monitoring Timer {
id: testTimer
interval: 3000
running: false
repeat: false
onTriggered: {
console.log("ProcessMonitorService: Starting test monitoring...")
enableMonitoring(true)
stopTestTimer.start()
}
}
Timer {
id: stopTestTimer
interval: 8000
running: false
repeat: false
onTriggered: {
console.log("ProcessMonitorService: Stopping test monitoring...")
enableMonitoring(false)
}
}
Process { Process {
id: systemInfoProcess id: systemInfoProcess
command: ["bash", "-c", "cat /proc/meminfo; echo '---CPU---'; nproc; echo '---CPUSTAT---'; grep '^cpu ' /proc/stat"] command: ["bash", "-c", "cat /proc/meminfo; echo '---CPU---'; nproc; echo '---CPUSTAT---'; grep '^cpu' /proc/stat | head -" + (root.cpuCount + 1)]
running: false running: false
stdout: StdioCollector { stdout: StdioCollector {
@@ -57,7 +91,34 @@ Singleton {
} }
} }
// Process monitoring with ps command Process {
id: networkStatsProcess
command: ["bash", "-c", "cat /proc/net/dev | grep -E '(wlan|eth|enp|wlp|ens|eno)' | awk '{print $1,$2,$10}' | sed 's/:/ /'"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
parseNetworkStats(text.trim())
}
}
}
}
Process {
id: diskStatsProcess
command: ["bash", "-c", "cat /proc/diskstats | grep -E ' (sd[a-z]+|nvme[0-9]+n[0-9]+|vd[a-z]+) ' | grep -v 'p[0-9]' | awk '{print $3,$6,$10}'"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
parseDiskStats(text.trim())
}
}
}
}
Process { 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)]
@@ -69,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])
@@ -112,36 +171,52 @@ 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: {
if (root.monitoringEnabled) { if (root.monitoringEnabled) {
updateSystemInfo() updateSystemInfo()
updateProcessList() updateProcessList()
updateNetworkStats()
updateDiskStats()
} }
} }
} }
// 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) {
// Immediately update when enabled root.cpuHistory = []
root.memoryHistory = []
root.networkHistory = ({rx: [], tx: []})
root.diskHistory = ({read: [], write: []})
updateSystemInfo() updateSystemInfo()
updateProcessList() updateProcessList()
updateNetworkStats()
updateDiskStats()
}
}
function updateNetworkStats() {
if (!networkStatsProcess.running && root.monitoringEnabled) {
networkStatsProcess.running = true
}
}
function updateDiskStats() {
if (!diskStatsProcess.running && root.monitoringEnabled) {
diskStatsProcess.running = true
} }
} }
@@ -149,7 +224,6 @@ Singleton {
if (!root.isUpdating && root.monitoringEnabled) { 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":
@@ -208,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"
@@ -216,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) {
@@ -244,6 +317,10 @@ Singleton {
function parseSystemInfo(text) { function parseSystemInfo(text) {
const lines = text.split('\n') const lines = text.split('\n')
let section = 'memory' let section = 'memory'
const coreUsages = []
let memFree = 0
let memBuffers = 0
let memCached = 0
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim() const line = lines[i].trim()
@@ -259,9 +336,12 @@ Singleton {
if (section === 'memory') { if (section === 'memory') {
if (line.startsWith('MemTotal:')) { if (line.startsWith('MemTotal:')) {
root.totalMemoryKB = parseInt(line.split(/\s+/)[1]) root.totalMemoryKB = parseInt(line.split(/\s+/)[1])
} else if (line.startsWith('MemAvailable:')) { } else if (line.startsWith('MemFree:')) {
const availableKB = parseInt(line.split(/\s+/)[1]) memFree = parseInt(line.split(/\s+/)[1])
root.usedMemoryKB = root.totalMemoryKB - availableKB } else if (line.startsWith('Buffers:')) {
memBuffers = parseInt(line.split(/\s+/)[1])
} else if (line.startsWith('Cached:')) {
memCached = parseInt(line.split(/\s+/)[1])
} else if (line.startsWith('SwapTotal:')) { } else if (line.startsWith('SwapTotal:')) {
root.totalSwapKB = parseInt(line.split(/\s+/)[1]) root.totalSwapKB = parseInt(line.split(/\s+/)[1])
} else if (line.startsWith('SwapFree:')) { } else if (line.startsWith('SwapFree:')) {
@@ -289,10 +369,104 @@ Singleton {
const used = total - idle - iowait const used = total - idle - iowait
root.totalCpuUsage = total > 0 ? (used / total) * 100 : 0 root.totalCpuUsage = total > 0 ? (used / total) * 100 : 0
} }
} else if (line.match(/^cpu\d+/)) {
const parts = line.split(/\s+/)
if (parts.length >= 8) {
const user = parseInt(parts[1])
const nice = parseInt(parts[2])
const system = parseInt(parts[3])
const idle = parseInt(parts[4])
const iowait = parseInt(parts[5])
const irq = parseInt(parts[6])
const softirq = parseInt(parts[7])
const total = user + nice + system + idle + iowait + irq + softirq
const used = total - idle - iowait
const usage = total > 0 ? (used / total) * 100 : 0
coreUsages.push(usage)
}
} }
} }
} }
// Calculate used memory as total minus free minus buffers minus cached
root.usedMemoryKB = root.totalMemoryKB - memFree - memBuffers - memCached
// Update per-core usage
root.perCoreCpuUsage = coreUsages
// Update history
addToHistory(root.cpuHistory, root.totalCpuUsage)
const memoryPercent = root.totalMemoryKB > 0 ? (root.usedMemoryKB / root.totalMemoryKB) * 100 : 0
addToHistory(root.memoryHistory, memoryPercent)
// console.log("ProcessMonitorService: Updated - CPU:", root.totalCpuUsage.toFixed(1) + "%", "Memory:", memoryPercent.toFixed(1) + "%", "History length:", root.cpuHistory.length)
root.systemInfoAvailable = true root.systemInfoAvailable = true
} }
function parseNetworkStats(text) {
const lines = text.split('\n')
let totalRx = 0
let totalTx = 0
for (const line of lines) {
const parts = line.trim().split(/\s+/)
if (parts.length >= 3) {
const rx = parseInt(parts[1])
const tx = parseInt(parts[2])
if (!isNaN(rx) && !isNaN(tx)) {
totalRx += rx
totalTx += tx
}
}
}
if (root.lastNetworkStats) {
const timeDiff = root.processUpdateInterval / 1000
root.networkRxRate = Math.max(0, (totalRx - root.lastNetworkStats.rx) / timeDiff)
root.networkTxRate = Math.max(0, (totalTx - root.lastNetworkStats.tx) / timeDiff)
addToHistory(root.networkHistory.rx, root.networkRxRate / 1024)
addToHistory(root.networkHistory.tx, root.networkTxRate / 1024)
}
root.lastNetworkStats = { rx: totalRx, tx: totalTx }
}
function parseDiskStats(text) {
const lines = text.split('\n')
let totalRead = 0
let totalWrite = 0
for (const line of lines) {
const parts = line.trim().split(/\s+/)
if (parts.length >= 3) {
const readSectors = parseInt(parts[1])
const writeSectors = parseInt(parts[2])
if (!isNaN(readSectors) && !isNaN(writeSectors)) {
totalRead += readSectors * 512
totalWrite += writeSectors * 512
}
}
}
if (root.lastDiskStats) {
const timeDiff = root.processUpdateInterval / 1000
root.diskReadRate = Math.max(0, (totalRead - root.lastDiskStats.read) / timeDiff)
root.diskWriteRate = Math.max(0, (totalWrite - root.lastDiskStats.write) / timeDiff)
addToHistory(root.diskHistory.read, root.diskReadRate / (1024 * 1024))
addToHistory(root.diskHistory.write, root.diskWriteRate / (1024 * 1024))
}
root.lastDiskStats = { read: totalRead, write: totalWrite }
}
function addToHistory(array, value) {
array.push(value)
if (array.length > root.historySize) {
array.shift()
}
}
} }

View File

@@ -7,16 +7,13 @@ pragma ComponentBehavior: Bound
Singleton { 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,28 @@ 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 string kernelVersion: ""
property string distribution: ""
property string hostname: ""
property string uptime: ""
property string scheduler: ""
property string architecture: ""
property string loadAverage: ""
property int processCount: 0
property int threadCount: 0
property string bootTime: ""
property string motherboard: ""
property string biosVersion: ""
property var diskMounts: []
property string diskUsage: ""
property int cpuUpdateInterval: 3000 property int cpuUpdateInterval: 3000
property int memoryUpdateInterval: 5000 property int memoryUpdateInterval: 5000
property int temperatureUpdateInterval: 10000 property int temperatureUpdateInterval: 10000
property int systemInfoUpdateInterval: 30000
// Performance control
property bool enabledForTopBar: true property bool enabledForTopBar: true
property bool enabledForDetailedView: false property bool enabledForDetailedView: false
@@ -40,10 +51,10 @@ Singleton {
console.log("SystemMonitorService: Starting initialization...") console.log("SystemMonitorService: Starting initialization...")
getCpuInfo() getCpuInfo()
updateSystemStats() updateSystemStats()
updateSystemInfo()
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 +80,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 +90,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 +114,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 +139,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 +159,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 +179,282 @@ Singleton {
} }
} }
// CPU monitoring timer Process {
id: kernelInfoProcess
command: ["bash", "-c", "uname -r"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.kernelVersion = text.trim()
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Kernel info check failed with exit code:", exitCode)
}
}
}
Process {
id: distributionProcess
command: ["bash", "-c", "grep PRETTY_NAME /etc/os-release 2>/dev/null | cut -d'=' -f2 | tr -d '\"' || echo 'Unknown'"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.distribution = text.trim()
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Distribution check failed with exit code:", exitCode)
}
}
}
Process {
id: hostnameProcess
command: ["bash", "-c", "hostname"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.hostname = text.trim()
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Hostname check failed with exit code:", exitCode)
}
}
}
Process {
id: uptimeProcess
command: ["bash", "-c", "uptime -p | sed 's/up //'"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.uptime = text.trim()
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Uptime check failed with exit code:", exitCode)
}
}
}
Process {
id: schedulerProcess
command: ["bash", "-c", "cat /sys/block/sda/queue/scheduler 2>/dev/null | grep -o '\\[.*\\]' | tr -d '[]' || echo 'Unknown'"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.scheduler = text.trim()
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Scheduler check failed with exit code:", exitCode)
}
}
}
Process {
id: architectureProcess
command: ["bash", "-c", "uname -m"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.architecture = text.trim()
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Architecture check failed with exit code:", exitCode)
}
}
}
Process {
id: loadAverageProcess
command: ["bash", "-c", "cat /proc/loadavg | cut -d' ' -f1,2,3"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.loadAverage = text.trim()
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Load average check failed with exit code:", exitCode)
}
}
}
Process {
id: processCountProcess
command: ["bash", "-c", "ps aux | wc -l"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.processCount = parseInt(text.trim()) - 1
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Process count check failed with exit code:", exitCode)
}
}
}
Process {
id: threadCountProcess
command: ["bash", "-c", "cat /proc/stat | grep processes | awk '{print $2}'"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.threadCount = parseInt(text.trim())
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Thread count check failed with exit code:", exitCode)
}
}
}
Process {
id: bootTimeProcess
command: ["bash", "-c", "who -b | awk '{print $3, $4}' || stat -c %w /proc/1 2>/dev/null | cut -d' ' -f1,2 || echo 'Unknown'"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.bootTime = text.trim()
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Boot time check failed with exit code:", exitCode)
}
}
}
Process {
id: motherboardProcess
command: ["bash", "-c", "if [ -r /sys/devices/virtual/dmi/id/board_vendor ] && [ -r /sys/devices/virtual/dmi/id/board_name ]; then echo \"$(cat /sys/devices/virtual/dmi/id/board_vendor 2>/dev/null) $(cat /sys/devices/virtual/dmi/id/board_name 2>/dev/null)\"; else echo 'Unknown'; fi"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.motherboard = text.trim()
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Motherboard check failed with exit code:", exitCode)
}
}
}
Process {
id: biosProcess
command: ["bash", "-c", "if [ -r /sys/devices/virtual/dmi/id/bios_version ] && [ -r /sys/devices/virtual/dmi/id/bios_date ]; then echo \"$(cat /sys/devices/virtual/dmi/id/bios_version 2>/dev/null) $(cat /sys/devices/virtual/dmi/id/bios_date 2>/dev/null)\"; else echo 'Unknown'; fi"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
root.biosVersion = text.trim()
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("BIOS check failed with exit code:", exitCode)
}
}
}
Process {
id: diskMountsProcess
command: ["bash", "-c", "df -h --output=source,target,fstype,size,used,avail,pcent | tail -n +2 | grep -v tmpfs | grep -v devtmpfs | head -10"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim()) {
let mounts = []
const lines = text.trim().split('\n')
for (const line of lines) {
const parts = line.split(/\s+/)
if (parts.length >= 7) {
mounts.push({
device: parts[0],
mount: parts[1],
fstype: parts[2],
size: parts[3],
used: parts[4],
avail: parts[5],
percent: parts[6]
})
}
}
root.diskMounts = mounts
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Disk mounts check failed with exit code:", exitCode)
}
}
}
Timer { Timer {
id: cpuTimer id: cpuTimer
interval: root.cpuUpdateInterval interval: root.cpuUpdateInterval
@@ -190,7 +469,6 @@ Singleton {
} }
} }
// Memory monitoring timer
Timer { Timer {
id: memoryTimer id: memoryTimer
interval: root.memoryUpdateInterval interval: root.memoryUpdateInterval
@@ -204,7 +482,6 @@ Singleton {
} }
} }
// Temperature monitoring timer
Timer { Timer {
id: temperatureTimer id: temperatureTimer
interval: root.temperatureUpdateInterval interval: root.temperatureUpdateInterval
@@ -218,7 +495,19 @@ Singleton {
} }
} }
// Public functions Timer {
id: systemInfoTimer
interval: root.systemInfoUpdateInterval
running: root.enabledForDetailedView
repeat: true
onTriggered: {
if (root.enabledForDetailedView) {
updateSystemInfo()
}
}
}
function getCpuInfo() { function getCpuInfo() {
cpuInfoProcess.running = true cpuInfoProcess.running = true
} }
@@ -234,6 +523,22 @@ Singleton {
} }
} }
function updateSystemInfo() {
kernelInfoProcess.running = true
distributionProcess.running = true
hostnameProcess.running = true
uptimeProcess.running = true
schedulerProcess.running = true
architectureProcess.running = true
loadAverageProcess.running = true
processCountProcess.running = true
threadCountProcess.running = true
bootTimeProcess.running = true
motherboardProcess.running = true
biosProcess.running = true
diskMountsProcess.running = true
}
function enableTopBarMonitoring(enabled) { function enableTopBarMonitoring(enabled) {
root.enabledForTopBar = enabled root.enabledForTopBar = enabled
} }
@@ -243,15 +548,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 +567,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

@@ -7,6 +7,8 @@ Rectangle {
property bool batteryPopupVisible: false property bool batteryPopupVisible: false
signal toggleBatteryPopup()
width: 70 // Increased width to accommodate percentage text width: 70 // Increased width to accommodate percentage text
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
@@ -63,7 +65,7 @@ Rectangle {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
batteryPopupVisible = !batteryPopupVisible toggleBatteryPopup()
} }
} }

View File

@@ -78,14 +78,14 @@ Rectangle {
} }
Column { Column {
anchors.centerIn: parent anchors.fill: parent
width: parent.width - theme.spacingM * 2 anchors.margins: theme.spacingS
spacing: theme.spacingM spacing: theme.spacingS
// Show different content based on whether we have active media // Show different content based on whether we have active media
Item { Item {
width: parent.width width: parent.width
height: 80 height: 60
// Placeholder when no media // Placeholder when no media
Column { Column {
@@ -117,8 +117,8 @@ Rectangle {
// Album Art // Album Art
Rectangle { Rectangle {
width: 80 width: 60
height: 80 height: 60
radius: theme.cornerRadius radius: theme.cornerRadius
color: Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.3) color: Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.3)
@@ -152,7 +152,7 @@ Rectangle {
// Track Info // Track Info
Column { Column {
width: parent.width - 80 - theme.spacingM width: parent.width - 60 - theme.spacingM
spacing: theme.spacingXS spacing: theme.spacingXS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -186,45 +186,52 @@ Rectangle {
} }
// Progress bar // Progress bar
Rectangle { Item {
id: progressBarBackground id: progressBarContainer
width: parent.width width: parent.width
height: 6 height: 24
radius: 3
color: Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.3)
visible: activePlayer !== null
Rectangle { Rectangle {
id: progressFill id: progressBarBackground
height: parent.height width: parent.width
radius: parent.radius height: 6
color: theme.primary radius: 3
color: Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.3)
width: parent.width * ratio() visible: activePlayer !== null
Behavior on width {
NumberAnimation { duration: 100 }
}
}
// Drag handle
Rectangle {
id: progressHandle
width: 12
height: 12
radius: 6
color: theme.primary
border.color: Qt.lighter(theme.primary, 1.3)
border.width: 1
x: Math.max(0, Math.min(parent.width - width, progressFill.width - width/2))
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: activePlayer && activePlayer.length > 0 Rectangle {
scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1.0 id: progressFill
height: parent.height
radius: parent.radius
color: theme.primary
width: parent.width * ratio()
Behavior on width {
NumberAnimation { duration: 100 }
}
}
Behavior on scale { // Drag handle
NumberAnimation { duration: 150 } Rectangle {
id: progressHandle
width: 12
height: 12
radius: 6
color: theme.primary
border.color: Qt.lighter(theme.primary, 1.3)
border.width: 1
x: Math.max(0, Math.min(parent.width - width, progressFill.width - width/2))
anchors.verticalCenter: parent.verticalCenter
visible: activePlayer && activePlayer.length > 0
scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1.0
Behavior on scale {
NumberAnimation { duration: 150 }
}
} }
} }
@@ -234,22 +241,14 @@ Rectangle {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
enabled: activePlayer && activePlayer.length > 0 && activePlayer.canSeek enabled: activePlayer && activePlayer.length > 0 && activePlayer.canSeek
preventStealing: true
property bool isSeeking: false property bool isSeeking: false
onClicked: function(mouse) {
if (activePlayer && activePlayer.length > 0) {
let ratio = mouse.x / width
let seekPosition = ratio * activePlayer.length
activePlayer.position = seekPosition
currentPosition = seekPosition
}
}
onPressed: function(mouse) { onPressed: function(mouse) {
isSeeking = true isSeeking = true
if (activePlayer && activePlayer.length > 0) { if (activePlayer && activePlayer.length > 0) {
let ratio = Math.max(0, Math.min(1, mouse.x / width)) let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width))
let seekPosition = ratio * activePlayer.length let seekPosition = ratio * activePlayer.length
activePlayer.position = seekPosition activePlayer.position = seekPosition
currentPosition = seekPosition currentPosition = seekPosition
@@ -261,21 +260,54 @@ Rectangle {
} }
onPositionChanged: function(mouse) { onPositionChanged: function(mouse) {
if (pressed && activePlayer && activePlayer.length > 0) { if (pressed && isSeeking && activePlayer && activePlayer.length > 0) {
let ratio = Math.max(0, Math.min(1, mouse.x / width)) let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width))
let seekPosition = ratio * activePlayer.length let seekPosition = ratio * activePlayer.length
activePlayer.position = seekPosition activePlayer.position = seekPosition
currentPosition = seekPosition currentPosition = seekPosition
} }
} }
onClicked: function(mouse) {
if (activePlayer && activePlayer.length > 0) {
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width))
let seekPosition = ratio * activePlayer.length
activePlayer.position = seekPosition
currentPosition = seekPosition
}
}
}
// Global mouse area for drag tracking
MouseArea {
id: progressGlobalMouseArea
anchors.fill: parent.parent.parent // Fill the entire media player widget
enabled: progressMouseArea.isSeeking
visible: false
preventStealing: true
onPositionChanged: function(mouse) {
if (progressMouseArea.isSeeking && activePlayer && activePlayer.length > 0) {
let globalPos = mapToItem(progressBarBackground, mouse.x, mouse.y)
let ratio = Math.max(0, Math.min(1, globalPos.x / progressBarBackground.width))
let seekPosition = ratio * activePlayer.length
activePlayer.position = seekPosition
currentPosition = seekPosition
}
}
onReleased: {
progressMouseArea.isSeeking = false
}
} }
} }
// Control buttons - always visible // Control buttons - always visible
Row { Row {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
spacing: theme.spacingL spacing: theme.spacingM
visible: activePlayer !== null visible: activePlayer !== null
height: 32
// Previous button // Previous button
Rectangle { Rectangle {
@@ -313,9 +345,9 @@ Rectangle {
// Play/Pause button // Play/Pause button
Rectangle { Rectangle {
width: 36 width: 32
height: 36 height: 32
radius: 18 radius: 16
color: theme.primary color: theme.primary
Text { Text {

View File

@@ -11,13 +11,14 @@ Item {
property int audioSubTab: 0 // 0: Output, 1: Input property int audioSubTab: 0 // 0: Output, 1: Input
// These should be bound from parent readonly property real volumeLevel: AudioService.volumeLevel
property real volumeLevel: 50 readonly property real micLevel: AudioService.micLevel
property real micLevel: 50 readonly property bool volumeMuted: AudioService.sinkMuted
property string currentAudioSink: "" readonly property bool micMuted: AudioService.sourceMuted
property string currentAudioSource: "" readonly property string currentAudioSink: AudioService.currentAudioSink
property var audioSinks: [] readonly property string currentAudioSource: AudioService.currentAudioSource
property var audioSources: [] readonly property var audioSinks: AudioService.audioSinks
readonly property var audioSources: AudioService.audioSources
Column { Column {
anchors.fill: parent anchors.fill: parent
@@ -102,50 +103,64 @@ Item {
spacing: Theme.spacingM spacing: Theme.spacingM
Text { Text {
text: "volume_down" text: audioTab.volumeMuted ? "volume_off" : "volume_down"
font.family: Theme.iconFont font.family: Theme.iconFont
font.pixelSize: Theme.iconSize font.pixelSize: Theme.iconSize
color: Theme.surfaceText color: audioTab.volumeMuted ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: AudioService.toggleMute()
}
} }
Rectangle { Item {
id: volumeSliderTrack id: volumeSliderContainer
width: parent.width - 80 width: parent.width - 80
height: 8 height: 32
radius: 4
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
Rectangle { Rectangle {
id: volumeSliderFill id: volumeSliderTrack
width: parent.width * (audioTab.volumeLevel / 100) width: parent.width
height: parent.height height: 8
radius: parent.radius radius: 4
color: Theme.primary color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
Behavior on width {
NumberAnimation { duration: 100 }
}
}
// Draggable handle
Rectangle {
id: volumeHandle
width: 18
height: 18
radius: 9
color: Theme.primary
border.color: Qt.lighter(Theme.primary, 1.3)
border.width: 2
x: Math.max(0, Math.min(parent.width - width, volumeSliderFill.width - width/2))
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
scale: volumeMouseArea.containsMouse || volumeMouseArea.pressed ? 1.2 : 1.0 Rectangle {
id: volumeSliderFill
width: parent.width * (audioTab.volumeLevel / 100)
height: parent.height
radius: parent.radius
color: Theme.primary
Behavior on width {
NumberAnimation { duration: 100 }
}
}
Behavior on scale { // Draggable handle
NumberAnimation { duration: 150 } Rectangle {
id: volumeHandle
width: 18
height: 18
radius: 9
color: Theme.primary
border.color: Qt.lighter(Theme.primary, 1.3)
border.width: 2
x: Math.max(0, Math.min(parent.width - width, volumeSliderFill.width - width/2))
anchors.verticalCenter: parent.verticalCenter
scale: volumeMouseArea.containsMouse || volumeMouseArea.pressed ? 1.2 : 1.0
Behavior on scale {
NumberAnimation { duration: 150 }
}
} }
} }
@@ -154,20 +169,56 @@ Item {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
preventStealing: true
onClicked: (mouse) => { property bool isDragging: false
let ratio = Math.max(0, Math.min(1, mouse.x / width))
onPressed: (mouse) => {
isDragging = true
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width))
let newVolume = Math.round(ratio * 100) let newVolume = Math.round(ratio * 100)
AudioService.setVolume(newVolume) AudioService.setVolume(newVolume)
} }
onReleased: {
isDragging = false
}
onPositionChanged: (mouse) => { onPositionChanged: (mouse) => {
if (pressed) { if (pressed && isDragging) {
let ratio = Math.max(0, Math.min(1, mouse.x / width)) let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width))
let newVolume = Math.round(ratio * 100) let newVolume = Math.round(ratio * 100)
AudioService.setVolume(newVolume) AudioService.setVolume(newVolume)
} }
} }
onClicked: (mouse) => {
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width))
let newVolume = Math.round(ratio * 100)
AudioService.setVolume(newVolume)
}
}
// Global mouse area for drag tracking
MouseArea {
id: volumeGlobalMouseArea
anchors.fill: parent.parent.parent.parent.parent // Fill the entire control center
enabled: volumeMouseArea.isDragging
visible: false
preventStealing: true
onPositionChanged: (mouse) => {
if (volumeMouseArea.isDragging) {
let globalPos = mapToItem(volumeSliderTrack, mouse.x, mouse.y)
let ratio = Math.max(0, Math.min(1, globalPos.x / volumeSliderTrack.width))
let newVolume = Math.round(ratio * 100)
AudioService.setVolume(newVolume)
}
}
onReleased: {
volumeMouseArea.isDragging = false
}
} }
} }
@@ -179,13 +230,6 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
Text {
text: audioTab.volumeLevel + "%"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
} }
// Output Devices // Output Devices
@@ -224,14 +268,7 @@ Item {
} }
Text { Text {
text: "Current: " + (function() { text: "Current: " + (AudioService.currentSinkDisplayName || "None")
for (let sink of audioTab.audioSinks) {
if (sink.name === audioTab.currentAudioSink) {
return sink.displayName
}
}
return audioTab.currentAudioSink
})()
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.primary color: Theme.primary
font.weight: Font.Medium font.weight: Font.Medium
@@ -283,10 +320,16 @@ Item {
} }
Text { Text {
text: modelData.active ? "Selected" : "" text: {
if (modelData.subtitle && modelData.subtitle !== "") {
return modelData.subtitle + (modelData.active ? " • Selected" : "")
} else {
return modelData.active ? "Selected" : ""
}
}
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.8) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: modelData.active visible: text !== ""
} }
} }
} }
@@ -298,8 +341,6 @@ Item {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
console.log("Clicked audio device:", JSON.stringify(modelData))
console.log("Device name to set:", modelData.name)
AudioService.setAudioSink(modelData.name) AudioService.setAudioSink(modelData.name)
} }
} }
@@ -337,50 +378,64 @@ Item {
spacing: Theme.spacingM spacing: Theme.spacingM
Text { Text {
text: "mic" text: audioTab.micMuted ? "mic_off" : "mic"
font.family: Theme.iconFont font.family: Theme.iconFont
font.pixelSize: Theme.iconSize font.pixelSize: Theme.iconSize
color: Theme.surfaceText color: audioTab.micMuted ? Theme.error : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: AudioService.toggleMicMute()
}
} }
Rectangle { Item {
id: micSliderTrack id: micSliderContainer
width: parent.width - 80 width: parent.width - 80
height: 8 height: 32
radius: 4
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
Rectangle { Rectangle {
id: micSliderFill id: micSliderTrack
width: parent.width * (audioTab.micLevel / 100) width: parent.width
height: parent.height height: 8
radius: parent.radius radius: 4
color: Theme.primary color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
Behavior on width {
NumberAnimation { duration: 100 }
}
}
// Draggable handle
Rectangle {
id: micHandle
width: 18
height: 18
radius: 9
color: Theme.primary
border.color: Qt.lighter(Theme.primary, 1.3)
border.width: 2
x: Math.max(0, Math.min(parent.width - width, micSliderFill.width - width/2))
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
scale: micMouseArea.containsMouse || micMouseArea.pressed ? 1.2 : 1.0 Rectangle {
id: micSliderFill
width: parent.width * (audioTab.micLevel / 100)
height: parent.height
radius: parent.radius
color: Theme.primary
Behavior on width {
NumberAnimation { duration: 100 }
}
}
Behavior on scale { // Draggable handle
NumberAnimation { duration: 150 } Rectangle {
id: micHandle
width: 18
height: 18
radius: 9
color: Theme.primary
border.color: Qt.lighter(Theme.primary, 1.3)
border.width: 2
x: Math.max(0, Math.min(parent.width - width, micSliderFill.width - width/2))
anchors.verticalCenter: parent.verticalCenter
scale: micMouseArea.containsMouse || micMouseArea.pressed ? 1.2 : 1.0
Behavior on scale {
NumberAnimation { duration: 150 }
}
} }
} }
@@ -389,20 +444,56 @@ Item {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
preventStealing: true
onClicked: (mouse) => { property bool isDragging: false
let ratio = Math.max(0, Math.min(1, mouse.x / width))
onPressed: (mouse) => {
isDragging = true
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width))
let newMicLevel = Math.round(ratio * 100) let newMicLevel = Math.round(ratio * 100)
AudioService.setMicLevel(newMicLevel) AudioService.setMicLevel(newMicLevel)
} }
onReleased: {
isDragging = false
}
onPositionChanged: (mouse) => { onPositionChanged: (mouse) => {
if (pressed) { if (pressed && isDragging) {
let ratio = Math.max(0, Math.min(1, mouse.x / width)) let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width))
let newMicLevel = Math.round(ratio * 100) let newMicLevel = Math.round(ratio * 100)
AudioService.setMicLevel(newMicLevel) AudioService.setMicLevel(newMicLevel)
} }
} }
onClicked: (mouse) => {
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width))
let newMicLevel = Math.round(ratio * 100)
AudioService.setMicLevel(newMicLevel)
}
}
// Global mouse area for drag tracking
MouseArea {
id: micGlobalMouseArea
anchors.fill: parent.parent.parent.parent.parent // Fill the entire control center
enabled: micMouseArea.isDragging
visible: false
preventStealing: true
onPositionChanged: (mouse) => {
if (micMouseArea.isDragging) {
let globalPos = mapToItem(micSliderTrack, mouse.x, mouse.y)
let ratio = Math.max(0, Math.min(1, globalPos.x / micSliderTrack.width))
let newMicLevel = Math.round(ratio * 100)
AudioService.setMicLevel(newMicLevel)
}
}
onReleased: {
micMouseArea.isDragging = false
}
} }
} }
@@ -415,12 +506,6 @@ Item {
} }
} }
Text {
text: audioTab.micLevel + "%"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
} }
// Input Devices // Input Devices
@@ -459,14 +544,7 @@ Item {
} }
Text { Text {
text: "Current: " + (function() { text: "Current: " + (AudioService.currentSourceDisplayName || "None")
for (let source of audioTab.audioSources) {
if (source.name === audioTab.currentAudioSource) {
return source.displayName
}
}
return audioTab.currentAudioSource
})()
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.primary color: Theme.primary
font.weight: Font.Medium font.weight: Font.Medium
@@ -517,10 +595,16 @@ Item {
} }
Text { Text {
text: modelData.active ? "Selected" : "" text: {
if (modelData.subtitle && modelData.subtitle !== "") {
return modelData.subtitle + (modelData.active ? " • Selected" : "")
} else {
return modelData.active ? "Selected" : ""
}
}
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.8) color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
visible: modelData.active visible: text !== ""
} }
} }
} }
@@ -532,8 +616,6 @@ Item {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
console.log("Clicked audio source:", JSON.stringify(modelData))
console.log("Source name to set:", modelData.name)
AudioService.setAudioSource(modelData.name) AudioService.setAudioSource(modelData.name)
} }
} }

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

@@ -676,14 +676,6 @@ PanelWindow {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingM anchors.margins: Theme.spacingM
visible: controlCenterPopup.currentTab === "audio" visible: controlCenterPopup.currentTab === "audio"
// Bind properties from root
volumeLevel: root.volumeLevel
micLevel: root.micLevel
currentAudioSink: root.currentAudioSink
currentAudioSource: root.currentAudioSource
audioSinks: root.audioSinks
audioSources: root.audioSources
} }
// Bluetooth Tab // Bluetooth Tab

View File

@@ -36,7 +36,7 @@ ScrollView {
rightIcon: "brightness_high" rightIcon: "brightness_high"
enabled: BrightnessService.brightnessAvailable enabled: BrightnessService.brightnessAvailable
onSliderValueChanged: (newValue) => { onSliderValueChanged: function(newValue) {
BrightnessService.setBrightness(newValue) BrightnessService.setBrightness(newValue)
} }
} }

View File

@@ -110,34 +110,79 @@ Item {
} }
} }
MouseArea { Item {
id: sliderMouseArea id: sliderContainer
anchors.fill: parent anchors.fill: parent
hoverEnabled: true
cursorShape: slider.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: slider.enabled
onClicked: (mouse) => { MouseArea {
if (slider.enabled) { id: sliderMouseArea
let ratio = Math.max(0, Math.min(1, mouse.x / width)) anchors.fill: parent
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum)) hoverEnabled: true
slider.value = newValue cursorShape: slider.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
slider.sliderValueChanged(newValue) enabled: slider.enabled
preventStealing: true
property bool isDragging: false
onPressed: (mouse) => {
if (slider.enabled) {
isDragging = true
let ratio = Math.max(0, Math.min(1, mouse.x / width))
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum))
slider.value = newValue
slider.sliderValueChanged(newValue)
}
}
onReleased: {
if (slider.enabled) {
isDragging = false
slider.sliderDragFinished(slider.value)
}
}
onPositionChanged: (mouse) => {
if (pressed && isDragging && slider.enabled) {
let ratio = Math.max(0, Math.min(1, mouse.x / width))
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum))
slider.value = newValue
slider.sliderValueChanged(newValue)
}
}
onClicked: (mouse) => {
if (slider.enabled) {
let ratio = Math.max(0, Math.min(1, mouse.x / width))
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum))
slider.value = newValue
slider.sliderValueChanged(newValue)
}
} }
} }
onPositionChanged: (mouse) => { // Global mouse area for drag tracking
if (pressed && slider.enabled) { MouseArea {
let ratio = Math.max(0, Math.min(1, mouse.x / width)) id: sliderGlobalMouseArea
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum)) anchors.fill: slider.parent // Fill the entire settings popup
slider.value = newValue enabled: sliderMouseArea.isDragging
slider.sliderValueChanged(newValue) visible: false
preventStealing: true
onPositionChanged: (mouse) => {
if (sliderMouseArea.isDragging && slider.enabled) {
let globalPos = mapToItem(sliderTrack, mouse.x, mouse.y)
let ratio = Math.max(0, Math.min(1, globalPos.x / sliderTrack.width))
let newValue = Math.round(slider.minimum + ratio * (slider.maximum - slider.minimum))
slider.value = newValue
slider.sliderValueChanged(newValue)
}
} }
}
onReleased: {
onReleased: { if (sliderMouseArea.isDragging && slider.enabled) {
if (slider.enabled) { sliderMouseArea.isDragging = false
slider.sliderDragFinished(slider.value) slider.sliderDragFinished(slider.value)
}
} }
} }
} }

View File

@@ -281,10 +281,10 @@ PanelWindow {
height: 80 height: 80
radius: Theme.cornerRadiusLarge radius: Theme.cornerRadiusLarge
color: ProcessMonitorService.totalSwapKB > 0 ? color: ProcessMonitorService.totalSwapKB > 0 ?
Qt.rgba(Theme.tertiary.r, Theme.tertiary.g, Theme.tertiary.b, 0.08) : Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08) :
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.04) Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.04)
border.color: ProcessMonitorService.totalSwapKB > 0 ? border.color: ProcessMonitorService.totalSwapKB > 0 ?
Qt.rgba(Theme.tertiary.r, Theme.tertiary.g, Theme.tertiary.b, 0.2) : Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.2) :
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.12) Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.12)
border.width: 1 border.width: 1
@@ -298,7 +298,7 @@ PanelWindow {
text: "Swap" text: "Swap"
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
color: ProcessMonitorService.totalSwapKB > 0 ? Theme.tertiary : Theme.surfaceText color: ProcessMonitorService.totalSwapKB > 0 ? Theme.warning : Theme.surfaceText
opacity: 0.8 opacity: 0.8
} }

View File

@@ -443,10 +443,10 @@ PanelWindow {
height: 80 height: 80
radius: Theme.cornerRadiusLarge radius: Theme.cornerRadiusLarge
color: ProcessMonitorService.totalSwapKB > 0 ? color: ProcessMonitorService.totalSwapKB > 0 ?
Qt.rgba(Theme.tertiary.r, Theme.tertiary.g, Theme.tertiary.b, 0.08) : Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08) :
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.04) Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.04)
border.color: ProcessMonitorService.totalSwapKB > 0 ? border.color: ProcessMonitorService.totalSwapKB > 0 ?
Qt.rgba(Theme.tertiary.r, Theme.tertiary.g, Theme.tertiary.b, 0.2) : Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.2) :
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.12) Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.12)
border.width: 1 border.width: 1
@@ -460,7 +460,7 @@ PanelWindow {
text: "Swap" text: "Swap"
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium font.weight: Font.Medium
color: ProcessMonitorService.totalSwapKB > 0 ? Theme.tertiary : Theme.surfaceText color: ProcessMonitorService.totalSwapKB > 0 ? Theme.warning : Theme.surfaceText
opacity: 0.8 opacity: 0.8
} }
@@ -749,36 +749,365 @@ PanelWindow {
// Define inline components for tabs // Define inline components for tabs
Component { Component {
id: performanceTabComponent id: performanceTabComponent
Rectangle {
color: "transparent" Column {
anchors.fill: parent
spacing: Theme.spacingM
Column { // CPU Section - Compact with per-core bars
anchors.centerIn: parent Rectangle {
spacing: Theme.spacingL width: parent.width
height: 200
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.04)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
border.width: 1
Text { Column {
text: "analytics" anchors.fill: parent
font.family: Theme.iconFont anchors.margins: Theme.spacingM
font.pixelSize: 48 spacing: Theme.spacingS
color: Theme.primary
opacity: 0.6 // CPU Header with overall usage
anchors.horizontalCenter: parent.horizontalCenter Row {
width: parent.width
height: 32
spacing: Theme.spacingM
Text {
text: "CPU"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Rectangle {
width: 80
height: 24
radius: Theme.cornerRadiusSmall
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
anchors.verticalCenter: parent.verticalCenter
Text {
text: ProcessMonitorService.totalCpuUsage.toFixed(1) + "%"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.primary
anchors.centerIn: parent
}
}
Item { width: parent.width - 280; height: 1 }
Text {
text: ProcessMonitorService.cpuCount + " cores"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
}
// Per-core CPU bars - Scrollable
ScrollView {
width: parent.width
height: parent.height - 40
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
Column {
width: parent.width
spacing: 6
Repeater {
model: ProcessMonitorService.perCoreCpuUsage.length
Row {
width: parent.width
height: 20
spacing: Theme.spacingS
// Core label
Text {
text: "C" + index
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: 24
anchors.verticalCenter: parent.verticalCenter
}
// Usage bar
Rectangle {
width: parent.width - 80
height: 6
radius: 3
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: parent.width * Math.min(1.0, ProcessMonitorService.perCoreCpuUsage[index] / 100)
height: parent.height
radius: parent.radius
color: {
const usage = ProcessMonitorService.perCoreCpuUsage[index]
if (usage > 80) return Theme.error
if (usage > 60) return Theme.warning
return Theme.primary
}
Behavior on width {
NumberAnimation { duration: Theme.shortDuration }
}
}
}
// Usage percentage
Text {
text: ProcessMonitorService.perCoreCpuUsage[index] ?
ProcessMonitorService.perCoreCpuUsage[index].toFixed(0) + "%" : "0%"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
width: 32
horizontalAlignment: Text.AlignRight
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
}
}
// Memory Section - Simplified
Rectangle {
width: parent.width
height: 80
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.04)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
border.width: 1
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingM
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 4
Text {
text: "Memory"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
}
Text {
text: ProcessMonitorService.formatSystemMemory(ProcessMonitorService.usedMemoryKB) +
" / " + ProcessMonitorService.formatSystemMemory(ProcessMonitorService.totalMemoryKB)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
Item { width: Theme.spacingL; height: 1 }
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 4
width: 200
Rectangle {
width: parent.width
height: 16
radius: 8
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
Rectangle {
width: ProcessMonitorService.totalMemoryKB > 0 ?
parent.width * (ProcessMonitorService.usedMemoryKB / ProcessMonitorService.totalMemoryKB) : 0
height: parent.height
radius: parent.radius
color: {
const usage = ProcessMonitorService.totalMemoryKB > 0 ?
(ProcessMonitorService.usedMemoryKB / ProcessMonitorService.totalMemoryKB) : 0
if (usage > 0.9) return Theme.error
if (usage > 0.7) return Theme.warning
return Theme.secondary
}
Behavior on width {
NumberAnimation { duration: Theme.mediumDuration }
}
}
}
Text {
text: ProcessMonitorService.totalMemoryKB > 0 ?
((ProcessMonitorService.usedMemoryKB / ProcessMonitorService.totalMemoryKB) * 100).toFixed(1) + "% used" :
"No data"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
Item { width: parent.width - 300; height: 1 }
// Swap info - compact
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 4
visible: ProcessMonitorService.totalSwapKB > 0
Text {
text: "Swap"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.warning
}
Text {
text: ProcessMonitorService.formatSystemMemory(ProcessMonitorService.usedSwapKB) +
" / " + ProcessMonitorService.formatSystemMemory(ProcessMonitorService.totalSwapKB)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
}
// Network & Disk I/O - Combined compact view
Row {
width: parent.width
height: 80
spacing: Theme.spacingM
// Network I/O
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 80
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.04)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
border.width: 1
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
Text {
text: "Network"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
Row {
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
Row {
spacing: 4
Text {
text: "↓"
font.pixelSize: Theme.fontSizeSmall
color: Theme.info
}
Text {
text: formatNetworkSpeed(ProcessMonitorService.networkRxRate)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
Row {
spacing: 4
Text {
text: "↑"
font.pixelSize: Theme.fontSizeSmall
color: Theme.error
}
Text {
text: formatNetworkSpeed(ProcessMonitorService.networkTxRate)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
}
}
} }
Text { // Disk I/O
text: "Performance Monitoring" Rectangle {
font.pixelSize: Theme.fontSizeLarge + 2 width: (parent.width - Theme.spacingM) / 2
font.weight: Font.Bold height: 80
color: Theme.surfaceText radius: Theme.cornerRadiusLarge
anchors.horizontalCenter: parent.horizontalCenter color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.04)
} border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.06)
border.width: 1
Text {
text: "Real-time system performance charts\nwill be displayed here" Column {
font.pixelSize: Theme.fontSizeMedium anchors.centerIn: parent
color: Theme.surfaceVariantText spacing: Theme.spacingXS
horizontalAlignment: Text.AlignHCenter
anchors.horizontalCenter: parent.horizontalCenter Text {
text: "Disk"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
}
Row {
spacing: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
Row {
spacing: 4
Text {
text: "R"
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
}
Text {
text: formatDiskSpeed(ProcessMonitorService.diskReadRate)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
Row {
spacing: 4
Text {
text: "W"
font.pixelSize: Theme.fontSizeSmall
color: Theme.warning
}
Text {
text: formatDiskSpeed(ProcessMonitorService.diskWriteRate)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
}
}
}
}
} }
} }
} }
@@ -786,36 +1115,481 @@ PanelWindow {
Component { Component {
id: systemTabComponent id: systemTabComponent
Rectangle {
color: "transparent" ScrollView {
anchors.fill: parent
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
Column { Column {
anchors.centerIn: parent width: parent.width
spacing: Theme.spacingL spacing: Theme.spacingM
Text { Row {
text: "settings" width: parent.width
font.family: Theme.iconFont height: 140
font.pixelSize: 48 spacing: Theme.spacingM
color: Theme.primary
opacity: 0.6 Rectangle {
anchors.horizontalCenter: parent.horizontalCenter width: (parent.width - Theme.spacingM) / 2
height: 140
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16)
border.width: 1
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
anchors.bottomMargin: Theme.spacingM + 4
spacing: Theme.spacingXS
Row {
width: parent.width
spacing: Theme.spacingS
Text {
text: "computer"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "System"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Text {
text: "Host: " + SystemMonitorService.hostname
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
Text {
text: "OS: " + SystemMonitorService.distribution
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
Text {
text: "Arch: " + SystemMonitorService.architecture
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
Text {
text: "Kernel: " + SystemMonitorService.kernelVersion
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
}
}
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 140
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
border.color: Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.16)
border.width: 1
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
anchors.bottomMargin: Theme.spacingM + 4
spacing: Theme.spacingXS
Row {
width: parent.width
spacing: Theme.spacingS
Text {
text: "developer_board"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: Theme.secondary
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Hardware"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Text {
text: "CPU: " + SystemMonitorService.cpuModel
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
Text {
text: "Motherboard: " + SystemMonitorService.motherboard
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
Text {
text: "BIOS: " + SystemMonitorService.biosVersion
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
Text {
text: "Memory: " + SystemMonitorService.formatMemory(SystemMonitorService.totalMemory)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
}
}
} }
Text { Row {
text: "System Information" width: parent.width
font.pixelSize: Theme.fontSizeLarge + 2 height: 120
font.weight: Font.Bold spacing: Theme.spacingM
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 120
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08)
border.color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.16)
border.width: 1
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
anchors.bottomMargin: Theme.spacingM + 4
spacing: Theme.spacingXS
Row {
width: parent.width
spacing: Theme.spacingS
Text {
text: "timer"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: Theme.warning
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Uptime"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Text {
text: SystemMonitorService.uptime
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
Text {
text: "Boot: " + SystemMonitorService.bootTime
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
Text {
text: "Load: " + SystemMonitorService.loadAverage
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
}
}
Rectangle {
width: (parent.width - Theme.spacingM) / 2
height: 120
radius: Theme.cornerRadiusLarge
color: Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.08)
border.color: Qt.rgba(Theme.info.r, Theme.info.g, Theme.info.b, 0.16)
border.width: 1
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
anchors.bottomMargin: Theme.spacingM + 4
spacing: Theme.spacingXS
Row {
width: parent.width
spacing: Theme.spacingS
Text {
text: "list_alt"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: Theme.info
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Processes"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Text {
text: SystemMonitorService.processCount + " Running"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
Text {
text: SystemMonitorService.threadCount + " Total Created"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
}
}
} }
Text { Rectangle {
text: "Kernel information, schedulers,\nand system details will be shown here" width: parent.width
font.pixelSize: Theme.fontSizeMedium height: Math.max(200, diskMountRepeater.count * 28 + 60)
color: Theme.surfaceVariantText radius: Theme.cornerRadiusLarge
horizontalAlignment: Text.AlignHCenter color: Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.08)
anchors.horizontalCenter: parent.horizontalCenter border.color: Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.16)
border.width: 1
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
Row {
width: parent.width
spacing: Theme.spacingS
Text {
text: "storage"
font.family: Theme.iconFont
font.pixelSize: Theme.iconSize
color: Theme.success
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Storage & Disks"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Text {
text: "I/O Scheduler: " + SystemMonitorService.scheduler
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
ScrollView {
width: parent.width
height: parent.height - 60
clip: true
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
Column {
width: parent.width
spacing: 2
Row {
width: parent.width
height: 24
spacing: Theme.spacingS
Text {
text: "Device"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.25
elide: Text.ElideRight
}
Text {
text: "Mount"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.2
elide: Text.ElideRight
}
Text {
text: "Size"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
}
Text {
text: "Used"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
}
Text {
text: "Available"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
}
Text {
text: "Use%"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Bold
color: Theme.surfaceText
width: parent.width * 0.1
elide: Text.ElideRight
}
}
Repeater {
id: diskMountRepeater
model: SystemMonitorService.diskMounts
Rectangle {
width: parent.width
height: 24
radius: Theme.cornerRadiusSmall
color: diskMouseArea.containsMouse ?
Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.04) :
"transparent"
MouseArea {
id: diskMouseArea
anchors.fill: parent
hoverEnabled: true
}
Row {
anchors.fill: parent
spacing: Theme.spacingS
Text {
text: modelData.device
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width * 0.25
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: modelData.mount
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width * 0.2
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: modelData.size
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: modelData.used
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: modelData.avail
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: parent.width * 0.15
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: modelData.percent
font.pixelSize: Theme.fontSizeSmall
color: {
const percent = parseInt(modelData.percent)
if (percent > 90) return Theme.error
if (percent > 75) return Theme.warning
return Theme.surfaceText
}
width: parent.width * 0.1
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
}
}
} }
} }
} }
@@ -1155,10 +1929,13 @@ PanelWindow {
processListWidget.isVisible = true processListWidget.isVisible = true
ProcessMonitorService.updateSystemInfo() ProcessMonitorService.updateSystemInfo()
ProcessMonitorService.updateProcessList() ProcessMonitorService.updateProcessList()
SystemMonitorService.enableDetailedMonitoring(true)
SystemMonitorService.updateSystemInfo()
} }
function hide() { function hide() {
processListWidget.isVisible = false processListWidget.isVisible = false
SystemMonitorService.enableDetailedMonitoring(false)
} }
function toggle() { function toggle() {
@@ -1168,4 +1945,27 @@ PanelWindow {
show() show()
} }
} }
// Helper functions for formatting
function formatNetworkSpeed(bytesPerSec) {
if (bytesPerSec < 1024) {
return bytesPerSec.toFixed(0) + " B/s"
} else if (bytesPerSec < 1024 * 1024) {
return (bytesPerSec / 1024).toFixed(1) + " KB/s"
} else if (bytesPerSec < 1024 * 1024 * 1024) {
return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s"
} else {
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s"
}
}
function formatDiskSpeed(bytesPerSec) {
if (bytesPerSec < 1024 * 1024) {
return (bytesPerSec / 1024).toFixed(1) + " KB/s"
} else if (bytesPerSec < 1024 * 1024 * 1024) {
return (bytesPerSec / (1024 * 1024)).toFixed(1) + " MB/s"
} else {
return (bytesPerSec / (1024 * 1024 * 1024)).toFixed(1) + " GB/s"
}
}
} }

View File

@@ -11,7 +11,6 @@ Item {
property bool hasActiveMedia: false property bool hasActiveMedia: false
property var activePlayer: null property var activePlayer: null
property bool cavaAvailable: false property bool cavaAvailable: false
property bool configCreated: false
width: 20 width: 20
height: Theme.iconSize height: Theme.iconSize
@@ -22,53 +21,20 @@ Item {
running: true running: true
onExited: (exitCode) => { onExited: (exitCode) => {
root.cavaAvailable = exitCode === 0 root.cavaAvailable = exitCode === 0
if (root.cavaAvailable && !root.configCreated) { if (root.cavaAvailable) {
console.log("cava found - creating config and enabling real audio visualization") console.log("cava found - enabling real audio visualization")
configWriter.running = true cavaProcess.running = Qt.binding(() => root.hasActiveMedia && root.activePlayer?.playbackState === MprisPlaybackState.Playing)
} else if (!root.cavaAvailable) { } else {
console.log("cava not found - using fallback animation") console.log("cava not found - using fallback animation")
fallbackTimer.running = Qt.binding(() => root.hasActiveMedia && root.activePlayer?.playbackState === MprisPlaybackState.Playing) fallbackTimer.running = Qt.binding(() => root.hasActiveMedia && root.activePlayer?.playbackState === MprisPlaybackState.Playing)
} }
} }
} }
Process {
id: configWriter
running: false
command: [
"sh", "-c",
`cat > /tmp/quickshell_cava_config << 'EOF'
[general]
mode = normal
framerate = 30
autosens = 0
sensitivity = 50
bars = 4
[output]
method = raw
raw_target = /dev/stdout
data_format = ascii
channels = mono
mono_option = average
[smoothing]
noise_reduction = 20
EOF`
]
onExited: {
root.configCreated = true
if (root.cavaAvailable) {
cavaProcess.running = Qt.binding(() => root.hasActiveMedia && root.activePlayer?.playbackState === MprisPlaybackState.Playing)
}
}
}
Process { Process {
id: cavaProcess id: cavaProcess
running: false running: false
command: ["cava", "-p", "/tmp/quickshell_cava_config"] command: ["sh", "-c", `printf '[general]\nmode=normal\nframerate=30\nautosens=0\nsensitivity=50\nbars=4\n[output]\nmethod=raw\nraw_target=/dev/stdout\ndata_format=ascii\nchannels=mono\nmono_option=average\n[smoothing]\nnoise_reduction=20' | cava -p /dev/stdin`]
stdout: SplitParser { stdout: SplitParser {
splitMarker: "\n" splitMarker: "\n"

View File

@@ -8,6 +8,7 @@ Rectangle {
property string networkStatus: "disconnected" property string networkStatus: "disconnected"
property string wifiSignalStrength: "good" property string wifiSignalStrength: "good"
property int volumeLevel: 50 property int volumeLevel: 50
property bool volumeMuted: false
property bool bluetoothAvailable: false property bool bluetoothAvailable: false
property bool bluetoothEnabled: false property bool bluetoothEnabled: false
property bool isActive: false property bool isActive: false
@@ -62,7 +63,7 @@ Rectangle {
// Audio Icon // Audio Icon
Text { Text {
text: root.volumeLevel === 0 ? "volume_off" : text: root.volumeMuted ? "volume_off" :
root.volumeLevel < 33 ? "volume_down" : "volume_up" root.volumeLevel < 33 ? "volume_down" : "volume_up"
font.family: Theme.iconFont font.family: Theme.iconFont
font.pixelSize: Theme.iconSize - 8 font.pixelSize: Theme.iconSize - 8

View File

@@ -41,6 +41,7 @@ PanelWindow {
property string networkStatus: "disconnected" property string networkStatus: "disconnected"
property string wifiSignalStrength: "good" property string wifiSignalStrength: "good"
property int volumeLevel: 50 property int volumeLevel: 50
property bool volumeMuted: false
property bool bluetoothAvailable: false property bool bluetoothAvailable: false
property bool bluetoothEnabled: false property bool bluetoothEnabled: false
@@ -298,6 +299,10 @@ PanelWindow {
// Battery Widget // Battery Widget
BatteryWidget { BatteryWidget {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
batteryPopupVisible: topBar.shellRoot.batteryPopupVisible
onToggleBatteryPopup: {
topBar.shellRoot.batteryPopupVisible = !topBar.shellRoot.batteryPopupVisible
}
} }
ControlCenterButton { ControlCenterButton {
@@ -305,6 +310,7 @@ PanelWindow {
networkStatus: topBar.networkStatus networkStatus: topBar.networkStatus
wifiSignalStrength: topBar.wifiSignalStrength wifiSignalStrength: topBar.wifiSignalStrength
volumeLevel: topBar.volumeLevel volumeLevel: topBar.volumeLevel
volumeMuted: topBar.volumeMuted
bluetoothAvailable: topBar.bluetoothAvailable bluetoothAvailable: topBar.bluetoothAvailable
bluetoothEnabled: topBar.bluetoothEnabled bluetoothEnabled: topBar.bluetoothEnabled
isActive: topBar.shellRoot ? topBar.shellRoot.controlCenterVisible : false isActive: topBar.shellRoot ? topBar.shellRoot.controlCenterVisible : false
@@ -314,7 +320,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
} }
} }
} }

View File

@@ -27,7 +27,7 @@ ShellRoot {
// Initialize service monitoring states based on preferences // Initialize service monitoring states based on preferences
SystemMonitorService.enableTopBarMonitoring(Prefs.showSystemResources) SystemMonitorService.enableTopBarMonitoring(Prefs.showSystemResources)
ProcessMonitorService.enableMonitoring(false) // Start disabled, enable when process dropdown is opened ProcessMonitorService.enableMonitoring(false) // Start disabled, enable when process dropdown is opened
AudioService.enableDeviceScanning(false) // Start disabled, enable when control center is opened // Audio service auto-updates devices, no manual scanning needed
} }
property bool calendarVisible: false property bool calendarVisible: false
@@ -44,14 +44,13 @@ ShellRoot {
property bool hasActiveMedia: activePlayer && (activePlayer.trackTitle || activePlayer.trackArtist) property bool hasActiveMedia: activePlayer && (activePlayer.trackTitle || activePlayer.trackArtist)
property bool controlCenterVisible: false property bool controlCenterVisible: false
// Monitor control center visibility to enable/disable audio device scanning // Monitor control center visibility to enable/disable bluetooth scanning
onControlCenterVisibleChanged: { onControlCenterVisibleChanged: {
console.log("Control center", controlCenterVisible ? "opened" : "closed") console.log("Control center", controlCenterVisible ? "opened" : "closed")
AudioService.enableDeviceScanning(controlCenterVisible)
BluetoothService.enableMonitoring(controlCenterVisible) BluetoothService.enableMonitoring(controlCenterVisible)
if (controlCenterVisible) { if (controlCenterVisible) {
// Immediately refresh devices when opening control center // Refresh devices when opening control center
AudioService.refreshDevices() AudioService.updateDevices()
} }
} }
property bool batteryPopupVisible: false property bool batteryPopupVisible: false
@@ -80,6 +79,7 @@ ShellRoot {
// Audio properties from AudioService // Audio properties from AudioService
property int volumeLevel: AudioService.volumeLevel property int volumeLevel: AudioService.volumeLevel
property bool volumeMuted: AudioService.sinkMuted
property var audioSinks: AudioService.audioSinks property var audioSinks: AudioService.audioSinks
property string currentAudioSink: AudioService.currentAudioSink property string currentAudioSink: AudioService.currentAudioSink
@@ -180,6 +180,7 @@ ShellRoot {
networkStatus: root.networkStatus networkStatus: root.networkStatus
wifiSignalStrength: root.wifiSignalStrength wifiSignalStrength: root.wifiSignalStrength
volumeLevel: root.volumeLevel volumeLevel: root.volumeLevel
volumeMuted: root.volumeMuted
bluetoothAvailable: root.bluetoothAvailable bluetoothAvailable: root.bluetoothAvailable
bluetoothEnabled: root.bluetoothEnabled bluetoothEnabled: root.bluetoothEnabled
shellRoot: root shellRoot: root