Merge branch 'noctalia-dev:main' into main

This commit is contained in:
Michael Thomas 2025-08-28 16:45:50 -04:00 committed by GitHub
commit 85bd0ed2f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 929 additions and 650 deletions

View file

@ -276,6 +276,9 @@ Singleton {
property string startTime: "20:00" property string startTime: "20:00"
property string stopTime: "07:00" property string stopTime: "07:00"
property bool autoSchedule: false property bool autoSchedule: false
// wlsunset temperatures (Kelvin)
property int lowTemp: 3500
property int highTemp: 6500
} }
} }
} }

View file

@ -34,7 +34,7 @@ NPanel {
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
NIcon { NIcon {
text: "system_update" text: "system_update_alt"
font.pointSize: Style.fontSizeXXL * scaling font.pointSize: Style.fontSizeXXL * scaling
color: Color.mPrimary color: Color.mPrimary
} }
@ -77,11 +77,9 @@ NPanel {
} }
// Unified list // Unified list
Rectangle { NBox {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
color: Color.mSurfaceVariant
radius: Style.radiusM * scaling
// Combine repo and AUR lists in order: repos first, then AUR // Combine repo and AUR lists in order: repos first, then AUR
property var items: (ArchUpdaterService.repoPackages || []).concat(ArchUpdaterService.aurPackages || []) property var items: (ArchUpdaterService.repoPackages || []).concat(ArchUpdaterService.aurPackages || [])
@ -89,33 +87,35 @@ NPanel {
ListView { ListView {
id: unifiedList id: unifiedList
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginS * scaling anchors.margins: Style.marginM * scaling
cacheBuffer: Math.round(300 * scaling)
clip: true clip: true
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
model: parent.items model: parent.items
spacing: Style.marginXS * scaling
cacheBuffer: 300 * scaling
delegate: Rectangle { delegate: Rectangle {
width: unifiedList.width width: unifiedList.width
height: 56 * scaling height: 44 * scaling
color: Color.transparent color: Color.transparent
radius: Style.radiusS * scaling radius: Style.radiusS * scaling
RowLayout { RowLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
// Checkbox for selection (pure bindings; no imperative state) // Checkbox for selection
NIconButton { NCheckbox {
id: checkbox id: checkbox
icon: ArchUpdaterService.isPackageSelected(modelData.name) ? "check_box" : "check_box_outline_blank" label: ""
onClicked: ArchUpdaterService.togglePackageSelection(modelData.name) description: ""
colorBg: Color.transparent checked: ArchUpdaterService.isPackageSelected(modelData.name)
colorFg: ArchUpdaterService.isPackageSelected( baseSize: Math.max(Style.baseWidgetSize * 0.7, 14)
modelData.name) ? ((modelData.source === "aur") ? Color.mSecondary : Color.mPrimary) : Color.mOnSurfaceVariant onToggled: function (checked) {
Layout.preferredWidth: 30 * scaling ArchUpdaterService.togglePackageSelection(modelData.name)
Layout.preferredHeight: 30 * scaling // Force refresh of the checked property
checkbox.checked = ArchUpdaterService.isPackageSelected(modelData.name)
}
} }
// Package info // Package info
@ -123,51 +123,43 @@ NPanel {
Layout.fillWidth: true Layout.fillWidth: true
spacing: Style.marginXXS * scaling spacing: Style.marginXXS * scaling
RowLayout { NText {
text: modelData.name
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true Layout.fillWidth: true
spacing: Style.marginXS * scaling Layout.alignment: Qt.AlignVCenter
NText {
text: modelData.name
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightMedium
color: Color.mOnSurface
Layout.fillWidth: true
}
// Source badge (custom rectangle)
Rectangle {
visible: !!modelData.source
radius: 9999
color: modelData.source === "aur" ? Color.mSecondary : Color.mPrimary
Layout.alignment: Qt.AlignVCenter
implicitHeight: Math.max(Style.fontSizeXS * 1.7 * scaling, 16 * scaling)
// Width based on label content + horizontal padding
implicitWidth: badgeText.implicitWidth + Math.max(12 * scaling, Style.marginS * scaling)
NText {
id: badgeText
anchors.centerIn: parent
text: modelData.source === "aur" ? "AUR" : "Repo"
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightBold
color: modelData.source === "aur" ? Color.mOnSecondary : Color.mOnPrimary
}
}
} }
NText { NText {
text: modelData.oldVersion + " → " + modelData.newVersion text: modelData.oldVersion + " → " + modelData.newVersion
font.pointSize: Style.fontSizeS * scaling font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnSurfaceVariant color: Color.mOnSurfaceVariant
Layout.fillWidth: true Layout.fillWidth: true
} }
} }
}
}
ScrollBar.vertical: ScrollBar { // Source tag (AUR vs PAC)
policy: ScrollBar.AsNeeded Rectangle {
visible: !!modelData.source
radius: width * 0.5
color: modelData.source === "aur" ? Color.mTertiary : Color.mSecondary
Layout.alignment: Qt.AlignVCenter
implicitHeight: Style.fontSizeS * 1.8 * scaling
// Width based on label content + horizontal padding
implicitWidth: badgeText.implicitWidth + Math.max(12 * scaling, Style.marginS * scaling)
NText {
id: badgeText
anchors.centerIn: parent
text: modelData.source === "aur" ? "AUR" : "PAC"
font.pointSize: Style.fontSizeXXS * scaling
font.weight: Style.fontWeightBold
color: modelData.source === "aur" ? Color.mOnTertiary : Color.mOnSecondary
}
}
}
} }
} }
} }
@ -190,7 +182,7 @@ NPanel {
} }
NIconButton { NIconButton {
icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "system_update" icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "system_update_alt"
tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update all packages" tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update all packages"
enabled: !ArchUpdaterService.updateInProgress enabled: !ArchUpdaterService.updateInProgress
onClicked: { onClicked: {
@ -203,7 +195,7 @@ NPanel {
} }
NIconButton { NIconButton {
icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "settings" icon: ArchUpdaterService.updateInProgress ? "hourglass_empty" : "check_box"
tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update selected packages" tooltipText: ArchUpdaterService.updateInProgress ? "Update in progress..." : "Update selected packages"
enabled: !ArchUpdaterService.updateInProgress && ArchUpdaterService.selectedPackagesCount > 0 enabled: !ArchUpdaterService.updateInProgress && ArchUpdaterService.selectedPackagesCount > 0
onClicked: { onClicked: {
@ -213,9 +205,9 @@ NPanel {
} }
} }
colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : (ArchUpdaterService.selectedPackagesCount colorBg: ArchUpdaterService.updateInProgress ? Color.mSurfaceVariant : (ArchUpdaterService.selectedPackagesCount
> 0 ? Color.mSecondary : Color.mSurfaceVariant) > 0 ? Color.mPrimary : Color.mSurfaceVariant)
colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : (ArchUpdaterService.selectedPackagesCount colorFg: ArchUpdaterService.updateInProgress ? Color.mOnSurfaceVariant : (ArchUpdaterService.selectedPackagesCount
> 0 ? Color.mOnSecondary : Color.mOnSurfaceVariant) > 0 ? Color.mOnPrimary : Color.mOnSurfaceVariant)
Layout.fillWidth: true Layout.fillWidth: true
} }
} }

View file

@ -14,72 +14,51 @@ NIconButton {
sizeRatio: 0.8 sizeRatio: 0.8
colorBg: Color.mSurfaceVariant colorBg: Color.mSurfaceVariant
// Highlight color based on update source
colorFg: {
if (ArchUpdaterService.totalUpdates === 0)
return Color.mOnSurface
if (ArchUpdaterService.updates > 0 && ArchUpdaterService.aurUpdates > 0)
return Color.mPrimary
if (ArchUpdaterService.updates > 0)
return Color.mPrimary
return Color.mSecondary
}
colorBorder: Color.transparent colorBorder: Color.transparent
colorBorderHover: Color.transparent colorBorderHover: Color.transparent
colorFg: (ArchUpdaterService.totalUpdates === 0) ? Color.mOnSurface : Color.mPrimary
// Icon states // Icon states
icon: { icon: {
if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) {
return "sync" return "sync"
}
if (ArchUpdaterService.totalUpdates > 0) { if (ArchUpdaterService.totalUpdates > 0) {
const count = ArchUpdaterService.totalUpdates return "system_update_alt"
if (count > 50)
return "system_update_alt"
if (count > 10)
return "system_update"
return "system_update"
} }
return "task_alt" return "task_alt"
} }
// Tooltip with repo vs AUR breakdown and sample lists // Tooltip with repo vs AUR breakdown and sample lists
tooltipText: { tooltipText: {
if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) {
return "Checking for updates…" return "Checking for updates…"
const repoCount = ArchUpdaterService.updates
const aurCount = ArchUpdaterService.aurUpdates
const total = ArchUpdaterService.totalUpdates
if (total === 0)
return "System is up to date ✓"
let header = total === 1 ? "One package can be upgraded:" : (total + " packages can be upgraded:")
function sampleList(arr, n, colorLabel) {
const limit = Math.min(arr.length, n)
let s = ""
for (var i = 0; i < limit; ++i) {
const p = arr[i]
s += (i ? "\n" : "") + (p.name + ": " + p.oldVersion + " → " + p.newVersion)
}
if (arr.length > limit)
s += "\n… and " + (arr.length - limit) + " more"
return (colorLabel ? (colorLabel + "\n") : "") + (s || "None")
} }
const repoHeader = repoCount > 0 ? ("Repo (" + repoCount + "):") : "Repo: 0" const total = ArchUpdaterService.totalUpdates
const aurHeader = aurCount > 0 ? ("AUR (" + aurCount + "):") : "AUR: 0" if (total === 0) {
return "System is up to date ✓"
}
let header = (total === 1) ? "1 package can be updated" : (total + " packages can be updated")
const repoBlock = repoCount > 0 ? (repoHeader + "\n\n" + sampleList(ArchUpdaterService.repoPackages, const pacCount = ArchUpdaterService.updates
5)) : repoHeader const aurCount = ArchUpdaterService.aurUpdates
const aurBlock = aurCount > 0 ? (aurHeader + "\n\n" + sampleList(ArchUpdaterService.aurPackages, 5)) : aurHeader const pacmanTooltip = (pacCount > 0) ? ((pacCount === 1) ? "1 system package" : pacCount + " system packages") : ""
const aurTooltip = (aurCount > 0) ? ((aurCount === 1) ? "1 AUR package" : aurCount + " AUR packages") : ""
return header + "\n\n" + repoBlock + "\n\n" + aurBlock + "\n\nClick to update system" let tooltip = header
if (pacmanTooltip !== "") {
tooltip += "\n" + pacmanTooltip
}
if (aurTooltip !== "") {
tooltip += "\n" + aurTooltip
}
return tooltip
} }
onClicked: { onClicked: {
if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) { if (ArchUpdaterService.busy || ArchUpdaterService.aurBusy) {
ToastService.showNotice("ArchUpdater", "Still fetching updates...")
return return
} }

View file

@ -20,16 +20,7 @@ NIconButton {
colorBorder: Color.transparent colorBorder: Color.transparent
colorBorderHover: Color.transparent colorBorderHover: Color.transparent
icon: { icon: "bluetooth"
// Show different icons based on connection status
if (BluetoothService.pairedDevices.length > 0) {
return "bluetooth_connected"
} else if (BluetoothService.discovering) {
return "bluetooth_searching"
} else {
return "bluetooth"
}
}
tooltipText: "Bluetooth Devices" tooltipText: "Bluetooth Devices"
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(screen, this) onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(screen, this)
} }

View file

@ -17,21 +17,19 @@ Item {
NPill { NPill {
id: pill id: pill
icon: NightLightService.isActive ? "bedtime" : "bedtime_off" icon: Settings.data.nightLight.enabled ? "bedtime" : "bedtime_off"
iconCircleColor: NightLightService.isActive ? Color.mSecondary : Color.mOnSurfaceVariant iconCircleColor: Settings.data.nightLight.enabled ? Color.mSecondary : Color.mOnSurfaceVariant
collapsedIconColor: NightLightService.isActive ? Color.mOnSecondary : Color.mOnSurface collapsedIconColor: Settings.data.nightLight.enabled ? Color.mOnSecondary : Color.mOnSurface
autoHide: false autoHide: false
text: NightLightService.isActive ? "On" : "Off" text: Settings.data.nightLight.enabled ? "On" : "Off"
tooltipText: { tooltipText: {
if (!Settings.isLoaded || !Settings.data.nightLight.enabled) { if (!Settings.isLoaded || !Settings.data.nightLight.enabled) {
return "Night Light: Disabled\nLeft click to open settings.\nRight click to enable." return "Night Light: Disabled\nLeft click to open settings.\nRight click to enable."
} }
var status = NightLightService.isActive ? "Active" : "Inactive (outside schedule)"
var intensity = Math.round(Settings.data.nightLight.intensity * 100) var intensity = Math.round(Settings.data.nightLight.intensity * 100)
var schedule = Settings.data.nightLight.autoSchedule ? `Schedule: ${Settings.data.nightLight.startTime} - ${Settings.data.nightLight.stopTime}` : "Manual mode" var schedule = Settings.data.nightLight.autoSchedule ? `Auto schedule` : `Manual: ${Settings.data.nightLight.startTime} - ${Settings.data.nightLight.stopTime}`
return `Night Light: Enabled\nIntensity: ${intensity}%\n${schedule}\nLeft click to open settings.\nRight click to toggle.`
return `Intensity: ${intensity}%\n${schedule}\nLeft click to open settings.\nRight click to toggle.`
} }
onClicked: { onClicked: {
@ -42,14 +40,11 @@ Item {
} }
onRightClicked: { onRightClicked: {
// Right click - toggle night light // Right click - toggle night light (debounced apply handled by service)
Settings.data.nightLight.enabled = !Settings.data.nightLight.enabled Settings.data.nightLight.enabled = !Settings.data.nightLight.enabled
NightLightService.apply()
} }
onWheel: delta => { // Wheel handler removed to avoid frequent rapid restarts/flicker
var diff = delta > 0 ? 0.05 : -0.05
Settings.data.nightLight.intensity = Math.max(0, Math.min(1.0,
Settings.data.nightLight.intensity + diff))
}
} }
} }

View file

@ -14,6 +14,7 @@ Item {
// Used to avoid opening the pill on Quickshell startup // Used to avoid opening the pill on Quickshell startup
property bool firstVolumeReceived: false property bool firstVolumeReceived: false
property int wheelAccumulator: 0
implicitWidth: pill.width implicitWidth: pill.width
implicitHeight: pill.height implicitHeight: pill.height
@ -59,10 +60,13 @@ Item {
tooltipText: "Volume: " + Math.round( tooltipText: "Volume: " + Math.round(
AudioService.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume." AudioService.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume."
onWheel: function (angle) { onWheel: function (delta) {
if (angle > 0) { wheelAccumulator += delta
if (wheelAccumulator >= 120) {
wheelAccumulator = 0
AudioService.increaseVolume() AudioService.increaseVolume()
} else if (angle < 0) { } else if (wheelAccumulator <= -120) {
wheelAccumulator = 0
AudioService.decreaseVolume() AudioService.decreaseVolume()
} }
} }

View file

@ -0,0 +1,262 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
ColumnLayout {
id: root
property string label: ""
property var model: {
}
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: root.label
font.pointSize: Style.fontSizeL * scaling
color: Color.mSecondary
font.weight: Style.fontWeightMedium
Layout.fillWidth: true
visible: root.model.length > 0
}
Repeater {
Layout.fillWidth: true
model: root.model
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
Rectangle {
property bool canConnect: BluetoothService.canConnect(modelData)
property bool isBusy: BluetoothService.isDeviceBusy(modelData)
Layout.fillWidth: true
Layout.preferredHeight: 64 * scaling + (10 * scaling * modelData.batteryAvailable)
radius: Style.radiusM * scaling
color: {
if (availableDeviceArea.containsMouse && !isBusy)
return Color.mTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mPrimary
if (modelData.blocked)
return Color.mError
return Color.mSurfaceVariant
}
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginM * scaling
spacing: Style.marginS * scaling
Layout.alignment: Qt.AlignVCenter
// One device BT icon
NIcon {
text: BluetoothService.getDeviceIcon(modelData)
font.pointSize: Style.fontSizeXXL * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
Layout.alignment: Qt.AlignVCenter
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXXS * scaling
// Device name
NText {
text: modelData.name || modelData.deviceName
font.pointSize: Style.fontSizeM * scaling
elide: Text.ElideRight
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
font.weight: Style.fontWeightMedium
Layout.fillWidth: true
}
// Signal Strength
RowLayout {
Layout.fillWidth: true
spacing: Style.marginXS * scaling
// Device signal strength - "Unknown" when not connected
NText {
text: BluetoothService.getSignalStrength(modelData)
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurfaceVariant
}
}
NIcon {
text: BluetoothService.getSignalIcon(modelData)
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && !modelData.pairing
&& !modelData.blocked
}
NText {
text: (modelData.signalStrength !== undefined
&& modelData.signalStrength > 0) ? modelData.signalStrength + "%" : ""
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && !modelData.pairing
&& !modelData.blocked
}
}
NText {
visible: modelData.batteryAvailable
text: BluetoothService.getBattery(modelData)
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurfaceVariant
}
}
}
// Spacer to push connect button to the right
Item {
Layout.fillWidth: true
}
// Call to action
Rectangle {
Layout.preferredWidth: 80 * scaling
Layout.preferredHeight: 28 * scaling
radius: Style.radiusM * scaling
visible: (modelData.state !== BluetoothDeviceState.Connecting)
color: Color.transparent
border.color: {
if (availableDeviceArea.containsMouse) {
return Color.mOnTertiary
} else {
return Color.mPrimary
}
}
border.width: Math.max(1, Style.borderS * scaling)
opacity: canConnect || isBusy ? 1 : 0.5
NText {
anchors.centerIn: parent
text: {
if (modelData.pairing) {
return "Pairing..."
}
if (modelData.blocked) {
return "Blocked"
}
if (modelData.paired || modelData.trusted) {
return "Disconnect"
}
return "Connect"
}
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightMedium
color: {
if (availableDeviceArea.containsMouse) {
return Color.mOnTertiary
} else {
return Color.mPrimary
}
}
}
}
}
MouseArea {
id: availableDeviceArea
anchors.fill: parent
hoverEnabled: true
cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
enabled: canConnect && !isBusy
onClicked: {
if (!modelData || modelData.pairing) {
return
}
if (modelData.paired || modelData.trusted) {
BluetoothService.disconnectDevice(modelData)
} else {
BluetoothService.connectDeviceWithTrust(modelData)
}
}
}
}
}
}

View file

@ -68,259 +68,56 @@ NPanel {
} }
ScrollView { ScrollView {
id: scrollView
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
clip: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded ScrollBar.vertical.policy: ScrollBar.AsNeeded
clip: true
contentWidth: availableWidth
// Available devices ColumnLayout {
Column { visible: BluetoothService.adapter && BluetoothService.adapter.enabled
id: column
width: parent.width width: parent.width
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
RowLayout { // Connected devices
width: parent.width BluetoothDevicesList {
spacing: Style.marginM * scaling label: "Connected devices"
NText {
text: "Available Devices"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
font.weight: Style.fontWeightMedium
}
}
Repeater {
model: { model: {
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) if (!BluetoothService.adapter || !Bluetooth.devices)
return [] return []
var filtered = Bluetooth.devices.values.filter(dev => { var filtered = Bluetooth.devices.values.filter(dev => {
return dev && !dev.paired && !dev.pairing && !dev.blocked return dev && !dev.blocked && (dev.paired || dev.trusted)
&& (dev.signalStrength === undefined
|| dev.signalStrength > 0)
}) })
return BluetoothService.sortDevices(filtered) return BluetoothService.sortDevices(filtered)
} }
Layout.fillWidth: true
Rectangle {
property bool canConnect: BluetoothService.canConnect(modelData)
property bool isBusy: BluetoothService.isDeviceBusy(modelData)
width: parent.width
height: 70
radius: Style.radiusM * scaling
color: {
if (availableDeviceArea.containsMouse && !isBusy)
return Color.mTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mPrimary
if (modelData.blocked)
return Color.mError
return Color.mSurfaceVariant
}
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
Row {
anchors.left: parent.left
anchors.leftMargin: Style.marginM * scaling
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
// One device BT icon
NIcon {
text: BluetoothService.getDeviceIcon(modelData)
font.pointSize: Style.fontSizeXXL * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: Style.marginXXS * scaling
anchors.verticalCenter: parent.verticalCenter
// One device name
NText {
text: modelData.name || modelData.deviceName
font.pointSize: Style.fonttSizeMedium * scaling
elide: Text.ElideRight
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
font.weight: Style.fontWeightMedium
}
Row {
spacing: Style.marginXS * scaling
Row {
spacing: Style.marginS * spacing
// One device signal strength - "Unknown" when not connected
NText {
text: {
if (modelData.pairing)
return "Pairing..."
if (modelData.blocked)
return "Blocked"
return BluetoothService.getSignalStrength(modelData)
}
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
}
NIcon {
text: BluetoothService.getSignalIcon(modelData)
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0
&& !modelData.pairing && !modelData.blocked
}
NText {
text: (modelData.signalStrength !== undefined
&& modelData.signalStrength > 0) ? modelData.signalStrength + "%" : ""
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0
&& !modelData.pairing && !modelData.blocked
}
}
}
}
}
Rectangle {
width: 80 * scaling
height: 28 * scaling
radius: Style.radiusM * scaling
anchors.right: parent.right
anchors.rightMargin: Style.marginM * scaling
anchors.verticalCenter: parent.verticalCenter
visible: modelData.state !== BluetoothDeviceState.Connecting
color: Color.transparent
border.color: {
if (availableDeviceArea.containsMouse) {
return Color.mOnTertiary
} else {
return Color.mPrimary
}
}
border.width: Math.max(1, Style.borderS * scaling)
opacity: canConnect || isBusy ? 1 : 0.5
// On device connect button
NText {
anchors.centerIn: parent
text: {
if (modelData.pairing)
return "Pairing..."
if (modelData.blocked)
return "Blocked"
return "Connect"
}
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightMedium
color: {
if (availableDeviceArea.containsMouse) {
return Color.mOnTertiary
} else {
return Color.mPrimary
}
}
}
}
MouseArea {
id: availableDeviceArea
anchors.fill: parent
hoverEnabled: true
cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
enabled: canConnect && !isBusy
onClicked: {
if (modelData)
BluetoothService.connectDeviceWithTrust(modelData)
}
}
}
} }
// Fallback if nothing available // Available devices
Column { BluetoothDevicesList {
width: parent.width label: "Available devices"
model: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return []
var filtered = Bluetooth.devices.values.filter(dev => {
return dev && !dev.blocked && !dev.paired && !dev.trusted
})
return BluetoothService.sortDevices(filtered)
}
Layout.fillWidth: true
}
// Fallback
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
visible: { visible: {
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) {
return false return false
}
var availableCount = Bluetooth.devices.values.filter(dev => { var availableCount = Bluetooth.devices.values.filter(dev => {
return dev && !dev.paired && !dev.pairing return dev && !dev.paired && !dev.pairing
@ -328,18 +125,17 @@ NPanel {
&& (dev.signalStrength === undefined && (dev.signalStrength === undefined
|| dev.signalStrength > 0) || dev.signalStrength > 0)
}).length }).length
return availableCount === 0 return (availableCount === 0)
} }
Row { RowLayout {
anchors.horizontalCenter: parent.horizontalCenter Layout.alignment: Qt.AlignHCenter
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
NIcon { NIcon {
text: "sync" text: "sync"
font.pointSize: Style.fontSizeXLL * 1.5 * scaling font.pointSize: Style.fontSizeXLL * 1.5 * scaling
color: Color.mPrimary color: Color.mPrimary
anchors.verticalCenter: parent.verticalCenter
RotationAnimation on rotation { RotationAnimation on rotation {
running: true running: true
@ -355,7 +151,6 @@ NPanel {
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface color: Color.mOnSurface
font.weight: Style.fontWeightMedium font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
} }
} }
@ -363,36 +158,15 @@ NPanel {
text: "Make sure your device is in pairing mode" text: "Make sure your device is in pairing mode"
font.pointSize: Style.fontSizeM * scaling font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant color: Color.mOnSurfaceVariant
anchors.horizontalCenter: parent.horizontalCenter Layout.alignment: Qt.AlignHCenter
} }
} }
NText { Item {
text: "No devices found. Put your device in pairing mode and click Start Scanning." Layout.fillHeight: true
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
visible: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return true
var availableCount = Bluetooth.devices.values.filter(dev => {
return dev && !dev.paired && !dev.pairing
&& !dev.blocked
&& (dev.signalStrength === undefined
|| dev.signalStrength > 0)
}).length
return availableCount === 0 && !BluetoothService.adapter.discovering
}
wrapMode: Text.WordWrap
width: parent.width
horizontalAlignment: Text.AlignHCenter
} }
} }
} }
// This item takes up all the remaining vertical space.
Item {
Layout.fillHeight: true
}
} }
} }
} }

View file

@ -99,6 +99,24 @@ Item {
} }
} }
IpcHandler {
target: "volume"
function increase() {
AudioService.increaseVolume()
}
function decrease() {
AudioService.decreaseVolume()
}
function muteOutput() {
AudioService.setMuted(!AudioService.muted)
}
function muteInput() {
if (AudioService.source?.ready && AudioService.source?.audio) {
AudioService.source.audio.muted = !AudioService.source.audio.muted
}
}
}
IpcHandler { IpcHandler {
target: "powerPanel" target: "powerPanel"
function toggle() { function toggle() {

View file

@ -1,46 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
Variants {
model: Quickshell.screens
delegate: Loader {
required property ShellScreen modelData
readonly property real scaling: ScalingService.scale(modelData)
active: NightLightService.isActive
sourceComponent: PanelWindow {
screen: modelData
color: Color.transparent
anchors {
top: true
bottom: true
left: true
right: true
}
// Ensure a full click through
mask: Region {}
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WlrLayershell.namespace: "noctalia-nightlight"
Rectangle {
anchors.fill: parent
color: NightLightService.overlayColor
Behavior on color {
ColorAnimation {
duration: Style.animationSlow
}
}
}
}
}
}

View file

@ -301,6 +301,7 @@ NPanel {
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded ScrollBar.vertical.policy: ScrollBar.AsNeeded
padding: Style.marginL * scaling padding: Style.marginL * scaling
clip: true
Loader { Loader {
active: true active: true

View file

@ -81,6 +81,54 @@ ColumnLayout {
} }
} }
// Input Volume
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
NLabel {
label: "Input Volume"
description: "Microphone input volume level."
}
RowLayout {
NSlider {
Layout.fillWidth: true
from: 0
to: 1.0
value: AudioService.inputVolume
stepSize: 0.01
onMoved: {
AudioService.setInputVolume(value)
}
}
NText {
text: Math.floor(AudioService.inputVolume * 100) + "%"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
color: Color.mOnSurface
}
}
}
// Input Mute Toggle
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
NToggle {
label: "Mute Audio Input"
description: "Mute or unmute the default audio input (microphone)."
checked: AudioService.inputMuted
onToggled: checked => {
AudioService.setInputMuted(checked)
}
}
}
// Volume Step Size // Volume Step Size
ColumnLayout { ColumnLayout {
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
@ -216,8 +264,6 @@ ColumnLayout {
} }
// Preferred player (persistent) // Preferred player (persistent)
NTextInput { NTextInput {
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
label: "Preferred Player" label: "Preferred Player"
description: "Substring to match MPRIS player (identity/bus/desktop)." description: "Substring to match MPRIS player (identity/bus/desktop)."
placeholderText: "e.g. spotify, vlc, mpv" placeholderText: "e.g. spotify, vlc, mpv"
@ -239,8 +285,6 @@ ColumnLayout {
NTextInput { NTextInput {
id: blacklistInput id: blacklistInput
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
label: "Blacklist player" label: "Blacklist player"
description: "Substring, e.g. plex, shim, mpv." description: "Substring, e.g. plex, shim, mpv."
placeholderText: "type substring and press +" placeholderText: "type substring and press +"

View file

@ -344,25 +344,24 @@ ColumnLayout {
visible: Settings.data.colorSchemes.useWallpaperColors visible: Settings.data.colorSchemes.useWallpaperColors
ColumnLayout { ColumnLayout {
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
Layout.fillWidth: true Layout.fillWidth: true
NText { NText {
text: "Matugen Templates" text: "Matugen Templates"
font.pointSize: Style.fontSizeXXL * scaling font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold font.weight: Style.fontWeightBold
color: Color.mSecondary color: Color.mSecondary
}
NText {
text: "Select which external components Matugen should apply theming to."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
} }
NText {
text: "Select which external components Matugen should apply theming to."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
}
NCheckbox { NCheckbox {
label: "GTK 4 (libadwaita)" label: "GTK 4 (libadwaita)"

View file

@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Io
import qs.Commons import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@ -27,6 +28,27 @@ ColumnLayout {
} }
} }
// Check for wlsunset availability when enabling Night Light
Process {
id: wlsunsetCheck
command: ["which", "wlsunset"]
running: false
onExited: function (exitCode) {
if (exitCode === 0) {
Settings.data.nightLight.enabled = true
NightLightService.apply()
ToastService.showNotice("Night Light", "Enabled")
} else {
Settings.data.nightLight.enabled = false
ToastService.showWarning("Night Light", "wlsunset not installed")
}
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
// Helper functions to update arrays immutably // Helper functions to update arrays immutably
function addMonitor(list, name) { function addMonitor(list, name) {
const arr = (list || []).slice() const arr = (list || []).slice()
@ -52,7 +74,6 @@ ColumnLayout {
color: Color.mOnSurfaceVariant color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredWidth: parent.width - (Style.marginL * 2 * scaling)
} }
ColumnLayout { ColumnLayout {
@ -231,7 +252,6 @@ ColumnLayout {
color: Color.mOnSurfaceVariant color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredWidth: parent.width - (Style.marginL * 2 * scaling)
} }
} }
@ -239,7 +259,16 @@ ColumnLayout {
label: "Enable Night Light" label: "Enable Night Light"
description: "Apply a warm color filter to reduce blue light emission." description: "Apply a warm color filter to reduce blue light emission."
checked: Settings.data.nightLight.enabled checked: Settings.data.nightLight.enabled
onToggled: checked => Settings.data.nightLight.enabled = checked onToggled: checked => {
if (checked) {
// Verify wlsunset exists before enabling
wlsunsetCheck.running = true
} else {
Settings.data.nightLight.enabled = false
NightLightService.apply()
ToastService.showNotice("Night Light", "Disabled")
}
}
} }
// Intensity settings // Intensity settings
@ -247,7 +276,7 @@ ColumnLayout {
visible: Settings.data.nightLight.enabled visible: Settings.data.nightLight.enabled
NLabel { NLabel {
label: "Intensity" label: "Intensity"
description: "Higher values create warmer light." description: "Higher values create warmer tones."
} }
RowLayout { RowLayout {
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
@ -257,7 +286,10 @@ ColumnLayout {
to: 1 to: 1
stepSize: 0.01 stepSize: 0.01
value: Settings.data.nightLight.intensity value: Settings.data.nightLight.intensity
onMoved: Settings.data.nightLight.intensity = value onMoved: {
Settings.data.nightLight.intensity = value
NightLightService.apply()
}
Layout.fillWidth: true Layout.fillWidth: true
Layout.minimumWidth: 150 * scaling Layout.minimumWidth: 150 * scaling
} }
@ -271,11 +303,74 @@ ColumnLayout {
} }
} }
// Temperature
ColumnLayout {
spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter
NLabel {
label: "Color temperature"
description: "Select two temperatures in Kelvin"
}
RowLayout {
visible: Settings.data.nightLight.enabled
spacing: Style.marginM * scaling
Layout.fillWidth: false
Layout.fillHeight: true
Layout.alignment: Qt.AlignVCenter
NText {
text: "Low"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
NTextInput {
text: Settings.data.nightLight.lowTemp.toString()
inputMethodHints: Qt.ImhDigitsOnly
Layout.alignment: Qt.AlignVCenter
onEditingFinished: {
var v = parseInt(text)
if (!isNaN(v)) {
Settings.data.nightLight.lowTemp = Math.max(1000, Math.min(6500, v))
NightLightService.apply()
}
}
}
Item {}
NText {
text: "High"
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignVCenter
}
NTextInput {
text: Settings.data.nightLight.highTemp.toString()
inputMethodHints: Qt.ImhDigitsOnly
Layout.alignment: Qt.AlignVCenter
onEditingFinished: {
var v = parseInt(text)
if (!isNaN(v)) {
Settings.data.nightLight.highTemp = Math.max(1000, Math.min(10000, v))
NightLightService.apply()
}
}
}
}
}
NToggle { NToggle {
label: "Auto Schedule" label: "Auto Schedule"
description: "Automatically enable night light based on time schedule." description: "Automatically enable night light based on time schedule."
checked: Settings.data.nightLight.autoSchedule checked: Settings.data.nightLight.autoSchedule
onToggled: checked => Settings.data.nightLight.autoSchedule = checked onToggled: checked => {
Settings.data.nightLight.autoSchedule = checked
NightLightService.apply()
}
visible: Settings.data.nightLight.enabled visible: Settings.data.nightLight.enabled
} }
@ -303,7 +398,10 @@ ColumnLayout {
model: timeOptions model: timeOptions
currentKey: Settings.data.nightLight.startTime currentKey: Settings.data.nightLight.startTime
placeholder: "Select start time" placeholder: "Select start time"
onSelected: key => Settings.data.nightLight.startTime = key onSelected: key => {
Settings.data.nightLight.startTime = key
NightLightService.apply()
}
preferredWidth: 120 * scaling preferredWidth: 120 * scaling
} }
@ -319,7 +417,10 @@ ColumnLayout {
model: timeOptions model: timeOptions
currentKey: Settings.data.nightLight.stopTime currentKey: Settings.data.nightLight.stopTime
placeholder: "Select stop time" placeholder: "Select stop time"
onSelected: key => Settings.data.nightLight.stopTime = key onSelected: key => {
Settings.data.nightLight.stopTime = key
NightLightService.apply()
}
preferredWidth: 120 * scaling preferredWidth: 120 * scaling
} }
} }

View file

@ -25,10 +25,9 @@ ColumnLayout {
NTextInput { NTextInput {
label: "Profile Picture" label: "Profile Picture"
description: "Your profile picture displayed in various places throughout the shell." description: "Your profile picture that appears throughout the interface."
text: Settings.data.general.avatarImage text: Settings.data.general.avatarImage
placeholderText: "/home/user/.face" placeholderText: "/home/user/.face"
Layout.fillWidth: true
onEditingFinished: { onEditingFinished: {
Settings.data.general.avatarImage = text Settings.data.general.avatarImage = text
} }

View file

@ -24,7 +24,8 @@ ColumnLayout {
onEditingFinished: { onEditingFinished: {
Settings.data.screenRecorder.directory = text Settings.data.screenRecorder.directory = text
} }
Layout.fillWidth: true
Layout.maximumWidth: 420 * scaling
} }
ColumnLayout { ColumnLayout {

View file

@ -10,6 +10,7 @@ ColumnLayout {
// Location section // Location section
RowLayout { RowLayout {
Layout.fillWidth: true
spacing: Style.marginL * scaling spacing: Style.marginL * scaling
NTextInput { NTextInput {
@ -25,6 +26,7 @@ ColumnLayout {
LocationService.resetWeather() LocationService.resetWeather()
} }
} }
Layout.maximumWidth: 420 * scaling
} }
NText { NText {

View file

@ -35,10 +35,10 @@ ColumnLayout {
label: "Wallpaper Directory" label: "Wallpaper Directory"
description: "Path to your wallpaper directory." description: "Path to your wallpaper directory."
text: Settings.data.wallpaper.directory text: Settings.data.wallpaper.directory
Layout.fillWidth: true
onEditingFinished: { onEditingFinished: {
Settings.data.wallpaper.directory = text Settings.data.wallpaper.directory = text
} }
Layout.maximumWidth: 420 * scaling
} }
NDivider { NDivider {
@ -79,12 +79,7 @@ ColumnLayout {
NText { NText {
// Show friendly H:MM format from current settings // Show friendly H:MM format from current settings
text: { text: Time.formatVagueHumanReadableDuration(Settings.data.wallpaper.randomInterval)
const s = Settings.data.wallpaper.randomInterval
const h = Math.floor(s / 3600)
const m = Math.floor((s % 3600) / 60)
return (h > 0 ? (h + "h ") : "") + (m > 0 ? (m + "m") : (h === 0 ? "0m" : ""))
}
Layout.alignment: Qt.AlignBottom | Qt.AlignRight Layout.alignment: Qt.AlignBottom | Qt.AlignRight
} }
} }
@ -284,14 +279,15 @@ ColumnLayout {
NTextInput { NTextInput {
label: "Custom Interval" label: "Custom Interval"
description: "Enter time as HH:MM (e.g., 1:30)." description: "Enter time as HH:MM (e.g., 01:30)."
inputMaxWidth: 100 * scaling
text: { text: {
const s = Settings.data.wallpaper.randomInterval const s = Settings.data.wallpaper.randomInterval
const h = Math.floor(s / 3600) const h = Math.floor(s / 3600)
const m = Math.floor((s % 3600) / 60) const m = Math.floor((s % 3600) / 60)
return h + ":" + (m < 10 ? ("0" + m) : m) return h + ":" + (m < 10 ? ("0" + m) : m)
} }
Layout.fillWidth: true
onEditingFinished: { onEditingFinished: {
const m = text.trim().match(/^(\d{1,2}):(\d{2})$/) const m = text.trim().match(/^(\d{1,2}):(\d{2})$/)
if (m) { if (m) {

View file

@ -27,11 +27,11 @@ Features a modern modular architecture with a status bar, notification system, c
## Preview ## Preview
![Launcher](https://assets.noctalia.dev/screenshots/launcher.png) ![Launcher](https://assets.noctalia.dev/screenshots/launcher.png?v=2)
![SettingsPanel](https://assets.noctalia.dev/screenshots/settings-panel.png) ![SettingsPanel](https://assets.noctalia.dev/screenshots/settings-panel.png?v=2)
![SidePanel](https://assets.noctalia.dev/screenshots/light-mode.png) ![SidePanel](https://assets.noctalia.dev/screenshots/light-mode.png?v=2)
--- ---
@ -70,7 +70,6 @@ Features a modern modular architecture with a status bar, notification system, c
- `gpu-screen-recorder` - Screen recording functionality - `gpu-screen-recorder` - Screen recording functionality
- `brightnessctl` - For internal/laptop monitor brightness - `brightnessctl` - For internal/laptop monitor brightness
- `ddcutil` - For desktop monitor brightness (might introduce some system instability with certain monitors) - `ddcutil` - For desktop monitor brightness (might introduce some system instability with certain monitors)
- `xdg-desktop-portal-gnome` - Desktop integration (or alternative portal)
### Optional ### Optional
@ -79,6 +78,7 @@ Features a modern modular architecture with a status bar, notification system, c
- `swww` - Wallpaper animations and effects - `swww` - Wallpaper animations and effects
- `matugen` - Material You color scheme generation - `matugen` - Material You color scheme generation
- `cava` - Audio visualizer component - `cava` - Audio visualizer component
- `wlsunset` - To be able to use NightLight
> There are 2 more optional dependencies. > There are 2 more optional dependencies.
> Any `polkit agent` to be able to use the ArchUpdater widget. > Any `polkit agent` to be able to use the ArchUpdater widget.
@ -152,14 +152,12 @@ Alternatively, you can add it to your NixOS configuration or flake:
quickshell = { quickshell = {
url = "github:outfoxxed/quickshell"; url = "github:outfoxxed/quickshell";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
inputs.quickshell.follows = "quickshell"
}; };
}; };
outputs = { self, nixpkgs, noctalia, quickshell, ... }: outputs = { self, nixpkgs, noctalia, quickshell, ... }:
let {
system = "x86_64-linux";
pkgs = import nixpkgs { inherit system; };
in {
nixosConfigurations.my-host = nixpkgs.lib.nixosSystem { nixosConfigurations.my-host = nixpkgs.lib.nixosSystem {
modules = [ modules = [
./configuration.nix ./configuration.nix
@ -173,8 +171,8 @@ Alternatively, you can add it to your NixOS configuration or flake:
```nix ```nix
{ {
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
noctalia.packages.${system}.default inputs.noctalia.packages.${system}.default
quickshell.packages.${system}.default inputs.quickshell.packages.${system}.default
]; ];
} }
``` ```
@ -196,6 +194,10 @@ The following commands apply to the Nix flake and also the AUR package installat
| Open Calculator | `noctalia-shell ipc call launcher calculator` | | Open Calculator | `noctalia-shell ipc call launcher calculator` |
| Increase Brightness | `noctalia-shell ipc call brightness increase` | | Increase Brightness | `noctalia-shell ipc call brightness increase` |
| Decrease Brightness | `noctalia-shell ipc call brightness decrease` | | Decrease Brightness | `noctalia-shell ipc call brightness decrease` |
| Increase Output Volume | `noctalia-shell ipc call volume increase` |
| Decrease Output Volume | `noctalia-shell ipc call volume decrease` |
| Toggle Mute Audio Output | `noctalia-shell ipc call volume muteOutput` |
| Toggle Mute Audio Input | `noctalia-shell ipc call volume muteInput` |
| Toggle Power Panel | `noctalia-shell ipc call powerPanel toggle` | | Toggle Power Panel | `noctalia-shell ipc call powerPanel toggle` |
| Toggle Idle Inhibitor | `noctalia-shell ipc call idleInhibitor toggle` | | Toggle Idle Inhibitor | `noctalia-shell ipc call idleInhibitor toggle` |
| Toggle Settings Window | `noctalia-shell ipc call settings toggle` | | Toggle Settings Window | `noctalia-shell ipc call settings toggle` |

View file

@ -142,28 +142,40 @@ Singleton {
return return
updateInProgress = true updateInProgress = true
// Split selected packages by source // Split selected packages by source
const repoPkgs = selectedPackages.filter(pkg => { const repoPkgs = []
const repoPkg = repoPackages.find(p => p.name === pkg) const aurPkgs = []
return repoPkg && repoPkg.source === "repo"
}) for (const pkgName of selectedPackages) {
const aurPkgs = selectedPackages.filter(pkg => { const repoPkg = repoPackages.find(p => p.name === pkgName)
const aurPkg = aurPackages.find(p => p.name === pkg) if (repoPkg && repoPkg.source === "repo") {
return aurPkg && aurPkg.source === "aur" repoPkgs.push(pkgName)
}) }
const aurPkg = aurPackages.find(p => p.name === pkgName)
if (aurPkg && aurPkg.source === "aur") {
aurPkgs.push(pkgName)
}
}
// Update repo packages // Update repo packages
if (repoPkgs.length > 0) { if (repoPkgs.length > 0) {
const repoCommand = ["pkexec", "pacman", "-S", "--noconfirm"].concat(repoPkgs) const repoCommand = ["pkexec", "pacman", "-S", "--noconfirm"].concat(repoPkgs)
Logger.log("ArchUpdater", "Running repo command:", repoCommand.join(" "))
Quickshell.execDetached(repoCommand) Quickshell.execDetached(repoCommand)
} }
// Update AUR packages // Update AUR packages
if (aurPkgs.length > 0) { if (aurPkgs.length > 0) {
const aurCommand = ["sh", "-c", `command -v yay >/dev/null 2>&1 && yay -S ${aurPkgs.join( const aurHelper = getAurHelper()
' ')} --noconfirm || command -v paru >/dev/null 2>&1 && paru -S ${aurPkgs.join( if (aurHelper) {
' ')} --noconfirm || true`] const aurCommand = [aurHelper, "-S", "--noconfirm"].concat(aurPkgs)
Quickshell.execDetached(aurCommand) Logger.log("ArchUpdater", "Running AUR command:", aurCommand.join(" "))
Quickshell.execDetached(aurCommand)
} else {
Logger.warn("ArchUpdater", "No AUR helper found for packages:", aurPkgs.join(", "))
}
} }
// Clear selection and refresh // Clear selection and refresh
@ -172,6 +184,22 @@ Singleton {
refreshAfterUpdate() refreshAfterUpdate()
} }
// Helper function to detect AUR helper
function getAurHelper() {
// Check for yay first, then paru
const yayCheck = Quickshell.exec("command -v yay", true)
if (yayCheck.exitCode === 0 && yayCheck.stdout.trim()) {
return "yay"
}
const paruCheck = Quickshell.exec("command -v paru", true)
if (paruCheck.exitCode === 0 && paruCheck.stdout.trim()) {
return "paru"
}
return null
}
// Package selection functions // Package selection functions
function togglePackageSelection(packageName) { function togglePackageSelection(packageName) {
const index = selectedPackages.indexOf(packageName) const index = selectedPackages.indexOf(packageName)

View file

@ -35,6 +35,13 @@ Singleton {
readonly property alias muted: root._muted readonly property alias muted: root._muted
property bool _muted: !!sink?.audio?.muted property bool _muted: !!sink?.audio?.muted
// Input volume [0..1] is readonly from outside
readonly property alias inputVolume: root._inputVolume
property real _inputVolume: source?.audio?.volume ?? 0
readonly property alias inputMuted: root._inputMuted
property bool _inputMuted: !!source?.audio?.muted
readonly property real stepVolume: Settings.data.audio.volumeStep / 100.0 readonly property real stepVolume: Settings.data.audio.volumeStep / 100.0
PwObjectTracker { PwObjectTracker {
@ -58,6 +65,23 @@ Singleton {
} }
} }
Connections {
target: source?.audio ? source?.audio : null
function onVolumeChanged() {
var vol = (source?.audio.volume ?? 0)
if (isNaN(vol)) {
vol = 0
}
root._inputVolume = vol
}
function onMutedChanged() {
root._inputMuted = (source?.audio.muted ?? true)
Logger.log("AudioService", "OnInputMuteChanged:", root._inputMuted)
}
}
function increaseVolume() { function increaseVolume() {
setVolume(volume + stepVolume) setVolume(volume + stepVolume)
} }
@ -85,6 +109,24 @@ Singleton {
} }
} }
function setInputVolume(newVolume: real) {
if (source?.ready && source?.audio) {
// Clamp it accordingly
source.audio.muted = false
source.audio.volume = Math.max(0, Math.min(1, newVolume))
} else {
Logger.warn("AudioService", "No source available")
}
}
function setInputMuted(muted: bool) {
if (source?.ready && source?.audio) {
source.audio.muted = muted
} else {
Logger.warn("AudioService", "No source available")
}
}
function setAudioSink(newSink: PwNode): void { function setAudioSink(newSink: PwNode): void {
Pipewire.preferredDefaultAudioSink = newSink Pipewire.preferredDefaultAudioSink = newSink
} }

View file

@ -13,17 +13,17 @@ Singleton {
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 devices: adapter ? adapter.devices : null
readonly property var pairedDevices: { readonly property var pairedDevices: {
if (!adapter || !adapter.devices) if (!adapter || !adapter.devices) {
return [] return []
}
return adapter.devices.values.filter(dev => { return adapter.devices.values.filter(dev => {
return dev && (dev.paired || dev.trusted) return dev && (dev.paired || dev.trusted)
}) })
} }
readonly property var allDevicesWithBattery: { readonly property var allDevicesWithBattery: {
if (!adapter || !adapter.devices) if (!adapter || !adapter.devices) {
return [] return []
}
return adapter.devices.values.filter(dev => { return adapter.devices.values.filter(dev => {
return dev && dev.batteryAvailable && dev.battery > 0 return dev && dev.batteryAvailable && dev.battery > 0
}) })
@ -49,34 +49,36 @@ Singleton {
} }
function getDeviceIcon(device) { function getDeviceIcon(device) {
if (!device) if (!device) {
return "bluetooth" return "bluetooth"
}
var name = (device.name || device.deviceName || "").toLowerCase() var name = (device.name || device.deviceName || "").toLowerCase()
var icon = (device.icon || "").toLowerCase() var icon = (device.icon || "").toLowerCase()
if (icon.includes("headset") || icon.includes("audio") || name.includes("headphone") || name.includes("airpod") if (icon.includes("headset") || icon.includes("audio") || name.includes("headphone") || name.includes("airpod")
|| name.includes("headset") || name.includes("arctis")) || name.includes("headset") || name.includes("arctis")) {
return "headset" return "headset"
}
if (icon.includes("mouse") || name.includes("mouse")) if (icon.includes("mouse") || name.includes("mouse")) {
return "mouse" return "mouse"
}
if (icon.includes("keyboard") || name.includes("keyboard")) if (icon.includes("keyboard") || name.includes("keyboard")) {
return "keyboard" return "keyboard"
}
if (icon.includes("phone") || name.includes("phone") || name.includes("iphone") || name.includes("android") if (icon.includes("phone") || name.includes("phone") || name.includes("iphone") || name.includes("android")
|| name.includes("samsung")) || name.includes("samsung")) {
return "smartphone" return "smartphone"
}
if (icon.includes("watch") || name.includes("watch")) if (icon.includes("watch") || name.includes("watch")) {
return "watch" return "watch"
}
if (icon.includes("speaker") || name.includes("speaker")) if (icon.includes("speaker") || name.includes("speaker")) {
return "speaker" return "speaker"
}
if (icon.includes("display") || name.includes("tv")) if (icon.includes("display") || name.includes("tv")) {
return "tv" return "tv"
}
return "bluetooth" return "bluetooth"
} }
@ -88,60 +90,91 @@ Singleton {
} }
function getSignalStrength(device) { function getSignalStrength(device) {
if (!device || device.signalStrength === undefined || device.signalStrength <= 0) if (device.pairing) {
return "Unknown" return "Pairing..."
}
if (device.blocked) {
return "Blocked"
}
if (!device || device.signalStrength === undefined || device.signalStrength <= 0) {
return "Signal: Unknown"
}
var signal = device.signalStrength var signal = device.signalStrength
if (signal >= 80) if (signal >= 80) {
return "Excellent" return "Signal: Excellent"
}
if (signal >= 60) {
return "Signal: Good"
}
if (signal >= 40) {
return "Signal: Fair"
}
if (signal >= 20) {
return "Signal: Poor"
}
return "Signal: Very Poor"
}
if (signal >= 60) function getBattery(device) {
return "Good" return `Battery: ${Math.round(device.battery * 100)}`
if (signal >= 40)
return "Fair"
if (signal >= 20)
return "Poor"
return "Very Poor"
} }
function getSignalIcon(device) { function getSignalIcon(device) {
if (!device || device.signalStrength === undefined || device.signalStrength <= 0) if (!device || device.signalStrength === undefined || device.signalStrength <= 0) {
return "signal_cellular_null" return "signal_cellular_null"
}
var signal = device.signalStrength var signal = device.signalStrength
if (signal >= 80) if (signal >= 80) {
return "signal_cellular_4_bar" return "signal_cellular_4_bar"
}
if (signal >= 60) if (signal >= 60) {
return "signal_cellular_3_bar" return "signal_cellular_3_bar"
}
if (signal >= 40) if (signal >= 40) {
return "signal_cellular_2_bar" return "signal_cellular_2_bar"
}
if (signal >= 20) if (signal >= 20) {
return "signal_cellular_1_bar" return "signal_cellular_1_bar"
}
return "signal_cellular_0_bar" return "signal_cellular_0_bar"
} }
function isDeviceBusy(device) { function isDeviceBusy(device) {
if (!device) if (!device) {
return false return false
}
return device.pairing || device.state === BluetoothDeviceState.Disconnecting return device.pairing || device.state === BluetoothDeviceState.Disconnecting
|| device.state === BluetoothDeviceState.Connecting || device.state === BluetoothDeviceState.Connecting
} }
function connectDeviceWithTrust(device) { function connectDeviceWithTrust(device) {
if (!device) if (!device) {
return return
}
device.trusted = true device.trusted = true
device.connect() device.connect()
} }
function disconnectDevice(device) {
if (!device) {
return
}
device.trusted = false
device.disconnect()
}
function forgetDevice(device) {
if (!device) {
return
}
device.trusted = false
device.forget()
}
function setBluetoothEnabled(enabled) { function setBluetoothEnabled(enabled) {
if (!adapter) { if (!adapter) {
Logger.warn("Bluetooth", "No adapter available") Logger.warn("Bluetooth", "No adapter available")

View file

@ -4,63 +4,129 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Commons import qs.Commons
import qs.Services
Singleton { Singleton {
id: root id: root
// Night Light properties - directly bound to settings // Night Light properties - directly bound to settings
readonly property var params: Settings.data.nightLight readonly property var params: Settings.data.nightLight
// Deprecated overlay flag removed; service only manages wlsunset now
property bool isActive: false
property bool isRunning: false
property string lastCommand: ""
property var nextCommand: []
// Computed properties Component.onCompleted: apply()
readonly property color overlayColor: params.enabled ? calculateOverlayColor() : "transparent"
property bool isActive: params.enabled && (params.autoSchedule ? isWithinSchedule() : true)
Component.onCompleted: { function buildCommand() {
Logger.log("NightLight", "Service started") var cmd = ["wlsunset"]
} // Use user-configured temps; if intensity is used, bias lowTemp towards user low
var i = Math.max(0, Math.min(1, params.intensity))
function calculateOverlayColor() { var loCfg = params.lowTemp || 3500
if (!isActive) { var hiCfg = params.highTemp || 6500
return "transparent" var lowTemp = Math.round(hiCfg - (hiCfg - loCfg) * Math.pow(i, 0.6))
} cmd.push("-t", lowTemp.toString())
cmd.push("-T", hiCfg.toString())
// More vibrant color formula - stronger effect at high warmth if (params.autoSchedule && LocationService.data.coordinatesReady && LocationService.data.stableLatitude !== ""
var red = 1.0 && LocationService.data.stableLongitude !== "") {
var green = 1.0 - (0.43 * params.intensity) cmd.push("-l", LocationService.data.stableLatitude)
var blue = 1.0 - (0.84 * params.intensity) cmd.push("-L", LocationService.data.stableLongitude)
var alpha = (params.intensity * 0.25) // Higher alpha for more noticeable effect
return Qt.rgba(red, green, blue, alpha)
}
function isWithinSchedule() {
if (!params.autoSchedule) {
return true
}
var now = new Date()
var currentTime = now.getHours() * 60 + now.getMinutes()
var startParts = params.startTime.split(":")
var stopParts = params.stopTime.split(":")
var startMinutes = parseInt(startParts[0]) * 60 + parseInt(startParts[1])
var stopMinutes = parseInt(stopParts[0]) * 60 + parseInt(stopParts[1])
// Handle overnight schedule (e.g., 20:00 to 07:00)
if (stopMinutes < startMinutes) {
return currentTime >= startMinutes || currentTime <= stopMinutes
} else { } else {
return currentTime >= startMinutes && currentTime <= stopMinutes // Manual schedule
if (params.startTime && params.stopTime) {
cmd.push("-S", params.startTime)
cmd.push("-s", params.stopTime)
}
// Optional: do not pass duration, use wlsunset defaults
}
return cmd
}
function stopIfRunning() {
// Best-effort stop; wlsunset runs as foreground, so pkill is simplest
Quickshell.execDetached(["pkill", "-x", "wlsunset"])
isRunning = false
}
function apply() {
if (!params.enabled) {
// Disable immediately
debounceStart.stop()
nextCommand = []
stopIfRunning()
return
}
// Debounce rapid changes (slider)
nextCommand = buildCommand()
lastCommand = nextCommand.join(" ")
stopIfRunning()
debounceStart.restart()
}
// Observe setting changes and location readiness
Connections {
target: Settings.data.nightLight
function onEnabledChanged() {
apply()
}
function onIntensityChanged() {
apply()
}
function onAutoScheduleChanged() {
apply()
}
function onStartTimeChanged() {
apply()
}
function onStopTimeChanged() {
apply()
} }
} }
// Timer to check schedule changes Connections {
target: LocationService.data
function onCoordinatesReadyChanged() {
if (params.enabled && params.autoSchedule)
apply()
}
function onStableLatitudeChanged() {
if (params.enabled && params.autoSchedule)
apply()
}
function onStableLongitudeChanged() {
if (params.enabled && params.autoSchedule)
apply()
}
}
// Foreground process runner
Process {
id: runner
running: false
onStarted: {
isRunning = true
Logger.log("NightLight", "Started wlsunset:", root.lastCommand)
}
onExited: function (code, status) {
isRunning = false
Logger.log("NightLight", "wlsunset exited:", code, status)
// Do not auto-restart here; debounceStart handles starts
}
stdout: StdioCollector {}
stderr: StdioCollector {}
}
// Debounce timer to avoid flicker when moving sliders
Timer { Timer {
interval: 60000 // Check every minute id: debounceStart
running: params.enabled && params.autoSchedule interval: 300
repeat: true repeat: false
onTriggered: { onTriggered: {
isActive = isWithinSchedule() if (params.enabled && nextCommand.length > 0) {
runner.command = nextCommand
runner.running = true
}
} }
} }
} }

View file

@ -11,8 +11,9 @@ RowLayout {
property string description: "" property string description: ""
property bool checked: false property bool checked: false
property bool hovering: false property bool hovering: false
// Smaller default footprint than NToggle property color activeColor: Color.mPrimary
property int baseSize: Math.max(Style.baseWidgetSize * 0.8, Math.round(14 / scaling)) property color activeOnColor: Color.mOnPrimary
property int baseSize: Math.max(Style.baseWidgetSize * 0.8, 14)
signal toggled(bool checked) signal toggled(bool checked)
signal entered signal entered
@ -23,6 +24,7 @@ RowLayout {
NLabel { NLabel {
label: root.label label: root.label
description: root.description description: root.description
visible: root.label !== "" || root.description !== ""
} }
Rectangle { Rectangle {
@ -30,16 +32,16 @@ RowLayout {
implicitWidth: root.baseSize * scaling implicitWidth: root.baseSize * scaling
implicitHeight: root.baseSize * scaling implicitHeight: root.baseSize * scaling
radius: Math.max(2 * scaling, Style.radiusXS * scaling) radius: Style.radiusXS * scaling
color: root.checked ? Color.mPrimary : Color.mSurface color: root.checked ? root.activeColor : Color.mSurface
border.color: root.checked ? Color.mPrimary : Color.mOutline border.color: root.checked ? root.activeColor : Color.mOutline
border.width: Math.max(1, Style.borderM * scaling) border.width: Math.max(1, Style.borderM * scaling)
NIcon { NIcon {
visible: root.checked visible: root.checked
anchors.centerIn: parent anchors.centerIn: parent
text: "check" text: "check"
color: Color.mOnPrimary color: root.activeOnColor
font.pointSize: Math.max(Style.fontSizeS, root.baseSize * 0.7) * scaling font.pointSize: Math.max(Style.fontSizeS, root.baseSize * 0.7) * scaling
} }

View file

@ -4,74 +4,68 @@ import QtQuick.Layouts
import qs.Commons import qs.Commons
import qs.Services import qs.Services
Item { ColumnLayout {
id: root id: root
property string label: "" property string label: ""
property string description: "" property string description: ""
property bool readOnly: false property bool readOnly: false
property bool enabled: true property bool enabled: true
property int inputMaxWidth: 420 * scaling
property alias text: input.text property alias text: input.text
property alias placeholderText: input.placeholderText property alias placeholderText: input.placeholderText
property alias inputMethodHints: input.inputMethodHints
signal editingFinished signal editingFinished
// Sizing spacing: Style.marginS * scaling
implicitWidth: Style.sliderWidth * 1.6 * scaling implicitHeight: frame.height
implicitHeight: Style.baseWidgetSize * 2.75 * scaling
ColumnLayout { NLabel {
spacing: Style.marginXXS * scaling label: root.label
Layout.fillWidth: true description: root.description
visible: root.label !== "" || root.description !== ""
}
NLabel { // Container
label: root.label Rectangle {
description: root.description id: frame
implicitWidth: parent.width
implicitHeight: Style.baseWidgetSize * 1.1 * scaling
Layout.minimumWidth: 80 * scaling
Layout.maximumWidth: root.inputMaxWidth
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
// Focus ring
Rectangle {
anchors.fill: parent
radius: frame.radius
color: Color.transparent
border.color: input.activeFocus ? Color.mSecondary : Color.transparent
border.width: input.activeFocus ? Math.max(1, Style.borderS * scaling) : 0
} }
// Container RowLayout {
Rectangle { anchors.fill: parent
id: frame anchors.leftMargin: Style.marginM * scaling
Layout.topMargin: Style.marginXS * scaling anchors.rightMargin: Style.marginM * scaling
implicitWidth: root.width spacing: Style.marginS * scaling
implicitHeight: Style.baseWidgetSize * 1.35 * scaling
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
// Focus ring TextField {
Rectangle { id: input
anchors.fill: parent Layout.fillWidth: true
radius: frame.radius echoMode: TextInput.Normal
color: Color.transparent readOnly: root.readOnly
border.color: input.activeFocus ? Color.mSecondary : Color.transparent enabled: root.enabled
border.width: input.activeFocus ? Math.max(1, Style.borderS * scaling) : 0 color: Color.mOnSurface
} placeholderTextColor: Color.mOnSurfaceVariant
background: null
RowLayout { font.pointSize: Style.fontSizeS * scaling
anchors.fill: parent onEditingFinished: root.editingFinished()
anchors.leftMargin: Style.marginM * scaling
anchors.rightMargin: Style.marginM * scaling
spacing: Style.marginS * scaling
// Optional leading icon slot in the future
// Item { Layout.preferredWidth: 0 }
TextField {
id: input
Layout.fillWidth: true
echoMode: TextInput.Normal
readOnly: root.readOnly
enabled: root.enabled
color: Color.mOnSurface
placeholderTextColor: Color.mOnSurface
background: null
font.pointSize: Style.fontSizeXS * scaling
onEditingFinished: root.editingFinished()
// Text changes are observable via the aliased 'text' property (root.text) and its 'textChanged' signal.
// No additional callback is invoked here to avoid conflicts with QML's onTextChanged handler semantics.
}
} }
} }
} }

View file

@ -21,7 +21,6 @@ import qs.Modules.Calendar
import qs.Modules.Dock import qs.Modules.Dock
import qs.Modules.IPC import qs.Modules.IPC
import qs.Modules.LockScreen import qs.Modules.LockScreen
import qs.Modules.NightLight
import qs.Modules.Notification import qs.Modules.Notification
import qs.Modules.SettingsPanel import qs.Modules.SettingsPanel
import qs.Modules.PowerPanel import qs.Modules.PowerPanel
@ -51,12 +50,10 @@ ShellRoot {
ToastOverlay {} ToastOverlay {}
NightLightOverlay {}
IPCManager {} IPCManager {}
// ------------------------------ // ------------------------------
// All the panels // All the NPanels
Launcher { Launcher {
id: launcherPanel id: launcherPanel
objectName: "launcherPanel" objectName: "launcherPanel"