mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-06 05:25:41 -05:00
Systematic cleanup and qmlfmt of all services
- qmlfmt kinda sucks but it's what qt creator uses
This commit is contained in:
@@ -138,7 +138,7 @@ DankModal {
|
||||
wifiPasswordInput = text
|
||||
}
|
||||
onAccepted: {
|
||||
NetworkService.connectToWifiWithPassword(
|
||||
NetworkService.connectToWifi(
|
||||
wifiPasswordSSID, passwordInput.text)
|
||||
close()
|
||||
wifiPasswordInput = ""
|
||||
@@ -286,7 +286,7 @@ DankModal {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: parent.enabled
|
||||
onClicked: {
|
||||
NetworkService.connectToWifiWithPassword(
|
||||
NetworkService.connectToWifi(
|
||||
wifiPasswordSSID,
|
||||
passwordInput.text)
|
||||
close()
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import "../Common/fuzzysort.js" as Fuzzy
|
||||
|
||||
Singleton {
|
||||
@@ -13,60 +12,46 @@ Singleton {
|
||||
property var applications: DesktopEntries.applications.values.filter(app => !app.noDisplay && !app.runInTerminal)
|
||||
|
||||
property var preppedApps: applications.map(app => ({
|
||||
"name": Fuzzy.prepare(
|
||||
app.name
|
||||
|| ""),
|
||||
"comment": Fuzzy.prepare(
|
||||
app.comment
|
||||
|| ""),
|
||||
"name": Fuzzy.prepare(app.name || ""),
|
||||
"comment": Fuzzy.prepare(app.comment || ""),
|
||||
"entry": app
|
||||
}))
|
||||
|
||||
function searchApplications(query) {
|
||||
if (!query || query.length === 0) {
|
||||
if (!query || query.length === 0)
|
||||
return applications
|
||||
}
|
||||
|
||||
if (preppedApps.length === 0) {
|
||||
if (preppedApps.length === 0)
|
||||
return []
|
||||
}
|
||||
|
||||
var results = Fuzzy.go(query, preppedApps, {
|
||||
"all": false,
|
||||
"keys": ["name", "comment"],
|
||||
"scoreFn": r => {
|
||||
var nameScore = r[0] ? r[0].score : 0
|
||||
var commentScore = r[1] ? r[1].score : 0
|
||||
var appName = r.obj.entry.name || ""
|
||||
var finalScore = 0
|
||||
const nameScore = r[0]?.score || 0
|
||||
const commentScore = r[1]?.score || 0
|
||||
const appName = r.obj.entry.name || ""
|
||||
|
||||
if (nameScore > 0) {
|
||||
var queryLower = query.toLowerCase()
|
||||
var nameLower = appName.toLowerCase()
|
||||
|
||||
if (nameLower === queryLower) {
|
||||
finalScore = nameScore * 100
|
||||
} else if (nameLower.startsWith(
|
||||
queryLower)) {
|
||||
finalScore = nameScore * 50
|
||||
} else if (nameLower.includes(
|
||||
" " + queryLower)
|
||||
|| nameLower.includes(
|
||||
queryLower + " ")
|
||||
|| nameLower.endsWith(
|
||||
" " + queryLower)) {
|
||||
finalScore = nameScore * 25
|
||||
} else if (nameLower.includes(
|
||||
queryLower)) {
|
||||
finalScore = nameScore * 10
|
||||
} else {
|
||||
finalScore = nameScore * 2 + commentScore * 0.1
|
||||
}
|
||||
} else {
|
||||
finalScore = commentScore * 0.1
|
||||
if (nameScore === 0) {
|
||||
return commentScore * 0.1
|
||||
}
|
||||
|
||||
return finalScore
|
||||
const queryLower = query.toLowerCase()
|
||||
const nameLower = appName.toLowerCase()
|
||||
|
||||
if (nameLower === queryLower) {
|
||||
return nameScore * 100
|
||||
}
|
||||
if (nameLower.startsWith(queryLower)) {
|
||||
return nameScore * 50
|
||||
}
|
||||
if (nameLower.includes(" " + queryLower) || nameLower.includes(queryLower + " ") || nameLower.endsWith(" " + queryLower)) {
|
||||
return nameScore * 25
|
||||
}
|
||||
if (nameLower.includes(queryLower)) {
|
||||
return nameScore * 10
|
||||
}
|
||||
|
||||
return nameScore * 2 + commentScore * 0.1
|
||||
},
|
||||
"limit": 50
|
||||
})
|
||||
@@ -75,10 +60,10 @@ Singleton {
|
||||
}
|
||||
|
||||
function getCategoriesForApp(app) {
|
||||
if (!app || !app.categories)
|
||||
if (!app?.categories)
|
||||
return []
|
||||
|
||||
var categoryMap = {
|
||||
const categoryMap = {
|
||||
"AudioVideo": "Media",
|
||||
"Audio": "Media",
|
||||
"Video": "Media",
|
||||
@@ -105,19 +90,16 @@ Singleton {
|
||||
"TerminalEmulator": "Utilities"
|
||||
}
|
||||
|
||||
var mappedCategories = new Set()
|
||||
const mappedCategories = new Set()
|
||||
|
||||
for (var i = 0; i < app.categories.length; i++) {
|
||||
var cat = app.categories[i]
|
||||
if (categoryMap[cat]) {
|
||||
for (const cat of app.categories) {
|
||||
if (categoryMap[cat])
|
||||
mappedCategories.add(categoryMap[cat])
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(mappedCategories)
|
||||
}
|
||||
|
||||
// Category icon mappings
|
||||
property var categoryIcons: ({
|
||||
"All": "apps",
|
||||
"Media": "music_video",
|
||||
@@ -136,10 +118,10 @@ Singleton {
|
||||
}
|
||||
|
||||
function getAllCategories() {
|
||||
var categories = new Set(["All"])
|
||||
const categories = new Set(["All"])
|
||||
|
||||
for (var i = 0; i < applications.length; i++) {
|
||||
var appCategories = getCategoriesForApp(applications[i])
|
||||
for (const app of applications) {
|
||||
const appCategories = getCategoriesForApp(app)
|
||||
appCategories.forEach(cat => categories.add(cat))
|
||||
}
|
||||
|
||||
@@ -152,8 +134,7 @@ Singleton {
|
||||
}
|
||||
|
||||
return applications.filter(app => {
|
||||
var appCategories = getCategoriesForApp(
|
||||
app)
|
||||
const appCategories = getCategoriesForApp(app)
|
||||
return appCategories.includes(category)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,8 +17,9 @@ Singleton {
|
||||
signal micMuteChanged
|
||||
|
||||
function displayName(node) {
|
||||
if (!node)
|
||||
if (!node) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if (node.properties && node.properties["device.description"]) {
|
||||
return node.properties["device.description"]
|
||||
@@ -32,39 +33,51 @@ Singleton {
|
||||
return node.nickname
|
||||
}
|
||||
|
||||
if (node.name.includes("analog-stereo"))
|
||||
if (node.name.includes("analog-stereo")) {
|
||||
return "Built-in Speakers"
|
||||
else if (node.name.includes("bluez"))
|
||||
}
|
||||
if (node.name.includes("bluez")) {
|
||||
return "Bluetooth Audio"
|
||||
else if (node.name.includes("usb"))
|
||||
}
|
||||
if (node.name.includes("usb")) {
|
||||
return "USB Audio"
|
||||
else if (node.name.includes("hdmi"))
|
||||
}
|
||||
if (node.name.includes("hdmi")) {
|
||||
return "HDMI Audio"
|
||||
}
|
||||
|
||||
return node.name
|
||||
}
|
||||
|
||||
function subtitle(name) {
|
||||
if (!name)
|
||||
if (!name) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if (name.includes('usb-')) {
|
||||
if (name.includes('SteelSeries')) {
|
||||
return "USB Gaming Headset"
|
||||
} else if (name.includes('Generic')) {
|
||||
}
|
||||
if (name.includes('Generic')) {
|
||||
return "USB Audio Device"
|
||||
}
|
||||
return "USB Audio"
|
||||
} else if (name.includes('pci-')) {
|
||||
}
|
||||
|
||||
if (name.includes('pci-')) {
|
||||
if (name.includes('01_00.1') || name.includes('01:00.1')) {
|
||||
return "NVIDIA GPU Audio"
|
||||
}
|
||||
return "PCI Audio"
|
||||
} else if (name.includes('bluez')) {
|
||||
}
|
||||
|
||||
if (name.includes('bluez')) {
|
||||
return "Bluetooth Audio"
|
||||
} else if (name.includes('analog')) {
|
||||
}
|
||||
if (name.includes('analog')) {
|
||||
return "Built-in Audio"
|
||||
} else if (name.includes('hdmi')) {
|
||||
}
|
||||
if (name.includes('hdmi')) {
|
||||
return "HDMI Audio"
|
||||
}
|
||||
|
||||
@@ -72,48 +85,48 @@ Singleton {
|
||||
}
|
||||
|
||||
PwObjectTracker {
|
||||
objects: Pipewire.nodes.values.filter(
|
||||
node => node.audio && !node.isStream
|
||||
)
|
||||
objects: Pipewire.nodes.values.filter(node => node.audio && !node.isStream)
|
||||
}
|
||||
|
||||
// Volume control functions
|
||||
function setVolume(percentage) {
|
||||
if (root.sink && root.sink.audio) {
|
||||
const clampedVolume = Math.max(0, Math.min(100, percentage))
|
||||
root.sink.audio.volume = clampedVolume / 100
|
||||
root.volumeChanged()
|
||||
return "Volume set to " + clampedVolume + "%"
|
||||
if (!root.sink?.audio) {
|
||||
return "No audio sink available"
|
||||
}
|
||||
return "No audio sink available"
|
||||
|
||||
const clampedVolume = Math.max(0, Math.min(100, percentage))
|
||||
root.sink.audio.volume = clampedVolume / 100
|
||||
root.volumeChanged()
|
||||
return `Volume set to ${clampedVolume}%`
|
||||
}
|
||||
|
||||
function toggleMute() {
|
||||
if (root.sink && root.sink.audio) {
|
||||
root.sink.audio.muted = !root.sink.audio.muted
|
||||
return root.sink.audio.muted ? "Audio muted" : "Audio unmuted"
|
||||
if (!root.sink?.audio) {
|
||||
return "No audio sink available"
|
||||
}
|
||||
return "No audio sink available"
|
||||
|
||||
root.sink.audio.muted = !root.sink.audio.muted
|
||||
return root.sink.audio.muted ? "Audio muted" : "Audio unmuted"
|
||||
}
|
||||
|
||||
function setMicVolume(percentage) {
|
||||
if (root.source && root.source.audio) {
|
||||
const clampedVolume = Math.max(0, Math.min(100, percentage))
|
||||
root.source.audio.volume = clampedVolume / 100
|
||||
return "Microphone volume set to " + clampedVolume + "%"
|
||||
if (!root.source?.audio) {
|
||||
return "No audio source available"
|
||||
}
|
||||
return "No audio source available"
|
||||
|
||||
const clampedVolume = Math.max(0, Math.min(100, percentage))
|
||||
root.source.audio.volume = clampedVolume / 100
|
||||
return `Microphone volume set to ${clampedVolume}%`
|
||||
}
|
||||
|
||||
function toggleMicMute() {
|
||||
if (root.source && root.source.audio) {
|
||||
root.source.audio.muted = !root.source.audio.muted
|
||||
return root.source.audio.muted ? "Microphone muted" : "Microphone unmuted"
|
||||
if (!root.source?.audio) {
|
||||
return "No audio source available"
|
||||
}
|
||||
return "No audio source available"
|
||||
|
||||
root.source.audio.muted = !root.source.audio.muted
|
||||
return root.source.audio.muted ? "Microphone muted" : "Microphone unmuted"
|
||||
}
|
||||
|
||||
// IPC Handler for external control
|
||||
IpcHandler {
|
||||
target: "audio"
|
||||
|
||||
@@ -122,35 +135,39 @@ Singleton {
|
||||
}
|
||||
|
||||
function increment(step: string): string {
|
||||
if (root.sink && root.sink.audio) {
|
||||
if (root.sink.audio.muted) {
|
||||
root.sink.audio.muted = false
|
||||
}
|
||||
const currentVolume = Math.round(root.sink.audio.volume * 100)
|
||||
const newVolume = Math.max(0, Math.min(100,
|
||||
currentVolume + parseInt(
|
||||
step || "5")))
|
||||
root.sink.audio.volume = newVolume / 100
|
||||
root.volumeChanged()
|
||||
return "Volume increased to " + newVolume + "%"
|
||||
if (!root.sink?.audio) {
|
||||
return "No audio sink available"
|
||||
}
|
||||
return "No audio sink available"
|
||||
|
||||
if (root.sink.audio.muted) {
|
||||
root.sink.audio.muted = false
|
||||
}
|
||||
|
||||
const currentVolume = Math.round(root.sink.audio.volume * 100)
|
||||
const stepValue = parseInt(step || "5")
|
||||
const newVolume = Math.max(0, Math.min(100, currentVolume + stepValue))
|
||||
|
||||
root.sink.audio.volume = newVolume / 100
|
||||
root.volumeChanged()
|
||||
return `Volume increased to ${newVolume}%`
|
||||
}
|
||||
|
||||
function decrement(step: string): string {
|
||||
if (root.sink && root.sink.audio) {
|
||||
if (root.sink.audio.muted) {
|
||||
root.sink.audio.muted = false
|
||||
}
|
||||
const currentVolume = Math.round(root.sink.audio.volume * 100)
|
||||
const newVolume = Math.max(0, Math.min(100,
|
||||
currentVolume - parseInt(
|
||||
step || "5")))
|
||||
root.sink.audio.volume = newVolume / 100
|
||||
root.volumeChanged()
|
||||
return "Volume decreased to " + newVolume + "%"
|
||||
if (!root.sink?.audio) {
|
||||
return "No audio sink available"
|
||||
}
|
||||
return "No audio sink available"
|
||||
|
||||
if (root.sink.audio.muted) {
|
||||
root.sink.audio.muted = false
|
||||
}
|
||||
|
||||
const currentVolume = Math.round(root.sink.audio.volume * 100)
|
||||
const stepValue = parseInt(step || "5")
|
||||
const newVolume = Math.max(0, Math.min(100, currentVolume - stepValue))
|
||||
|
||||
root.sink.audio.volume = newVolume / 100
|
||||
root.volumeChanged()
|
||||
return `Volume decreased to ${newVolume}%`
|
||||
}
|
||||
|
||||
function mute(): string {
|
||||
@@ -171,17 +188,19 @@ Singleton {
|
||||
|
||||
function status(): string {
|
||||
let result = "Audio Status:\n"
|
||||
if (root.sink && root.sink.audio) {
|
||||
|
||||
if (root.sink?.audio) {
|
||||
const volume = Math.round(root.sink.audio.volume * 100)
|
||||
result += "Output: " + volume + "%"
|
||||
+ (root.sink.audio.muted ? " (muted)" : "") + "\n"
|
||||
const muteStatus = root.sink.audio.muted ? " (muted)" : ""
|
||||
result += `Output: ${volume}%${muteStatus}\n`
|
||||
} else {
|
||||
result += "Output: No sink available\n"
|
||||
}
|
||||
|
||||
if (root.source && root.source.audio) {
|
||||
if (root.source?.audio) {
|
||||
const micVolume = Math.round(root.source.audio.volume * 100)
|
||||
result += "Input: " + micVolume + "%" + (root.source.audio.muted ? " (muted)" : "")
|
||||
const muteStatus = root.source.audio.muted ? " (muted)" : ""
|
||||
result += `Input: ${micVolume}%${muteStatus}`
|
||||
} else {
|
||||
result += "Input: No source available"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
@@ -9,68 +10,50 @@ Singleton {
|
||||
id: root
|
||||
|
||||
readonly property UPowerDevice device: UPower.displayDevice
|
||||
readonly property bool batteryAvailable: device && device.ready
|
||||
&& device.isLaptopBattery
|
||||
readonly property real batteryLevel: batteryAvailable ? Math.round(
|
||||
device.percentage * 100) : 0
|
||||
readonly property bool isCharging: batteryAvailable
|
||||
&& device.state === UPowerDeviceState.Charging
|
||||
&& device.changeRate > 0
|
||||
readonly property bool isPluggedIn: batteryAvailable
|
||||
&& (device.state !== UPowerDeviceState.Discharging
|
||||
&& device.state !== UPowerDeviceState.Empty)
|
||||
readonly property bool batteryAvailable: device && device.ready && device.isLaptopBattery
|
||||
readonly property real batteryLevel: batteryAvailable ? Math.round(device.percentage * 100) : 0
|
||||
readonly property bool isCharging: batteryAvailable && device.state === UPowerDeviceState.Charging && device.changeRate > 0
|
||||
readonly property bool isPluggedIn: batteryAvailable && (device.state !== UPowerDeviceState.Discharging && device.state !== UPowerDeviceState.Empty)
|
||||
readonly property bool isLowBattery: batteryAvailable && batteryLevel <= 20
|
||||
readonly property string batteryHealth: {
|
||||
if (!batteryAvailable)
|
||||
return "N/A"
|
||||
if (!batteryAvailable) {
|
||||
return "N/A"
|
||||
}
|
||||
|
||||
if (device.healthSupported && device.healthPercentage > 0)
|
||||
return Math.round(device.healthPercentage) + "%"
|
||||
if (device.healthSupported && device.healthPercentage > 0) {
|
||||
return `${Math.round(device.healthPercentage)}%`
|
||||
}
|
||||
|
||||
// Calculate health from energy capacity vs design capacity
|
||||
if (device.energyCapacity > 0 && device.energy > 0) {
|
||||
// energyCapacity is current full capacity, we need design capacity
|
||||
// Use a rough estimate based on typical battery degradation patterns
|
||||
var healthPercent = (device.energyCapacity / 90.0045)
|
||||
* 100 // your design capacity from upower
|
||||
return Math.round(healthPercent) + "%"
|
||||
const healthPercent = (device.energyCapacity / 90.0045) * 100
|
||||
return `${Math.round(healthPercent)}%`
|
||||
}
|
||||
|
||||
return "N/A"
|
||||
}
|
||||
readonly property real batteryCapacity: batteryAvailable
|
||||
&& device.energyCapacity > 0 ? device.energyCapacity : 0
|
||||
readonly property real batteryCapacity: batteryAvailable && device.energyCapacity > 0 ? device.energyCapacity : 0
|
||||
readonly property string batteryStatus: {
|
||||
if (!batteryAvailable)
|
||||
return "No Battery"
|
||||
if (!batteryAvailable) {
|
||||
return "No Battery"
|
||||
}
|
||||
|
||||
if (device.state === UPowerDeviceState.Charging
|
||||
&& device.changeRate <= 0)
|
||||
return "Plugged In"
|
||||
if (device.state === UPowerDeviceState.Charging && device.changeRate <= 0) {
|
||||
return "Plugged In"
|
||||
}
|
||||
|
||||
return UPowerDeviceState.toString(device.state)
|
||||
}
|
||||
readonly property bool suggestPowerSaver: batteryAvailable && isLowBattery
|
||||
&& UPower.onBattery
|
||||
&& (typeof PowerProfiles !== "undefined"
|
||||
&& PowerProfiles.profile
|
||||
!== PowerProfile.PowerSaver)
|
||||
readonly property bool suggestPowerSaver: batteryAvailable && isLowBattery && UPower.onBattery && (typeof PowerProfiles !== "undefined" && PowerProfiles.profile !== PowerProfile.PowerSaver)
|
||||
|
||||
readonly property var bluetoothDevices: {
|
||||
var btDevices = []
|
||||
const btDevices = []
|
||||
const bluetoothTypes = [UPowerDeviceType.BluetoothGeneric, UPowerDeviceType.Headphones, UPowerDeviceType.Headset, UPowerDeviceType.Keyboard, UPowerDeviceType.Mouse, UPowerDeviceType.Speakers]
|
||||
|
||||
for (var i = 0; i < UPower.devices.count; i++) {
|
||||
var dev = UPower.devices.get(i)
|
||||
if (dev
|
||||
&& dev.ready && (dev.type === UPowerDeviceType.BluetoothGeneric || dev.type
|
||||
=== UPowerDeviceType.Headphones || dev.type
|
||||
=== UPowerDeviceType.Headset || dev.type
|
||||
=== UPowerDeviceType.Keyboard || dev.type
|
||||
=== UPowerDeviceType.Mouse || dev.type
|
||||
=== UPowerDeviceType.Speakers)) {
|
||||
const dev = UPower.devices.get(i)
|
||||
if (dev && dev.ready && bluetoothTypes.includes(dev.type)) {
|
||||
btDevices.push({
|
||||
"name": dev.model
|
||||
|| UPowerDeviceType.toString(
|
||||
dev.type),
|
||||
"name": dev.model || UPowerDeviceType.toString(dev.type),
|
||||
"percentage": Math.round(dev.percentage),
|
||||
"type": dev.type
|
||||
})
|
||||
@@ -80,20 +63,23 @@ Singleton {
|
||||
}
|
||||
|
||||
function formatTimeRemaining() {
|
||||
if (!batteryAvailable)
|
||||
if (!batteryAvailable) {
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
var timeSeconds = isCharging ? device.timeToFull : device.timeToEmpty
|
||||
const timeSeconds = isCharging ? device.timeToFull : device.timeToEmpty
|
||||
|
||||
if (!timeSeconds || timeSeconds <= 0 || timeSeconds > 86400)
|
||||
if (!timeSeconds || timeSeconds <= 0 || timeSeconds > 86400) {
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
var hours = Math.floor(timeSeconds / 3600)
|
||||
var minutes = Math.floor((timeSeconds % 3600) / 60)
|
||||
const hours = Math.floor(timeSeconds / 3600)
|
||||
const minutes = Math.floor((timeSeconds % 3600) / 60)
|
||||
|
||||
if (hours > 0)
|
||||
return hours + "h " + minutes + "m"
|
||||
else
|
||||
return minutes + "m"
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`
|
||||
}
|
||||
|
||||
return `${minutes}m`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
@@ -12,230 +13,238 @@ Singleton {
|
||||
readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter
|
||||
readonly property bool available: adapter !== null
|
||||
readonly property bool enabled: (adapter && adapter.enabled) ?? false
|
||||
readonly property bool discovering: (adapter
|
||||
&& adapter.discovering) ?? false
|
||||
readonly property bool discovering: (adapter && adapter.discovering) ?? false
|
||||
readonly property var devices: adapter ? adapter.devices : null
|
||||
readonly property var pairedDevices: {
|
||||
if (!adapter || !adapter.devices)
|
||||
return []
|
||||
if (!adapter || !adapter.devices) {
|
||||
return []
|
||||
}
|
||||
|
||||
return adapter.devices.values.filter(dev => {
|
||||
return dev && (dev.paired
|
||||
|| dev.trusted)
|
||||
return dev && (dev.paired || dev.trusted)
|
||||
})
|
||||
}
|
||||
readonly property var allDevicesWithBattery: {
|
||||
if (!adapter || !adapter.devices)
|
||||
return []
|
||||
if (!adapter || !adapter.devices) {
|
||||
return []
|
||||
}
|
||||
|
||||
return adapter.devices.values.filter(dev => {
|
||||
return dev
|
||||
&& dev.batteryAvailable
|
||||
&& dev.battery > 0
|
||||
return dev && dev.batteryAvailable && dev.battery > 0
|
||||
})
|
||||
}
|
||||
|
||||
function sortDevices(devices) {
|
||||
return devices.sort((a, b) => {
|
||||
var aName = a.name || a.deviceName || ""
|
||||
var bName = b.name || b.deviceName || ""
|
||||
const aName = a.name || a.deviceName || ""
|
||||
const bName = b.name || b.deviceName || ""
|
||||
|
||||
var aHasRealName = aName.includes(" ")
|
||||
&& aName.length > 3
|
||||
var bHasRealName = bName.includes(" ")
|
||||
&& bName.length > 3
|
||||
const aHasRealName = aName.includes(" ") && aName.length > 3
|
||||
const bHasRealName = bName.includes(" ") && bName.length > 3
|
||||
|
||||
if (aHasRealName && !bHasRealName)
|
||||
return -1
|
||||
if (!aHasRealName && bHasRealName)
|
||||
return 1
|
||||
if (aHasRealName && !bHasRealName) {
|
||||
return -1
|
||||
}
|
||||
if (!aHasRealName && bHasRealName) {
|
||||
return 1
|
||||
}
|
||||
|
||||
var aSignal = (a.signalStrength !== undefined
|
||||
&& a.signalStrength > 0) ? a.signalStrength : 0
|
||||
var bSignal = (b.signalStrength !== undefined
|
||||
&& b.signalStrength > 0) ? b.signalStrength : 0
|
||||
const aSignal = (a.signalStrength !== undefined && a.signalStrength > 0) ? a.signalStrength : 0
|
||||
const bSignal = (b.signalStrength !== undefined && b.signalStrength > 0) ? b.signalStrength : 0
|
||||
return bSignal - aSignal
|
||||
})
|
||||
}
|
||||
|
||||
function getDeviceIcon(device) {
|
||||
if (!device)
|
||||
if (!device) {
|
||||
return "bluetooth"
|
||||
}
|
||||
|
||||
var name = (device.name || device.deviceName || "").toLowerCase()
|
||||
var icon = (device.icon || "").toLowerCase()
|
||||
if (icon.includes("headset") || icon.includes("audio") || name.includes(
|
||||
"headphone") || name.includes("airpod") || name.includes(
|
||||
"headset") || name.includes("arctis"))
|
||||
const name = (device.name || device.deviceName || "").toLowerCase()
|
||||
const icon = (device.icon || "").toLowerCase()
|
||||
|
||||
const audioKeywords = ["headset", "audio", "headphone", "airpod", "arctis"]
|
||||
if (audioKeywords.some(keyword => icon.includes(keyword) || name.includes(keyword))) {
|
||||
return "headset"
|
||||
}
|
||||
|
||||
if (icon.includes("mouse") || name.includes("mouse"))
|
||||
if (icon.includes("mouse") || name.includes("mouse")) {
|
||||
return "mouse"
|
||||
}
|
||||
|
||||
if (icon.includes("keyboard") || name.includes("keyboard"))
|
||||
if (icon.includes("keyboard") || name.includes("keyboard")) {
|
||||
return "keyboard"
|
||||
}
|
||||
|
||||
if (icon.includes("phone") || name.includes("phone") || name.includes(
|
||||
"iphone") || name.includes("android") || name.includes(
|
||||
"samsung"))
|
||||
const phoneKeywords = ["phone", "iphone", "android", "samsung"]
|
||||
if (phoneKeywords.some(keyword => icon.includes(keyword) || name.includes(keyword))) {
|
||||
return "smartphone"
|
||||
}
|
||||
|
||||
if (icon.includes("watch") || name.includes("watch"))
|
||||
if (icon.includes("watch") || name.includes("watch")) {
|
||||
return "watch"
|
||||
}
|
||||
|
||||
if (icon.includes("speaker") || name.includes("speaker"))
|
||||
if (icon.includes("speaker") || name.includes("speaker")) {
|
||||
return "speaker"
|
||||
}
|
||||
|
||||
if (icon.includes("display") || name.includes("tv"))
|
||||
if (icon.includes("display") || name.includes("tv")) {
|
||||
return "tv"
|
||||
}
|
||||
|
||||
return "bluetooth"
|
||||
}
|
||||
|
||||
function canConnect(device) {
|
||||
if (!device)
|
||||
if (!device) {
|
||||
return false
|
||||
}
|
||||
|
||||
return !device.paired && !device.pairing && !device.blocked
|
||||
}
|
||||
|
||||
function getSignalStrength(device) {
|
||||
if (!device || device.signalStrength === undefined
|
||||
|| device.signalStrength <= 0)
|
||||
if (!device || device.signalStrength === undefined || device.signalStrength <= 0) {
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
var signal = device.signalStrength
|
||||
if (signal >= 80)
|
||||
const signal = device.signalStrength
|
||||
if (signal >= 80) {
|
||||
return "Excellent"
|
||||
|
||||
if (signal >= 60)
|
||||
}
|
||||
if (signal >= 60) {
|
||||
return "Good"
|
||||
|
||||
if (signal >= 40)
|
||||
}
|
||||
if (signal >= 40) {
|
||||
return "Fair"
|
||||
|
||||
if (signal >= 20)
|
||||
}
|
||||
if (signal >= 20) {
|
||||
return "Poor"
|
||||
}
|
||||
|
||||
return "Very Poor"
|
||||
}
|
||||
|
||||
function getSignalIcon(device) {
|
||||
if (!device || device.signalStrength === undefined
|
||||
|| device.signalStrength <= 0)
|
||||
if (!device || device.signalStrength === undefined || device.signalStrength <= 0) {
|
||||
return "signal_cellular_null"
|
||||
}
|
||||
|
||||
var signal = device.signalStrength
|
||||
if (signal >= 80)
|
||||
const signal = device.signalStrength
|
||||
if (signal >= 80) {
|
||||
return "signal_cellular_4_bar"
|
||||
|
||||
if (signal >= 60)
|
||||
}
|
||||
if (signal >= 60) {
|
||||
return "signal_cellular_3_bar"
|
||||
|
||||
if (signal >= 40)
|
||||
}
|
||||
if (signal >= 40) {
|
||||
return "signal_cellular_2_bar"
|
||||
|
||||
if (signal >= 20)
|
||||
}
|
||||
if (signal >= 20) {
|
||||
return "signal_cellular_1_bar"
|
||||
}
|
||||
|
||||
return "signal_cellular_0_bar"
|
||||
}
|
||||
|
||||
function isDeviceBusy(device) {
|
||||
if (!device)
|
||||
if (!device) {
|
||||
return false
|
||||
return device.pairing
|
||||
|| device.state === BluetoothDeviceState.Disconnecting
|
||||
|| device.state === BluetoothDeviceState.Connecting
|
||||
}
|
||||
return device.pairing || device.state === BluetoothDeviceState.Disconnecting || device.state === BluetoothDeviceState.Connecting
|
||||
}
|
||||
|
||||
function connectDeviceWithTrust(device) {
|
||||
if (!device)
|
||||
if (!device) {
|
||||
return
|
||||
}
|
||||
|
||||
device.trusted = true
|
||||
device.connect()
|
||||
}
|
||||
|
||||
function getCardName(device) {
|
||||
if (!device)
|
||||
if (!device) {
|
||||
return ""
|
||||
return "bluez_card." + device.address.replace(/:/g, "_")
|
||||
}
|
||||
return `bluez_card.${device.address.replace(/:/g, "_")}`
|
||||
}
|
||||
|
||||
function isAudioDevice(device) {
|
||||
if (!device)
|
||||
if (!device) {
|
||||
return false
|
||||
let icon = getDeviceIcon(device)
|
||||
}
|
||||
const icon = getDeviceIcon(device)
|
||||
return icon === "headset" || icon === "speaker"
|
||||
}
|
||||
|
||||
function getCodecInfo(codecName) {
|
||||
let codec = codecName.replace(/-/g, "_").toUpperCase()
|
||||
|
||||
let codecMap = {
|
||||
const codec = codecName.replace(/-/g, "_").toUpperCase()
|
||||
|
||||
const codecMap = {
|
||||
"LDAC": {
|
||||
name: "LDAC",
|
||||
description: "Highest quality • Higher battery usage",
|
||||
qualityColor: "#4CAF50"
|
||||
"name": "LDAC",
|
||||
"description": "Highest quality • Higher battery usage",
|
||||
"qualityColor": "#4CAF50"
|
||||
},
|
||||
"APTX_HD": {
|
||||
name: "aptX HD",
|
||||
description: "High quality • Balanced battery",
|
||||
qualityColor: "#FF9800"
|
||||
"name": "aptX HD",
|
||||
"description": "High quality • Balanced battery",
|
||||
"qualityColor": "#FF9800"
|
||||
},
|
||||
"APTX": {
|
||||
name: "aptX",
|
||||
description: "Good quality • Low latency",
|
||||
qualityColor: "#FF9800"
|
||||
"name": "aptX",
|
||||
"description": "Good quality • Low latency",
|
||||
"qualityColor": "#FF9800"
|
||||
},
|
||||
"AAC": {
|
||||
name: "AAC",
|
||||
description: "Balanced quality and battery",
|
||||
qualityColor: "#2196F3"
|
||||
"name": "AAC",
|
||||
"description": "Balanced quality and battery",
|
||||
"qualityColor": "#2196F3"
|
||||
},
|
||||
"SBC_XQ": {
|
||||
name: "SBC-XQ",
|
||||
description: "Enhanced SBC • Better compatibility",
|
||||
qualityColor: "#2196F3"
|
||||
"name": "SBC-XQ",
|
||||
"description": "Enhanced SBC • Better compatibility",
|
||||
"qualityColor": "#2196F3"
|
||||
},
|
||||
"SBC": {
|
||||
name: "SBC",
|
||||
description: "Basic quality • Universal compatibility",
|
||||
qualityColor: "#9E9E9E"
|
||||
"name": "SBC",
|
||||
"description": "Basic quality • Universal compatibility",
|
||||
"qualityColor": "#9E9E9E"
|
||||
},
|
||||
"MSBC": {
|
||||
name: "mSBC",
|
||||
description: "Modified SBC • Optimized for speech",
|
||||
qualityColor: "#9E9E9E"
|
||||
"name": "mSBC",
|
||||
"description": "Modified SBC • Optimized for speech",
|
||||
"qualityColor": "#9E9E9E"
|
||||
},
|
||||
"CVSD": {
|
||||
name: "CVSD",
|
||||
description: "Basic speech codec • Legacy compatibility",
|
||||
qualityColor: "#9E9E9E"
|
||||
"name": "CVSD",
|
||||
"description": "Basic speech codec • Legacy compatibility",
|
||||
"qualityColor": "#9E9E9E"
|
||||
}
|
||||
}
|
||||
|
||||
return codecMap[codec] || {
|
||||
name: codecName,
|
||||
description: "Unknown codec",
|
||||
qualityColor: "#9E9E9E"
|
||||
"name": codecName,
|
||||
"description": "Unknown codec",
|
||||
"qualityColor": "#9E9E9E"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
property var deviceCodecs: ({})
|
||||
|
||||
|
||||
function updateDeviceCodec(deviceAddress, codec) {
|
||||
deviceCodecs[deviceAddress] = codec
|
||||
deviceCodecsChanged()
|
||||
}
|
||||
|
||||
|
||||
function refreshDeviceCodec(device) {
|
||||
if (!device || !device.connected || !isAudioDevice(device)) {
|
||||
return
|
||||
}
|
||||
|
||||
let cardName = getCardName(device)
|
||||
|
||||
const cardName = getCardName(device)
|
||||
codecQueryProcess.cardName = cardName
|
||||
codecQueryProcess.deviceAddress = device.address
|
||||
codecQueryProcess.availableCodecs = []
|
||||
@@ -243,14 +252,14 @@ Singleton {
|
||||
codecQueryProcess.detectedCodec = ""
|
||||
codecQueryProcess.running = true
|
||||
}
|
||||
|
||||
|
||||
function getCurrentCodec(device, callback) {
|
||||
if (!device || !device.connected || !isAudioDevice(device)) {
|
||||
callback("")
|
||||
return
|
||||
}
|
||||
|
||||
let cardName = getCardName(device)
|
||||
|
||||
const cardName = getCardName(device)
|
||||
codecQueryProcess.cardName = cardName
|
||||
codecQueryProcess.callback = callback
|
||||
codecQueryProcess.availableCodecs = []
|
||||
@@ -258,14 +267,14 @@ Singleton {
|
||||
codecQueryProcess.detectedCodec = ""
|
||||
codecQueryProcess.running = true
|
||||
}
|
||||
|
||||
|
||||
function getAvailableCodecs(device, callback) {
|
||||
if (!device || !device.connected || !isAudioDevice(device)) {
|
||||
callback([], "")
|
||||
return
|
||||
}
|
||||
|
||||
let cardName = getCardName(device)
|
||||
|
||||
const cardName = getCardName(device)
|
||||
codecFullQueryProcess.cardName = cardName
|
||||
codecFullQueryProcess.callback = callback
|
||||
codecFullQueryProcess.availableCodecs = []
|
||||
@@ -273,33 +282,33 @@ Singleton {
|
||||
codecFullQueryProcess.detectedCodec = ""
|
||||
codecFullQueryProcess.running = true
|
||||
}
|
||||
|
||||
|
||||
function switchCodec(device, profileName, callback) {
|
||||
if (!device || !isAudioDevice(device)) {
|
||||
callback(false, "Invalid device")
|
||||
return
|
||||
}
|
||||
|
||||
let cardName = getCardName(device)
|
||||
|
||||
const cardName = getCardName(device)
|
||||
codecSwitchProcess.cardName = cardName
|
||||
codecSwitchProcess.profile = profileName
|
||||
codecSwitchProcess.callback = callback
|
||||
codecSwitchProcess.running = true
|
||||
}
|
||||
|
||||
|
||||
Process {
|
||||
id: codecQueryProcess
|
||||
|
||||
|
||||
property string cardName: ""
|
||||
property string deviceAddress: ""
|
||||
property var callback: null
|
||||
property bool parsingTargetCard: false
|
||||
property string detectedCodec: ""
|
||||
property var availableCodecs: []
|
||||
|
||||
|
||||
command: ["pactl", "list", "cards"]
|
||||
|
||||
onExited: function(exitCode, exitStatus) {
|
||||
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
if (exitCode === 0 && detectedCodec) {
|
||||
if (deviceAddress) {
|
||||
root.updateDeviceCodec(deviceAddress, detectedCodec)
|
||||
@@ -310,35 +319,35 @@ Singleton {
|
||||
} else if (callback) {
|
||||
callback("")
|
||||
}
|
||||
|
||||
|
||||
parsingTargetCard = false
|
||||
detectedCodec = ""
|
||||
availableCodecs = []
|
||||
deviceAddress = ""
|
||||
callback = null
|
||||
}
|
||||
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: (data) => {
|
||||
onRead: data => {
|
||||
let line = data.trim()
|
||||
|
||||
|
||||
if (line.includes(`Name: ${codecQueryProcess.cardName}`)) {
|
||||
codecQueryProcess.parsingTargetCard = true
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (codecQueryProcess.parsingTargetCard && line.startsWith("Name: ") && !line.includes(codecQueryProcess.cardName)) {
|
||||
codecQueryProcess.parsingTargetCard = false
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (codecQueryProcess.parsingTargetCard) {
|
||||
if (line.startsWith("Active Profile:")) {
|
||||
let profile = line.split(": ")[1] || ""
|
||||
let activeCodec = codecQueryProcess.availableCodecs.find((c) => {
|
||||
return c.profile === profile
|
||||
})
|
||||
let activeCodec = codecQueryProcess.availableCodecs.find(c => {
|
||||
return c.profile === profile
|
||||
})
|
||||
if (activeCodec) {
|
||||
codecQueryProcess.detectedCodec = activeCodec.name
|
||||
}
|
||||
@@ -352,16 +361,16 @@ Singleton {
|
||||
let codecMatch = description.match(/codec ([^\)\s]+)/i)
|
||||
let codecName = codecMatch ? codecMatch[1].toUpperCase() : "UNKNOWN"
|
||||
let codecInfo = root.getCodecInfo(codecName)
|
||||
if (codecInfo && !codecQueryProcess.availableCodecs.some((c) => {
|
||||
return c.profile === profile
|
||||
})) {
|
||||
if (codecInfo && !codecQueryProcess.availableCodecs.some(c => {
|
||||
return c.profile === profile
|
||||
})) {
|
||||
let newCodecs = codecQueryProcess.availableCodecs.slice()
|
||||
newCodecs.push({
|
||||
"name": codecInfo.name,
|
||||
"profile": profile,
|
||||
"description": codecInfo.description,
|
||||
"qualityColor": codecInfo.qualityColor
|
||||
})
|
||||
"name": codecInfo.name,
|
||||
"profile": profile,
|
||||
"description": codecInfo.description,
|
||||
"qualityColor": codecInfo.qualityColor
|
||||
})
|
||||
codecQueryProcess.availableCodecs = newCodecs
|
||||
}
|
||||
}
|
||||
@@ -370,19 +379,19 @@ Singleton {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Process {
|
||||
id: codecFullQueryProcess
|
||||
|
||||
|
||||
property string cardName: ""
|
||||
property var callback: null
|
||||
property bool parsingTargetCard: false
|
||||
property string detectedCodec: ""
|
||||
property var availableCodecs: []
|
||||
|
||||
|
||||
command: ["pactl", "list", "cards"]
|
||||
|
||||
onExited: function(exitCode, exitStatus) {
|
||||
|
||||
onExited: function (exitCode, exitStatus) {
|
||||
if (callback) {
|
||||
callback(exitCode === 0 ? availableCodecs : [], exitCode === 0 ? detectedCodec : "")
|
||||
}
|
||||
@@ -391,28 +400,28 @@ Singleton {
|
||||
availableCodecs = []
|
||||
callback = null
|
||||
}
|
||||
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: (data) => {
|
||||
onRead: data => {
|
||||
let line = data.trim()
|
||||
|
||||
|
||||
if (line.includes(`Name: ${codecFullQueryProcess.cardName}`)) {
|
||||
codecFullQueryProcess.parsingTargetCard = true
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (codecFullQueryProcess.parsingTargetCard && line.startsWith("Name: ") && !line.includes(codecFullQueryProcess.cardName)) {
|
||||
codecFullQueryProcess.parsingTargetCard = false
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (codecFullQueryProcess.parsingTargetCard) {
|
||||
if (line.startsWith("Active Profile:")) {
|
||||
let profile = line.split(": ")[1] || ""
|
||||
let activeCodec = codecFullQueryProcess.availableCodecs.find((c) => {
|
||||
return c.profile === profile
|
||||
})
|
||||
let activeCodec = codecFullQueryProcess.availableCodecs.find(c => {
|
||||
return c.profile === profile
|
||||
})
|
||||
if (activeCodec) {
|
||||
codecFullQueryProcess.detectedCodec = activeCodec.name
|
||||
}
|
||||
@@ -426,16 +435,16 @@ Singleton {
|
||||
let codecMatch = description.match(/codec ([^\)\s]+)/i)
|
||||
let codecName = codecMatch ? codecMatch[1].toUpperCase() : "UNKNOWN"
|
||||
let codecInfo = root.getCodecInfo(codecName)
|
||||
if (codecInfo && !codecFullQueryProcess.availableCodecs.some((c) => {
|
||||
return c.profile === profile
|
||||
})) {
|
||||
if (codecInfo && !codecFullQueryProcess.availableCodecs.some(c => {
|
||||
return c.profile === profile
|
||||
})) {
|
||||
let newCodecs = codecFullQueryProcess.availableCodecs.slice()
|
||||
newCodecs.push({
|
||||
"name": codecInfo.name,
|
||||
"profile": profile,
|
||||
"description": codecInfo.description,
|
||||
"qualityColor": codecInfo.qualityColor
|
||||
})
|
||||
"name": codecInfo.name,
|
||||
"profile": profile,
|
||||
"description": codecInfo.description,
|
||||
"qualityColor": codecInfo.qualityColor
|
||||
})
|
||||
codecFullQueryProcess.availableCodecs = newCodecs
|
||||
}
|
||||
}
|
||||
@@ -444,32 +453,32 @@ Singleton {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Process {
|
||||
id: codecSwitchProcess
|
||||
|
||||
|
||||
property string cardName: ""
|
||||
property string profile: ""
|
||||
property var callback: null
|
||||
|
||||
|
||||
command: ["pactl", "set-card-profile", cardName, profile]
|
||||
|
||||
onExited: function(exitCode, exitStatus) {
|
||||
|
||||
onExited: function (exitCode, exitStatus) {
|
||||
if (callback) {
|
||||
callback(exitCode === 0, exitCode === 0 ? "Codec switched successfully" : "Failed to switch codec")
|
||||
}
|
||||
|
||||
|
||||
// If successful, refresh the codec for this device
|
||||
if (exitCode === 0) {
|
||||
if (root.adapter && root.adapter.devices) {
|
||||
root.adapter.devices.values.forEach(device => {
|
||||
if (device && root.getCardName(device) === cardName) {
|
||||
Qt.callLater(() => root.refreshDeviceCodec(device))
|
||||
}
|
||||
})
|
||||
if (device && root.getCardName(device) === cardName) {
|
||||
Qt.callLater(() => root.refreshDeviceCodec(device))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
callback = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
@@ -43,11 +44,9 @@ Singleton {
|
||||
onRead: data => {
|
||||
if (root.refCount > 0 && data.trim()) {
|
||||
let points = data.split(";").map(p => {
|
||||
return parseInt(
|
||||
p.trim(), 10)
|
||||
return parseInt(p.trim(), 10)
|
||||
}).filter(p => {
|
||||
return !isNaN(
|
||||
p)
|
||||
return !isNaN(p)
|
||||
})
|
||||
if (points.length >= 6) {
|
||||
root.values = points.slice(0, 6)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
@@ -10,7 +11,6 @@ import Quickshell.Hyprland
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Compositor detection
|
||||
property bool isHyprland: false
|
||||
property bool isNiri: false
|
||||
property string compositor: "unknown"
|
||||
@@ -19,62 +19,64 @@ Singleton {
|
||||
readonly property string niriSocket: Quickshell.env("NIRI_SOCKET")
|
||||
|
||||
property bool useNiriSorting: isNiri && NiriService
|
||||
property bool useHyprlandSorting: false
|
||||
|
||||
property var sortedToplevels: {
|
||||
if (!ToplevelManager.toplevels || !ToplevelManager.toplevels.values) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Only use niri sorting when both compositor is niri AND niri service is ready
|
||||
|
||||
if (useNiriSorting) {
|
||||
return NiriService.sortToplevels(ToplevelManager.toplevels.values)
|
||||
}
|
||||
|
||||
if (isHyprland) {
|
||||
const hyprlandToplevels = Array.from(Hyprland.toplevels.values)
|
||||
|
||||
|
||||
const sortedHyprland = hyprlandToplevels.sort((a, b) => {
|
||||
// Sort by monitor first
|
||||
if (a.monitor && b.monitor) {
|
||||
const monitorCompare = a.monitor.name.localeCompare(b.monitor.name)
|
||||
if (monitorCompare !== 0) return monitorCompare
|
||||
}
|
||||
|
||||
// Then by workspace
|
||||
if (a.workspace && b.workspace) {
|
||||
const workspaceCompare = a.workspace.id - b.workspace.id
|
||||
if (workspaceCompare !== 0) return workspaceCompare
|
||||
}
|
||||
|
||||
|
||||
if (a.lastIpcObject && b.lastIpcObject && a.lastIpcObject.at && b.lastIpcObject.at) {
|
||||
const aX = a.lastIpcObject.at[0]
|
||||
const bX = b.lastIpcObject.at[0]
|
||||
const aY = a.lastIpcObject.at[1]
|
||||
const bY = b.lastIpcObject.at[1]
|
||||
|
||||
const xCompare = aX - bX
|
||||
if (Math.abs(xCompare) > 10) return xCompare
|
||||
return aY - bY
|
||||
}
|
||||
|
||||
if (a.lastIpcObject && !b.lastIpcObject) return -1
|
||||
if (!a.lastIpcObject && b.lastIpcObject) return 1
|
||||
|
||||
if (a.title && b.title) {
|
||||
return a.title.localeCompare(b.title)
|
||||
}
|
||||
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
// Return the wayland Toplevel objects
|
||||
if (a.monitor && b.monitor) {
|
||||
const monitorCompare = a.monitor.name.localeCompare(b.monitor.name)
|
||||
if (monitorCompare !== 0) {
|
||||
return monitorCompare
|
||||
}
|
||||
}
|
||||
|
||||
if (a.workspace && b.workspace) {
|
||||
const workspaceCompare = a.workspace.id - b.workspace.id
|
||||
if (workspaceCompare !== 0) {
|
||||
return workspaceCompare
|
||||
}
|
||||
}
|
||||
|
||||
if (a.lastIpcObject && b.lastIpcObject && a.lastIpcObject.at && b.lastIpcObject.at) {
|
||||
const aX = a.lastIpcObject.at[0]
|
||||
const bX = b.lastIpcObject.at[0]
|
||||
const aY = a.lastIpcObject.at[1]
|
||||
const bY = b.lastIpcObject.at[1]
|
||||
|
||||
const xCompare = aX - bX
|
||||
if (Math.abs(xCompare) > 10) {
|
||||
return xCompare
|
||||
}
|
||||
return aY - bY
|
||||
}
|
||||
|
||||
if (a.lastIpcObject && !b.lastIpcObject) {
|
||||
return -1
|
||||
}
|
||||
if (!a.lastIpcObject && b.lastIpcObject) {
|
||||
return 1
|
||||
}
|
||||
|
||||
if (a.title && b.title) {
|
||||
return a.title.localeCompare(b.title)
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
return sortedHyprland.map(hyprToplevel => hyprToplevel.wayland).filter(wayland => wayland !== null)
|
||||
}
|
||||
|
||||
// For other compositors or when services aren't ready yet, return unsorted toplevels
|
||||
return ToplevelManager.toplevels.values
|
||||
}
|
||||
|
||||
@@ -82,7 +84,7 @@ Singleton {
|
||||
detectCompositor()
|
||||
}
|
||||
|
||||
function filterCurrentWorkspace(toplevels, screen){
|
||||
function filterCurrentWorkspace(toplevels, screen) {
|
||||
if (useNiriSorting) {
|
||||
return NiriService.filterCurrentWorkspace(toplevels, screen)
|
||||
}
|
||||
@@ -97,11 +99,10 @@ Singleton {
|
||||
return toplevels
|
||||
}
|
||||
|
||||
var currentWorkspaceId = null
|
||||
let currentWorkspaceId = null
|
||||
const hyprlandToplevels = Array.from(Hyprland.toplevels.values)
|
||||
|
||||
for (var i = 0; i < hyprlandToplevels.length; i++) {
|
||||
var hyprToplevel = hyprlandToplevels[i]
|
||||
|
||||
for (const hyprToplevel of hyprlandToplevels) {
|
||||
if (hyprToplevel.monitor && hyprToplevel.monitor.name === screenName && hyprToplevel.workspace) {
|
||||
if (hyprToplevel.activated) {
|
||||
currentWorkspaceId = hyprToplevel.workspace.id
|
||||
@@ -115,8 +116,7 @@ Singleton {
|
||||
|
||||
if (currentWorkspaceId === null && Hyprland.workspaces) {
|
||||
const workspaces = Array.from(Hyprland.workspaces.values)
|
||||
for (var k = 0; k < workspaces.length; k++) {
|
||||
var workspace = workspaces[k]
|
||||
for (const workspace of workspaces) {
|
||||
if (workspace.monitor && workspace.monitor === screenName) {
|
||||
if (Hyprland.focusedWorkspace && workspace.id === Hyprland.focusedWorkspace.id) {
|
||||
currentWorkspaceId = workspace.id
|
||||
@@ -134,18 +134,16 @@ Singleton {
|
||||
}
|
||||
|
||||
return toplevels.filter(toplevel => {
|
||||
for (var j = 0; j < hyprlandToplevels.length; j++) {
|
||||
var hyprToplevel = hyprlandToplevels[j]
|
||||
if (hyprToplevel.wayland === toplevel) {
|
||||
return hyprToplevel.workspace && hyprToplevel.workspace.id === currentWorkspaceId
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
for (const hyprToplevel of hyprlandToplevels) {
|
||||
if (hyprToplevel.wayland === toplevel) {
|
||||
return hyprToplevel.workspace && hyprToplevel.workspace.id === currentWorkspaceId
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
function detectCompositor() {
|
||||
// Check for Hyprland first
|
||||
if (hyprlandSignature && hyprlandSignature.length > 0) {
|
||||
isHyprland = true
|
||||
isNiri = false
|
||||
@@ -154,12 +152,9 @@ Singleton {
|
||||
return
|
||||
}
|
||||
|
||||
// Check for Niri
|
||||
if (niriSocket && niriSocket.length > 0) {
|
||||
// Verify the socket actually exists
|
||||
niriSocketCheck.running = true
|
||||
} else {
|
||||
// No compositor detected, default to Niri
|
||||
isHyprland = false
|
||||
isNiri = false
|
||||
compositor = "unknown"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
@@ -94,8 +95,7 @@ Singleton {
|
||||
// Increment reference count for this module
|
||||
const currentCount = moduleRefCounts[module] || 0
|
||||
moduleRefCounts[module] = currentCount + 1
|
||||
console.log("Adding ref for module:", module, "count:",
|
||||
moduleRefCounts[module])
|
||||
console.log("Adding ref for module:", module, "count:", moduleRefCounts[module])
|
||||
|
||||
// Add to enabled modules if not already there
|
||||
if (enabledModules.indexOf(module) === -1) {
|
||||
@@ -107,8 +107,7 @@ Singleton {
|
||||
|
||||
if (modulesChanged || refCount === 1) {
|
||||
enabledModules = enabledModules.slice() // Force property change
|
||||
moduleRefCounts = Object.assign(
|
||||
{}, moduleRefCounts) // Force property change
|
||||
moduleRefCounts = Object.assign({}, moduleRefCounts) // Force property change
|
||||
updateAllStats()
|
||||
} else if (gpuPciIds.length > 0 && refCount > 0) {
|
||||
// If we have GPU PCI IDs and active modules, make sure to update
|
||||
@@ -128,8 +127,7 @@ Singleton {
|
||||
if (currentCount > 1) {
|
||||
// Decrement reference count
|
||||
moduleRefCounts[module] = currentCount - 1
|
||||
console.log("Removing ref for module:", module, "count:",
|
||||
moduleRefCounts[module])
|
||||
console.log("Removing ref for module:", module, "count:", moduleRefCounts[module])
|
||||
} else if (currentCount === 1) {
|
||||
// Remove completely when count reaches 0
|
||||
delete moduleRefCounts[module]
|
||||
@@ -137,8 +135,7 @@ Singleton {
|
||||
if (index > -1) {
|
||||
enabledModules.splice(index, 1)
|
||||
modulesChanged = true
|
||||
console.log("Disabling module:", module,
|
||||
"(no more refs)")
|
||||
console.log("Disabling module:", module, "(no more refs)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,8 +143,7 @@ Singleton {
|
||||
|
||||
if (modulesChanged) {
|
||||
enabledModules = enabledModules.slice() // Force property change
|
||||
moduleRefCounts = Object.assign(
|
||||
{}, moduleRefCounts) // Force property change
|
||||
moduleRefCounts = Object.assign({}, moduleRefCounts) // Force property change
|
||||
|
||||
// Clear cursor data when CPU or process modules are no longer active
|
||||
if (!enabledModules.includes("cpu")) {
|
||||
@@ -174,8 +170,7 @@ Singleton {
|
||||
gpuPciIds = gpuPciIds.concat([pciId])
|
||||
}
|
||||
|
||||
console.log("Adding GPU PCI ID ref:", pciId, "count:",
|
||||
gpuPciIdRefCounts[pciId])
|
||||
console.log("Adding GPU PCI ID ref:", pciId, "count:", gpuPciIdRefCounts[pciId])
|
||||
// Force property change notification
|
||||
gpuPciIdRefCounts = Object.assign({}, gpuPciIdRefCounts)
|
||||
}
|
||||
@@ -185,8 +180,7 @@ Singleton {
|
||||
if (currentCount > 1) {
|
||||
// Decrement reference count
|
||||
gpuPciIdRefCounts[pciId] = currentCount - 1
|
||||
console.log("Removing GPU PCI ID ref:", pciId, "count:",
|
||||
gpuPciIdRefCounts[pciId])
|
||||
console.log("Removing GPU PCI ID ref:", pciId, "count:", gpuPciIdRefCounts[pciId])
|
||||
} else if (currentCount === 1) {
|
||||
// Remove completely when count reaches 0
|
||||
delete gpuPciIdRefCounts[pciId]
|
||||
@@ -271,12 +265,10 @@ Singleton {
|
||||
}
|
||||
|
||||
// Add cursor data if available for accurate CPU percentages
|
||||
if ((enabledModules.includes("cpu") || enabledModules.includes("all"))
|
||||
&& cpuCursor) {
|
||||
if ((enabledModules.includes("cpu") || enabledModules.includes("all")) && cpuCursor) {
|
||||
cmd.push("--cpu-cursor", cpuCursor)
|
||||
}
|
||||
if ((enabledModules.includes("processes") || enabledModules.includes(
|
||||
"all")) && procCursor) {
|
||||
if ((enabledModules.includes("processes") || enabledModules.includes("all")) && procCursor) {
|
||||
cmd.push("--proc-cursor", procCursor)
|
||||
}
|
||||
|
||||
@@ -284,8 +276,7 @@ Singleton {
|
||||
cmd.push("--gpu-pci-ids", gpuPciIds.join(","))
|
||||
}
|
||||
|
||||
if (enabledModules.indexOf("processes") !== -1
|
||||
|| enabledModules.indexOf("all") !== -1) {
|
||||
if (enabledModules.indexOf("processes") !== -1 || enabledModules.indexOf("all") !== -1) {
|
||||
cmd.push("--limit", "100") // Get more data for client sorting
|
||||
cmd.push("--sort", "cpu") // Always get CPU sorted data
|
||||
if (noCpu) {
|
||||
@@ -301,7 +292,6 @@ Singleton {
|
||||
const cpu = data.cpu
|
||||
cpuSampleCount++
|
||||
|
||||
// Use dgop CPU numbers directly without modification
|
||||
cpuUsage = cpu.usage || 0
|
||||
cpuFrequency = cpu.frequency || 0
|
||||
cpuTemperature = cpu.temperature || 0
|
||||
@@ -310,7 +300,6 @@ Singleton {
|
||||
perCoreCpuUsage = cpu.coreUsage || []
|
||||
addToHistory(cpuHistory, cpuUsage)
|
||||
|
||||
// Store the opaque cursor string for next sampling
|
||||
if (cpu.cursor) {
|
||||
cpuCursor = cpu.cursor
|
||||
}
|
||||
@@ -322,14 +311,12 @@ Singleton {
|
||||
const availableKB = mem.available || 0
|
||||
const freeKB = mem.free || 0
|
||||
|
||||
// Update MB properties
|
||||
totalMemoryMB = totalKB / 1024
|
||||
availableMemoryMB = availableKB / 1024
|
||||
freeMemoryMB = freeKB / 1024
|
||||
usedMemoryMB = totalMemoryMB - availableMemoryMB
|
||||
memoryUsage = totalKB > 0 ? ((totalKB - availableKB) / totalKB) * 100 : 0
|
||||
|
||||
// Update KB properties for compatibility
|
||||
totalMemoryKB = totalKB
|
||||
usedMemoryKB = totalKB - availableKB
|
||||
totalSwapKB = mem.swaptotal || 0
|
||||
@@ -339,7 +326,6 @@ Singleton {
|
||||
}
|
||||
|
||||
if (data.network && Array.isArray(data.network)) {
|
||||
// Store raw network interface data
|
||||
networkInterfaces = data.network
|
||||
|
||||
let totalRx = 0
|
||||
@@ -365,7 +351,6 @@ Singleton {
|
||||
}
|
||||
|
||||
if (data.disk && Array.isArray(data.disk)) {
|
||||
// Store raw disk device data
|
||||
diskDevices = data.disk
|
||||
|
||||
let totalRead = 0
|
||||
@@ -399,53 +384,40 @@ Singleton {
|
||||
processSampleCount++
|
||||
|
||||
for (const proc of data.processes) {
|
||||
// Only show CPU usage if we have had at least 2 samples (first sample is inaccurate)
|
||||
const cpuUsage = processSampleCount >= 2 ? (proc.cpu || 0) : 0
|
||||
|
||||
newProcesses.push({
|
||||
"pid": proc.pid || 0,
|
||||
"ppid": proc.ppid || 0,
|
||||
"cpu": cpuUsage,
|
||||
"memoryPercent": proc.memoryPercent
|
||||
|| proc.pssPercent || 0,
|
||||
"memoryKB": proc.memoryKB
|
||||
|| proc.pssKB || 0,
|
||||
"memoryPercent": proc.memoryPercent || proc.pssPercent || 0,
|
||||
"memoryKB": proc.memoryKB || proc.pssKB || 0,
|
||||
"command": proc.command || "",
|
||||
"fullCommand": proc.fullCommand || "",
|
||||
"displayName": (proc.command
|
||||
&& proc.command.length
|
||||
> 15) ? proc.command.substring(
|
||||
0,
|
||||
15) + "..." : (proc.command || "")
|
||||
"displayName": (proc.command && proc.command.length > 15) ? proc.command.substring(0, 15) + "..." : (proc.command || "")
|
||||
})
|
||||
}
|
||||
allProcesses = newProcesses
|
||||
applySorting()
|
||||
|
||||
// Store the single opaque cursor string for the entire process list
|
||||
if (data.cursor) {
|
||||
procCursor = data.cursor
|
||||
}
|
||||
}
|
||||
|
||||
// Handle both gpu and gpu-temp module data
|
||||
const gpuData = (data.gpu && data.gpu.gpus)
|
||||
|| data.gpus // Handle both meta format and direct gpu command format
|
||||
const gpuData = (data.gpu && data.gpu.gpus) || data.gpus
|
||||
if (gpuData && Array.isArray(gpuData)) {
|
||||
// Check if this is temperature update data (has PCI IDs being monitored)
|
||||
if (gpuPciIds.length > 0 && availableGpus
|
||||
&& availableGpus.length > 0) {
|
||||
if (gpuPciIds.length > 0 && availableGpus && availableGpus.length > 0) {
|
||||
// This is temperature data - merge with existing GPU metadata
|
||||
const updatedGpus = availableGpus.slice()
|
||||
for (var i = 0; i < updatedGpus.length; i++) {
|
||||
const existingGpu = updatedGpus[i]
|
||||
const tempGpu = gpuData.find(
|
||||
g => g.pciId === existingGpu.pciId)
|
||||
const tempGpu = gpuData.find(g => g.pciId === existingGpu.pciId)
|
||||
// Only update temperature if this GPU's PCI ID is being monitored
|
||||
if (tempGpu && gpuPciIds.includes(existingGpu.pciId)) {
|
||||
updatedGpus[i] = Object.assign({}, existingGpu, {
|
||||
"temperature": tempGpu.temperature
|
||||
|| 0
|
||||
"temperature": tempGpu.temperature || 0
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -454,8 +426,7 @@ Singleton {
|
||||
// This is initial GPU metadata - set the full list
|
||||
const gpuList = []
|
||||
for (const gpu of gpuData) {
|
||||
let displayName = gpu.displayName || gpu.name
|
||||
|| "Unknown GPU"
|
||||
let displayName = gpu.displayName || gpu.name || "Unknown GPU"
|
||||
let fullName = gpu.fullName || gpu.name || "Unknown GPU"
|
||||
|
||||
gpuList.push({
|
||||
@@ -501,24 +472,24 @@ Singleton {
|
||||
|
||||
function getProcessIcon(command) {
|
||||
const cmd = command.toLowerCase()
|
||||
if (cmd.includes("firefox") || cmd.includes("chrome") ||
|
||||
cmd.includes("browser") || cmd.includes("chromium"))
|
||||
if (cmd.includes("firefox") || cmd.includes("chrome") || cmd.includes("browser") || cmd.includes("chromium")) {
|
||||
return "web"
|
||||
if (cmd.includes("code") || cmd.includes("editor")
|
||||
|| cmd.includes("vim"))
|
||||
}
|
||||
if (cmd.includes("code") || cmd.includes("editor") || cmd.includes("vim")) {
|
||||
return "code"
|
||||
if (cmd.includes("terminal") || cmd.includes("bash")
|
||||
|| cmd.includes("zsh"))
|
||||
}
|
||||
if (cmd.includes("terminal") || cmd.includes("bash") || cmd.includes("zsh")) {
|
||||
return "terminal"
|
||||
if (cmd.includes("music") || cmd.includes("audio") || cmd.includes(
|
||||
"spotify"))
|
||||
}
|
||||
if (cmd.includes("music") || cmd.includes("audio") || cmd.includes("spotify")) {
|
||||
return "music_note"
|
||||
if (cmd.includes("video") || cmd.includes("vlc") || cmd.includes("mpv"))
|
||||
}
|
||||
if (cmd.includes("video") || cmd.includes("vlc") || cmd.includes("mpv")) {
|
||||
return "play_circle"
|
||||
if (cmd.includes("systemd") || cmd.includes("elogind") ||
|
||||
cmd.includes("kernel") || cmd.includes("kthread") ||
|
||||
cmd.includes("kworker"))
|
||||
}
|
||||
if (cmd.includes("systemd") || cmd.includes("elogind") || cmd.includes("kernel") || cmd.includes("kthread") || cmd.includes("kworker")) {
|
||||
return "settings"
|
||||
}
|
||||
return "memory"
|
||||
}
|
||||
|
||||
@@ -528,22 +499,25 @@ Singleton {
|
||||
|
||||
function formatMemoryUsage(memoryKB) {
|
||||
const mem = memoryKB || 0
|
||||
if (mem < 1024)
|
||||
if (mem < 1024) {
|
||||
return mem.toFixed(0) + " KB"
|
||||
else if (mem < 1024 * 1024)
|
||||
} else if (mem < 1024 * 1024) {
|
||||
return (mem / 1024).toFixed(1) + " MB"
|
||||
else
|
||||
} else {
|
||||
return (mem / (1024 * 1024)).toFixed(1) + " GB"
|
||||
}
|
||||
}
|
||||
|
||||
function formatSystemMemory(memoryKB) {
|
||||
const mem = memoryKB || 0
|
||||
if (mem === 0)
|
||||
if (mem === 0) {
|
||||
return "--"
|
||||
if (mem < 1024 * 1024)
|
||||
}
|
||||
if (mem < 1024 * 1024) {
|
||||
return (mem / 1024).toFixed(0) + " MB"
|
||||
else
|
||||
} else {
|
||||
return (mem / (1024 * 1024)).toFixed(1) + " GB"
|
||||
}
|
||||
}
|
||||
|
||||
function killProcess(pid) {
|
||||
@@ -560,8 +534,9 @@ Singleton {
|
||||
}
|
||||
|
||||
function applySorting() {
|
||||
if (!allProcesses || allProcesses.length === 0)
|
||||
if (!allProcesses || allProcesses.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const sorted = allProcesses.slice()
|
||||
sorted.sort((a, b) => {
|
||||
@@ -595,8 +570,7 @@ Singleton {
|
||||
Timer {
|
||||
id: updateTimer
|
||||
interval: root.updateInterval
|
||||
running: root.dgopAvailable && root.refCount > 0
|
||||
&& root.enabledModules.length > 0
|
||||
running: root.dgopAvailable && root.refCount > 0 && root.enabledModules.length > 0
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: root.updateAllStats()
|
||||
@@ -611,12 +585,11 @@ Singleton {
|
||||
//console.log("DgopService command:", JSON.stringify(command))
|
||||
}
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Dgop process failed with exit code:",
|
||||
exitCode)
|
||||
isUpdating = false
|
||||
}
|
||||
}
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Dgop process failed with exit code:", exitCode)
|
||||
isUpdating = false
|
||||
}
|
||||
}
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
@@ -638,12 +611,10 @@ Singleton {
|
||||
command: ["dgop", "gpu", "--json"]
|
||||
running: false
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn(
|
||||
"GPU init process failed with exit code:",
|
||||
exitCode)
|
||||
}
|
||||
}
|
||||
if (exitCode !== 0) {
|
||||
console.warn("GPU init process failed with exit code:", exitCode)
|
||||
}
|
||||
}
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
@@ -663,24 +634,23 @@ Singleton {
|
||||
command: ["which", "dgop"]
|
||||
running: false
|
||||
onExited: exitCode => {
|
||||
dgopAvailable = (exitCode === 0)
|
||||
if (dgopAvailable) {
|
||||
initializeGpuMetadata()
|
||||
// Load persisted GPU PCI IDs from session state
|
||||
if (SessionData.enabledGpuPciIds
|
||||
&& SessionData.enabledGpuPciIds.length > 0) {
|
||||
for (const pciId of SessionData.enabledGpuPciIds) {
|
||||
addGpuPciId(pciId)
|
||||
}
|
||||
// Trigger update if we already have active modules
|
||||
if (refCount > 0 && enabledModules.length > 0) {
|
||||
updateAllStats()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn("dgop is not installed or not in PATH")
|
||||
}
|
||||
}
|
||||
dgopAvailable = (exitCode === 0)
|
||||
if (dgopAvailable) {
|
||||
initializeGpuMetadata()
|
||||
// Load persisted GPU PCI IDs from session state
|
||||
if (SessionData.enabledGpuPciIds && SessionData.enabledGpuPciIds.length > 0) {
|
||||
for (const pciId of SessionData.enabledGpuPciIds) {
|
||||
addGpuPciId(pciId)
|
||||
}
|
||||
// Trigger update if we already have active modules
|
||||
if (refCount > 0 && enabledModules.length > 0) {
|
||||
updateAllStats()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn("dgop is not installed or not in PATH")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
@@ -688,10 +658,10 @@ Singleton {
|
||||
command: ["cat", "/etc/os-release"]
|
||||
running: false
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Failed to read /etc/os-release")
|
||||
}
|
||||
}
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Failed to read /etc/os-release")
|
||||
}
|
||||
}
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text.trim()) {
|
||||
@@ -703,11 +673,9 @@ Singleton {
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim()
|
||||
if (trimmedLine.startsWith('PRETTY_NAME=')) {
|
||||
prettyName = trimmedLine.substring(12).replace(
|
||||
/^["']|["']$/g, '')
|
||||
prettyName = trimmedLine.substring(12).replace(/^["']|["']$/g, '')
|
||||
} else if (trimmedLine.startsWith('NAME=')) {
|
||||
name = trimmedLine.substring(5).replace(
|
||||
/^["']|["']$/g, '')
|
||||
name = trimmedLine.substring(5).replace(/^["']|["']$/g, '')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
@@ -21,9 +22,10 @@ Singleton {
|
||||
property bool skipDdcRead: false
|
||||
property int brightnessLevel: {
|
||||
const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice)
|
||||
if (!deviceToUse) return 50
|
||||
if (!deviceToUse) {
|
||||
return 50
|
||||
}
|
||||
|
||||
// Always use cached values for consistency
|
||||
return getDeviceBrightness(deviceToUse)
|
||||
}
|
||||
property int maxBrightness: 100
|
||||
@@ -34,7 +36,6 @@ Singleton {
|
||||
|
||||
property bool nightModeActive: nightModeEnabled
|
||||
|
||||
// Night Mode Properties
|
||||
property bool nightModeEnabled: false
|
||||
property bool automationAvailable: false
|
||||
property bool geoclueAvailable: false
|
||||
@@ -47,32 +48,25 @@ Singleton {
|
||||
|
||||
function setBrightnessInternal(percentage, device) {
|
||||
const clampedValue = Math.max(1, Math.min(100, percentage))
|
||||
const actualDevice = device === "" ? getDefaultDevice(
|
||||
) : (device || currentDevice
|
||||
|| getDefaultDevice())
|
||||
const actualDevice = device === "" ? getDefaultDevice() : (device || currentDevice || getDefaultDevice())
|
||||
|
||||
// Update the device brightness cache immediately for all devices
|
||||
if (actualDevice) {
|
||||
var newBrightness = Object.assign({}, deviceBrightness)
|
||||
const newBrightness = Object.assign({}, deviceBrightness)
|
||||
newBrightness[actualDevice] = clampedValue
|
||||
deviceBrightness = newBrightness
|
||||
}
|
||||
|
||||
|
||||
const deviceInfo = getCurrentDeviceInfoByName(actualDevice)
|
||||
|
||||
if (deviceInfo && deviceInfo.class === "ddc") {
|
||||
// Use ddcutil for DDC devices
|
||||
ddcBrightnessSetProcess.command = ["ddcutil", "setvcp", "-d", String(
|
||||
deviceInfo.ddcDisplay), "10", String(
|
||||
clampedValue)]
|
||||
ddcBrightnessSetProcess.command = ["ddcutil", "setvcp", "-d", String(deviceInfo.ddcDisplay), "10", String(clampedValue)]
|
||||
ddcBrightnessSetProcess.running = true
|
||||
} else {
|
||||
// Use brightnessctl for regular devices
|
||||
if (device)
|
||||
brightnessSetProcess.command
|
||||
= ["brightnessctl", "-d", device, "set", clampedValue + "%"]
|
||||
else
|
||||
brightnessSetProcess.command = ["brightnessctl", "set", clampedValue + "%"]
|
||||
if (device) {
|
||||
brightnessSetProcess.command = ["brightnessctl", "-d", device, "set", `${clampedValue}%`]
|
||||
} else {
|
||||
brightnessSetProcess.command = ["brightnessctl", "set", `${clampedValue}%`]
|
||||
}
|
||||
brightnessSetProcess.running = true
|
||||
}
|
||||
}
|
||||
@@ -83,26 +77,23 @@ Singleton {
|
||||
}
|
||||
|
||||
function setCurrentDevice(deviceName, saveToSession = false) {
|
||||
if (currentDevice === deviceName)
|
||||
if (currentDevice === deviceName) {
|
||||
return
|
||||
}
|
||||
|
||||
currentDevice = deviceName
|
||||
lastIpcDevice = deviceName
|
||||
|
||||
// Only save to session if explicitly requested (user choice)
|
||||
if (saveToSession) {
|
||||
SessionData.setLastBrightnessDevice(deviceName)
|
||||
}
|
||||
|
||||
deviceSwitched()
|
||||
|
||||
// Check if this is a DDC device
|
||||
const deviceInfo = getCurrentDeviceInfoByName(deviceName)
|
||||
if (deviceInfo && deviceInfo.class === "ddc") {
|
||||
// For DDC devices, never read after initial - just use cached values
|
||||
return
|
||||
} else {
|
||||
// For regular devices, use brightnessctl
|
||||
brightnessGetProcess.command = ["brightnessctl", "-m", "-d", deviceName, "get"]
|
||||
brightnessGetProcess.running = true
|
||||
}
|
||||
@@ -116,19 +107,19 @@ Singleton {
|
||||
const allDevices = [...devices, ...ddcDevices]
|
||||
|
||||
allDevices.sort((a, b) => {
|
||||
if (a.class === "backlight"
|
||||
&& b.class !== "backlight")
|
||||
return -1
|
||||
if (a.class !== "backlight"
|
||||
&& b.class === "backlight")
|
||||
return 1
|
||||
if (a.class === "backlight" && b.class !== "backlight") {
|
||||
return -1
|
||||
}
|
||||
if (a.class !== "backlight" && b.class === "backlight") {
|
||||
return 1
|
||||
}
|
||||
|
||||
if (a.class === "ddc" && b.class !== "ddc"
|
||||
&& b.class !== "backlight")
|
||||
return -1
|
||||
if (a.class !== "ddc" && b.class === "ddc"
|
||||
&& a.class !== "backlight")
|
||||
return 1
|
||||
if (a.class === "ddc" && b.class !== "ddc" && b.class !== "backlight") {
|
||||
return -1
|
||||
}
|
||||
if (a.class !== "ddc" && b.class === "ddc" && a.class !== "backlight") {
|
||||
return 1
|
||||
}
|
||||
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
@@ -141,25 +132,26 @@ Singleton {
|
||||
if (deviceExists) {
|
||||
setCurrentDevice(lastDevice, false)
|
||||
} else {
|
||||
const nonKbdDevice = devices.find(d => !d.name.includes("kbd"))
|
||||
|| devices[0]
|
||||
const nonKbdDevice = devices.find(d => !d.name.includes("kbd")) || devices[0]
|
||||
setCurrentDevice(nonKbdDevice.name, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getDeviceBrightness(deviceName) {
|
||||
if (!deviceName) return 50
|
||||
|
||||
if (!deviceName) {
|
||||
return
|
||||
} 50
|
||||
|
||||
const deviceInfo = getCurrentDeviceInfoByName(deviceName)
|
||||
if (!deviceInfo) return 50
|
||||
|
||||
// For DDC devices, always use cached values
|
||||
if (!deviceInfo) {
|
||||
return 50
|
||||
}
|
||||
|
||||
if (deviceInfo.class === "ddc") {
|
||||
return deviceBrightness[deviceName] || 50
|
||||
}
|
||||
|
||||
// For regular devices, try cache first, then device info
|
||||
|
||||
return deviceBrightness[deviceName] || deviceInfo.percentage || 50
|
||||
}
|
||||
|
||||
@@ -173,11 +165,10 @@ Singleton {
|
||||
}
|
||||
|
||||
function getCurrentDeviceInfo() {
|
||||
const deviceToUse = lastIpcDevice === "" ? getDefaultDevice(
|
||||
) : (lastIpcDevice
|
||||
|| currentDevice)
|
||||
if (!deviceToUse)
|
||||
const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice)
|
||||
if (!deviceToUse) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (const device of devices) {
|
||||
if (device.name === deviceToUse) {
|
||||
@@ -188,11 +179,10 @@ Singleton {
|
||||
}
|
||||
|
||||
function isCurrentDeviceReady() {
|
||||
const deviceToUse = lastIpcDevice === "" ? getDefaultDevice(
|
||||
) : (lastIpcDevice
|
||||
|| currentDevice)
|
||||
if (!deviceToUse)
|
||||
const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice)
|
||||
if (!deviceToUse) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (ddcPendingInit[deviceToUse]) {
|
||||
return false
|
||||
@@ -202,8 +192,9 @@ Singleton {
|
||||
}
|
||||
|
||||
function getCurrentDeviceInfoByName(deviceName) {
|
||||
if (!deviceName)
|
||||
if (!deviceName) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (const device of devices) {
|
||||
if (device.name === deviceName) {
|
||||
@@ -219,8 +210,7 @@ Singleton {
|
||||
}
|
||||
|
||||
const displayId = ddcInitQueue.shift()
|
||||
ddcInitialBrightnessProcess.command = ["ddcutil", "getvcp", "-d", String(
|
||||
displayId), "10", "--brief"]
|
||||
ddcInitialBrightnessProcess.command = ["ddcutil", "getvcp", "-d", String(displayId), "10", "--brief"]
|
||||
ddcInitialBrightnessProcess.running = true
|
||||
}
|
||||
|
||||
@@ -233,7 +223,7 @@ Singleton {
|
||||
|
||||
nightModeEnabled = true
|
||||
SessionData.setNightModeEnabled(true)
|
||||
|
||||
|
||||
// Apply immediately or start automation
|
||||
if (SessionData.nightModeAutoEnabled) {
|
||||
startAutomation()
|
||||
@@ -266,10 +256,7 @@ Singleton {
|
||||
|
||||
function applyNightModeDirectly() {
|
||||
const temperature = SessionData.nightModeTemperature || 4500
|
||||
gammaStepProcess.command = buildGammastepCommand([
|
||||
"-m", "wayland",
|
||||
"-O", String(temperature)
|
||||
])
|
||||
gammaStepProcess.command = buildGammastepCommand(["-m", "wayland", "-O", String(temperature)])
|
||||
gammaStepProcess.running = true
|
||||
}
|
||||
|
||||
@@ -279,17 +266,19 @@ Singleton {
|
||||
}
|
||||
|
||||
function startAutomation() {
|
||||
if (!automationAvailable) return
|
||||
if (!automationAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
const mode = SessionData.nightModeAutoMode || "time"
|
||||
|
||||
|
||||
switch (mode) {
|
||||
case "time":
|
||||
startTimeBasedMode()
|
||||
break
|
||||
case "location":
|
||||
startLocationBasedMode()
|
||||
break
|
||||
case "time":
|
||||
startTimeBasedMode()
|
||||
break
|
||||
case "location":
|
||||
startLocationBasedMode()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,29 +299,19 @@ Singleton {
|
||||
function startLocationBasedMode() {
|
||||
const temperature = SessionData.nightModeTemperature || 4500
|
||||
const dayTemp = 6500
|
||||
|
||||
|
||||
if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
|
||||
automationProcess.command = buildGammastepCommand([
|
||||
"-m", "wayland",
|
||||
"-l", `${SessionData.latitude.toFixed(6)}:${SessionData.longitude.toFixed(6)}`,
|
||||
"-t", `${dayTemp}:${temperature}`,
|
||||
"-v"
|
||||
])
|
||||
automationProcess.command = buildGammastepCommand(["-m", "wayland", "-l", `${SessionData.latitude.toFixed(6)}:${SessionData.longitude.toFixed(6)}`, "-t", `${dayTemp}:${temperature}`, "-v"])
|
||||
automationProcess.running = true
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (SessionData.nightModeLocationProvider === "geoclue2") {
|
||||
automationProcess.command = buildGammastepCommand([
|
||||
"-m", "wayland",
|
||||
"-l", "geoclue2",
|
||||
"-t", `${dayTemp}:${temperature}`,
|
||||
"-v"
|
||||
])
|
||||
automationProcess.command = buildGammastepCommand(["-m", "wayland", "-l", "geoclue2", "-t", `${dayTemp}:${temperature}`, "-v"])
|
||||
automationProcess.running = true
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
console.warn("DisplayService: Location mode selected but no coordinates or geoclue provider set")
|
||||
}
|
||||
|
||||
@@ -347,7 +326,7 @@ Singleton {
|
||||
const endMinutes = SessionData.nightModeEndHour * 60 + SessionData.nightModeEndMinute
|
||||
|
||||
let shouldBeNight = false
|
||||
|
||||
|
||||
if (startMinutes > endMinutes) {
|
||||
shouldBeNight = (currentTime >= startMinutes) || (currentTime < endMinutes)
|
||||
} else {
|
||||
@@ -356,7 +335,7 @@ Singleton {
|
||||
|
||||
if (shouldBeNight !== isAutomaticNightTime) {
|
||||
isAutomaticNightTime = shouldBeNight
|
||||
|
||||
|
||||
if (shouldBeNight) {
|
||||
applyNightModeDirectly()
|
||||
} else {
|
||||
@@ -373,14 +352,14 @@ Singleton {
|
||||
SessionData.setNightModeAutoMode(mode)
|
||||
}
|
||||
|
||||
function evaluateNightMode() {
|
||||
function evaluateNightMode() {
|
||||
// Always stop all processes first to clean slate
|
||||
stopAutomation()
|
||||
|
||||
|
||||
if (!nightModeEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (SessionData.nightModeAutoEnabled) {
|
||||
restartTimer.nextAction = "automation"
|
||||
restartTimer.start()
|
||||
@@ -399,7 +378,7 @@ Singleton {
|
||||
property string nextAction: ""
|
||||
interval: 100
|
||||
repeat: false
|
||||
|
||||
|
||||
onTriggered: {
|
||||
if (nextAction === "automation") {
|
||||
startAutomation()
|
||||
@@ -476,8 +455,7 @@ Singleton {
|
||||
}
|
||||
|
||||
ddcDevices = newDdcDevices
|
||||
console.log("DisplayService: Found", ddcDevices.length,
|
||||
"DDC displays")
|
||||
console.log("DisplayService: Found", ddcDevices.length, "DDC displays")
|
||||
|
||||
// Queue initial brightness readings for DDC devices
|
||||
ddcInitQueue = []
|
||||
@@ -496,16 +474,13 @@ Singleton {
|
||||
// Retry setting last device now that DDC devices are available
|
||||
const lastDevice = SessionData.lastBrightnessDevice || ""
|
||||
if (lastDevice) {
|
||||
const deviceExists = devices.some(
|
||||
d => d.name === lastDevice)
|
||||
if (deviceExists && (!currentDevice
|
||||
|| currentDevice !== lastDevice)) {
|
||||
const deviceExists = devices.some(d => d.name === lastDevice)
|
||||
if (deviceExists && (!currentDevice || currentDevice !== lastDevice)) {
|
||||
setCurrentDevice(lastDevice, false)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("DisplayService: Failed to parse DDC devices:",
|
||||
error)
|
||||
console.warn("DisplayService: Failed to parse DDC devices:", error)
|
||||
ddcDevices = []
|
||||
}
|
||||
}
|
||||
@@ -513,8 +488,7 @@ Singleton {
|
||||
|
||||
onExited: function (exitCode) {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("DisplayService: Failed to detect DDC displays:",
|
||||
exitCode)
|
||||
console.warn("DisplayService: Failed to detect DDC displays:", exitCode)
|
||||
ddcDevices = []
|
||||
}
|
||||
}
|
||||
@@ -526,8 +500,7 @@ Singleton {
|
||||
command: ["brightnessctl", "-m", "-l"]
|
||||
onExited: function (exitCode) {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("DisplayService: Failed to list devices:",
|
||||
exitCode)
|
||||
console.warn("DisplayService: Failed to list devices:", exitCode)
|
||||
brightnessAvailable = false
|
||||
}
|
||||
}
|
||||
@@ -542,7 +515,7 @@ Singleton {
|
||||
const newDevices = []
|
||||
for (const line of lines) {
|
||||
const parts = line.split(",")
|
||||
if (parts.length >= 5)
|
||||
if (parts.length >= 5) {
|
||||
newDevices.push({
|
||||
"name": parts[0],
|
||||
"class": parts[1],
|
||||
@@ -550,10 +523,11 @@ Singleton {
|
||||
"percentage": parseInt(parts[3]),
|
||||
"max": parseInt(parts[4])
|
||||
})
|
||||
}
|
||||
}
|
||||
// Store brightnessctl devices separately
|
||||
devices = newDevices
|
||||
|
||||
|
||||
// Always refresh to combine with DDC devices and set up device selection
|
||||
refreshDevicesInternal()
|
||||
}
|
||||
@@ -565,9 +539,9 @@ Singleton {
|
||||
|
||||
running: false
|
||||
onExited: function (exitCode) {
|
||||
if (exitCode !== 0)
|
||||
console.warn("DisplayService: Failed to set brightness:",
|
||||
exitCode)
|
||||
if (exitCode !== 0) {
|
||||
console.warn("DisplayService: Failed to set brightness:", exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,10 +550,9 @@ Singleton {
|
||||
|
||||
running: false
|
||||
onExited: function (exitCode) {
|
||||
if (exitCode !== 0)
|
||||
console.warn(
|
||||
"DisplayService: Failed to set DDC brightness:",
|
||||
exitCode)
|
||||
if (exitCode !== 0) {
|
||||
console.warn("DisplayService: Failed to set DDC brightness:", exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,9 +561,9 @@ Singleton {
|
||||
|
||||
running: false
|
||||
onExited: function (exitCode) {
|
||||
if (exitCode !== 0)
|
||||
console.warn("DisplayService: Failed to get initial DDC brightness:",
|
||||
exitCode)
|
||||
if (exitCode !== 0) {
|
||||
console.warn("DisplayService: Failed to get initial DDC brightness:", exitCode)
|
||||
}
|
||||
|
||||
processNextDdcInit()
|
||||
}
|
||||
@@ -598,7 +571,7 @@ Singleton {
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (!text.trim())
|
||||
return
|
||||
return
|
||||
|
||||
const parts = text.trim().split(" ")
|
||||
if (parts.length >= 5) {
|
||||
@@ -619,8 +592,7 @@ Singleton {
|
||||
delete newPending[deviceName]
|
||||
ddcPendingInit = newPending
|
||||
|
||||
console.log("DisplayService: Initial DDC Device",
|
||||
deviceName, "brightness:", brightness + "%")
|
||||
console.log("DisplayService: Initial DDC Device", deviceName, "brightness:", brightness + "%")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -632,15 +604,15 @@ Singleton {
|
||||
|
||||
running: false
|
||||
onExited: function (exitCode) {
|
||||
if (exitCode !== 0)
|
||||
console.warn("DisplayService: Failed to get brightness:",
|
||||
exitCode)
|
||||
if (exitCode !== 0) {
|
||||
console.warn("DisplayService: Failed to get brightness:", exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (!text.trim())
|
||||
return
|
||||
return
|
||||
|
||||
const parts = text.trim().split(",")
|
||||
if (parts.length >= 5) {
|
||||
@@ -657,8 +629,7 @@ Singleton {
|
||||
}
|
||||
|
||||
brightnessInitialized = true
|
||||
console.log("DisplayService: Device", currentDevice,
|
||||
"brightness:", brightness + "%")
|
||||
console.log("DisplayService: Device", currentDevice, "brightness:", brightness + "%")
|
||||
brightnessChanged()
|
||||
}
|
||||
}
|
||||
@@ -670,16 +641,15 @@ Singleton {
|
||||
|
||||
running: false
|
||||
onExited: function (exitCode) {
|
||||
if (exitCode !== 0)
|
||||
console.warn(
|
||||
"DisplayService: Failed to get DDC brightness:",
|
||||
exitCode)
|
||||
if (exitCode !== 0) {
|
||||
console.warn("DisplayService: Failed to get DDC brightness:", exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (!text.trim())
|
||||
return
|
||||
return
|
||||
|
||||
// Parse ddcutil getvcp output format: "VCP 10 C 50 100"
|
||||
const parts = text.trim().split(" ")
|
||||
@@ -697,8 +667,7 @@ Singleton {
|
||||
}
|
||||
|
||||
brightnessInitialized = true
|
||||
console.log("DisplayService: DDC Device", currentDevice,
|
||||
"brightness:", brightness + "%")
|
||||
console.log("DisplayService: DDC Device", currentDevice, "brightness:", brightness + "%")
|
||||
brightnessChanged()
|
||||
}
|
||||
}
|
||||
@@ -709,12 +678,12 @@ Singleton {
|
||||
id: gammastepAvailabilityProcess
|
||||
command: ["which", "gammastep"]
|
||||
running: false
|
||||
|
||||
onExited: function(exitCode) {
|
||||
|
||||
onExited: function (exitCode) {
|
||||
automationAvailable = (exitCode === 0)
|
||||
if (automationAvailable) {
|
||||
detectLocationProviders()
|
||||
|
||||
|
||||
// If night mode should be enabled on startup
|
||||
if (nightModeEnabled && SessionData.nightModeAutoEnabled) {
|
||||
startAutomation()
|
||||
@@ -748,7 +717,7 @@ Singleton {
|
||||
automationAvailable = true
|
||||
nightModeEnabled = true
|
||||
SessionData.setNightModeEnabled(true)
|
||||
|
||||
|
||||
if (SessionData.nightModeAutoEnabled) {
|
||||
startAutomation()
|
||||
} else {
|
||||
@@ -789,121 +758,149 @@ Singleton {
|
||||
// Session Data Connections
|
||||
Connections {
|
||||
target: SessionData
|
||||
|
||||
|
||||
function onNightModeEnabledChanged() {
|
||||
nightModeEnabled = SessionData.nightModeEnabled
|
||||
evaluateNightMode()
|
||||
}
|
||||
|
||||
function onNightModeAutoEnabledChanged() { evaluateNightMode() }
|
||||
function onNightModeAutoModeChanged() { evaluateNightMode() }
|
||||
function onNightModeStartHourChanged() { evaluateNightMode() }
|
||||
function onNightModeStartMinuteChanged() { evaluateNightMode() }
|
||||
function onNightModeEndHourChanged() { evaluateNightMode() }
|
||||
function onNightModeEndMinuteChanged() { evaluateNightMode() }
|
||||
function onNightModeTemperatureChanged() { evaluateNightMode() }
|
||||
function onLatitudeChanged() { evaluateNightMode() }
|
||||
function onLongitudeChanged() { evaluateNightMode() }
|
||||
function onNightModeLocationProviderChanged() { evaluateNightMode() }
|
||||
|
||||
function onNightModeAutoEnabledChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onNightModeAutoModeChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onNightModeStartHourChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onNightModeStartMinuteChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onNightModeEndHourChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onNightModeEndMinuteChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onNightModeTemperatureChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onLatitudeChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onLongitudeChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
function onNightModeLocationProviderChanged() {
|
||||
evaluateNightMode()
|
||||
}
|
||||
}
|
||||
|
||||
// IPC Handler for external control
|
||||
IpcHandler {
|
||||
function set(percentage: string, device: string): string {
|
||||
if (!root.brightnessAvailable)
|
||||
if (!root.brightnessAvailable) {
|
||||
return "Brightness control not available"
|
||||
}
|
||||
|
||||
const value = parseInt(percentage)
|
||||
if (isNaN(value)) {
|
||||
return "Invalid brightness value: " + percentage
|
||||
}
|
||||
|
||||
|
||||
const clampedValue = Math.max(1, Math.min(100, value))
|
||||
const targetDevice = device || ""
|
||||
|
||||
|
||||
// Ensure device exists if specified
|
||||
if (targetDevice && !root.devices.some(d => d.name === targetDevice)) {
|
||||
return "Device not found: " + targetDevice
|
||||
}
|
||||
|
||||
|
||||
root.lastIpcDevice = targetDevice
|
||||
if (targetDevice && targetDevice !== root.currentDevice) {
|
||||
root.setCurrentDevice(targetDevice, false)
|
||||
}
|
||||
root.setBrightness(clampedValue, targetDevice)
|
||||
|
||||
if (targetDevice)
|
||||
|
||||
if (targetDevice) {
|
||||
return "Brightness set to " + clampedValue + "% on " + targetDevice
|
||||
else
|
||||
} else {
|
||||
return "Brightness set to " + clampedValue + "%"
|
||||
}
|
||||
}
|
||||
|
||||
function increment(step: string, device: string): string {
|
||||
if (!root.brightnessAvailable)
|
||||
if (!root.brightnessAvailable) {
|
||||
return "Brightness control not available"
|
||||
}
|
||||
|
||||
const targetDevice = device || ""
|
||||
const actualDevice = targetDevice === "" ? root.getDefaultDevice() : targetDevice
|
||||
|
||||
|
||||
// Ensure device exists
|
||||
if (actualDevice && !root.devices.some(d => d.name === actualDevice)) {
|
||||
return "Device not found: " + actualDevice
|
||||
}
|
||||
|
||||
|
||||
const currentLevel = actualDevice ? root.getDeviceBrightness(actualDevice) : root.brightnessLevel
|
||||
const stepValue = parseInt(step || "10")
|
||||
const newLevel = Math.max(1, Math.min(100, currentLevel + stepValue))
|
||||
|
||||
|
||||
root.lastIpcDevice = targetDevice
|
||||
if (targetDevice && targetDevice !== root.currentDevice) {
|
||||
root.setCurrentDevice(targetDevice, false)
|
||||
}
|
||||
root.setBrightness(newLevel, targetDevice)
|
||||
|
||||
if (targetDevice)
|
||||
|
||||
if (targetDevice) {
|
||||
return "Brightness increased to " + newLevel + "% on " + targetDevice
|
||||
else
|
||||
} else {
|
||||
return "Brightness increased to " + newLevel + "%"
|
||||
}
|
||||
}
|
||||
|
||||
function decrement(step: string, device: string): string {
|
||||
if (!root.brightnessAvailable)
|
||||
if (!root.brightnessAvailable) {
|
||||
return "Brightness control not available"
|
||||
}
|
||||
|
||||
const targetDevice = device || ""
|
||||
const actualDevice = targetDevice === "" ? root.getDefaultDevice() : targetDevice
|
||||
|
||||
|
||||
// Ensure device exists
|
||||
if (actualDevice && !root.devices.some(d => d.name === actualDevice)) {
|
||||
return "Device not found: " + actualDevice
|
||||
}
|
||||
|
||||
|
||||
const currentLevel = actualDevice ? root.getDeviceBrightness(actualDevice) : root.brightnessLevel
|
||||
const stepValue = parseInt(step || "10")
|
||||
const newLevel = Math.max(1, Math.min(100, currentLevel - stepValue))
|
||||
|
||||
|
||||
root.lastIpcDevice = targetDevice
|
||||
if (targetDevice && targetDevice !== root.currentDevice) {
|
||||
root.setCurrentDevice(targetDevice, false)
|
||||
}
|
||||
root.setBrightness(newLevel, targetDevice)
|
||||
|
||||
if (targetDevice)
|
||||
|
||||
if (targetDevice) {
|
||||
return "Brightness decreased to " + newLevel + "% on " + targetDevice
|
||||
else
|
||||
} else {
|
||||
return "Brightness decreased to " + newLevel + "%"
|
||||
}
|
||||
}
|
||||
|
||||
function status(): string {
|
||||
if (!root.brightnessAvailable)
|
||||
if (!root.brightnessAvailable) {
|
||||
return "Brightness control not available"
|
||||
}
|
||||
|
||||
return "Device: " + root.currentDevice + " - Brightness: " + root.brightnessLevel + "%"
|
||||
}
|
||||
|
||||
function list(): string {
|
||||
if (!root.brightnessAvailable)
|
||||
if (!root.brightnessAvailable) {
|
||||
return "No brightness devices available"
|
||||
}
|
||||
|
||||
let result = "Available devices:\\n"
|
||||
for (const device of root.devices) {
|
||||
@@ -974,4 +971,4 @@ Singleton {
|
||||
|
||||
target: "night"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
@@ -1,56 +1,60 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Mpris
|
||||
import Quickshell.Widgets
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property list<MprisPlayer> availablePlayers: Mpris.players.values
|
||||
|
||||
property MprisPlayer activePlayer: availablePlayers.find(p => p.isPlaying)
|
||||
?? availablePlayers.find(
|
||||
p => p.canControl
|
||||
&& p.canPlay) ?? null
|
||||
property MprisPlayer activePlayer: availablePlayers.find(p => p.isPlaying) ?? availablePlayers.find(p => p.canControl && p.canPlay) ?? null
|
||||
|
||||
IpcHandler {
|
||||
target: "mpris"
|
||||
|
||||
function list(): string {
|
||||
return root.availablePlayers.map(p => p.identity).join("")
|
||||
return root.availablePlayers.map(p => p.identity).join("\n")
|
||||
}
|
||||
|
||||
function play(): void {
|
||||
if (root.activePlayer?.canPlay)
|
||||
if (root.activePlayer && root.activePlayer.canPlay) {
|
||||
root.activePlayer.play()
|
||||
}
|
||||
}
|
||||
|
||||
function pause(): void {
|
||||
if (root.activePlayer?.canPause)
|
||||
if (root.activePlayer && root.activePlayer.canPause) {
|
||||
root.activePlayer.pause()
|
||||
}
|
||||
}
|
||||
|
||||
function playPause(): void {
|
||||
if (root.activePlayer?.canTogglePlaying)
|
||||
if (root.activePlayer && root.activePlayer.canTogglePlaying) {
|
||||
root.activePlayer.togglePlaying()
|
||||
}
|
||||
}
|
||||
|
||||
function previous(): void {
|
||||
if (root.activePlayer?.canGoPrevious)
|
||||
if (root.activePlayer && root.activePlayer.canGoPrevious) {
|
||||
root.activePlayer.previous()
|
||||
}
|
||||
}
|
||||
|
||||
function next(): void {
|
||||
if (root.activePlayer?.canGoNext)
|
||||
if (root.activePlayer && root.activePlayer.canGoNext) {
|
||||
root.activePlayer.next()
|
||||
}
|
||||
}
|
||||
|
||||
function stop(): void {
|
||||
root.activePlayer?.stop()
|
||||
if (root.activePlayer) {
|
||||
root.activePlayer.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
@@ -9,35 +10,32 @@ import qs.Common
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Core network state
|
||||
property int refCount: 0
|
||||
property string networkStatus: "disconnected" // "ethernet", "wifi", "disconnected"
|
||||
property string primaryConnection: "" // Active connection UUID
|
||||
property string networkStatus: "disconnected"
|
||||
property string primaryConnection: ""
|
||||
|
||||
// Ethernet properties
|
||||
property string ethernetIP: ""
|
||||
property string ethernetInterface: ""
|
||||
property bool ethernetConnected: false
|
||||
property string ethernetConnectionUuid: ""
|
||||
|
||||
// WiFi properties
|
||||
property string wifiIP: ""
|
||||
property string wifiInterface: ""
|
||||
property bool wifiConnected: false
|
||||
property bool wifiEnabled: true
|
||||
property string wifiConnectionUuid: ""
|
||||
|
||||
// WiFi details
|
||||
property string currentWifiSSID: ""
|
||||
property int wifiSignalStrength: 0
|
||||
property var wifiNetworks: []
|
||||
property var savedConnections: []
|
||||
property var ssidToConnectionName: {}
|
||||
property var ssidToConnectionName: {
|
||||
|
||||
}
|
||||
property var wifiSignalIcon: {
|
||||
if (!wifiConnected || networkStatus !== "wifi") {
|
||||
return "signal_wifi_off"
|
||||
}
|
||||
// Use nmcli signal strength percentage
|
||||
if (wifiSignalStrength >= 70) {
|
||||
return "signal_wifi_4_bar"
|
||||
}
|
||||
@@ -53,17 +51,14 @@ Singleton {
|
||||
return "signal_wifi_bad"
|
||||
}
|
||||
|
||||
// Connection management
|
||||
property string userPreference: "auto" // "auto", "wifi", "ethernet"
|
||||
property bool isConnecting: false
|
||||
property string connectingSSID: ""
|
||||
property string connectionError: ""
|
||||
|
||||
// Scanning
|
||||
property bool isScanning: false
|
||||
property bool autoScan: false
|
||||
|
||||
// Legacy compatibility properties
|
||||
property bool wifiAvailable: true
|
||||
property bool wifiToggling: false
|
||||
property bool changingPreference: false
|
||||
@@ -76,7 +71,6 @@ Singleton {
|
||||
property string wifiPassword: ""
|
||||
property string forgetSSID: ""
|
||||
|
||||
// Network info properties
|
||||
property string networkInfoSSID: ""
|
||||
property string networkInfoDetails: ""
|
||||
property bool networkInfoLoading: false
|
||||
@@ -84,15 +78,13 @@ Singleton {
|
||||
signal networksUpdated
|
||||
signal connectionChanged
|
||||
|
||||
// Helper: split nmcli -t output respecting escaped colons (\:)
|
||||
function splitNmcliFields(line) {
|
||||
let parts = []
|
||||
const parts = []
|
||||
let cur = ""
|
||||
let escape = false
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
for (var i = 0; i < line.length; i++) {
|
||||
const ch = line[i]
|
||||
if (escape) {
|
||||
// Keep literal for escaped colon and other sequences
|
||||
cur += ch
|
||||
escape = false
|
||||
} else if (ch === '\\') {
|
||||
@@ -140,11 +132,7 @@ Singleton {
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: line => {
|
||||
if (line.includes("StateChanged") || line.includes(
|
||||
"PrimaryConnectionChanged") || line.includes(
|
||||
"WirelessEnabled") || line.includes(
|
||||
"ActiveConnection") || line.includes(
|
||||
"PropertiesChanged")) {
|
||||
if (line.includes("StateChanged") || line.includes("PrimaryConnectionChanged") || line.includes("WirelessEnabled") || line.includes("ActiveConnection") || line.includes("PropertiesChanged")) {
|
||||
refreshNetworkState()
|
||||
}
|
||||
}
|
||||
@@ -316,8 +304,9 @@ Singleton {
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const match = text.match(/inet (\d+\.\d+\.\d+\.\d+)/)
|
||||
if (match)
|
||||
root.ethernetIP = match[1]
|
||||
if (match) {
|
||||
root.ethernetIP = match[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -415,22 +404,21 @@ Singleton {
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const match = text.match(/inet (\d+\.\d+\.\d+\.\d+)/)
|
||||
if (match)
|
||||
root.wifiIP = match[1]
|
||||
if (match) {
|
||||
root.wifiIP = match[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: getCurrentWifiInfo
|
||||
// Prefer IN-USE,SIGNAL,SSID, but we'll also parse legacy ACTIVE format
|
||||
command: root.wifiInterface ? ["nmcli", "-t", "-f", "IN-USE,SIGNAL,SSID", "device", "wifi", "list", "ifname", root.wifiInterface] : []
|
||||
running: false
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: line => {
|
||||
// IN-USE format: "*:SIGNAL:SSID"
|
||||
if (line.startsWith("*:")) {
|
||||
const rest = line.substring(2)
|
||||
const parts = root.splitNmcliFields(rest)
|
||||
@@ -455,7 +443,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function updateActiveConnections() {
|
||||
getActiveConnections.running = true
|
||||
}
|
||||
@@ -475,11 +462,9 @@ Singleton {
|
||||
const type = parts[1]
|
||||
const device = parts[2]
|
||||
const state = parts[3]
|
||||
if (type === "802-3-ethernet"
|
||||
&& state === "activated") {
|
||||
if (type === "802-3-ethernet" && state === "activated") {
|
||||
root.ethernetConnectionUuid = uuid
|
||||
} else if (type === "802-11-wireless"
|
||||
&& state === "activated") {
|
||||
} else if (type === "802-11-wireless" && state === "activated") {
|
||||
root.wifiConnectionUuid = uuid
|
||||
}
|
||||
}
|
||||
@@ -514,8 +499,9 @@ Singleton {
|
||||
onStreamFinished: {
|
||||
if (!root.currentWifiSSID) {
|
||||
const name = text.trim()
|
||||
if (name)
|
||||
root.currentWifiSSID = name
|
||||
if (name) {
|
||||
root.currentWifiSSID = name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -539,8 +525,9 @@ Singleton {
|
||||
}
|
||||
|
||||
function scanWifi() {
|
||||
if (root.isScanning || !root.wifiEnabled)
|
||||
if (root.isScanning || !root.wifiEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
root.isScanning = true
|
||||
requestWifiScan.running = true
|
||||
@@ -578,7 +565,7 @@ Singleton {
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
let networks = []
|
||||
const networks = []
|
||||
const lines = text.trim().split('\n')
|
||||
const seen = new Set()
|
||||
|
||||
@@ -596,7 +583,7 @@ Singleton {
|
||||
"secured": parts[2] !== "",
|
||||
"bssid": parts[3],
|
||||
"connected": ssid === root.currentWifiSSID,
|
||||
"saved": false // Will be updated by saved connections check
|
||||
"saved": false
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -617,8 +604,8 @@ Singleton {
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
let saved = []
|
||||
let mapping = {}
|
||||
const saved = []
|
||||
const mapping = {}
|
||||
const lines = text.trim().split('\n')
|
||||
|
||||
for (const line of lines) {
|
||||
@@ -640,8 +627,8 @@ Singleton {
|
||||
root.savedWifiNetworks = saved
|
||||
root.ssidToConnectionName = mapping
|
||||
|
||||
let updated = [...root.wifiNetworks]
|
||||
for (let network of updated) {
|
||||
const updated = [...root.wifiNetworks]
|
||||
for (const network of updated) {
|
||||
network.saved = saved.some(s => s.ssid === network.ssid)
|
||||
}
|
||||
root.wifiNetworks = updated
|
||||
@@ -650,15 +637,15 @@ Singleton {
|
||||
}
|
||||
|
||||
function connectToWifi(ssid, password = "") {
|
||||
if (root.isConnecting)
|
||||
if (root.isConnecting) {
|
||||
return
|
||||
}
|
||||
|
||||
root.isConnecting = true
|
||||
root.connectingSSID = ssid
|
||||
root.connectionError = ""
|
||||
root.connectionStatus = "connecting"
|
||||
|
||||
// For saved networks without password, try connection up first
|
||||
if (!password && root.ssidToConnectionName[ssid]) {
|
||||
const connectionName = root.ssidToConnectionName[ssid]
|
||||
wifiConnector.command = ["nmcli", "connection", "up", connectionName]
|
||||
@@ -670,10 +657,6 @@ Singleton {
|
||||
wifiConnector.running = true
|
||||
}
|
||||
|
||||
function connectToWifiWithPassword(ssid, password) {
|
||||
connectToWifi(ssid, password)
|
||||
}
|
||||
|
||||
Process {
|
||||
id: wifiConnector
|
||||
running: false
|
||||
@@ -688,8 +671,7 @@ Singleton {
|
||||
root.connectionError = ""
|
||||
root.connectionStatus = "connected"
|
||||
|
||||
if (root.userPreference === "wifi"
|
||||
|| root.userPreference === "auto") {
|
||||
if (root.userPreference === "wifi" || root.userPreference === "auto") {
|
||||
setConnectionPriority("wifi")
|
||||
}
|
||||
}
|
||||
@@ -701,8 +683,7 @@ Singleton {
|
||||
root.connectionError = text
|
||||
root.lastConnectionError = text
|
||||
if (!wifiConnector.connectionSucceeded && text.trim() !== "") {
|
||||
if (text.includes("password") || text.includes(
|
||||
"authentication")) {
|
||||
if (text.includes("password") || text.includes("authentication")) {
|
||||
root.connectionStatus = "invalid_password"
|
||||
root.passwordDialogShouldReopen = true
|
||||
} else {
|
||||
@@ -715,7 +696,6 @@ Singleton {
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0 || wifiConnector.connectionSucceeded) {
|
||||
if (!wifiConnector.connectionSucceeded) {
|
||||
// Command succeeded but we didn't see "successfully" - still mark as success
|
||||
ToastService.showInfo(`Connected to ${root.connectingSSID}`)
|
||||
root.connectionStatus = "connected"
|
||||
}
|
||||
@@ -724,11 +704,9 @@ Singleton {
|
||||
root.connectionStatus = "failed"
|
||||
}
|
||||
if (root.connectionStatus === "invalid_password") {
|
||||
ToastService.showError(
|
||||
`Invalid password for ${root.connectingSSID}`)
|
||||
ToastService.showError(`Invalid password for ${root.connectingSSID}`)
|
||||
} else {
|
||||
ToastService.showError(
|
||||
`Failed to connect to ${root.connectingSSID}`)
|
||||
ToastService.showError(`Failed to connect to ${root.connectingSSID}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -740,8 +718,9 @@ Singleton {
|
||||
}
|
||||
|
||||
function disconnectWifi() {
|
||||
if (!root.wifiInterface)
|
||||
if (!root.wifiInterface) {
|
||||
return
|
||||
}
|
||||
|
||||
wifiDisconnector.command = ["nmcli", "dev", "disconnect", root.wifiInterface]
|
||||
wifiDisconnector.running = true
|
||||
@@ -776,13 +755,11 @@ Singleton {
|
||||
if (exitCode === 0) {
|
||||
ToastService.showInfo(`Forgot network ${root.forgetSSID}`)
|
||||
|
||||
root.savedConnections = root.savedConnections.filter(
|
||||
s => s.ssid !== root.forgetSSID)
|
||||
root.savedWifiNetworks = root.savedWifiNetworks.filter(
|
||||
s => s.ssid !== root.forgetSSID)
|
||||
root.savedConnections = root.savedConnections.filter(s => s.ssid !== root.forgetSSID)
|
||||
root.savedWifiNetworks = root.savedWifiNetworks.filter(s => s.ssid !== root.forgetSSID)
|
||||
|
||||
let updated = [...root.wifiNetworks]
|
||||
for (let network of updated) {
|
||||
const updated = [...root.wifiNetworks]
|
||||
for (const network of updated) {
|
||||
if (network.ssid === root.forgetSSID) {
|
||||
network.saved = false
|
||||
if (network.connected) {
|
||||
@@ -800,8 +777,9 @@ Singleton {
|
||||
}
|
||||
|
||||
function toggleWifiRadio() {
|
||||
if (root.wifiToggling)
|
||||
if (root.wifiToggling) {
|
||||
return
|
||||
}
|
||||
|
||||
root.wifiToggling = true
|
||||
const targetState = root.wifiEnabled ? "off" : "on"
|
||||
@@ -819,15 +797,12 @@ Singleton {
|
||||
onExited: exitCode => {
|
||||
root.wifiToggling = false
|
||||
if (exitCode === 0) {
|
||||
// Don't manually toggle wifiEnabled - let DBus monitoring handle it
|
||||
ToastService.showInfo(
|
||||
targetState === "on" ? "WiFi enabled" : "WiFi disabled")
|
||||
ToastService.showInfo(targetState === "on" ? "WiFi enabled" : "WiFi disabled")
|
||||
}
|
||||
refreshNetworkState()
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Network Preference Management =====
|
||||
function setNetworkPreference(preference) {
|
||||
root.userPreference = preference
|
||||
root.changingPreference = true
|
||||
@@ -839,7 +814,6 @@ Singleton {
|
||||
} else if (preference === "ethernet") {
|
||||
setConnectionPriority("ethernet")
|
||||
}
|
||||
// "auto" uses default NetworkManager behavior
|
||||
}
|
||||
|
||||
function setConnectionPriority(type) {
|
||||
@@ -865,9 +839,7 @@ Singleton {
|
||||
|
||||
Process {
|
||||
id: restartConnections
|
||||
command: ["bash", "-c", "nmcli -t -f UUID,TYPE connection show --active | "
|
||||
+ "grep -E '802-11-wireless|802-3-ethernet' | cut -d: -f1 | "
|
||||
+ "xargs -I {} sh -c 'nmcli connection down {} && nmcli connection up {}'"]
|
||||
command: ["bash", "-c", "nmcli -t -f UUID,TYPE connection show --active | " + "grep -E '802-11-wireless|802-3-ethernet' | cut -d: -f1 | " + "xargs -I {} sh -c 'nmcli connection down {} && nmcli connection up {}'"]
|
||||
running: false
|
||||
|
||||
onExited: {
|
||||
@@ -890,7 +862,6 @@ Singleton {
|
||||
root.autoRefreshEnabled = false
|
||||
}
|
||||
|
||||
// ===== Network Info =====
|
||||
function fetchNetworkInfo(ssid) {
|
||||
root.networkInfoSSID = ssid
|
||||
root.networkInfoLoading = true
|
||||
@@ -907,23 +878,21 @@ Singleton {
|
||||
onStreamFinished: {
|
||||
let details = ""
|
||||
if (text.trim()) {
|
||||
let lines = text.trim().split('\n')
|
||||
let bands = []
|
||||
|
||||
// Collect all access points for this SSID
|
||||
for (let line of lines) {
|
||||
let parts = line.split(':')
|
||||
const lines = text.trim().split('\n')
|
||||
const bands = []
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.split(':')
|
||||
if (parts.length >= 11 && parts[0] === root.networkInfoSSID) {
|
||||
let signal = parts[1] || "0"
|
||||
let security = parts[2] || "Open"
|
||||
let freq = parts[3] || "Unknown"
|
||||
let rate = parts[4] || "Unknown"
|
||||
let channel = parts[6] || "Unknown"
|
||||
let isActive = parts[9] === "yes"
|
||||
// BSSID is the last field, find it by counting colons
|
||||
const signal = parts[1] || "0"
|
||||
const security = parts[2] || "Open"
|
||||
const freq = parts[3] || "Unknown"
|
||||
const rate = parts[4] || "Unknown"
|
||||
const channel = parts[6] || "Unknown"
|
||||
const isActive = parts[9] === "yes"
|
||||
let colonCount = 0
|
||||
let bssidStart = -1
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
for (var i = 0; i < line.length; i++) {
|
||||
if (line[i] === ':') {
|
||||
colonCount++
|
||||
if (colonCount === 10) {
|
||||
@@ -932,10 +901,10 @@ Singleton {
|
||||
}
|
||||
}
|
||||
}
|
||||
let bssid = bssidStart >= 0 ? line.substring(bssidStart).replace(/\\:/g, ":") : ""
|
||||
const bssid = bssidStart >= 0 ? line.substring(bssidStart).replace(/\\:/g, ":") : ""
|
||||
|
||||
let band = "Unknown"
|
||||
let freqNum = parseInt(freq)
|
||||
const freqNum = parseInt(freq)
|
||||
if (freqNum >= 2400 && freqNum <= 2500) {
|
||||
band = "2.4 GHz"
|
||||
} else if (freqNum >= 5000 && freqNum <= 6000) {
|
||||
@@ -945,28 +914,31 @@ Singleton {
|
||||
}
|
||||
|
||||
bands.push({
|
||||
band: band,
|
||||
freq: freq,
|
||||
channel: channel,
|
||||
signal: signal,
|
||||
rate: rate,
|
||||
security: security,
|
||||
isActive: isActive,
|
||||
bssid: bssid
|
||||
})
|
||||
"band": band,
|
||||
"freq": freq,
|
||||
"channel": channel,
|
||||
"signal": signal,
|
||||
"rate": rate,
|
||||
"security": security,
|
||||
"isActive": isActive,
|
||||
"bssid": bssid
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (bands.length > 0) {
|
||||
// Sort bands: active first, then by signal strength
|
||||
bands.sort((a, b) => {
|
||||
if (a.isActive && !b.isActive) return -1
|
||||
if (!a.isActive && b.isActive) return 1
|
||||
return parseInt(b.signal) - parseInt(a.signal)
|
||||
})
|
||||
|
||||
for (let i = 0; i < bands.length; i++) {
|
||||
let b = bands[i]
|
||||
if (a.isActive && !b.isActive) {
|
||||
return -1
|
||||
}
|
||||
if (!a.isActive && b.isActive) {
|
||||
return 1
|
||||
}
|
||||
return parseInt(b.signal) - parseInt(a.signal)
|
||||
})
|
||||
|
||||
for (var i = 0; i < bands.length; i++) {
|
||||
const b = bands[i]
|
||||
if (b.isActive) {
|
||||
details += "● " + b.band + " (Connected) - " + b.signal + "%\\n"
|
||||
} else {
|
||||
@@ -998,18 +970,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function refreshNetworkStatus() {
|
||||
refreshNetworkState()
|
||||
}
|
||||
|
||||
function delayedRefreshNetworkStatus() {
|
||||
refreshNetworkState()
|
||||
}
|
||||
|
||||
function updateCurrentWifiInfo() {
|
||||
getCurrentWifiInfo.running = true
|
||||
}
|
||||
|
||||
function enableWifiDevice() {
|
||||
wifiDeviceEnabler.running = true
|
||||
}
|
||||
@@ -1030,7 +990,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function connectToWifiAndSetPreference(ssid, password) {
|
||||
connectToWifiWithPassword(ssid, password)
|
||||
connectToWifi(ssid, password)
|
||||
setNetworkPreference("wifi")
|
||||
}
|
||||
|
||||
@@ -1066,8 +1026,9 @@ Singleton {
|
||||
|
||||
function getNetworkInfo(ssid) {
|
||||
const network = root.wifiNetworks.find(n => n.ssid === ssid)
|
||||
if (!network)
|
||||
if (!network) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
"ssid": network.ssid,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
@@ -9,7 +10,6 @@ import Quickshell.Wayland
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Workspace management
|
||||
property var workspaces: ({})
|
||||
property var allWorkspaces: []
|
||||
property int focusedWorkspaceIndex: 0
|
||||
@@ -17,24 +17,18 @@ Singleton {
|
||||
property var currentOutputWorkspaces: []
|
||||
property string currentOutput: ""
|
||||
|
||||
// Output/Monitor management
|
||||
property var outputs: ({}) // Map of output name to output info with positions
|
||||
property var outputs: ({})
|
||||
|
||||
// Window management
|
||||
property var windows: []
|
||||
|
||||
// Overview state
|
||||
property bool inOverview: false
|
||||
|
||||
// Keyboard layout state
|
||||
property int currentKeyboardLayoutIndex: 0
|
||||
property var keyboardLayoutNames: []
|
||||
|
||||
// Internal state (not exposed to external components)
|
||||
property string configValidationOutput: ""
|
||||
property bool hasInitialConnection: false
|
||||
|
||||
|
||||
readonly property string socketPath: Quickshell.env("NIRI_SOCKET")
|
||||
|
||||
Component.onCompleted: {
|
||||
@@ -54,11 +48,9 @@ Singleton {
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
try {
|
||||
var outputsData = JSON.parse(text)
|
||||
const outputsData = JSON.parse(text)
|
||||
outputs = outputsData
|
||||
console.log("NiriService: Loaded",
|
||||
Object.keys(outputsData).length, "outputs")
|
||||
// Re-sort windows with monitor positions
|
||||
console.log("NiriService: Loaded", Object.keys(outputsData).length, "outputs")
|
||||
if (windows.length > 0) {
|
||||
windows = sortWindowsByLayout(windows)
|
||||
}
|
||||
@@ -70,9 +62,7 @@ Singleton {
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn(
|
||||
"NiriService: Failed to fetch outputs, exit code:",
|
||||
exitCode)
|
||||
console.warn("NiriService: Failed to fetch outputs, exit code:", exitCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,59 +98,40 @@ Singleton {
|
||||
|
||||
function sortWindowsByLayout(windowList) {
|
||||
return [...windowList].sort((a, b) => {
|
||||
// Get workspace info for both windows
|
||||
var aWorkspace = workspaces[a.workspace_id]
|
||||
var bWorkspace = workspaces[b.workspace_id]
|
||||
const aWorkspace = workspaces[a.workspace_id]
|
||||
const bWorkspace = workspaces[b.workspace_id]
|
||||
|
||||
if (aWorkspace && bWorkspace) {
|
||||
var aOutput = aWorkspace.output
|
||||
var bOutput = bWorkspace.output
|
||||
const aOutput = aWorkspace.output
|
||||
const bOutput = bWorkspace.output
|
||||
|
||||
// 1. First, sort by monitor position (left to right, top to bottom)
|
||||
var aOutputInfo = outputs[aOutput]
|
||||
var bOutputInfo = outputs[bOutput]
|
||||
const aOutputInfo = outputs[aOutput]
|
||||
const bOutputInfo = outputs[bOutput]
|
||||
|
||||
if (aOutputInfo && bOutputInfo
|
||||
&& aOutputInfo.logical
|
||||
&& bOutputInfo.logical) {
|
||||
// Sort by monitor X position (left to right)
|
||||
if (aOutputInfo.logical.x
|
||||
!== bOutputInfo.logical.x) {
|
||||
return aOutputInfo.logical.x
|
||||
- bOutputInfo.logical.x
|
||||
if (aOutputInfo && bOutputInfo && aOutputInfo.logical && bOutputInfo.logical) {
|
||||
if (aOutputInfo.logical.x !== bOutputInfo.logical.x) {
|
||||
return aOutputInfo.logical.x - bOutputInfo.logical.x
|
||||
}
|
||||
// If same X, sort by Y position (top to bottom)
|
||||
if (aOutputInfo.logical.y
|
||||
!== bOutputInfo.logical.y) {
|
||||
return aOutputInfo.logical.y
|
||||
- bOutputInfo.logical.y
|
||||
if (aOutputInfo.logical.y !== bOutputInfo.logical.y) {
|
||||
return aOutputInfo.logical.y - bOutputInfo.logical.y
|
||||
}
|
||||
}
|
||||
|
||||
// 2. If same monitor, sort by workspace index
|
||||
if (aOutput === bOutput
|
||||
&& aWorkspace.idx !== bWorkspace.idx) {
|
||||
if (aOutput === bOutput && aWorkspace.idx !== bWorkspace.idx) {
|
||||
return aWorkspace.idx - bWorkspace.idx
|
||||
}
|
||||
}
|
||||
|
||||
// 3. If same workspace, sort by actual position within workspace
|
||||
if (a.workspace_id === b.workspace_id
|
||||
&& a.layout && b.layout) {
|
||||
if (a.workspace_id === b.workspace_id && a.layout && b.layout) {
|
||||
|
||||
// Use pos_in_scrolling_layout [x, y] coordinates
|
||||
if (a.layout.pos_in_scrolling_layout
|
||||
&& b.layout.pos_in_scrolling_layout) {
|
||||
var aPos = a.layout.pos_in_scrolling_layout
|
||||
var bPos = b.layout.pos_in_scrolling_layout
|
||||
if (a.layout.pos_in_scrolling_layout && b.layout.pos_in_scrolling_layout) {
|
||||
const aPos = a.layout.pos_in_scrolling_layout
|
||||
const bPos = b.layout.pos_in_scrolling_layout
|
||||
|
||||
if (aPos.length > 1
|
||||
&& bPos.length > 1) {
|
||||
// Sort by X (horizontal) position first
|
||||
if (aPos.length > 1 && bPos.length > 1) {
|
||||
if (aPos[0] !== bPos[0]) {
|
||||
return aPos[0] - bPos[0]
|
||||
}
|
||||
// Then sort by Y (vertical) position
|
||||
if (aPos[1] !== bPos[1]) {
|
||||
return aPos[1] - bPos[1]
|
||||
}
|
||||
@@ -168,37 +139,50 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Fallback to window ID for consistent ordering
|
||||
return a.id - b.id
|
||||
})
|
||||
}
|
||||
|
||||
function handleNiriEvent(event) {
|
||||
if (event.WorkspacesChanged) {
|
||||
handleWorkspacesChanged(event.WorkspacesChanged)
|
||||
} else if (event.WorkspaceActivated) {
|
||||
handleWorkspaceActivated(event.WorkspaceActivated)
|
||||
} else if (event.WorkspaceActiveWindowChanged) {
|
||||
handleWorkspaceActiveWindowChanged(
|
||||
event.WorkspaceActiveWindowChanged)
|
||||
} else if (event.WindowsChanged) {
|
||||
handleWindowsChanged(event.WindowsChanged)
|
||||
} else if (event.WindowClosed) {
|
||||
handleWindowClosed(event.WindowClosed)
|
||||
} else if (event.WindowOpenedOrChanged) {
|
||||
handleWindowOpenedOrChanged(event.WindowOpenedOrChanged)
|
||||
} else if (event.WindowLayoutsChanged) {
|
||||
handleWindowLayoutsChanged(event.WindowLayoutsChanged)
|
||||
} else if (event.OutputsChanged) {
|
||||
handleOutputsChanged(event.OutputsChanged)
|
||||
} else if (event.OverviewOpenedOrClosed) {
|
||||
handleOverviewChanged(event.OverviewOpenedOrClosed)
|
||||
} else if (event.ConfigLoaded) {
|
||||
handleConfigLoaded(event.ConfigLoaded)
|
||||
} else if (event.KeyboardLayoutsChanged) {
|
||||
handleKeyboardLayoutsChanged(event.KeyboardLayoutsChanged)
|
||||
} else if (event.KeyboardLayoutSwitched) {
|
||||
handleKeyboardLayoutSwitched(event.KeyboardLayoutSwitched)
|
||||
const eventType = Object.keys(event)[0];
|
||||
|
||||
switch (eventType) {
|
||||
case 'WorkspacesChanged':
|
||||
handleWorkspacesChanged(event.WorkspacesChanged);
|
||||
break;
|
||||
case 'WorkspaceActivated':
|
||||
handleWorkspaceActivated(event.WorkspaceActivated);
|
||||
break;
|
||||
case 'WorkspaceActiveWindowChanged':
|
||||
handleWorkspaceActiveWindowChanged(event.WorkspaceActiveWindowChanged);
|
||||
break;
|
||||
case 'WindowsChanged':
|
||||
handleWindowsChanged(event.WindowsChanged);
|
||||
break;
|
||||
case 'WindowClosed':
|
||||
handleWindowClosed(event.WindowClosed);
|
||||
break;
|
||||
case 'WindowOpenedOrChanged':
|
||||
handleWindowOpenedOrChanged(event.WindowOpenedOrChanged);
|
||||
break;
|
||||
case 'WindowLayoutsChanged':
|
||||
handleWindowLayoutsChanged(event.WindowLayoutsChanged);
|
||||
break;
|
||||
case 'OutputsChanged':
|
||||
handleOutputsChanged(event.OutputsChanged);
|
||||
break;
|
||||
case 'OverviewOpenedOrClosed':
|
||||
handleOverviewChanged(event.OverviewOpenedOrClosed);
|
||||
break;
|
||||
case 'ConfigLoaded':
|
||||
handleConfigLoaded(event.ConfigLoaded);
|
||||
break;
|
||||
case 'KeyboardLayoutsChanged':
|
||||
handleKeyboardLayoutsChanged(event.KeyboardLayoutsChanged);
|
||||
break;
|
||||
case 'KeyboardLayoutSwitched':
|
||||
handleKeyboardLayoutSwitched(event.KeyboardLayoutSwitched);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +198,7 @@ Singleton {
|
||||
|
||||
focusedWorkspaceIndex = allWorkspaces.findIndex(w => w.is_focused)
|
||||
if (focusedWorkspaceIndex >= 0) {
|
||||
var focusedWs = allWorkspaces[focusedWorkspaceIndex]
|
||||
const focusedWs = allWorkspaces[focusedWorkspaceIndex]
|
||||
focusedWorkspaceId = focusedWs.id
|
||||
currentOutput = focusedWs.output || ""
|
||||
} else {
|
||||
@@ -227,8 +211,9 @@ Singleton {
|
||||
|
||||
function handleWorkspaceActivated(data) {
|
||||
const ws = root.workspaces[data.id]
|
||||
if (!ws)
|
||||
if (!ws) {
|
||||
return
|
||||
}
|
||||
const output = ws.output
|
||||
|
||||
for (const id in root.workspaces) {
|
||||
@@ -251,23 +236,18 @@ Singleton {
|
||||
currentOutput = allWorkspaces[focusedWorkspaceIndex].output || ""
|
||||
}
|
||||
|
||||
allWorkspaces = Object.values(root.workspaces).sort(
|
||||
(a, b) => a.idx - b.idx)
|
||||
allWorkspaces = Object.values(root.workspaces).sort((a, b) => a.idx - b.idx)
|
||||
|
||||
updateCurrentOutputWorkspaces()
|
||||
workspacesChanged()
|
||||
}
|
||||
|
||||
function handleWorkspaceActiveWindowChanged(data) {
|
||||
// Update the focused window when workspace's active window changes
|
||||
// This is crucial for handling floating window close scenarios
|
||||
if (data.active_window_id !== null
|
||||
&& data.active_window_id !== undefined) {
|
||||
// Create new windows array with updated focus states to trigger property change
|
||||
let updatedWindows = []
|
||||
if (data.active_window_id !== null && data.active_window_id !== undefined) {
|
||||
const updatedWindows = []
|
||||
for (var i = 0; i < windows.length; i++) {
|
||||
let w = windows[i]
|
||||
let updatedWindow = {}
|
||||
const w = windows[i]
|
||||
const updatedWindow = {}
|
||||
for (let prop in w) {
|
||||
updatedWindow[prop] = w[prop]
|
||||
}
|
||||
@@ -276,17 +256,14 @@ Singleton {
|
||||
}
|
||||
windows = updatedWindows
|
||||
} else {
|
||||
// No active window in this workspace
|
||||
// Create new windows array with cleared focus states for this workspace
|
||||
let updatedWindows = []
|
||||
const updatedWindows = []
|
||||
for (var i = 0; i < windows.length; i++) {
|
||||
let w = windows[i]
|
||||
let updatedWindow = {}
|
||||
const w = windows[i]
|
||||
const updatedWindow = {}
|
||||
for (let prop in w) {
|
||||
updatedWindow[prop] = w[prop]
|
||||
}
|
||||
updatedWindow.is_focused = w.workspace_id
|
||||
== data.workspace_id ? false : w.is_focused
|
||||
updatedWindow.is_focused = w.workspace_id == data.workspace_id ? false : w.is_focused
|
||||
updatedWindows.push(updatedWindow)
|
||||
}
|
||||
windows = updatedWindows
|
||||
@@ -302,28 +279,28 @@ Singleton {
|
||||
}
|
||||
|
||||
function handleWindowOpenedOrChanged(data) {
|
||||
if (!data.window)
|
||||
if (!data.window) {
|
||||
return
|
||||
}
|
||||
|
||||
const window = data.window
|
||||
const existingIndex = windows.findIndex(w => w.id === window.id)
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
let updatedWindows = [...windows]
|
||||
const updatedWindows = [...windows]
|
||||
updatedWindows[existingIndex] = window
|
||||
windows = sortWindowsByLayout(updatedWindows)
|
||||
} else {
|
||||
windows = sortWindowsByLayout([...windows, window])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function handleWindowLayoutsChanged(data) {
|
||||
// Update layout positions for windows that have changed
|
||||
if (!data.changes)
|
||||
if (!data.changes) {
|
||||
return
|
||||
}
|
||||
|
||||
let updatedWindows = [...windows]
|
||||
const updatedWindows = [...windows]
|
||||
let hasChanges = false
|
||||
|
||||
for (const change of data.changes) {
|
||||
@@ -332,8 +309,7 @@ Singleton {
|
||||
|
||||
const windowIndex = updatedWindows.findIndex(w => w.id === windowId)
|
||||
if (windowIndex >= 0) {
|
||||
// Create a new object with updated layout
|
||||
var updatedWindow = {}
|
||||
const updatedWindow = {}
|
||||
for (var prop in updatedWindows[windowIndex]) {
|
||||
updatedWindow[prop] = updatedWindows[windowIndex][prop]
|
||||
}
|
||||
@@ -345,7 +321,6 @@ Singleton {
|
||||
|
||||
if (hasChanges) {
|
||||
windows = sortWindowsByLayout(updatedWindows)
|
||||
// Trigger update in dock and widgets
|
||||
windowsChanged()
|
||||
}
|
||||
}
|
||||
@@ -353,7 +328,6 @@ Singleton {
|
||||
function handleOutputsChanged(data) {
|
||||
if (data.outputs) {
|
||||
outputs = data.outputs
|
||||
// Re-sort windows with new monitor positions
|
||||
windows = sortWindowsByLayout(windows)
|
||||
}
|
||||
}
|
||||
@@ -367,8 +341,7 @@ Singleton {
|
||||
validateProcess.running = true
|
||||
} else {
|
||||
configValidationOutput = ""
|
||||
if (ToastService.toastVisible
|
||||
&& ToastService.currentLevel === ToastService.levelError) {
|
||||
if (ToastService.toastVisible && ToastService.currentLevel === ToastService.levelError) {
|
||||
ToastService.hideToast()
|
||||
}
|
||||
if (hasInitialConnection) {
|
||||
@@ -398,13 +371,10 @@ Singleton {
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const lines = text.split('\n')
|
||||
const trimmedLines = lines.map(line => line.replace(/\s+$/,
|
||||
'')).filter(
|
||||
line => line.length > 0)
|
||||
const trimmedLines = lines.map(line => line.replace(/\s+$/, '')).filter(line => line.length > 0)
|
||||
configValidationOutput = trimmedLines.join('\n').trim()
|
||||
if (hasInitialConnection) {
|
||||
ToastService.showError("niri: failed to load config",
|
||||
configValidationOutput)
|
||||
ToastService.showError("niri: failed to load config", configValidationOutput)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -422,13 +392,14 @@ Singleton {
|
||||
return
|
||||
}
|
||||
|
||||
var outputWs = allWorkspaces.filter(w => w.output === currentOutput)
|
||||
const outputWs = allWorkspaces.filter(w => w.output === currentOutput)
|
||||
currentOutputWorkspaces = outputWs
|
||||
}
|
||||
|
||||
function send(request) {
|
||||
if (!CompositorService.isNiri || !requestSocket.connected)
|
||||
if (!CompositorService.isNiri || !requestSocket.connected) {
|
||||
return false
|
||||
}
|
||||
requestSocket.write(JSON.stringify(request) + "\n")
|
||||
return true
|
||||
}
|
||||
@@ -444,41 +415,36 @@ Singleton {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function focusWindow(windowId) {
|
||||
return send({
|
||||
"Action": {
|
||||
"FocusWindow": {
|
||||
"id": windowId
|
||||
}
|
||||
}
|
||||
})
|
||||
"Action": {
|
||||
"FocusWindow": {
|
||||
"id": windowId
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function getCurrentOutputWorkspaceNumbers() {
|
||||
return currentOutputWorkspaces.map(
|
||||
w => w.idx + 1) // niri uses 0-based, UI shows 1-based
|
||||
return currentOutputWorkspaces.map(w => w.idx + 1)
|
||||
}
|
||||
|
||||
function getCurrentWorkspaceNumber() {
|
||||
if (focusedWorkspaceIndex >= 0
|
||||
&& focusedWorkspaceIndex < allWorkspaces.length) {
|
||||
if (focusedWorkspaceIndex >= 0 && focusedWorkspaceIndex < allWorkspaces.length) {
|
||||
return allWorkspaces[focusedWorkspaceIndex].idx + 1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
function getCurrentKeyboardLayoutName() {
|
||||
if (currentKeyboardLayoutIndex >= 0
|
||||
&& currentKeyboardLayoutIndex < keyboardLayoutNames.length) {
|
||||
if (currentKeyboardLayoutIndex >= 0 && currentKeyboardLayoutIndex < keyboardLayoutNames.length) {
|
||||
return keyboardLayoutNames[currentKeyboardLayoutIndex]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
function cycleKeyboardLayout() {
|
||||
return send({
|
||||
"Action": {
|
||||
@@ -499,16 +465,19 @@ Singleton {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
function findNiriWindow(toplevel) {
|
||||
if (!toplevel.appId) return null
|
||||
|
||||
if (!toplevel.appId) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (var j = 0; j < windows.length; j++) {
|
||||
var niriWindow = windows[j]
|
||||
const niriWindow = windows[j]
|
||||
if (niriWindow.app_id === toplevel.appId) {
|
||||
if (!niriWindow.title || niriWindow.title === toplevel.title) {
|
||||
return { niriIndex: j, niriWindow: niriWindow }
|
||||
return {
|
||||
"niriIndex": j,
|
||||
"niriWindow": niriWindow
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -519,69 +488,71 @@ Singleton {
|
||||
if (!toplevels || toplevels.length === 0 || !CompositorService.isNiri || windows.length === 0) {
|
||||
return [...toplevels]
|
||||
}
|
||||
|
||||
|
||||
return [...toplevels].sort((a, b) => {
|
||||
var aNiri = findNiriWindow(a)
|
||||
var bNiri = findNiriWindow(b)
|
||||
|
||||
if (!aNiri && !bNiri) return 0
|
||||
if (!aNiri) return 1
|
||||
if (!bNiri) return -1
|
||||
|
||||
var aWindow = aNiri.niriWindow
|
||||
var bWindow = bNiri.niriWindow
|
||||
var aWorkspace = allWorkspaces.find(ws => ws.id === aWindow.workspace_id)
|
||||
var bWorkspace = allWorkspaces.find(ws => ws.id === bWindow.workspace_id)
|
||||
|
||||
if (aWorkspace && bWorkspace) {
|
||||
if (aWorkspace.output !== bWorkspace.output) {
|
||||
return aWorkspace.output.localeCompare(bWorkspace.output)
|
||||
}
|
||||
|
||||
if (aWorkspace.output === bWorkspace.output && aWorkspace.idx !== bWorkspace.idx) {
|
||||
return aWorkspace.idx - bWorkspace.idx
|
||||
}
|
||||
}
|
||||
|
||||
if (aWindow.workspace_id === bWindow.workspace_id &&
|
||||
aWindow.layout && bWindow.layout &&
|
||||
aWindow.layout.pos_in_scrolling_layout &&
|
||||
bWindow.layout.pos_in_scrolling_layout) {
|
||||
var aPos = aWindow.layout.pos_in_scrolling_layout
|
||||
var bPos = bWindow.layout.pos_in_scrolling_layout
|
||||
|
||||
if (aPos.length > 1 && bPos.length > 1) {
|
||||
if (aPos[0] !== bPos[0]) {
|
||||
return aPos[0] - bPos[0]
|
||||
}
|
||||
if (aPos[1] !== bPos[1]) {
|
||||
return aPos[1] - bPos[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return aWindow.id - bWindow.id
|
||||
})
|
||||
const aNiri = findNiriWindow(a)
|
||||
const bNiri = findNiriWindow(b)
|
||||
|
||||
if (!aNiri && !bNiri) {
|
||||
return 0
|
||||
}
|
||||
if (!aNiri) {
|
||||
return 1
|
||||
}
|
||||
if (!bNiri) {
|
||||
return -1
|
||||
}
|
||||
|
||||
const aWindow = aNiri.niriWindow
|
||||
const bWindow = bNiri.niriWindow
|
||||
const aWorkspace = allWorkspaces.find(ws => ws.id === aWindow.workspace_id)
|
||||
const bWorkspace = allWorkspaces.find(ws => ws.id === bWindow.workspace_id)
|
||||
|
||||
if (aWorkspace && bWorkspace) {
|
||||
if (aWorkspace.output !== bWorkspace.output) {
|
||||
return aWorkspace.output.localeCompare(bWorkspace.output)
|
||||
}
|
||||
|
||||
if (aWorkspace.output === bWorkspace.output && aWorkspace.idx !== bWorkspace.idx) {
|
||||
return aWorkspace.idx - bWorkspace.idx
|
||||
}
|
||||
}
|
||||
|
||||
if (aWindow.workspace_id === bWindow.workspace_id && aWindow.layout && bWindow.layout && aWindow.layout.pos_in_scrolling_layout && bWindow.layout.pos_in_scrolling_layout) {
|
||||
const aPos = aWindow.layout.pos_in_scrolling_layout
|
||||
const bPos = bWindow.layout.pos_in_scrolling_layout
|
||||
|
||||
if (aPos.length > 1 && bPos.length > 1) {
|
||||
if (aPos[0] !== bPos[0]) {
|
||||
return aPos[0] - bPos[0]
|
||||
}
|
||||
if (aPos[1] !== bPos[1]) {
|
||||
return aPos[1] - bPos[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return aWindow.id - bWindow.id
|
||||
})
|
||||
}
|
||||
|
||||
function filterCurrentWorkspace(toplevels, screenName){
|
||||
var currentWorkspaceId = null
|
||||
function filterCurrentWorkspace(toplevels, screenName) {
|
||||
let currentWorkspaceId = null
|
||||
for (var i = 0; i < allWorkspaces.length; i++) {
|
||||
var ws = allWorkspaces[i]
|
||||
if (ws.output === screenName && ws.is_active){
|
||||
const ws = allWorkspaces[i]
|
||||
if (ws.output === screenName && ws.is_active) {
|
||||
currentWorkspaceId = ws.id
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (currentWorkspaceId === null) {
|
||||
return toplevels
|
||||
}
|
||||
|
||||
return toplevels.filter(toplevel => {
|
||||
var niriMatch = findNiriWindow(toplevel)
|
||||
return niriMatch && niriMatch.niriWindow.workspace_id === currentWorkspaceId
|
||||
})
|
||||
const niriMatch = findNiriWindow(toplevel)
|
||||
return niriMatch && niriMatch.niriWindow.workspace_id === currentWorkspaceId
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
@@ -13,8 +14,7 @@ Singleton {
|
||||
|
||||
readonly property list<NotifWrapper> notifications: []
|
||||
readonly property list<NotifWrapper> allWrappers: []
|
||||
readonly property list<NotifWrapper> popups: allWrappers.filter(
|
||||
n => n && n.popup)
|
||||
readonly property list<NotifWrapper> popups: allWrappers.filter(n => n && n.popup)
|
||||
|
||||
property list<NotifWrapper> notificationQueue: []
|
||||
property list<NotifWrapper> visibleNotifications: []
|
||||
@@ -34,14 +34,19 @@ Singleton {
|
||||
property int _dismissBatchSize: 8
|
||||
property int _dismissTickMs: 8
|
||||
property bool _suspendGrouping: false
|
||||
property var _groupCache: ({"notifications": [], "popups": []})
|
||||
property var _groupCache: ({
|
||||
"notifications": [],
|
||||
"popups": []
|
||||
})
|
||||
property bool _groupsDirty: false
|
||||
|
||||
Component.onCompleted: {
|
||||
_recomputeGroups()
|
||||
}
|
||||
|
||||
function _nowSec() { return Date.now() / 1000.0 }
|
||||
function _nowSec() {
|
||||
return Date.now() / 1000.0
|
||||
}
|
||||
|
||||
function _ingressAllowed(notif) {
|
||||
const t = _nowSec()
|
||||
@@ -50,22 +55,26 @@ Singleton {
|
||||
_ingressCountThisSec = 0
|
||||
}
|
||||
_ingressCountThisSec += 1
|
||||
if (notif.urgency === NotificationUrgency.Critical)
|
||||
if (notif.urgency === NotificationUrgency.Critical) {
|
||||
return true
|
||||
}
|
||||
return _ingressCountThisSec <= maxIngressPerSecond
|
||||
}
|
||||
|
||||
function _enqueuePopup(wrapper) {
|
||||
if (notificationQueue.length >= maxQueueSize) {
|
||||
const gk = getGroupKey(wrapper)
|
||||
let idx = notificationQueue.findIndex(w =>
|
||||
w && getGroupKey(w) === gk && w.urgency !== NotificationUrgency.Critical)
|
||||
let idx = notificationQueue.findIndex(w => w && getGroupKey(w) === gk && w.urgency !== NotificationUrgency.Critical)
|
||||
if (idx === -1) {
|
||||
idx = notificationQueue.findIndex(w => w && w.urgency !== NotificationUrgency.Critical)
|
||||
}
|
||||
if (idx === -1) idx = 0
|
||||
if (idx === -1) {
|
||||
idx = 0
|
||||
}
|
||||
const victim = notificationQueue[idx]
|
||||
if (victim) victim.popup = false
|
||||
if (victim) {
|
||||
victim.popup = false
|
||||
}
|
||||
notificationQueue.splice(idx, 1)
|
||||
}
|
||||
notificationQueue = [...notificationQueue, wrapper]
|
||||
@@ -80,18 +89,26 @@ Singleton {
|
||||
function _trimStored() {
|
||||
if (notifications.length > maxStoredNotifications) {
|
||||
const overflow = notifications.length - maxStoredNotifications
|
||||
let toDrop = []
|
||||
for (let i = notifications.length - 1; i >= 0 && toDrop.length < overflow; --i) {
|
||||
const toDrop = []
|
||||
for (var i = notifications.length - 1; i >= 0 && toDrop.length < overflow; --i) {
|
||||
const w = notifications[i]
|
||||
if (w && w.notification && w.urgency !== NotificationUrgency.Critical)
|
||||
if (w && w.notification && w.urgency !== NotificationUrgency.Critical) {
|
||||
toDrop.push(w)
|
||||
}
|
||||
}
|
||||
for (let i = notifications.length - 1; i >= 0 && toDrop.length < overflow; --i) {
|
||||
for (var i = notifications.length - 1; i >= 0 && toDrop.length < overflow; --i) {
|
||||
const w = notifications[i]
|
||||
if (w && w.notification && toDrop.indexOf(w) === -1)
|
||||
if (w && w.notification && toDrop.indexOf(w) === -1) {
|
||||
toDrop.push(w)
|
||||
}
|
||||
}
|
||||
for (const w of toDrop) {
|
||||
try {
|
||||
w.notification.dismiss()
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
for (const w of toDrop) { try { w.notification.dismiss() } catch(e) {} }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,11 +161,15 @@ Singleton {
|
||||
running: false
|
||||
onTriggered: {
|
||||
let n = Math.min(_dismissBatchSize, _dismissQueue.length)
|
||||
for (let i = 0; i < n; ++i) {
|
||||
for (var i = 0; i < n; ++i) {
|
||||
const w = _dismissQueue.pop()
|
||||
try {
|
||||
if (w && w.notification) w.notification.dismiss()
|
||||
} catch (e) {}
|
||||
if (w && w.notification) {
|
||||
w.notification.dismiss()
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
if (_dismissQueue.length === 0) {
|
||||
dismissPump.stop()
|
||||
@@ -195,7 +216,11 @@ Singleton {
|
||||
|
||||
if (!_ingressAllowed(notif)) {
|
||||
if (notif.urgency !== NotificationUrgency.Critical) {
|
||||
try { notif.dismiss() } catch(e) {}
|
||||
try {
|
||||
notif.dismiss()
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -212,8 +237,8 @@ Singleton {
|
||||
_trimStored()
|
||||
|
||||
Qt.callLater(() => {
|
||||
_initWrapperPersistence(wrapper)
|
||||
})
|
||||
_initWrapperPersistence(wrapper)
|
||||
})
|
||||
|
||||
if (shouldShowPopup) {
|
||||
_enqueuePopup(wrapper)
|
||||
@@ -241,8 +266,9 @@ Singleton {
|
||||
|
||||
readonly property Timer timer: Timer {
|
||||
interval: {
|
||||
if (!wrapper.notification)
|
||||
return 5000
|
||||
if (!wrapper.notification) {
|
||||
return 5000
|
||||
}
|
||||
|
||||
switch (wrapper.notification.urgency) {
|
||||
case NotificationUrgency.Low:
|
||||
@@ -273,17 +299,15 @@ Singleton {
|
||||
const hours = Math.floor(minutes / 60)
|
||||
|
||||
if (hours < 1) {
|
||||
if (minutes < 1)
|
||||
return "now"
|
||||
if (minutes < 1) {
|
||||
return "now"
|
||||
}
|
||||
return `${minutes}m ago`
|
||||
}
|
||||
|
||||
const nowDate = new Date(now.getFullYear(), now.getMonth(),
|
||||
now.getDate())
|
||||
const timeDate = new Date(time.getFullYear(), time.getMonth(),
|
||||
time.getDate())
|
||||
const daysDiff = Math.floor(
|
||||
(nowDate - timeDate) / (1000 * 60 * 60 * 24))
|
||||
const nowDate = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
const timeDate = new Date(time.getFullYear(), time.getMonth(), time.getDate())
|
||||
const daysDiff = Math.floor((nowDate - timeDate) / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (daysDiff === 0) {
|
||||
return formatTime(time)
|
||||
@@ -299,8 +323,7 @@ Singleton {
|
||||
function formatTime(date) {
|
||||
let use24Hour = true
|
||||
try {
|
||||
if (typeof SettingsData !== "undefined"
|
||||
&& SettingsData.use24HourClock !== undefined) {
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.use24HourClock !== undefined) {
|
||||
use24Hour = SettingsData.use24HourClock
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -318,7 +341,9 @@ Singleton {
|
||||
readonly property string summary: notification.summary
|
||||
readonly property string body: notification.body
|
||||
readonly property string htmlBody: {
|
||||
if (!popup && !root.popupsDisabled) return ""
|
||||
if (!popup && !root.popupsDisabled) {
|
||||
return ""
|
||||
}
|
||||
if (body && (body.includes('<') && body.includes('>'))) {
|
||||
return body
|
||||
}
|
||||
@@ -337,8 +362,9 @@ Singleton {
|
||||
readonly property string desktopEntry: notification.desktopEntry
|
||||
readonly property string image: notification.image
|
||||
readonly property string cleanImage: {
|
||||
if (!image)
|
||||
return ""
|
||||
if (!image) {
|
||||
return ""
|
||||
}
|
||||
if (image.startsWith("file://")) {
|
||||
return image.substring(7)
|
||||
}
|
||||
@@ -354,12 +380,12 @@ Singleton {
|
||||
root.allWrappers = root.allWrappers.filter(w => w !== wrapper)
|
||||
root.notifications = root.notifications.filter(w => w !== wrapper)
|
||||
|
||||
if (root.bulkDismissing)
|
||||
if (root.bulkDismissing) {
|
||||
return
|
||||
}
|
||||
|
||||
const groupKey = getGroupKey(wrapper)
|
||||
const remainingInGroup = root.notifications.filter(
|
||||
n => getGroupKey(n) === groupKey)
|
||||
const remainingInGroup = root.notifications.filter(n => getGroupKey(n) === groupKey)
|
||||
|
||||
if (remainingInGroup.length <= 1) {
|
||||
clearGroupExpansionState(groupKey)
|
||||
@@ -392,20 +418,23 @@ Singleton {
|
||||
visibleNotifications = []
|
||||
|
||||
_dismissQueue = notifications.slice()
|
||||
if (notifications.length)
|
||||
if (notifications.length) {
|
||||
notifications = []
|
||||
}
|
||||
expandedGroups = {}
|
||||
expandedMessages = {}
|
||||
|
||||
_suspendGrouping = true
|
||||
|
||||
if (!dismissPump.running && _dismissQueue.length)
|
||||
if (!dismissPump.running && _dismissQueue.length) {
|
||||
dismissPump.start()
|
||||
}
|
||||
}
|
||||
|
||||
function dismissNotification(wrapper) {
|
||||
if (!wrapper || !wrapper.notification)
|
||||
if (!wrapper || !wrapper.notification) {
|
||||
return
|
||||
}
|
||||
wrapper.popup = false
|
||||
wrapper.notification.dismiss()
|
||||
}
|
||||
@@ -422,14 +451,18 @@ Singleton {
|
||||
}
|
||||
|
||||
function processQueue() {
|
||||
if (addGateBusy)
|
||||
if (addGateBusy) {
|
||||
return
|
||||
if (popupsDisabled)
|
||||
}
|
||||
if (popupsDisabled) {
|
||||
return
|
||||
if (SessionData.doNotDisturb)
|
||||
}
|
||||
if (SessionData.doNotDisturb) {
|
||||
return
|
||||
if (notificationQueue.length === 0)
|
||||
}
|
||||
if (notificationQueue.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const activePopupCount = visibleNotifications.filter(n => n && n.popup).length
|
||||
if (activePopupCount >= 4) {
|
||||
@@ -461,10 +494,12 @@ Singleton {
|
||||
|
||||
if (w && w.destroy && !w.isPersistent && notifications.indexOf(w) === -1) {
|
||||
Qt.callLater(() => {
|
||||
try {
|
||||
w.destroy()
|
||||
} catch (e) {}
|
||||
})
|
||||
try {
|
||||
w.destroy()
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,8 +525,9 @@ Singleton {
|
||||
|
||||
function _recomputeGroupsLater() {
|
||||
_groupsDirty = true
|
||||
if (!groupsDebounce.running)
|
||||
if (!groupsDebounce.running) {
|
||||
groupsDebounce.start()
|
||||
}
|
||||
}
|
||||
|
||||
function _calcGroupedNotifications() {
|
||||
@@ -520,15 +556,12 @@ Singleton {
|
||||
}
|
||||
|
||||
return Object.values(groups).sort((a, b) => {
|
||||
const aUrgency = a.latestNotification.urgency
|
||||
|| NotificationUrgency.Low
|
||||
const bUrgency = b.latestNotification.urgency
|
||||
|| NotificationUrgency.Low
|
||||
const aUrgency = a.latestNotification.urgency || NotificationUrgency.Low
|
||||
const bUrgency = b.latestNotification.urgency || NotificationUrgency.Low
|
||||
if (aUrgency !== bUrgency) {
|
||||
return bUrgency - aUrgency
|
||||
}
|
||||
return b.latestNotification.time.getTime(
|
||||
) - a.latestNotification.time.getTime()
|
||||
return b.latestNotification.time.getTime() - a.latestNotification.time.getTime()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -558,8 +591,7 @@ Singleton {
|
||||
}
|
||||
|
||||
return Object.values(groups).sort((a, b) => {
|
||||
return b.latestNotification.time.getTime(
|
||||
) - a.latestNotification.time.getTime()
|
||||
return b.latestNotification.time.getTime() - a.latestNotification.time.getTime()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -582,8 +614,7 @@ Singleton {
|
||||
}
|
||||
} else {
|
||||
for (const notif of allWrappers) {
|
||||
if (notif && notif.notification && getGroupKey(
|
||||
notif) === groupKey) {
|
||||
if (notif && notif.notification && getGroupKey(notif) === groupKey) {
|
||||
notif.notification.dismiss()
|
||||
}
|
||||
}
|
||||
@@ -617,8 +648,7 @@ Singleton {
|
||||
expandedGroups = newExpandedGroups
|
||||
let newExpandedMessages = {}
|
||||
for (const messageId in expandedMessages) {
|
||||
if (currentMessageIds.has(messageId)
|
||||
&& expandedMessages[messageId]) {
|
||||
if (currentMessageIds.has(messageId) && expandedMessages[messageId]) {
|
||||
newExpandedMessages[messageId] = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
@@ -14,9 +15,7 @@ Singleton {
|
||||
property bool settingsPortalAvailable: false
|
||||
property int systemColorScheme: 0 // 0=default, 1=prefer-dark, 2=prefer-light
|
||||
|
||||
function init() {
|
||||
// Stub just to force IPC registration
|
||||
}
|
||||
function init() {}
|
||||
|
||||
function getSystemProfileImage() {
|
||||
systemProfileCheckProcess.running = true
|
||||
@@ -40,22 +39,23 @@ Singleton {
|
||||
}
|
||||
|
||||
function setSystemColorScheme(isLightMode) {
|
||||
if (!settingsPortalAvailable)
|
||||
if (!settingsPortalAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
var colorScheme = isLightMode ? "prefer-light" : "prefer-dark"
|
||||
var script = "gsettings set org.gnome.desktop.interface color-scheme '" + colorScheme + "'"
|
||||
const colorScheme = isLightMode ? "prefer-light" : "prefer-dark"
|
||||
const script = `gsettings set org.gnome.desktop.interface color-scheme '${colorScheme}'`
|
||||
|
||||
systemColorSchemeSetProcess.command = ["bash", "-c", script]
|
||||
systemColorSchemeSetProcess.running = true
|
||||
}
|
||||
|
||||
function setSystemProfileImage(imagePath) {
|
||||
if (!accountsServiceAvailable || !imagePath)
|
||||
if (!accountsServiceAvailable || !imagePath) {
|
||||
return
|
||||
}
|
||||
|
||||
var script = ["dbus-send --system --print-reply --dest=org.freedesktop.Accounts", "/org/freedesktop/Accounts/User$(id -u)", "org.freedesktop.Accounts.User.SetIconFile", "string:'" + imagePath + "'"].join(
|
||||
" ")
|
||||
const script = `dbus-send --system --print-reply --dest=org.freedesktop.Accounts /org/freedesktop/Accounts/User$(id -u) org.freedesktop.Accounts.User.SetIconFile string:'${imagePath}'`
|
||||
|
||||
systemProfileSetProcess.command = ["bash", "-c", script]
|
||||
systemProfileSetProcess.running = true
|
||||
@@ -94,9 +94,8 @@ Singleton {
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var match = text.match(/string\s+"([^"]+)"/)
|
||||
if (match && match[1] && match[1] !== ""
|
||||
&& match[1] !== "/var/lib/AccountsService/icons/") {
|
||||
const match = text.match(/string\s+"([^"]+)"/)
|
||||
if (match && match[1] && match[1] !== "" && match[1] !== "/var/lib/AccountsService/icons/") {
|
||||
root.systemProfileImage = match[1]
|
||||
|
||||
if (!root.profileImage || root.profileImage === "") {
|
||||
@@ -144,12 +143,12 @@ Singleton {
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var match = text.match(/uint32 (\d+)/)
|
||||
const match = text.match(/uint32 (\d+)/)
|
||||
if (match && match[1]) {
|
||||
root.systemColorScheme = parseInt(match[1])
|
||||
|
||||
if (typeof Theme !== "undefined") {
|
||||
var shouldBeLightMode = (root.systemColorScheme === 2)
|
||||
const shouldBeLightMode = (root.systemColorScheme === 2)
|
||||
if (Theme.isLightMode !== shouldBeLightMode) {
|
||||
Theme.isLightMode = shouldBeLightMode
|
||||
if (typeof SessionData !== "undefined") {
|
||||
@@ -193,9 +192,7 @@ Singleton {
|
||||
return "ERROR: No path provided"
|
||||
}
|
||||
|
||||
var absolutePath = path.startsWith(
|
||||
"/") ? path : StandardPaths.writableLocation(
|
||||
StandardPaths.HomeLocation) + "/" + path
|
||||
const absolutePath = path.startsWith("/") ? path : `${StandardPaths.writableLocation(StandardPaths.HomeLocation)}/${path}`
|
||||
|
||||
try {
|
||||
root.setProfileImage(absolutePath)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
@@ -10,13 +11,15 @@ Singleton {
|
||||
id: root
|
||||
|
||||
readonly property bool microphoneActive: {
|
||||
if (!Pipewire.ready || !Pipewire.nodes?.values)
|
||||
return false
|
||||
if (!Pipewire.ready || !Pipewire.nodes?.values) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (var i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||
const node = Pipewire.nodes.values[i]
|
||||
if (!node)
|
||||
continue
|
||||
if (!node) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ((node.type & PwNodeType.AudioInStream) === PwNodeType.AudioInStream) {
|
||||
if (!looksLikeSystemVirtualMic(node)) {
|
||||
@@ -32,22 +35,21 @@ Singleton {
|
||||
}
|
||||
|
||||
PwObjectTracker {
|
||||
objects: Pipewire.nodes.values.filter(
|
||||
node => node.audio && !node.isStream
|
||||
)
|
||||
objects: Pipewire.nodes.values.filter(node => node.audio && !node.isStream)
|
||||
}
|
||||
|
||||
readonly property bool cameraActive: {
|
||||
if (!Pipewire.ready || !Pipewire.nodes?.values)
|
||||
return false
|
||||
if (!Pipewire.ready || !Pipewire.nodes?.values) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (var i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||
const node = Pipewire.nodes.values[i]
|
||||
if (!node || !node.ready)
|
||||
continue
|
||||
if (!node || !node.ready) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (node.properties
|
||||
&& node.properties["media.class"] === "Stream/Input/Video") {
|
||||
if (node.properties && node.properties["media.class"] === "Stream/Input/Video") {
|
||||
if (node.properties["stream.is-live"] === "true") {
|
||||
return true
|
||||
}
|
||||
@@ -57,13 +59,15 @@ Singleton {
|
||||
}
|
||||
|
||||
readonly property bool screensharingActive: {
|
||||
if (!Pipewire.ready || !Pipewire.nodes?.values)
|
||||
return false
|
||||
if (!Pipewire.ready || !Pipewire.nodes?.values) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (var i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||
const node = Pipewire.nodes.values[i]
|
||||
if (!node || !node.ready)
|
||||
continue
|
||||
if (!node || !node.ready) {
|
||||
continue
|
||||
}
|
||||
|
||||
if ((node.type & PwNodeType.VideoSource) === PwNodeType.VideoSource) {
|
||||
if (looksLikeScreencast(node)) {
|
||||
@@ -71,15 +75,11 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
if (node.properties
|
||||
&& node.properties["media.class"] === "Stream/Input/Audio") {
|
||||
const mediaName = (node.properties["media.name"]
|
||||
|| "").toLowerCase()
|
||||
const appName = (node.properties["application.name"]
|
||||
|| "").toLowerCase()
|
||||
if (node.properties && node.properties["media.class"] === "Stream/Input/Audio") {
|
||||
const mediaName = (node.properties["media.name"] || "").toLowerCase()
|
||||
const appName = (node.properties["application.name"] || "").toLowerCase()
|
||||
|
||||
if (mediaName.includes("desktop") || appName.includes("screen")
|
||||
|| appName === "obs") {
|
||||
if (mediaName.includes("desktop") || appName.includes("screen") || appName === "obs") {
|
||||
if (node.properties["stream.is-live"] === "true") {
|
||||
if (node.audio && node.audio.muted) {
|
||||
return false
|
||||
@@ -92,30 +92,27 @@ Singleton {
|
||||
return false
|
||||
}
|
||||
|
||||
readonly property bool anyPrivacyActive: microphoneActive || cameraActive
|
||||
|| screensharingActive
|
||||
readonly property bool anyPrivacyActive: microphoneActive || cameraActive || screensharingActive
|
||||
|
||||
function looksLikeSystemVirtualMic(node) {
|
||||
if (!node)
|
||||
if (!node) {
|
||||
return false
|
||||
}
|
||||
const name = (node.name || "").toLowerCase()
|
||||
const mediaName = (node.properties && node.properties["media.name"]
|
||||
|| "").toLowerCase()
|
||||
const appName = (node.properties && node.properties["application.name"]
|
||||
|| "").toLowerCase()
|
||||
const mediaName = (node.properties && node.properties["media.name"] || "").toLowerCase()
|
||||
const appName = (node.properties && node.properties["application.name"] || "").toLowerCase()
|
||||
const combined = name + " " + mediaName + " " + appName
|
||||
return /cava|monitor|system/.test(combined)
|
||||
}
|
||||
|
||||
function looksLikeScreencast(node) {
|
||||
if (!node)
|
||||
if (!node) {
|
||||
return false
|
||||
const appName = (node.properties && node.properties["application.name"]
|
||||
|| "").toLowerCase()
|
||||
}
|
||||
const appName = (node.properties && node.properties["application.name"] || "").toLowerCase()
|
||||
const nodeName = (node.name || "").toLowerCase()
|
||||
const combined = appName + " " + nodeName
|
||||
return /xdg-desktop-portal|xdpw|screencast|screen|gnome shell|kwin|obs/.test(
|
||||
combined)
|
||||
return /xdg-desktop-portal|xdpw|screencast|screen|gnome shell|kwin|obs/.test(combined)
|
||||
}
|
||||
|
||||
function getMicrophoneStatus() {
|
||||
@@ -132,14 +129,16 @@ Singleton {
|
||||
|
||||
function getPrivacySummary() {
|
||||
const active = []
|
||||
if (microphoneActive)
|
||||
if (microphoneActive) {
|
||||
active.push("microphone")
|
||||
if (cameraActive)
|
||||
}
|
||||
if (cameraActive) {
|
||||
active.push("camera")
|
||||
if (screensharingActive)
|
||||
}
|
||||
if (screensharingActive) {
|
||||
active.push("screensharing")
|
||||
}
|
||||
|
||||
return active.length > 0 ? "Privacy active: " + active.join(
|
||||
", ") : "No privacy concerns detected"
|
||||
return active.length > 0 ? `Privacy active: ${active.join(", ")}` : "No privacy concerns detected"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
@@ -41,11 +42,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// ! TODO - hacky because uwsm doesnt behave as expected
|
||||
// uwsm idk, always passes the is-active check even if it's not a session
|
||||
// It reutrns exit code 0 when uwsm stop fails
|
||||
// They have flaws in their system, so we need to be hacky to just try it and
|
||||
// detect random text
|
||||
Process {
|
||||
id: uwsmLogout
|
||||
command: ["uwsm", "stop"]
|
||||
@@ -53,14 +49,14 @@ Singleton {
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: (data) => {
|
||||
onRead: data => {
|
||||
if (data.trim().toLowerCase().includes("not running")) {
|
||||
_logout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: function(exitCode) {
|
||||
onExited: function (exitCode) {
|
||||
if (exitCode === 0) {
|
||||
return
|
||||
}
|
||||
@@ -69,8 +65,9 @@ Singleton {
|
||||
}
|
||||
|
||||
function logout() {
|
||||
if (hasUwsm)
|
||||
if (hasUwsm) {
|
||||
uwsmLogout.running = true
|
||||
}
|
||||
_logout()
|
||||
}
|
||||
|
||||
@@ -100,15 +97,17 @@ Singleton {
|
||||
signal inhibitorChanged
|
||||
|
||||
function enableIdleInhibit() {
|
||||
if (idleInhibited)
|
||||
if (idleInhibited) {
|
||||
return
|
||||
}
|
||||
idleInhibited = true
|
||||
inhibitorChanged()
|
||||
}
|
||||
|
||||
function disableIdleInhibit() {
|
||||
if (!idleInhibited)
|
||||
if (!idleInhibited) {
|
||||
return
|
||||
}
|
||||
idleInhibited = false
|
||||
inhibitorChanged()
|
||||
}
|
||||
@@ -129,8 +128,9 @@ Singleton {
|
||||
idleInhibited = false
|
||||
|
||||
Qt.callLater(() => {
|
||||
if (wasActive)
|
||||
idleInhibited = true
|
||||
if (wasActive) {
|
||||
idleInhibited = true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -143,16 +143,14 @@ Singleton {
|
||||
return ["true"]
|
||||
}
|
||||
|
||||
return [isElogind ? "elogind-inhibit" : "systemd-inhibit", "--what=idle", "--who=quickshell", "--why="
|
||||
+ inhibitReason, "--mode=block", "sleep", "infinity"]
|
||||
return [isElogind ? "elogind-inhibit" : "systemd-inhibit", "--what=idle", "--who=quickshell", `--why=${inhibitReason}`, "--mode=block", "sleep", "infinity"]
|
||||
}
|
||||
|
||||
running: idleInhibited
|
||||
|
||||
onExited: function (exitCode) {
|
||||
if (idleInhibited && exitCode !== 0) {
|
||||
console.warn("SessionService: Inhibitor process crashed with exit code:",
|
||||
exitCode)
|
||||
console.warn("SessionService: Inhibitor process crashed with exit code:", exitCode)
|
||||
idleInhibited = false
|
||||
ToastService.showWarning("Idle inhibitor failed")
|
||||
}
|
||||
@@ -181,11 +179,11 @@ Singleton {
|
||||
|
||||
function reason(newReason: string): string {
|
||||
if (!newReason) {
|
||||
return "Current reason: " + root.inhibitReason
|
||||
return `Current reason: ${root.inhibitReason}`
|
||||
}
|
||||
|
||||
root.setInhibitReason(newReason)
|
||||
return "Inhibit reason set to: " + newReason
|
||||
return `Inhibit reason set to: ${newReason}`
|
||||
}
|
||||
|
||||
target: "inhibit"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
@@ -24,8 +25,9 @@ Singleton {
|
||||
"level": level,
|
||||
"details": details
|
||||
})
|
||||
if (!toastVisible)
|
||||
if (!toastVisible) {
|
||||
processQueue()
|
||||
}
|
||||
}
|
||||
|
||||
function showInfo(message, details = "") {
|
||||
@@ -48,13 +50,15 @@ Singleton {
|
||||
currentLevel = levelInfo
|
||||
toastTimer.stop()
|
||||
resetToastState()
|
||||
if (toastQueue.length > 0)
|
||||
if (toastQueue.length > 0) {
|
||||
processQueue()
|
||||
}
|
||||
}
|
||||
|
||||
function processQueue() {
|
||||
if (toastQueue.length === 0)
|
||||
if (toastQueue.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const toast = toastQueue.shift()
|
||||
currentMessage = toast.message
|
||||
@@ -68,8 +72,7 @@ Singleton {
|
||||
toastTimer.interval = 8000
|
||||
toastTimer.start()
|
||||
} else {
|
||||
toastTimer.interval = toast.level
|
||||
=== levelError ? 5000 : toast.level === levelWarn ? 4000 : 3000
|
||||
toastTimer.interval = toast.level === levelError ? 5000 : toast.level === levelWarn ? 4000 : 3000
|
||||
toastTimer.start()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
@@ -33,7 +34,6 @@ Singleton {
|
||||
getUptime()
|
||||
}
|
||||
|
||||
// Get username and full name
|
||||
Process {
|
||||
id: userInfoProcess
|
||||
|
||||
@@ -60,7 +60,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Get system uptime
|
||||
Process {
|
||||
id: uptimeProcess
|
||||
|
||||
@@ -81,17 +80,21 @@ Singleton {
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
|
||||
const parts = []
|
||||
if (days > 0)
|
||||
parts.push(`${days} day${days === 1 ? "" : "s"}`)
|
||||
if (hours > 0)
|
||||
parts.push(`${hours} hour${hours === 1 ? "" : "s"}`)
|
||||
if (minutes > 0)
|
||||
parts.push(`${minutes} minute${minutes === 1 ? "" : "s"}`)
|
||||
if (days > 0) {
|
||||
parts.push(`${days} day${days === 1 ? "" : "s"}`)
|
||||
}
|
||||
if (hours > 0) {
|
||||
parts.push(`${hours} hour${hours === 1 ? "" : "s"}`)
|
||||
}
|
||||
if (minutes > 0) {
|
||||
parts.push(`${minutes} minute${minutes === 1 ? "" : "s"}`)
|
||||
}
|
||||
|
||||
if (parts.length > 0)
|
||||
root.uptime = "up " + parts.join(", ")
|
||||
else
|
||||
root.uptime = `up ${seconds} seconds`
|
||||
if (parts.length > 0) {
|
||||
root.uptime = `up ${parts.join(", ")}`
|
||||
} else {
|
||||
root.uptime = `up ${seconds} seconds`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
pragma ComponentBehavior
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
@@ -34,7 +35,6 @@ Singleton {
|
||||
property int minFetchInterval: 30000 // 30 seconds minimum between fetches
|
||||
property int persistentRetryCount: 0 // Track persistent retry attempts for backoff
|
||||
|
||||
// Weather icon mapping (based on wttr.in weather codes)
|
||||
property var weatherIcons: ({
|
||||
"113": "clear_day",
|
||||
"116": "partly_cloudy_day",
|
||||
@@ -105,9 +105,7 @@ Singleton {
|
||||
function addRef() {
|
||||
refCount++
|
||||
|
||||
if (refCount === 1 && !weather.available
|
||||
&& SettingsData.weatherEnabled) {
|
||||
// Start fetching when first consumer appears and weather is enabled
|
||||
if (refCount === 1 && !weather.available && SettingsData.weatherEnabled) {
|
||||
fetchWeather()
|
||||
}
|
||||
}
|
||||
@@ -117,7 +115,6 @@ Singleton {
|
||||
}
|
||||
|
||||
function fetchWeather() {
|
||||
// Only fetch if someone is consuming the data and weather is enabled
|
||||
if (root.refCount === 0 || !SettingsData.weatherEnabled) {
|
||||
return
|
||||
}
|
||||
@@ -127,7 +124,6 @@ Singleton {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we've fetched recently to prevent spam
|
||||
const now = Date.now()
|
||||
if (now - root.lastFetchTime < root.minFetchInterval) {
|
||||
console.log("Weather fetch throttled, too soon since last fetch")
|
||||
@@ -137,9 +133,7 @@ Singleton {
|
||||
console.log("Fetching weather from:", getWeatherUrl())
|
||||
root.lastFetchTime = now
|
||||
root.weather.loading = true
|
||||
weatherFetcher.command
|
||||
= ["bash", "-c", `curl -s --connect-timeout 10 --max-time 30 '${getWeatherUrl(
|
||||
)}'`]
|
||||
weatherFetcher.command = ["bash", "-c", `curl -s --connect-timeout 10 --max-time 30 '${getWeatherUrl()}'`]
|
||||
weatherFetcher.running = true
|
||||
}
|
||||
|
||||
@@ -151,12 +145,10 @@ Singleton {
|
||||
|
||||
function handleWeatherSuccess() {
|
||||
root.retryAttempts = 0
|
||||
root.persistentRetryCount = 0 // Reset persistent retry count on success
|
||||
// Stop any persistent retry timer if running
|
||||
root.persistentRetryCount = 0
|
||||
if (persistentRetryTimer.running) {
|
||||
persistentRetryTimer.stop()
|
||||
}
|
||||
// Don't restart the timer - let it continue its normal interval
|
||||
if (updateTimer.interval !== root.updateInterval) {
|
||||
updateTimer.interval = root.updateInterval
|
||||
}
|
||||
@@ -165,18 +157,14 @@ Singleton {
|
||||
function handleWeatherFailure() {
|
||||
root.retryAttempts++
|
||||
if (root.retryAttempts < root.maxRetryAttempts) {
|
||||
console.log(`Weather fetch failed, retrying in ${root.retryDelay
|
||||
/ 1000}s (attempt ${root.retryAttempts}/${root.maxRetryAttempts})`)
|
||||
console.log(`Weather fetch failed, retrying in ${root.retryDelay / 1000}s (attempt ${root.retryAttempts}/${root.maxRetryAttempts})`)
|
||||
retryTimer.start()
|
||||
} else {
|
||||
console.warn("Weather fetch failed after maximum retry attempts, will keep trying...")
|
||||
root.weather.available = false
|
||||
root.weather.loading = false
|
||||
// Reset retry count but keep trying with exponential backoff
|
||||
root.retryAttempts = 0
|
||||
// Use exponential backoff: 1min, 2min, 4min, then cap at 5min
|
||||
const backoffDelay = Math.min(60000 * Math.pow(
|
||||
2, persistentRetryCount), 300000)
|
||||
const backoffDelay = Math.min(60000 * Math.pow(2, persistentRetryCount), 300000)
|
||||
persistentRetryCount++
|
||||
console.log(`Scheduling persistent retry in ${backoffDelay / 1000}s`)
|
||||
persistentRetryTimer.interval = backoffDelay
|
||||
@@ -186,8 +174,7 @@ Singleton {
|
||||
|
||||
Process {
|
||||
id: weatherFetcher
|
||||
command: ["bash", "-c", `curl -s --connect-timeout 10 --max-time 30 '${root.getWeatherUrl(
|
||||
)}'`]
|
||||
command: ["bash", "-c", `curl -s --connect-timeout 10 --max-time 30 '${root.getWeatherUrl()}'`]
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
@@ -206,8 +193,7 @@ Singleton {
|
||||
const location = data.nearest_area[0] || {}
|
||||
const astronomy = data.weather[0]?.astronomy[0] || {}
|
||||
|
||||
if (!Object.keys(current).length || !Object.keys(
|
||||
location).length) {
|
||||
if (!Object.keys(current).length || !Object.keys(location).length) {
|
||||
throw new Error("Required fields missing")
|
||||
}
|
||||
|
||||
@@ -226,8 +212,7 @@ Singleton {
|
||||
"pressure": Number(current.pressure) || 0
|
||||
}
|
||||
|
||||
console.log("Weather updated:", root.weather.city,
|
||||
`${root.weather.temp}°C`)
|
||||
console.log("Weather updated:", root.weather.city, `${root.weather.temp}°C`)
|
||||
|
||||
root.handleWeatherSuccess()
|
||||
} catch (e) {
|
||||
@@ -268,7 +253,7 @@ Singleton {
|
||||
|
||||
Timer {
|
||||
id: persistentRetryTimer
|
||||
interval: 60000 // Will be dynamically set
|
||||
interval: 60000
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
@@ -279,8 +264,7 @@ Singleton {
|
||||
|
||||
Component.onCompleted: {
|
||||
SettingsData.weatherCoordinatesChanged.connect(() => {
|
||||
console.log(
|
||||
"Weather location changed, force refreshing weather")
|
||||
console.log("Weather location changed, force refreshing weather")
|
||||
root.weather = {
|
||||
"available": false,
|
||||
"loading": true,
|
||||
@@ -300,16 +284,13 @@ Singleton {
|
||||
})
|
||||
|
||||
SettingsData.weatherLocationChanged.connect(() => {
|
||||
console.log(
|
||||
"Weather location display name changed")
|
||||
const currentWeather = Object.assign(
|
||||
{}, root.weather)
|
||||
console.log("Weather location display name changed")
|
||||
const currentWeather = Object.assign({}, root.weather)
|
||||
root.weather = currentWeather
|
||||
})
|
||||
|
||||
SettingsData.useAutoLocationChanged.connect(() => {
|
||||
console.log(
|
||||
"Auto location setting changed, force refreshing weather")
|
||||
console.log("Auto location setting changed, force refreshing weather")
|
||||
root.weather = {
|
||||
"available": false,
|
||||
"loading": true,
|
||||
@@ -329,16 +310,10 @@ Singleton {
|
||||
})
|
||||
|
||||
SettingsData.weatherEnabledChanged.connect(() => {
|
||||
console.log(
|
||||
"Weather enabled setting changed:",
|
||||
SettingsData.weatherEnabled)
|
||||
if (SettingsData.weatherEnabled
|
||||
&& root.refCount > 0
|
||||
&& !root.weather.available) {
|
||||
// Start fetching when weather is re-enabled
|
||||
console.log("Weather enabled setting changed:", SettingsData.weatherEnabled)
|
||||
if (SettingsData.weatherEnabled && root.refCount > 0 && !root.weather.available) {
|
||||
root.forceRefresh()
|
||||
} else if (!SettingsData.weatherEnabled) {
|
||||
// Stop all timers when weather is disabled
|
||||
updateTimer.stop()
|
||||
retryTimer.stop()
|
||||
persistentRetryTimer.stop()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# https://github.com/jesperhh/qmlfmt
|
||||
find . -name "*.qml" -exec qmlfmt -t 4 -i 4 -w {} \;
|
||||
find . -name "*.qml" -exec qmlfmt -t 4 -i 4 -b 250 -w {} \;
|
||||
|
||||
Reference in New Issue
Block a user