Compare commits

...

57 Commits

Author SHA1 Message Date
LemmyCook
860e721709 Hotfix: do not filter our the screenrecorder indicator, as it messes with widgets index and settings. 2025-09-18 23:12:35 -04:00
LemmyCook
88ece93db2 2.12.0-dev 2025-09-18 22:09:38 -04:00
LemmyCook
2d290bf5f7 Release v2.12.0 2025-09-18 22:06:05 -04:00
LemmyCook
891c8660e3 Properly hide ScreenRecorderIndicator when inactive (no spacing) 2025-09-18 22:05:55 -04:00
LemmyCook
a734235cd0 Autoformating 2025-09-18 22:05:33 -04:00
Lemmy
8fdc6a0f72 Merge pull request #314 from kevindiaz314/main
fix(clock): respect monthBeforeDay setting in vertical clock date dis…
2025-09-18 21:38:31 -04:00
LemmyCook
603f499355 Settings: removed systemic capitalization improved labels and descriptions. 2025-09-18 21:34:30 -04:00
Kevin Diaz
2b8b97ab3b fix(clock): respect monthBeforeDay setting in vertical clock date display 2025-09-18 20:30:22 -04:00
LemmyCook
458ef3c0d5 TrayMenu: not using 'Screen' as we have a proper 'screen' 2025-09-18 18:28:01 -04:00
LemmyCook
c4008e3899 CustomButtonSettings: Don't use Screen with a capital 'S' unless really necessary. 2025-09-18 18:25:15 -04:00
LemmyCook
6c3299ad10 Merge branch 'wallpaper-selector' 2025-09-18 18:22:32 -04:00
LemmyCook
6fe498ce19 Wallpaper Selector: auto-focus search field 2025-09-18 17:47:26 -04:00
LemmyCook
4e67f26576 Wallpaper Selector: fix for multi screens / multi directories setup 2025-09-18 17:35:25 -04:00
LemmyCook
b2d46ab759 Settings: cleanup since we moved the wallpaper selector out. 2025-09-18 17:34:55 -04:00
Lemmy
0d3cc917fa Merge pull request #302 from randibudi/main
NixOS: Add Night Light Dependency and Enable Required Services
2025-09-18 15:51:48 -04:00
Lemmy
ac591da6c5 Update README.md 2025-09-18 15:51:21 -04:00
Lemmy
c7709b5f21 Update README.md 2025-09-18 15:50:19 -04:00
Lemmy
e6370904cd Update README.md 2025-09-18 15:47:37 -04:00
Randi Budi
e412cee52f Merge branch 'main' into main 2025-09-19 01:32:07 +07:00
Ly-sec
c3019230ae WallpaperSelector: even more layout changes 2025-09-18 20:04:03 +02:00
Ly-sec
c7ab350cbd MatugenService: add check for Settings.isLoaded 2025-09-18 19:53:06 +02:00
Ly-sec
b65d82d895 WallpaperSelector: more layout changes 2025-09-18 19:51:45 +02:00
Ly-sec
89eb5ecde6 IPCManager: add wallpaper selector toggle 2025-09-18 19:31:04 +02:00
Ly-sec
b374f167ef WallpaperSelectorPanel: rename to WallpaperSelector 2025-09-18 19:26:35 +02:00
Ly-sec
28026a4c37 NPanel: add bar detection while dragging
WallpaperSelectorPanel: adjust layout
2025-09-18 19:24:00 +02:00
Ly-sec
b8bce3d421 NPanel: add border while dragging 2025-09-18 18:34:48 +02:00
Ly-sec
6fba3457f7 NPanel: add drag support 2025-09-18 18:27:35 +02:00
Ly-sec
07a6a16011 WallpaperSelector: cleanup 2025-09-18 18:11:37 +02:00
Ly-sec
6b61599633 WallpaperSelector: change sizing 2025-09-18 18:06:18 +02:00
Ly-sec
1bd093db7f WallpaperSelector overhaul: initial commit 2025-09-18 17:55:30 +02:00
Ly-sec
3d9295856c Launcher: add sort by most used option 2025-09-18 16:53:38 +02:00
LemmyCook
a1aabd02f5 Toast: reworked the display and logic to make it more robust.
+ some bluetooth logic debouncing to avoid extra toast when adapter
comes back to life after suspend.
2025-09-18 10:10:40 -04:00
Ly-sec
ae2d3eddd6 README: revert Credits & Acknowledgment sections 2025-09-18 11:12:48 +02:00
Ly-sec
b75c358f54 README: full overhaul, linking to docs 2025-09-18 11:10:29 +02:00
Lysec
0972a55aad Merge pull request #312 from nalakawula/lockScreen/adjust-password-prompt
Make password prompt look like a terminal/tty
2025-09-18 11:02:15 +02:00
sumarsono
112f71b633 Make password prompt look like a terminal/tty 2025-09-18 15:52:45 +07:00
LemmyCook
e67d7166de Merge branch 'bar-service' 2025-09-17 22:50:56 -04:00
LemmyCook
6e88118ca9 Calendar: add conditional week number column. New option is in the Location tab of the settings. 2025-09-17 22:32:44 -04:00
LemmyCook
75b7f0fcb0 Bluetooth device: fixed missing busy icon on the call to action. 2025-09-17 21:58:44 -04:00
LemmyCook
47f72d9498 Location/Clock: Moved use12hourformat and reverseDaymonth from the clock widget settings to the main settings, location tab
- Fix #303
2025-09-17 21:10:51 -04:00
LemmyCook
85d7dc2506 Settings/Notification: typo fix 2025-09-17 15:40:10 -04:00
LemmyCook
1305efec24 Settings/Notification: fixed typo 2025-09-17 15:38:25 -04:00
LemmyCook
8af8bf2e2e BarService: to keep tracks of bar widgets and improve IPC behavior. 2025-09-17 10:19:55 -04:00
Lemmy
abd6a66297 Merge pull request #295 from knuesel/colorscheme-kanagawa
Kanagawa colorscheme
2025-09-17 09:34:31 -04:00
LemmyCook
2e9a812513 PowerProfile: Standardization + Factorisation. Fix #307 2025-09-17 09:30:23 -04:00
Jeremie Knuesel
8d845e7cd0 Kanagawa colorscheme 2025-09-17 14:56:13 +02:00
Ly-sec
a1dcef8dec Revert "Brightness: holding down keybind with brightness IPC now keeps changing brightness until release"
This reverts commit 38e0bb8e64.
2025-09-17 12:51:02 +02:00
Ly-sec
38e0bb8e64 Brightness: holding down keybind with brightness IPC now keeps changing brightness until release 2025-09-17 12:50:19 +02:00
Ly-sec
8811cb3d13 Notification: display links as plain text 2025-09-17 12:40:52 +02:00
ItsLemmy
a872682eb8 Brightness: fix #300 2025-09-17 00:28:57 -04:00
LemmyCook
46b8317330 v2.11.0-dev 2025-09-16 23:30:04 -04:00
LemmyCook
8204460112 v2.11.0 2025-09-16 23:29:02 -04:00
LemmyCook
292337dc00 Settings: Put monitor configs below other settings on Bar and Notif. tabs 2025-09-16 23:26:35 -04:00
LemmyCook
0b790c219d Dimming: replaced dimmer by panel dimming, now that we have no margins it works fine. 2025-09-16 23:23:16 -04:00
LemmyCook
7acca17b83 2.10.0-dev 2025-09-16 23:10:12 -04:00
Randi Budi
cdfb110007 fix(nixos): power profile and battery monitoring with module 2025-09-17 04:20:37 +07:00
Randi Budi
b7d8f92414 fix(nixos): add wlsunset dependency for night light 2025-09-17 00:59:13 +07:00
62 changed files with 1530 additions and 1242 deletions

View File

@@ -0,0 +1,34 @@
{
"dark": {
"mPrimary": "#76946a",
"mOnPrimary": "#1f1f28",
"mSecondary": "#c0a36e",
"mOnSecondary": "#1f1f28",
"mTertiary": "#7e9cd8",
"mOnTertiary": "#1f1f28",
"mError": "#c34043",
"mOnError": "#1f1f28",
"mSurface": "#1f1f28",
"mOnSurface": "#717c7c",
"mSurfaceVariant": "#2a2a37",
"mOnSurfaceVariant": "#c8c093",
"mOutline": "#363646",
"mShadow": "#1f1f28"
},
"light": {
"mPrimary": "#6f894e",
"mOnPrimary": "#f2ecbc",
"mSecondary": "#77713f",
"mOnSecondary": "#f2ecbc",
"mTertiary": "#4d699b",
"mOnTertiary": "#f2ecbc",
"mError": "#c84053",
"mOnError": "#f2ecbc",
"mSurface": "#f2ecbc",
"mOnSurface": "#8a8980",
"mSurfaceVariant": "#e5ddb0",
"mOnSurfaceVariant": "#545464",
"mOutline": "#cfc49c",
"mShadow": "#f2ecbc"
}
}

View File

@@ -26,6 +26,7 @@ Singleton {
"search": "search",
"warning": "exclamation-circle",
"stop": "player-stop-filled",
"busy": "hourglass-empty",
"media-pause": "player-pause-filled",
"media-play": "player-play-filled",
"media-prev": "player-skip-back-filled",
@@ -101,13 +102,13 @@ Singleton {
"settings-display": "device-desktop",
"settings-network": "sitemap",
"settings-brightness": "brightness-up",
"settings-weather": "cloud-sun",
"settings-location": "world-pin",
"settings-color-scheme": "palette",
"settings-wallpaper": "paint",
"settings-wallpaper-selector": "library-photo",
"settings-screen-recorder": "video",
"settings-hooks": "link",
"settings-notification": "bell",
"settings-notifications": "bell",
"settings-about": "info-square-rounded",
"bluetooth": "bluetooth",
"bt-device-generic": "bluetooth",

View File

@@ -134,37 +134,17 @@ Singleton {
// Backup the widget definition before altering
const widgetBefore = JSON.stringify(widget)
// Migrate old bar settings to proper per widget settings
switch (widget.id) {
case "ActiveWindow":
widget.showIcon = widget.showIcon !== undefined ? widget.showIcon : adapter.bar.showActiveWindowIcon
break
case "Battery":
widget.alwaysShowPercentage = widget.alwaysShowPercentage !== undefined ? widget.alwaysShowPercentage : adapter.bar.alwaysShowBatteryPercentage
break
// Get back to global settings for these two clock settings
case "Clock":
widget.use12HourClock = widget.use12HourClock !== undefined ? widget.use12HourClock : adapter.location.use12HourClock
widget.reverseDayMonth = widget.reverseDayMonth !== undefined ? widget.reverseDayMonth : adapter.location.reverseDayMonth
if (widget.showDate !== undefined) {
widget.displayFormat = "time-date"
} else if (widget.showSeconds) {
widget.displayFormat = "time-seconds"
if (widget.use12HourClock !== undefined) {
adapter.location.use12hourFormat = widget.use12HourClock
delete widget.use12HourClock
}
if (widget.reverseDayMonth !== undefined) {
adapter.location.monthBeforeDay = widget.reverseDayMonth
delete widget.reverseDayMonth
}
delete widget.showDate
delete widget.showSeconds
break
case "MediaMini":
widget.showAlbumArt = widget.showAlbumArt !== undefined ? widget.showAlbumArt : adapter.audio.showMiniplayerAlbumArt
widget.showVisualizer = widget.showVisualizer !== undefined ? widget.showVisualizer : adapter.audio.showMiniplayerCava
break
case "SidePanelToggle":
widget.useDistroLogo = widget.useDistroLogo !== undefined ? widget.useDistroLogo : adapter.bar.useDistroLogo
break
case "SystemMonitor":
widget.showNetworkStats = widget.showNetworkStats !== undefined ? widget.showNetworkStats : adapter.bar.showNetworkStats
break
case "Workspace":
widget.labelMode = widget.labelMode !== undefined ? widget.labelMode : adapter.bar.showWorkspaceLabel
break
}
@@ -197,6 +177,9 @@ Singleton {
MatugenService.init()
// Ensure wallpapers are restored after settings have been loaded
WallpaperService.init()
FontService.init()
HooksService.init()
@@ -280,12 +263,6 @@ Singleton {
property real marginVertical: 0.25
property real marginHorizontal: 0.25
property bool showActiveWindowIcon: true // TODO: delete
property bool alwaysShowBatteryPercentage: false // TODO: delete
property bool showNetworkStats: false // TODO: delete
property bool useDistroLogo: false // TODO: delete
property string showWorkspaceLabel: "none" // TODO: delete
// Widget configuration for modular bar system
property JsonObject widgets
widgets: JsonObject {
@@ -333,7 +310,6 @@ Singleton {
property bool forceBlackScreenCorners: false
property real radiusRatio: 1.0
property real screenRadiusRatio: 1.0
// Animation speed multiplier (0.1x - 2.0x)
property real animationSpeed: 1.0
}
@@ -341,10 +317,9 @@ Singleton {
property JsonObject location: JsonObject {
property string name: defaultLocation
property bool useFahrenheit: false
property bool reverseDayMonth: false // TODO: delete
property bool use12HourClock: false // TODO: delete
property bool showDateWithClock: false // TODO: delete
property bool use12hourFormat: false
property bool monthBeforeDay: false
property bool showWeekNumberInCalendar: false
}
// screen recorder
@@ -378,13 +353,13 @@ Singleton {
// applauncher
property JsonObject appLauncher: JsonObject {
// When disabled, Launcher hides clipboard command and ignores cliphist
property bool enableClipboardHistory: false
// Position: center, top_left, top_right, bottom_left, bottom_right, bottom_center, top_center
property string position: "center"
property real backgroundOpacity: 1.0
property list<string> pinnedExecs: []
property bool useApp2Unit: false
property bool sortByMostUsed: true
}
// dock
@@ -406,7 +381,7 @@ Singleton {
property JsonObject notifications: JsonObject {
property bool doNotDisturb: false
property list<string> monitors: []
// Last time the user opened the notification history (ms since epoch)
// Last time the user opened the notification history (ms since e899999999999998poch)
property real lastSeenTs: 0
// Duration settings for different urgency levels (in seconds)
property int lowUrgencyDuration: 3
@@ -421,9 +396,6 @@ Singleton {
property string visualizerType: "linear"
property list<string> mprisBlacklist: []
property string preferredPlayer: ""
property bool showMiniplayerAlbumArt: false // TODO: delete
property bool showMiniplayerCava: false // TODO: delete
}
// ui

View File

@@ -15,7 +15,7 @@ Singleton {
return Math.floor(date / 1000)
}
function formatDate(reverseDayMonth = true) {
function formatDate(monthBeforeDay = true) {
let now = date
let dayName = now.toLocaleDateString(Qt.locale(), "ddd")
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
@@ -40,7 +40,7 @@ Singleton {
let month = now.toLocaleDateString(Qt.locale(), "MMMM")
let year = now.toLocaleDateString(Qt.locale(), "yyyy")
return `${dayName}, ` + (reverseDayMonth ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`)
return `${dayName}, ` + (monthBeforeDay ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`)
}

View File

@@ -1,76 +0,0 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
Variants {
model: Quickshell.screens
delegate: Loader {
required property ShellScreen modelData
// Dimmer is only active on the screen where the panel is currently open.
active: {
if (Settings.isLoaded && Settings.data.general.dimDesktop && modelData !== undefined && PanelService.openedPanel !== null && PanelService.openedPanel.item !== undefined && PanelService.openedPanel.item !== null) {
return (PanelService.openedPanel.item.screen === modelData)
}
return false
}
sourceComponent: PanelWindow {
id: panel
property real customOpacity: 0
Component.onCompleted: {
if (modelData) {
Logger.log("Dimmer", "Loaded on", modelData.name)
}
// When a NPanel opens it seems it is initialized with the primary screen for a very brief moment
// before the screen actually updates to the proper value. We use a timer to delay the fade in to avoid
// a single frame flicker on the main screen when opening a panel on another screen.
fadeInTimer.start()
}
Connections {
target: PanelService
function onWillClose() {
customOpacity = Style.opacityNone
}
}
Timer {
id: fadeInTimer
interval: 100
onTriggered: customOpacity = Style.opacityHeavy
}
screen: modelData
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WlrLayershell.namespace: "quickshell-dimmer"
// mask: Region {}
anchors {
top: true
bottom: true
right: true
left: true
}
color: Qt.alpha(Color.mShadow, customOpacity)
Behavior on color {
ColorAnimation {
duration: Style.animationSlow
}
}
}
}
}

View File

@@ -8,6 +8,7 @@ import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.Notification
import qs.Modules.Bar.Extras
Variants {
model: Quickshell.screens
@@ -88,7 +89,7 @@ Variants {
Repeater {
model: Settings.data.bar.widgets.left
delegate: NWidgetLoader {
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
@@ -111,7 +112,7 @@ Variants {
Repeater {
model: Settings.data.bar.widgets.center
delegate: NWidgetLoader {
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
@@ -135,7 +136,7 @@ Variants {
Repeater {
model: Settings.data.bar.widgets.right
delegate: NWidgetLoader {
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
@@ -169,7 +170,7 @@ Variants {
Repeater {
model: Settings.data.bar.widgets.left
delegate: NWidgetLoader {
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
@@ -194,7 +195,7 @@ Variants {
Repeater {
model: Settings.data.bar.widgets.center
delegate: NWidgetLoader {
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
@@ -220,7 +221,7 @@ Variants {
Repeater {
model: Settings.data.bar.widgets.right
delegate: NWidgetLoader {
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,

View File

@@ -8,12 +8,14 @@ Item {
property string widgetId: ""
property var widgetProps: ({})
property bool enabled: true
property string screenName: widgetProps.screen ? widgetProps.screen.name : ""
property string section: widgetProps.section || ""
property int sectionIndex: widgetProps.sectionWidgetIndex || 0
Connections {
target: ScalingService
function onScaleChanged(screenName, scale) {
if (loader.item && loader.item.screen && screenName === loader.item.screen.name) {
function onScaleChanged(aScreenName, scale) {
if (loader.item && loader.item.screen && aScreenName === screenName) {
loader.item['scaling'] = scale
}
}
@@ -27,7 +29,7 @@ Item {
id: loader
anchors.fill: parent
active: Settings.isLoaded && enabled && widgetId !== ""
active: Settings.isLoaded && widgetId !== ""
sourceComponent: {
if (!active) {
return null
@@ -45,18 +47,30 @@ Item {
}
}
// Register this widget instance with BarService
if (screenName && section) {
BarService.registerWidget(screenName, section, widgetId, sectionIndex, item)
}
if (item.hasOwnProperty("onLoaded")) {
item.onLoaded()
}
Logger.log("NWidgetLoader", "Loaded", widgetId, "on screen", item.screen.name)
//Logger.log("BarWidgetLoader", "Loaded", widgetId, "on screen", item.screen.name)
}
Component.onDestruction: {
// Unregister when destroyed
if (screenName && section) {
BarService.unregisterWidget(screenName, section, widgetId, sectionIndex)
}
}
}
// Error handling
onWidgetIdChanged: {
if (widgetId && !BarWidgetRegistry.hasWidget(widgetId)) {
Logger.warn("WidgetLoader", "Widget not found in registry:", widgetId)
Logger.warn("BarWidgetLoader", "Widget not found in bar registry:", widgetId)
}
}
}

View File

@@ -235,13 +235,13 @@ PopupWindow {
openLeft = false
} else {
// Bar is horizontal (top/bottom) or undefined, use space-based logic
openLeft = (globalPos.x + entry.width + submenuWidth > (screen ? screen.width : Screen.width))
openLeft = (globalPos.x + entry.width + submenuWidth > screen.width)
// Secondary check: ensure we don't open off-screen
if (openLeft && globalPos.x - submenuWidth < 0) {
// Would open off the left edge, force right opening
openLeft = false
} else if (!openLeft && globalPos.x + entry.width + submenuWidth > (screen ? screen.width : Screen.width)) {
} else if (!openLeft && globalPos.x + entry.width + submenuWidth > screen.width) {
// Would open off the right edge, force left opening
openLeft = true
}

View File

@@ -86,7 +86,7 @@ Item {
id: pill
compact: (Settings.data.bar.density === "compact")
rightOpen: BarWidgetRegistry.getPillDirection(root)
rightOpen: BarService.getPillDirection(root)
icon: testMode ? BatteryService.getIcon(testPercent, testCharging, true) : BatteryService.getIcon(percent, charging, isReady)
text: (isReady || testMode) ? Math.round(percent) : "-"
suffix: "%"

View File

@@ -78,7 +78,7 @@ Item {
id: pill
compact: (Settings.data.bar.density === "compact")
rightOpen: BarWidgetRegistry.getPillDirection(root)
rightOpen: BarService.getPillDirection(root)
icon: getIcon()
autoHide: false // Important to be false so we can hover as long as we want
text: {

View File

@@ -30,10 +30,10 @@ Rectangle {
readonly property string barPosition: Settings.data.bar.position
readonly property bool compact: (Settings.data.bar.density === "compact")
readonly property bool use12h: Settings.data.location.use12hourFormat
readonly property bool monthBeforeDay: Settings.data.location.monthBeforeDay
// Resolve settings: try user settings or defaults from BarWidgetRegistry
readonly property bool use12h: widgetSettings.use12HourClock !== undefined ? widgetSettings.use12HourClock : widgetMetadata.use12HourClock
readonly property bool reverseDayMonth: widgetSettings.reverseDayMonth !== undefined ? widgetSettings.reverseDayMonth : widgetMetadata.reverseDayMonth
readonly property string displayFormat: widgetSettings.displayFormat !== undefined ? widgetSettings.displayFormat : widgetMetadata.displayFormat
// Use compact mode for vertical bars
@@ -119,7 +119,7 @@ Rectangle {
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
const day = now.getDate().toString().padStart(2, '0')
let month = now.toLocaleDateString(Qt.locale(), "MMM")
timeStr += " - " + (reverseDayMonth ? `${dayName}, ${month} ${day}` : `${dayName}, ${day} ${month}`)
timeStr += " - " + (monthBeforeDay ? `${dayName}, ${month} ${day}` : `${dayName}, ${day} ${month}`)
}
return timeStr
@@ -158,11 +158,9 @@ Rectangle {
// Compact mode: date section (last 2 lines)
switch (index) {
case 0:
// Day
return now.getDate().toString().padStart(2, '0')
return monthBeforeDay ? (now.getMonth() + 1).toString().padStart(2, '0') : now.getDate().toString().padStart(2, '0')
case 1:
// Month
return (now.getMonth() + 1).toString().padStart(2, '0')
return monthBeforeDay ? now.getDate().toString().padStart(2, '0') : (now.getMonth() + 1).toString().padStart(2, '0')
default:
return ""
}
@@ -184,7 +182,7 @@ Rectangle {
const now = Time.date
const day = now.getDate().toString().padStart(2, '0')
const month = (now.getMonth() + 1).toString().padStart(2, '0')
return reverseDayMonth ? `${month}/${day}` : `${day}/${month}`
return monthBeforeDay ? `${month}/${day}` : `${day}/${month}`
}
// Enable fixed-width font for consistent spacing
@@ -199,7 +197,7 @@ Rectangle {
NTooltip {
id: tooltip
text: `${Time.formatDate(reverseDayMonth)}.`
text: `${Time.formatDate(monthBeforeDay)}.`
target: clockContainer
positionAbove: Settings.data.bar.position === "bottom"
}

View File

@@ -47,7 +47,7 @@ Item {
BarPill {
id: pill
rightOpen: BarWidgetRegistry.getPillDirection(root)
rightOpen: BarService.getPillDirection(root)
icon: customIcon
text: _dynamicText
compact: (Settings.data.bar.density === "compact")

View File

@@ -44,7 +44,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
compact: (Settings.data.bar.density === "compact")
rightOpen: BarWidgetRegistry.getPillDirection(root)
rightOpen: BarService.getPillDirection(root)
icon: "keyboard"
autoHide: false // Important to be false so we can hover as long as we want
text: currentLayout.toUpperCase()

View File

@@ -89,7 +89,7 @@ Item {
BarPill {
id: pill
rightOpen: BarWidgetRegistry.getPillDirection(root)
rightOpen: BarService.getPillDirection(root)
icon: getIcon()
compact: (Settings.data.bar.density === "compact")
autoHide: false // Important to be false so we can hover as long as we want

View File

@@ -11,45 +11,16 @@ NIconButton {
property ShellScreen screen
property real scaling: 1.0
readonly property bool hasPP: PowerProfileService.available
baseSize: Style.capsuleHeight
visible: hasPP
visible: PowerProfileService.available
function profileIcon() {
if (!hasPP)
return "balanced"
if (PowerProfileService.profile === PowerProfile.Performance)
return "performance"
if (PowerProfileService.profile === PowerProfile.Balanced)
return "balanced"
if (PowerProfileService.profile === PowerProfile.PowerSaver)
return "powersaver"
}
function profileName() {
if (!hasPP)
return "Unknown"
if (PowerProfileService.profile === PowerProfile.Performance)
return "Performance"
if (PowerProfileService.profile === PowerProfile.Balanced)
return "Balanced"
if (PowerProfileService.profile === PowerProfile.PowerSaver)
return "Power Saver"
}
function changeProfile() {
if (!hasPP)
return
PowerProfileService.cycleProfile()
}
icon: root.profileIcon()
tooltipText: root.profileName()
icon: PowerProfileService.getIcon()
tooltipText: `Current power profile is "${PowerProfileService.getName()}".`
compact: (Settings.data.bar.density === "compact")
colorBg: (PowerProfileService.profile === PowerProfile.Balanced) ? (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent) : Color.mPrimary
colorFg: (PowerProfileService.profile === PowerProfile.Balanced) ? Color.mOnSurface : Color.mOnPrimary
colorBorder: Color.transparent
colorBorderHover: Color.transparent
onClicked: root.changeProfile()
onClicked: PowerProfileService.cycleProfile()
}

View File

@@ -12,7 +12,7 @@ NIconButton {
visible: ScreenRecorderService.isRecording
icon: "camera-video"
tooltipText: "Screen recording is active\nClick to stop recording"
tooltipText: "Screen recording is active.\nClick to stop recording."
compact: (Settings.data.bar.density === "compact")
baseSize: Style.capsuleHeight
colorBg: Color.mPrimary

View File

@@ -76,7 +76,7 @@ Item {
id: pill
compact: (Settings.data.bar.density === "compact")
rightOpen: BarWidgetRegistry.getPillDirection(root)
rightOpen: BarService.getPillDirection(root)
icon: getIcon()
autoHide: false // Important to be false so we can hover as long as we want
text: Math.floor(AudioService.volume * 100)

View File

@@ -163,7 +163,7 @@ ColumnLayout {
}
return "Connect"
}
icon: (isBusy ? "hourglass-split" : null)
icon: (isBusy ? "busy" : null)
onClicked: {
if (modelData.connected) {
BluetoothService.disconnectDevice(modelData)

View File

@@ -10,7 +10,7 @@ import qs.Widgets
NPanel {
id: root
preferredWidth: 340
preferredWidth: Settings.data.location.showWeekNumberInCalendar ? 350 : 330
preferredHeight: 320
// Main Column
@@ -60,7 +60,7 @@ NPanel {
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginS * scaling
Layout.bottomMargin: Style.marginM * scaling
Layout.bottomMargin: Style.marginL * scaling
}
// Columns label (respects locale's first day of week)
@@ -68,62 +68,172 @@ NPanel {
Layout.fillWidth: true
Layout.leftMargin: Style.marginS * scaling // Align with grid
Layout.rightMargin: Style.marginS * scaling
Layout.bottomMargin: Style.marginM * scaling
spacing: 0
Repeater {
model: 7
// Week header spacer or label (same width as week number column)
Item {
visible: Settings.data.location.showWeekNumberInCalendar
Layout.preferredWidth: visible ? Style.baseWidgetSize * scaling : 0
NText {
text: {
// Use the locale's first day of week setting
let firstDay = Qt.locale().firstDayOfWeek
let dayIndex = (firstDay + index) % 7
return Qt.locale().dayName(dayIndex, Locale.ShortFormat)
}
color: Color.mSecondary
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
anchors.centerIn: parent
text: "Week"
color: Color.mOutline
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightRegular
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
Layout.preferredWidth: Style.baseWidgetSize * scaling
}
}
// Day name headers - now properly aligned with calendar grid
GridLayout {
Layout.fillWidth: true
Layout.fillHeight: true
columns: 7
rows: 1
columnSpacing: 0
rowSpacing: 0
Repeater {
model: 7
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredWidth: Style.baseWidgetSize * scaling
NText {
anchors.centerIn: parent
text: {
// Use the locale's first day of week setting
let firstDay = Qt.locale().firstDayOfWeek
let dayIndex = (firstDay + index) % 7
return Qt.locale().dayName(dayIndex, Locale.ShortFormat)
}
color: Color.mSecondary
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
horizontalAlignment: Text.AlignHCenter
}
}
}
}
}
// Grids: days
MonthGrid {
id: grid
// Grids: days with optional week numbers
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true // Take remaining space
Layout.fillHeight: true
Layout.leftMargin: Style.marginS * scaling
Layout.rightMargin: Style.marginS * scaling
spacing: 0
month: Time.date.getMonth()
year: Time.date.getFullYear()
locale: Qt.locale() // Use system locale
delegate: Rectangle {
width: (Style.baseWidgetSize * scaling)
height: (Style.baseWidgetSize * scaling)
radius: Style.radiusS * scaling
color: model.today ? Color.mPrimary : Color.transparent
// Week numbers column (only visible when enabled)
GridLayout {
visible: Settings.data.location.showWeekNumberInCalendar
Layout.preferredWidth: visible ? Style.baseWidgetSize * scaling : 0
Layout.fillHeight: true
columns: 1
rows: 6
columnSpacing: 0
rowSpacing: 0
NText {
anchors.centerIn: parent
text: model.day
color: model.today ? Color.mOnPrimary : Color.mOnSurface
opacity: model.month === grid.month ? Style.opacityHeavy : Style.opacityLight
font.pointSize: (Style.fontSizeM * scaling)
font.weight: model.today ? Style.fontWeightBold : Style.fontWeightRegular
Repeater {
model: 6 // Maximum 6 weeks in a month view
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Color.transparent
NText {
anchors.centerIn: parent
color: Color.mOutline
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightBold
text: {
// Calculate the first day shown in the calendar grid
let firstDay = new Date(grid.year, grid.month, 1)
let firstDayOfWeek = Qt.locale().firstDayOfWeek
let startOffset = (firstDay.getDay() - firstDayOfWeek + 7) % 7
let gridStartDate = new Date(grid.year, grid.month, 1 - startOffset)
// Get the date for the start of this specific row
let rowDate = new Date(gridStartDate)
rowDate.setDate(gridStartDate.getDate() + (index * 7))
// Calculate week number based on the Thursday of the visual row
// This correctly handles rows that span two different ISO weeks.
let thursdayOfRow = new Date(rowDate)
let offsetToThursday = (4 - thursdayOfRow.getDay() + 7) % 7
thursdayOfRow.setDate(thursdayOfRow.getDate() + offsetToThursday)
// Check if this row is visible (contains days from current month)
let rowEndDate = new Date(rowDate)
rowEndDate.setDate(rowDate.getDate() + 6)
if (rowDate.getMonth() === grid.month || rowEndDate.getMonth() === grid.month || (rowDate.getMonth() < grid.month && rowEndDate.getMonth() > grid.month)) {
return `${getISOWeekNumber(thursdayOfRow)}`
}
return ""
}
}
}
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
// The actual calendar grid
MonthGrid {
id: grid
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 0
month: Time.date.getMonth()
year: Time.date.getFullYear()
locale: Qt.locale()
delegate: Rectangle {
width: Style.baseWidgetSize * scaling
height: Style.baseWidgetSize * scaling
radius: Style.radiusS * scaling
color: model.today ? Color.mPrimary : Color.transparent
NText {
anchors.centerIn: parent
text: model.day
color: model.today ? Color.mOnPrimary : Color.mOnSurface
opacity: model.month === grid.month ? Style.opacityHeavy : Style.opacityLight
font.pointSize: Style.fontSizeM * scaling
font.weight: model.today ? Style.fontWeightBold : Style.fontWeightRegular
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
}
}
}
}
function getISOWeekNumber(date) {
// Create a copy of the date and normalize to noon to prevent DST issues
const targetDate = new Date(date.getTime())
targetDate.setHours(12, 0, 0, 0)
// Roll the date to the Thursday of the week.
// getDay() is 0 for Sunday, we want Monday to be 1 and Sunday to be 7.
const dayOfWeek = targetDate.getDay() || 7
targetDate.setDate(targetDate.getDate() - dayOfWeek + 4)
// Get the first day of that Thursday's year
const yearStart = new Date(targetDate.getFullYear(), 0, 1)
// Calculate the difference in days and find the week number
const dayOfYear = ((targetDate - yearStart) / 86400000) + 1
return Math.ceil(dayOfYear / 7)
}
}

View File

@@ -27,7 +27,8 @@ Item {
IpcHandler {
target: "notifications"
function toggleHistory() {
notificationHistoryPanel.toggle()
// Will attempt to open the panel next to the bar button if any.
notificationHistoryPanel.toggle(BarService.lookupWidget("NotificationHistory"))
}
function toggleDND() {
Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
@@ -118,13 +119,20 @@ Item {
IpcHandler {
target: "sidePanel"
function toggle() {
sidePanel.toggle()
// Will attempt to open the panel next to the bar button if any.
sidePanel.toggle(BarService.lookupWidget("SidePanelToggle"))
}
}
// Wallpaper IPC: trigger a new random wallpaper
IpcHandler {
target: "wallpaper"
function toggle() {
if (Settings.data.wallpaper.enabled) {
wallpaperSelector.toggle()
}
}
function random() {
if (Settings.data.wallpaper.enabled) {
WallpaperService.setRandomWallpaper()

View File

@@ -11,6 +11,38 @@ Item {
property bool handleSearch: true
property var entries: []
// Persistent usage tracking stored in cacheDir
property string usageFilePath: Settings.cacheDir + "launcher_app_usage.json"
// Debounced saver to avoid excessive IO
Timer {
id: saveTimer
interval: 750
repeat: false
onTriggered: usageFile.writeAdapter()
}
FileView {
id: usageFile
path: usageFilePath
printErrors: false
watchChanges: false
onLoadFailed: function (error) {
if (error.toString().includes("No such file") || error === 2) {
writeAdapter()
}
}
onAdapterUpdated: saveTimer.start()
JsonAdapter {
id: usageAdapter
// key: app id/command, value: integer count
property var counts: ({})
}
}
function init() {
loadApplications()
}
@@ -36,8 +68,20 @@ Item {
return []
if (!query || query.trim() === "") {
// Return all apps alphabetically
return entries.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())).map(app => createResultEntry(app))
// Return all apps, optionally sorted by usage
let sorted
if (Settings.data.appLauncher.sortByMostUsed) {
sorted = entries.slice().sort((a, b) => {
const ua = getUsageCount(a)
const ub = getUsageCount(b)
if (ub !== ua)
return ub - ua
return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase())
})
} else {
sorted = entries.slice().sort((a, b) => (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase()))
}
return sorted.map(app => createResultEntry(app))
}
// Use fuzzy search if available, fallback to simple search
@@ -84,6 +128,9 @@ Item {
launcher.closeCompleted()
Logger.log("ApplicationsPlugin", `Launching: ${app.name}`)
// Record usage and persist asynchronously
if (Settings.data.appLauncher.sortByMostUsed)
recordUsage(app)
if (Settings.data.appLauncher.useApp2Unit && app.id) {
Logger.log("ApplicationsPlugin", `Using app2unit for: ${app.id}`)
if (app.runInTerminal)
@@ -98,4 +145,33 @@ Item {
}
}
}
// -------------------------
// Usage tracking helpers
function getAppKey(app) {
if (app && app.id)
return String(app.id)
if (app && app.command && app.command.join)
return app.command.join(" ")
return String(app && app.name ? app.name : "unknown")
}
function getUsageCount(app) {
const key = getAppKey(app)
const m = usageAdapter && usageAdapter.counts ? usageAdapter.counts : null
if (!m)
return 0
const v = m[key]
return typeof v === 'number' && isFinite(v) ? v : 0
}
function recordUsage(app) {
const key = getAppKey(app)
if (!usageAdapter.counts)
usageAdapter.counts = ({})
const current = getUsageCount(app)
usageAdapter.counts[key] = current + 1
// Trigger save via debounced timer
saveTimer.restart()
}
}

View File

@@ -26,6 +26,15 @@ Loader {
}
}
function formatTime() {
return Settings.data.location.use12hourFormat ? Qt.formatDateTime(new Date(), "h:mm A") : Qt.formatDateTime(new Date(), "HH:mm")
}
function formatDate() {
// For full text date, day is always before month, so we use this format for everybody: Wednesday, September 17.
return Qt.formatDateTime(new Date(), "dddd, MMMM d")
}
function scheduleUnloadAfterUnlock() {
unloadAfterUnlockTimer.start()
}
@@ -137,9 +146,10 @@ Loader {
NText {
id: timeText
text: Qt.formatDateTime(new Date(), "HH:mm")
text: formatTime()
font.family: Settings.data.ui.fontBillboard
font.pointSize: Style.fontSizeXXXL * 6 * scaling
// Smaller time display when using longer 12 hour format
font.pointSize: Settings.data.location.use12hourFormat ? Style.fontSizeXXXL * 4 * scaling : Style.fontSizeXXXL * 5 * scaling
font.weight: Style.fontWeightBold
font.letterSpacing: -2 * scaling
color: Color.mOnSurface
@@ -163,7 +173,7 @@ Loader {
NText {
id: dateText
text: Qt.formatDateTime(new Date(), "dddd, MMMM d")
text: formatDate()
font.family: Settings.data.ui.fontBillboard
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Font.Light
@@ -505,6 +515,19 @@ Loader {
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: "Password:"
color: Color.mPrimary
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
}
TextInput {
id: passwordInput
@@ -877,8 +900,8 @@ Loader {
running: true
repeat: true
onTriggered: {
timeText.text = Qt.formatDateTime(new Date(), "HH:mm")
dateText.text = Qt.formatDateTime(new Date(), "dddd, MMMM d")
timeText.text = formatTime()
dateText.text = formatDate()
}
}
}

View File

@@ -259,6 +259,7 @@ Variants {
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightMedium
color: Color.mOnSurface
textFormat: Text.PlainText
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Layout.fillWidth: true
maximumLineCount: 3
@@ -269,6 +270,7 @@ Variants {
text: model.body || ""
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurface
textFormat: Text.PlainText
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Layout.fillWidth: true
maximumLineCount: 5

View File

@@ -169,6 +169,7 @@ NPanel {
font.pointSize: Style.fontSizeM * scaling
font.weight: Font.Medium
color: Color.mPrimary
textFormat: Text.PlainText
wrapMode: Text.Wrap
Layout.fillWidth: true
maximumLineCount: 2
@@ -179,6 +180,7 @@ NPanel {
text: (body || "").substring(0, 150)
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurface
textFormat: Text.PlainText
wrapMode: Text.Wrap
Layout.fillWidth: true
maximumLineCount: 3

View File

@@ -15,14 +15,10 @@ ColumnLayout {
// Local state
property string valueDisplayFormat: widgetData.displayFormat !== undefined ? widgetData.displayFormat : widgetMetadata.displayFormat
property bool valueUse12h: widgetData.use12HourClock !== undefined ? widgetData.use12HourClock : widgetMetadata.use12HourClock
property bool valueReverseDayMonth: widgetData.reverseDayMonth !== undefined ? widgetData.reverseDayMonth : widgetMetadata.reverseDayMonth
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.displayFormat = valueDisplayFormat
settings.use12HourClock = valueUse12h
settings.reverseDayMonth = valueReverseDayMonth
return settings
}
@@ -50,16 +46,4 @@ ColumnLayout {
onSelected: key => valueDisplayFormat = key
minimumWidth: 230 * scaling
}
NToggle {
label: "Use 12-hour clock"
checked: valueUse12h
onToggled: checked => valueUse12h = checked
}
NToggle {
label: "Reverse day and month"
checked: valueReverseDayMonth
onToggled: checked => valueReverseDayMonth = checked
}
}

View File

@@ -51,13 +51,13 @@ ColumnLayout {
id: iconPicker
modal: true
width: {
var w = Math.round(Math.max(Screen.width * 0.35, 900) * scaling)
w = Math.min(w, Screen.width - Style.marginL * 2)
var w = Math.round(Math.max(screen.width * 0.35, 900) * scaling)
w = Math.min(w, screen.width - Style.marginL * 2)
return w
}
height: {
var h = Math.round(Math.max(Screen.height * 0.65, 700) * scaling)
h = Math.min(h, Screen.height - Style.barHeight * scaling - Style.marginL * 2)
var h = Math.round(Math.max(screen.height * 0.65, 700) * scaling)
h = Math.min(h, screen.height - Style.barHeight * scaling - Style.marginL * 2)
return h
}
anchors.centerIn: Overlay.overlay

View File

@@ -21,23 +21,24 @@ NPanel {
panelKeyboardFocus: true
draggable: true
// Tabs enumeration, order is NOT relevant
enum Tab {
About,
Audio,
Bar,
Dock,
Hooks,
Launcher,
ColorScheme,
Display,
Dock,
General,
Hooks,
Launcher,
Location,
Network,
Notification,
Notifications,
ScreenRecorder,
Weather,
Wallpaper,
WallpaperSelector
Wallpaper
}
property int requestedTab: SettingsPanel.Tab.General
@@ -45,13 +46,6 @@ NPanel {
property var tabsModel: []
property var activeScrollView: null
Connections {
target: Settings.data.wallpaper
function onEnabledChanged() {
updateTabsModel()
}
}
Component.onCompleted: {
updateTabsModel()
}
@@ -81,8 +75,8 @@ NPanel {
Tabs.NetworkTab {}
}
Component {
id: weatherTab
Tabs.WeatherTab {}
id: locationTab
Tabs.LocationTab {}
}
Component {
id: colorSchemeTab
@@ -92,10 +86,6 @@ NPanel {
id: wallpaperTab
Tabs.WallpaperTab {}
}
Component {
id: wallpaperSelectorTab
Tabs.WallpaperSelectorTab {}
}
Component {
id: screenRecorderTab
Tabs.ScreenRecorderTab {}
@@ -113,8 +103,8 @@ NPanel {
Tabs.DockTab {}
}
Component {
id: notificationTab
Tabs.NotificationTab {}
id: notificationsTab
Tabs.NotificationsTab {}
}
// Order *DOES* matter
@@ -150,20 +140,20 @@ NPanel {
"icon": "settings-display",
"source": displayTab
}, {
"id": SettingsPanel.Tab.Notification,
"label": "Notification",
"icon": "settings-notification",
"source": notificationTab
"id": SettingsPanel.Tab.Notifications,
"label": "Notifications",
"icon": "settings-notifications",
"source": notificationsTab
}, {
"id": SettingsPanel.Tab.Network,
"label": "Network",
"icon": "settings-network",
"source": networkTab
}, {
"id": SettingsPanel.Tab.Weather,
"label": "Weather",
"icon": "settings-weather",
"source": weatherTab
"id": SettingsPanel.Tab.Location,
"label": "Location",
"icon": "settings-location",
"source": locationTab
}, {
"id": SettingsPanel.Tab.ColorScheme,
"label": "Color Scheme",
@@ -174,35 +164,23 @@ NPanel {
"label": "Wallpaper",
"icon": "settings-wallpaper",
"source": wallpaperTab
}, {
"id": SettingsPanel.Tab.ScreenRecorder,
"label": "Screen Recorder",
"icon": "settings-screen-recorder",
"source": screenRecorderTab
}, {
"id": SettingsPanel.Tab.Hooks,
"label": "Hooks",
"icon": "settings-hooks",
"source": hooksTab
}, {
"id": SettingsPanel.Tab.About,
"label": "About",
"icon": "settings-about",
"source": aboutTab
}]
// Only add the Wallpaper Selector tab if the feature is enabled
if (Settings.data.wallpaper.enabled) {
newTabs.push({
"id": SettingsPanel.Tab.WallpaperSelector,
"label": "Wallpaper Selector",
"icon": "settings-wallpaper-selector",
"source": wallpaperSelectorTab
})
}
newTabs.push({
"id": SettingsPanel.Tab.ScreenRecorder,
"label": "Screen Recorder",
"icon": "settings-screen-recorder",
"source": screenRecorderTab
}, {
"id": SettingsPanel.Tab.Hooks,
"label": "Hooks",
"icon": "settings-hooks",
"source": hooksTab
}, {
"id": SettingsPanel.Tab.About,
"label": "About",
"icon": "settings-about",
"source": aboutTab
})
root.tabsModel = newTabs // Assign the generated list to the model
}
// When the panel opens, choose the appropriate tab

View File

@@ -17,7 +17,7 @@ ColumnLayout {
property var contributors: GitHubService.contributors
NHeader {
label: "Noctalia Shell"
label: "Noctalia shell"
description: "A sleek and minimal desktop shell thoughtfully crafted for Wayland, built with Quickshell."
}
@@ -31,7 +31,7 @@ ColumnLayout {
columnSpacing: Style.marginS * scaling
NText {
text: "Latest Version:"
text: "Latest version:"
color: Color.mOnSurface
}
@@ -42,7 +42,7 @@ ColumnLayout {
}
NText {
text: "Installed Version:"
text: "Installed version:"
color: Color.mOnSurface
}

View File

@@ -12,7 +12,7 @@ ColumnLayout {
NHeader {
label: "Volumes"
description: "Configure volume controls and audio levels."
description: "Adjust volume controls and audio levels."
}
property real localVolume: AudioService.volume
@@ -30,7 +30,7 @@ ColumnLayout {
Layout.fillWidth: true
NLabel {
label: "Output Volume"
label: "Output volume"
description: "System-wide volume level."
}
@@ -67,8 +67,8 @@ ColumnLayout {
Layout.fillWidth: true
NToggle {
label: "Mute Audio Output"
description: "Mute or unmute the default audio output."
label: "Mute audio output"
description: "Mute the system's main audio output."
checked: AudioService.muted
onToggled: checked => {
if (AudioService.sink && AudioService.sink.audio) {
@@ -84,7 +84,7 @@ ColumnLayout {
Layout.fillWidth: true
NLabel {
label: "Input Volume"
label: "Input volume"
description: "Microphone input volume level."
}
@@ -105,8 +105,8 @@ ColumnLayout {
Layout.fillWidth: true
NToggle {
label: "Mute Audio Input"
description: "Mute or unmute the default audio input (microphone)."
label: "Mute audio input"
description: "Mute the default audio input (microphone)."
checked: AudioService.inputMuted
onToggled: checked => AudioService.setInputMuted(checked)
}
@@ -119,7 +119,7 @@ ColumnLayout {
NSpinBox {
Layout.fillWidth: true
label: "Volume Step Size"
label: "Volume step size"
description: "Adjust the step size for volume changes (scroll wheel, keyboard shortcuts)."
minimum: 1
maximum: 25
@@ -141,8 +141,8 @@ ColumnLayout {
spacing: Style.marginS * scaling
NHeader {
label: "Audio Devices"
description: "Configure audio input and output devices."
label: "Audio devices"
description: "Choose your audio input and output devices."
}
// -------------------------------
@@ -157,7 +157,7 @@ ColumnLayout {
Layout.bottomMargin: Style.marginL * scaling
NLabel {
label: "Output Device"
label: "Output device"
description: "Select the desired audio output device."
}
@@ -184,7 +184,7 @@ ColumnLayout {
Layout.fillWidth: true
NLabel {
label: "Input Device"
label: "Input device"
description: "Select the desired audio input device."
}
@@ -213,14 +213,14 @@ ColumnLayout {
spacing: Style.marginL * scaling
NHeader {
label: "Media Player"
description: "Configure your favorite media players."
label: "Media players"
description: "Set your preferred and ignored media applications."
}
// Preferred player
NTextInput {
label: "Preferred Player"
description: "Substring to match MPRIS player (identity/bus/desktop)."
label: "Primary player"
description: "Enter a keyword to identify your main player."
placeholderText: "e.g. spotify, vlc, mpv"
text: Settings.data.audio.preferredPlayer
onTextChanged: {
@@ -240,8 +240,8 @@ ColumnLayout {
NTextInput {
id: blacklistInput
label: "Blacklist player"
description: "Substring, e.g. plex, shim, mpv."
label: "Excluded player"
description: "Add keywords for players you want the system to ignore. Each keyword should be on a new line."
placeholderText: "type substring and press +"
}
@@ -336,14 +336,14 @@ ColumnLayout {
Layout.fillWidth: true
NHeader {
label: "Audio Visualizer"
label: "Audio visualizer"
description: "Customize visual effects that respond to audio playback."
}
// AudioService Visualizer section
NComboBox {
id: audioVisualizerCombo
label: "Visualization Type"
label: "Visualization type"
description: "Choose a visualization type for media playback"
model: ListModel {
ListElement {
@@ -368,8 +368,8 @@ ColumnLayout {
}
NComboBox {
label: "Frame Rate"
description: "Target frame rate for audio visualizer."
label: "Frame rate"
description: "Higher rates are smoother but use more resources."
model: ListModel {
ListElement {
key: "30"

View File

@@ -42,12 +42,12 @@ ColumnLayout {
NHeader {
label: "Appearance"
description: "Configure bar appearance and positioning."
description: "Customize the bar's appearance and position."
}
NComboBox {
Layout.fillWidth: true
label: "Bar Position"
label: "Bar position"
description: "Choose where to place the bar on the screen."
model: ListModel {
ListElement {
@@ -73,8 +73,8 @@ ColumnLayout {
NComboBox {
Layout.fillWidth: true
label: "Bar Density"
description: "Choose the density of the bar."
label: "Bar density"
description: "Adjust the bar's padding for a compact or spacious look."
model: ListModel {
ListElement {
key: "compact"
@@ -98,7 +98,7 @@ ColumnLayout {
Layout.fillWidth: true
NLabel {
label: "Background Opacity"
label: "Background opacity"
description: "Adjust the background opacity of the bar."
}
@@ -115,16 +115,16 @@ ColumnLayout {
NToggle {
Layout.fillWidth: true
label: "Show Capsule"
description: "Adds a capsule behind each widget to improve readability on transparent bars."
label: "Show capsule"
description: "Show widget backgrounds."
checked: Settings.data.bar.showCapsule
onToggled: checked => Settings.data.bar.showCapsule = checked
}
NToggle {
Layout.fillWidth: true
label: "Floating Bar"
description: "Make the bar float with rounded corners and margins. Screen corners will move to screen edges."
label: "Floating bar"
description: "Displays the bar as a floating 'pill'. Note: This will move the screen corners to the edges."
checked: Settings.data.bar.floating
onToggled: checked => Settings.data.bar.floating = checked
}
@@ -192,47 +192,13 @@ ColumnLayout {
Layout.bottomMargin: Style.marginXL * scaling
}
// Monitor Configuration
ColumnLayout {
spacing: Style.marginM * scaling
Layout.fillWidth: true
NHeader {
label: "Monitors Configuration"
description: "Show bar on specific monitors. Defaults to all if none are chosen."
}
Repeater {
model: Quickshell.screens || []
delegate: NCheckbox {
Layout.fillWidth: true
label: modelData.name || "Unknown"
description: `${modelData.model} - ${modelData.width}x${modelData.height} [x:${modelData.x} y:${modelData.y}]`
checked: (Settings.data.bar.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.bar.monitors = addMonitor(Settings.data.bar.monitors, modelData.name)
} else {
Settings.data.bar.monitors = removeMonitor(Settings.data.bar.monitors, modelData.name)
}
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Widgets Management Section
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NHeader {
label: "Widgets Positioning"
label: "Widgets positioning"
description: "Drag and drop widgets to reorder them within each section, or use the add/remove buttons to manage widgets."
}
@@ -293,6 +259,40 @@ ColumnLayout {
Layout.bottomMargin: Style.marginXL * scaling
}
// Monitor Configuration
ColumnLayout {
spacing: Style.marginM * scaling
Layout.fillWidth: true
NHeader {
label: "Monitor display"
description: "Show bar on specific monitors. Defaults to all if none are chosen."
}
Repeater {
model: Quickshell.screens || []
delegate: NCheckbox {
Layout.fillWidth: true
label: modelData.name || "Unknown"
description: `${modelData.model} (${modelData.width}x${modelData.height})`
checked: (Settings.data.bar.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.bar.monitors = addMonitor(Settings.data.bar.monitors, modelData.name)
} else {
Settings.data.bar.monitors = removeMonitor(Settings.data.bar.monitors, modelData.name)
}
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// ---------------------------------
// Signal functions
// ---------------------------------

View File

@@ -106,14 +106,14 @@ ColumnLayout {
// Main Toggles - Dark Mode / Matugen
NHeader {
label: "Behavior"
label: "Color Source"
description: "Main settings for Noctalia's colors."
}
// Dark Mode Toggle (affects both Matugen and predefined schemes that provide variants)
NToggle {
label: "Dark Mode"
description: Settings.data.colorSchemes.useWallpaperColors ? "Generate dark theme colors when using Matugen." : "Use a dark variant if available."
label: "Dark mode"
description: "Switches to a darker theme for easier viewing at night."
checked: Settings.data.colorSchemes.darkMode
enabled: true
onToggled: checked => Settings.data.colorSchemes.darkMode = checked
@@ -152,7 +152,7 @@ ColumnLayout {
Layout.fillWidth: true
NHeader {
label: "Predefined Color Schemes"
label: "Predefined color schemes"
description: "To use these color schemes, you must turn off Matugen. With Matugen enabled, colors are automatically generated from your wallpaper."
}

View File

@@ -52,8 +52,8 @@ ColumnLayout {
spacing: Style.marginL * scaling
NHeader {
label: "Monitor-specific configuration"
description: "Configure scaling and brightness settings individually for each connected display."
label: "Per-monitor settings"
description: "Adjust scaling and brightness for each display."
}
ColumnLayout {
@@ -89,7 +89,7 @@ ColumnLayout {
NLabel {
label: modelData.name || "Unknown"
description: `${modelData.model} - ${modelData.width}x${modelData.height} [x:${modelData.x} y:${modelData.y}]`
description: `${modelData.model} (${modelData.width}x${modelData.height})`
}
// Scale
@@ -204,7 +204,7 @@ ColumnLayout {
NSpinBox {
Layout.fillWidth: true
label: "Brightness Step Size"
label: "Brightness step size"
description: "Adjust the step size for brightness changes (scroll wheel and keyboard shortcuts)."
minimum: 1
maximum: 50
@@ -228,13 +228,13 @@ ColumnLayout {
Layout.fillWidth: true
NHeader {
label: "Night Light"
label: "Night light"
description: "Reduce blue light emission to help you sleep better and reduce eye strain."
}
}
NToggle {
label: "Enable Night Light"
label: "Enable night light"
description: "Apply a warm color filter to reduce blue light emission."
checked: Settings.data.nightLight.enabled
onToggled: checked => {
@@ -257,7 +257,7 @@ ColumnLayout {
NLabel {
label: "Color temperature"
description: "Choose two temperatures in Kelvin."
description: "Set the color warmth for nighttime and daytime."
}
RowLayout {
@@ -373,7 +373,7 @@ ColumnLayout {
// Force activation toggle
NToggle {
label: "Force activation"
description: "Immediately apply night temperature without scheduling or fade."
description: "Ignores the schedule and applies the night filter immediately."
checked: Settings.data.nightLight.forced
onToggled: checked => {
Settings.data.nightLight.forced = checked

View File

@@ -25,7 +25,7 @@ ColumnLayout {
NHeader {
label: "Appearance"
description: "Configure dock behavior and appearance."
description: "Customize the dock's behavior and appearance."
}
NToggle {
@@ -36,8 +36,8 @@ ColumnLayout {
}
NToggle {
label: "Exclusive Zone"
description: "Ensure windows don't open underneath."
label: "Exclusive zone"
description: "Prevent window overlap."
checked: Settings.data.dock.exclusive
onToggled: checked => Settings.data.dock.exclusive = checked
}
@@ -46,8 +46,8 @@ ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NLabel {
label: "Background Opacity"
description: "Adjust the background opacity."
label: "Background opacity"
description: "Adjust the dock's background opacity."
}
NValueSlider {
Layout.fillWidth: true
@@ -65,7 +65,7 @@ ColumnLayout {
Layout.fillWidth: true
NLabel {
label: "Dock Floating Distance"
label: "Dock floating distance"
description: "Adjust the floating distance from the screen edge."
}
@@ -92,8 +92,8 @@ ColumnLayout {
Layout.fillWidth: true
NHeader {
label: "Monitors Configuration"
description: "Show dock on specific monitors."
label: "Monitor display"
description: "Choose which monitor to display the dock on."
}
Repeater {
@@ -101,7 +101,7 @@ ColumnLayout {
delegate: NCheckbox {
Layout.fillWidth: true
label: modelData.name || "Unknown"
description: `${modelData.model} - ${modelData.width}x${modelData.height} [x:${modelData.x} y:${modelData.y}]`
description: `${modelData.model} (${modelData.width}x${modelData.height})`
checked: (Settings.data.dock.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {

View File

@@ -11,7 +11,7 @@ ColumnLayout {
NHeader {
label: "Profile"
description: "Configure your user profile and avatar settings."
description: "Edit your user details and avatar."
}
// Profile section
@@ -54,12 +54,12 @@ ColumnLayout {
Layout.fillWidth: true
NHeader {
label: "User Interface"
description: "Main settings for the user interface."
label: "User interface"
description: "Customize the look, feel, and behavior of the interface."
}
NToggle {
label: "Dim Desktop"
label: "Dim desktop"
description: "Dim the desktop when panels or menus are open."
checked: Settings.data.general.dimDesktop
onToggled: checked => Settings.data.general.dimDesktop = checked
@@ -71,7 +71,7 @@ ColumnLayout {
NLabel {
label: "Border radius"
description: "Adjust the rounded border of all UI elements."
description: "Controls the corner roundness of windows, buttons, and other elements."
}
NValueSlider {
@@ -91,7 +91,7 @@ ColumnLayout {
Layout.fillWidth: true
NLabel {
label: "Animation Speed"
label: "Animation speed"
description: "Adjust global animation speed."
}
@@ -117,21 +117,22 @@ ColumnLayout {
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NHeader {
label: "Screen Corners"
label: "Screen corners"
description: "Customize screen corner rounding and visual effects."
}
NToggle {
label: "Show Screen Corners"
label: "Show screen corners"
description: "Display rounded corners on the edge of the screen."
checked: Settings.data.general.showScreenCorners
onToggled: checked => Settings.data.general.showScreenCorners = checked
}
NToggle {
label: "Solid Black Corners"
description: "Force screen corners to always render as solid black."
label: "Solid black corners"
description: "Use solid black instead of the bar background color."
checked: Settings.data.general.forceBlackScreenCorners
onToggled: checked => Settings.data.general.forceBlackScreenCorners = checked
}
@@ -141,7 +142,7 @@ ColumnLayout {
Layout.fillWidth: true
NLabel {
label: "Screen Corners Radius"
label: "Screen corners radius"
description: "Adjust the rounded corners of the screen."
}
@@ -169,7 +170,7 @@ ColumnLayout {
NHeader {
label: "Fonts"
description: "Configure interface typography."
description: "Choose the fonts used throughout the interface."
}
// Font configuration section
@@ -178,7 +179,7 @@ ColumnLayout {
Layout.fillWidth: true
NSearchableComboBox {
label: "Default Font"
label: "Default font"
description: "Main font used throughout the interface."
model: FontService.availableFonts
currentKey: Settings.data.ui.fontDefault
@@ -192,8 +193,8 @@ ColumnLayout {
}
NSearchableComboBox {
label: "Fixed Width Font"
description: "Monospace font used for terminal and code display."
label: "Monospaced font"
description: "Monospaced font used for numbers and stats display."
model: FontService.monospaceFonts
currentKey: Settings.data.ui.fontFixed
placeholder: "Select monospace font..."
@@ -206,8 +207,8 @@ ColumnLayout {
}
NSearchableComboBox {
label: "Billboard Font"
description: "Large font used for clocks and prominent displays."
label: "Accent font"
description: "Large font used for prominent displays."
model: FontService.displayFonts
currentKey: Settings.data.ui.fontBillboard
placeholder: "Select display font..."

View File

@@ -11,13 +11,13 @@ ColumnLayout {
width: root.width
NHeader {
label: "System Hooks"
label: "System hooks"
description: "Configure commands to be executed when system events occur."
}
// Enable/Disable Toggle
NToggle {
label: "Enable Hooks"
label: "Enable hooks"
description: "Enable or disable all hook commands."
checked: Settings.data.hooks.enabled
onToggled: checked => Settings.data.hooks.enabled = checked

View File

@@ -11,7 +11,7 @@ ColumnLayout {
NHeader {
label: "Appearance"
description: "Configure the launcher behavior and appearance."
description: "Customize the launcher's behavior and appearance."
}
NComboBox {
@@ -26,27 +26,27 @@ ColumnLayout {
}
ListElement {
key: "top_left"
name: "Top Left"
name: "Top left"
}
ListElement {
key: "top_right"
name: "Top Right"
name: "Top right"
}
ListElement {
key: "bottom_left"
name: "Bottom Left"
name: "Bottom left"
}
ListElement {
key: "bottom_right"
name: "Bottom Right"
name: "Bottom right"
}
ListElement {
key: "bottom_center"
name: "Bottom Center"
name: "Bottom center"
}
ListElement {
key: "top_center"
name: "Top Center"
name: "Top center"
}
}
currentKey: Settings.data.appLauncher.position
@@ -60,7 +60,7 @@ ColumnLayout {
Layout.fillWidth: true
NText {
text: "Background Opacity"
text: "Background opacity"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
@@ -87,15 +87,22 @@ ColumnLayout {
}
NToggle {
label: "Enable Clipboard History"
description: "Show clipboard history in the launcher."
label: "Enable clipboard history"
description: "Access previously copied items from the launcher."
checked: Settings.data.appLauncher.enableClipboardHistory
onToggled: checked => Settings.data.appLauncher.enableClipboardHistory = checked
}
NToggle {
label: "Use App2Unit for Launching"
description: "Use app2unit -- 'desktop-entry' when launching applications for better systemd integration."
label: "Sort by most used"
description: "When enabled, frequently launched apps appear first in the list."
checked: Settings.data.appLauncher.sortByMostUsed
onToggled: checked => Settings.data.appLauncher.sortByMostUsed = checked
}
NToggle {
label: "Use App2Unit to launch applications"
description: "Uses an alternative launch method to better manage app processes and prevent issues."
checked: Settings.data.appLauncher.useApp2Unit
onToggled: checked => Settings.data.appLauncher.useApp2Unit = checked
}

View File

@@ -10,8 +10,8 @@ ColumnLayout {
spacing: Style.marginL * scaling
NHeader {
label: "Your Location"
description: "Set your location for weather, time zones, and scheduling."
label: "Your location"
description: "Get accurate weather and night light scheduling by setting your location."
}
// Location section
@@ -20,8 +20,8 @@ ColumnLayout {
spacing: Style.marginL * scaling
NTextInput {
label: "Location name"
description: "Choose a known location near you."
label: "Search for a location"
description: "e.g., Toronto, ON"
text: Settings.data.location.name || Settings.defaultLocation
placeholderText: "Enter the location name"
onEditingFinished: {
@@ -65,11 +65,11 @@ ColumnLayout {
NHeader {
label: "Weather"
description: "Configure weather display preferences and temperature units."
description: "Choose your preferred temperature unit."
}
NToggle {
label: "Use Fahrenheit"
label: "Display temperature in Fahrenheit (°F)"
description: "Display temperature in Fahrenheit instead of Celsius."
checked: Settings.data.location.useFahrenheit
onToggled: checked => Settings.data.location.useFahrenheit = checked
@@ -81,4 +81,42 @@ ColumnLayout {
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Weather section
ColumnLayout {
spacing: Style.marginM * scaling
Layout.fillWidth: true
NHeader {
label: "Date & time"
description: "Customize how date and time appear."
}
NToggle {
label: "Use 12-hour time format"
description: "On for AM/PM format (e.g., 8:00 PM), off for 24-hour format (e.g., 20:00)."
checked: Settings.data.location.use12hourFormat
onToggled: checked => Settings.data.location.use12hourFormat = checked
}
NToggle {
label: "Show month before day"
description: "On for 09/17/2025, off for 17/09/2025."
checked: Settings.data.location.monthBeforeDay
onToggled: checked => Settings.data.location.monthBeforeDay = checked
}
NToggle {
label: "Show week numbers"
description: "Displays the week of the year (e.g., Week 38) in the calendar."
checked: Settings.data.location.showWeekNumberInCalendar
onToggled: checked => Settings.data.location.showWeekNumberInCalendar = checked
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
}

View File

@@ -12,20 +12,17 @@ ColumnLayout {
spacing: Style.marginL * scaling
NHeader {
label: "Network Settings"
description: "Configure Wi-Fi and Bluetooth connectivity options."
label: "Manage Wi-Fi and Bluetooth connections."
}
NToggle {
label: "Enable Wi-Fi"
description: "Enable Wi-Fi connectivity."
checked: Settings.data.network.wifiEnabled
onToggled: checked => NetworkService.setWifiEnabled(checked)
}
NToggle {
label: "Enable Bluetooth"
description: "Enable Bluetooth connectivity."
checked: Settings.data.network.bluetoothEnabled
onToggled: checked => BluetoothService.setBluetoothEnabled(checked)
}

View File

@@ -33,7 +33,7 @@ ColumnLayout {
}
NToggle {
label: "Do Not Disturb"
label: "Do not disturb"
description: "Disable all notification popups when enabled."
checked: Settings.data.notifications.doNotDisturb
onToggled: checked => Settings.data.notifications.doNotDisturb = checked
@@ -46,47 +46,13 @@ ColumnLayout {
Layout.bottomMargin: Style.marginXL * scaling
}
// Monitor Configuration
ColumnLayout {
spacing: Style.marginM * scaling
Layout.fillWidth: true
NHeader {
label: "Monitors Configuration"
description: "Show bar on specific monitors. Defaults to all if none are chosen."
}
Repeater {
model: Quickshell.screens || []
delegate: NCheckbox {
Layout.fillWidth: true
label: modelData.name || "Unknown"
description: `${modelData.model} - ${modelData.width}x${modelData.height} [x:${modelData.x} y:${modelData.y}]`
checked: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.notifications.monitors = addMonitor(Settings.data.notifications.monitors, modelData.name)
} else {
Settings.data.notifications.monitors = removeMonitor(Settings.data.notifications.monitors, modelData.name)
}
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Notification Duration Settings
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NHeader {
label: "Notification Duration"
label: "Notification duration"
description: "Configure how long notifications stay visible based on their urgency level."
}
@@ -96,7 +62,7 @@ ColumnLayout {
Layout.fillWidth: true
NLabel {
label: "Low Urgency Duration"
label: "Low urgency"
description: "How long low priority notifications stay visible."
}
@@ -117,7 +83,7 @@ ColumnLayout {
Layout.fillWidth: true
NLabel {
label: "Normal Urgency Duration"
label: "Normal urgency"
description: "How long normal priority notifications stay visible."
}
@@ -138,7 +104,7 @@ ColumnLayout {
Layout.fillWidth: true
NLabel {
label: "Critical Urgency Duration"
label: "Critical urgency"
description: "How long critical priority notifications stay visible."
}
@@ -159,4 +125,38 @@ ColumnLayout {
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Monitor Configuration
ColumnLayout {
spacing: Style.marginM * scaling
Layout.fillWidth: true
NHeader {
label: "Monitors display"
description: "Show notification on specific monitors. Defaults to all if none are chosen."
}
Repeater {
model: Quickshell.screens || []
delegate: NCheckbox {
Layout.fillWidth: true
label: modelData.name || "Unknown"
description: `${modelData.model} (${modelData.width}x${modelData.height})`
checked: (Settings.data.notifications.monitors || []).indexOf(modelData.name) !== -1
onToggled: checked => {
if (checked) {
Settings.data.notifications.monitors = addMonitor(Settings.data.notifications.monitors, modelData.name)
} else {
Settings.data.notifications.monitors = removeMonitor(Settings.data.notifications.monitors, modelData.name)
}
}
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
}

View File

@@ -11,18 +11,18 @@ ColumnLayout {
spacing: Style.marginL * scaling
NHeader {
label: "General Settings"
description: "Configure screen recording output and content."
label: "General settings"
description: "Manage screen recording output and content."
}
// Output Directory
// Output Folder
ColumnLayout {
spacing: Style.marginS * scaling
Layout.fillWidth: true
NTextInput {
label: "Output Directory"
description: "Directory where screen recordings will be saved."
label: "Output folder"
description: "Folder where screen recordings will be saved."
placeholderText: "/home/xxx/Videos"
text: Settings.data.screenRecorder.directory
onEditingFinished: {
@@ -38,7 +38,7 @@ ColumnLayout {
Layout.topMargin: Style.marginM * scaling
// Show Cursor
NToggle {
label: "Show Cursor"
label: "Show cursor"
description: "Record mouse cursor in the video."
checked: Settings.data.screenRecorder.showCursor
onToggled: checked => Settings.data.screenRecorder.showCursor = checked
@@ -58,12 +58,12 @@ ColumnLayout {
Layout.fillWidth: true
NHeader {
label: "Video Settings"
label: "Video settings"
}
// Source
NComboBox {
label: "Video Source"
label: "Video source"
description: "Portal is recommended, if you get artifacts try Screen."
model: ListModel {
ListElement {
@@ -81,7 +81,7 @@ ColumnLayout {
// Frame Rate
NComboBox {
label: "Frame Rate"
label: "Frame rate"
description: "Target frame rate for screen recordings."
model: ListModel {
ListElement {
@@ -119,7 +119,7 @@ ColumnLayout {
// Video Quality
NComboBox {
label: "Video Quality"
label: "Video quality"
description: "Higher quality results in larger file sizes."
model: ListModel {
ListElement {
@@ -132,7 +132,7 @@ ColumnLayout {
}
ListElement {
key: "very_high"
name: "Very High"
name: "Very high"
}
ListElement {
key: "ultra"
@@ -145,7 +145,7 @@ ColumnLayout {
// Video Codec
NComboBox {
label: "Video Codec"
label: "Video codec"
description: "h264 is the most common codec."
model: ListModel {
ListElement {
@@ -175,7 +175,7 @@ ColumnLayout {
// Color Range
NComboBox {
label: "Color Range"
label: "Color range"
description: "Limited is recommended for better compatibility."
model: ListModel {
ListElement {
@@ -204,25 +204,25 @@ ColumnLayout {
Layout.fillWidth: true
NHeader {
label: "Audio Settings"
label: "Audio settings"
}
// Audio Source
NComboBox {
label: "Audio Source"
label: "Audio source"
description: "Audio source to capture during recording."
model: ListModel {
ListElement {
key: "default_output"
name: "System Output"
name: "System output"
}
ListElement {
key: "default_input"
name: "Microphone Input"
name: "Microphone input"
}
ListElement {
key: "both"
name: "System Output + Microphone Input"
name: "System output + Microphone input"
}
}
currentKey: Settings.data.screenRecorder.audioSource
@@ -231,7 +231,7 @@ ColumnLayout {
// Audio Codec
NComboBox {
label: "Audio Codec"
label: "Audio codec"
description: "Opus is recommended for best performance and smallest audio size."
model: ListModel {
ListElement {

View File

@@ -1,268 +0,0 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Qt.labs.folderlistmodel
import qs.Commons
import qs.Services
import qs.Widgets
ColumnLayout {
id: root
width: parent.width
spacing: Style.marginL * scaling
property list<string> wallpapersList: []
property string currentWallpaper: ""
Component.onCompleted: {
wallpapersList = screen ? WallpaperService.getWallpapersList(screen.name) : []
currentWallpaper = screen ? WallpaperService.getWallpaper(screen.name) : ""
}
Connections {
target: WallpaperService
function onWallpaperChanged(screenName, path) {
if (screenName === screen.name) {
currentWallpaper = WallpaperService.getWallpaper(screen.name)
}
}
function onWallpaperDirectoryChanged(screenName, directory) {
if (screenName === screen.name) {
wallpapersList = WallpaperService.getWallpapersList(screen.name)
currentWallpaper = WallpaperService.getWallpaper(screen.name)
}
}
function onWallpaperListChanged(screenName, count) {
if (screenName === screen.name) {
wallpapersList = WallpaperService.getWallpapersList(screen.name)
currentWallpaper = WallpaperService.getWallpaper(screen.name)
}
}
}
// Current wallpaper display
NHeader {
label: "Current Wallpaper"
description: "Preview and manage your desktop background."
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 140 * scaling
radius: Style.radiusM * scaling
color: Color.transparent
NImageRounded {
anchors.fill: parent
anchors.margins: Style.marginXS * scaling
imagePath: currentWallpaper
fallbackIcon: "image"
imageRadius: Style.radiusM * scaling
borderColor: Color.mSecondary
borderWidth: Style.borderL * 2 * scaling
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Wallpaper selector
RowLayout {
Layout.fillWidth: true
ColumnLayout {
Layout.fillWidth: true
// Wallpaper grid
NHeader {
label: "Wallpaper Selector"
description: "Click on a wallpaper to set it as your current wallpaper."
}
}
NIconButton {
icon: "refresh"
tooltipText: "Refresh wallpaper list"
onClicked: {
WallpaperService.refreshWallpapersList()
}
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
}
}
NToggle {
label: "Apply to all monitors"
description: "Apply selected wallpaper to all monitors at once."
checked: Settings.data.wallpaper.setWallpaperOnAllMonitors
onToggled: checked => Settings.data.wallpaper.setWallpaperOnAllMonitors = checked
visible: (wallpapersList.length > 0)
}
// Wallpaper grid container
Item {
visible: !WallpaperService.scanning
Layout.fillWidth: true
Layout.preferredHeight: {
return Math.ceil(wallpapersList.length / wallpaperGridView.columns) * wallpaperGridView.cellHeight
}
GridView {
id: wallpaperGridView
anchors.fill: parent
model: wallpapersList
interactive: false
clip: true
property int columns: 4
property int itemSize: Math.floor((width - leftMargin - rightMargin - (4 * Style.marginS * scaling)) / columns)
cellWidth: Math.floor((width - leftMargin - rightMargin) / columns)
cellHeight: Math.floor(itemSize * 0.67) + Style.marginS * scaling
leftMargin: Style.marginS * scaling
rightMargin: Style.marginS * scaling
topMargin: Style.marginS * scaling
bottomMargin: Style.marginS * scaling
delegate: Rectangle {
id: wallpaperItem
property string wallpaperPath: modelData
property bool isSelected: screen ? (wallpaperPath === currentWallpaper) : false
width: wallpaperGridView.itemSize
height: Math.round(wallpaperGridView.itemSize * 0.67)
color: Color.transparent
// NImageCached relies on the image being visible to work properly.
// MultiEffect relies on the image being invisible to apply effects.
// That's why we don't have rounded corners here, as we don't want to bring back qt5compat.
NImageCached {
id: img
imagePath: wallpaperPath
anchors.fill: parent
}
// Borders on top
Rectangle {
anchors.fill: parent
color: Color.transparent
border.color: isSelected ? Color.mSecondary : Color.mSurface
border.width: Math.max(1, Style.borderL * 1.5 * scaling)
}
// Selection tick-mark
Rectangle {
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Style.marginS * scaling
width: 28 * scaling
height: 28 * scaling
radius: width / 2
color: Color.mSecondary
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
visible: isSelected
NIcon {
icon: "check"
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSecondary
anchors.centerIn: parent
}
}
// Hover effect
Rectangle {
anchors.fill: parent
color: Color.mSurface
opacity: (mouseArea.containsMouse || isSelected) ? 0 : 0.3
radius: parent.radius
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton
hoverEnabled: true
onPressed: {
if (Settings.data.wallpaper.setWallpaperOnAllMonitors) {
WallpaperService.changeWallpaper(wallpaperPath, undefined)
} else if (screen) {
WallpaperService.changeWallpaper(wallpaperPath, screen.name)
}
}
}
}
}
}
// Empty state
Rectangle {
color: Color.mSurface
radius: Style.radiusM * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
visible: wallpapersList.length === 0 || WallpaperService.scanning
Layout.fillWidth: true
Layout.preferredHeight: 130 * scaling
ColumnLayout {
anchors.fill: parent
visible: WallpaperService.scanning
NBusyIndicator {
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
}
ColumnLayout {
anchors.fill: parent
visible: wallpapersList.length === 0 && !WallpaperService.scanning
Item {
Layout.fillHeight: true
}
NIcon {
icon: "folder-open"
font.pointSize: Style.fontSizeXXL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "No wallpaper found."
color: Color.mOnSurface
font.weight: Style.fontWeightBold
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Make sure your wallpaper directory is configured and contains image files."
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.alignment: Qt.AlignHCenter
}
Item {
Layout.fillHeight: true
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
}

View File

@@ -12,12 +12,12 @@ ColumnLayout {
spacing: Style.marginL * scaling
NHeader {
label: "Wallpaper Settings"
label: "Wallpaper settings"
description: "Control how wallpapers are managed and displayed."
}
NToggle {
label: "Enable Wallpaper Management"
label: "Enable wallpaper management"
description: "Manage wallpapers with Noctalia. (Uncheck if you prefer using another application)."
checked: Settings.data.wallpaper.enabled
onToggled: checked => Settings.data.wallpaper.enabled = checked
@@ -30,8 +30,8 @@ ColumnLayout {
Layout.fillWidth: true
NTextInput {
label: "Wallpaper Directory"
description: "Path to your common wallpaper directory."
label: "Wallpaper folder"
description: "Path to your main wallpaper folder."
text: Settings.data.wallpaper.directory
onEditingFinished: {
Settings.data.wallpaper.directory = text
@@ -42,7 +42,7 @@ ColumnLayout {
// Monitor-specific directories
NToggle {
label: "Monitor-specific directories"
description: "Enable multi-monitor wallpaper directory management."
description: "Set a different wallpaper folder for each monitor."
checked: Settings.data.wallpaper.enableMultiMonitorDirectories
onToggled: checked => Settings.data.wallpaper.enableMultiMonitorDirectories = checked
}
@@ -97,12 +97,12 @@ ColumnLayout {
Layout.fillWidth: true
NHeader {
label: "Look & Feel"
label: "Look & feel"
}
// Fill Mode
NComboBox {
label: "Fill Mode"
label: "Fill mode"
description: "Select how the image should scale to match your monitor's resolution."
model: WallpaperService.fillModeModel
currentKey: Settings.data.wallpaper.fillMode
@@ -111,7 +111,7 @@ ColumnLayout {
RowLayout {
NLabel {
label: "Fill Color"
label: "Fill color"
description: "Choose a fill color that may appear behind the wallpaper."
Layout.alignment: Qt.AlignTop
}
@@ -124,7 +124,7 @@ ColumnLayout {
// Transition Type
NComboBox {
label: "Transition Type"
label: "Transition type"
description: "Animation type when switching between wallpapers."
model: WallpaperService.transitionsModel
currentKey: Settings.data.wallpaper.transitionType
@@ -134,7 +134,7 @@ ColumnLayout {
// Transition Duration
ColumnLayout {
NLabel {
label: "Transition Duration"
label: "Transition duration"
description: "Duration of transition animations in seconds."
}
@@ -152,8 +152,8 @@ ColumnLayout {
// Edge Smoothness
ColumnLayout {
NLabel {
label: "Transition Edge Smoothness"
description: "Duration of transition animations in seconds."
label: "Soften transition edge"
description: "Applies a soft, feathered effect to the edge of transitions."
}
NValueSlider {
@@ -185,7 +185,7 @@ ColumnLayout {
// Random Wallpaper
NToggle {
label: "Random Wallpaper"
label: "Random wallpaper"
description: "Schedule random wallpaper changes at regular intervals."
checked: Settings.data.wallpaper.randomEnabled
onToggled: checked => Settings.data.wallpaper.randomEnabled = checked
@@ -196,7 +196,7 @@ ColumnLayout {
visible: Settings.data.wallpaper.randomEnabled
RowLayout {
NLabel {
label: "Wallpaper Interval"
label: "Wallpaper interval"
description: "How often to change wallpapers automatically."
Layout.fillWidth: true
}
@@ -262,7 +262,7 @@ ColumnLayout {
Layout.topMargin: Style.marginS * scaling
NTextInput {
label: "Custom Interval"
label: "Custom interval"
description: "Enter time as HH:MM (e.g., 01:30)."
text: {
const s = Settings.data.wallpaper.randomIntervalSec

View File

@@ -25,45 +25,33 @@ NBox {
}
// Performance
NIconButton {
icon: "performance"
tooltipText: "Set performance power profile."
icon: PowerProfileService.getIcon(PowerProfile.Performance)
tooltipText: `Set "${PowerProfileService.getName(PowerProfile.Performance)}" power profile.`
enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBg: (enabled && PowerProfileService.profile === PowerProfile.Performance) ? Color.mPrimary : Color.mSurfaceVariant
colorFg: (enabled && PowerProfileService.profile === PowerProfile.Performance) ? Color.mOnPrimary : Color.mPrimary
onClicked: {
if (enabled) {
PowerProfileService.setProfile(PowerProfile.Performance)
}
}
onClicked: PowerProfileService.setProfile(PowerProfile.Performance)
}
// Balanced
NIconButton {
icon: "balanced"
tooltipText: "Set balanced power profile."
icon: PowerProfileService.getIcon(PowerProfile.Balanced)
tooltipText: `Set "${PowerProfileService.getName(PowerProfile.Balanced)}" power profile.`
enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBg: (enabled && PowerProfileService.profile === PowerProfile.Balanced) ? Color.mPrimary : Color.mSurfaceVariant
colorFg: (enabled && PowerProfileService.profile === PowerProfile.Balanced) ? Color.mOnPrimary : Color.mPrimary
onClicked: {
if (enabled) {
PowerProfileService.setProfile(PowerProfile.Balanced)
}
}
onClicked: PowerProfileService.setProfile(PowerProfile.Balanced)
}
// Eco
NIconButton {
icon: "powersaver"
tooltipText: "Set eco power profile."
icon: PowerProfileService.getIcon(PowerProfile.PowerSaver)
tooltipText: `Set "${PowerProfileService.getName(PowerProfile.PowerSaver)}" power profile.`
enabled: hasPP
opacity: enabled ? Style.opacityFull : Style.opacityMedium
colorBg: (enabled && PowerProfileService.profile === PowerProfile.PowerSaver) ? Color.mPrimary : Color.mSurfaceVariant
colorFg: (enabled && PowerProfileService.profile === PowerProfile.PowerSaver) ? Color.mOnPrimary : Color.mPrimary
onClicked: {
if (enabled) {
PowerProfileService.setProfile(PowerProfile.PowerSaver)
}
}
onClicked: PowerProfileService.setProfile(PowerProfile.PowerSaver)
}
Item {
Layout.fillWidth: true

View File

@@ -55,14 +55,8 @@ NBox {
visible: Settings.data.wallpaper.enabled
icon: "wallpaper-selector"
tooltipText: "Left click: Open wallpaper selector.\nRight click: Set random wallpaper."
onClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel")
settingsPanel.requestedTab = SettingsPanel.Tab.WallpaperSelector
settingsPanel.open()
}
onRightClicked: {
WallpaperService.setRandomWallpaper()
}
onClicked: PanelService.getPanel("wallpaperSelector")?.toggle(this)
onRightClicked: WallpaperService.setRandomWallpaper()
}
Item {

View File

@@ -15,14 +15,12 @@ Rectangle {
signal hidden
width: Math.min(500 * scaling, parent.width * 0.8)
height: Math.max(60 * scaling, contentLayout.implicitHeight + Style.marginL * 2 * scaling)
width: parent.width
height: Math.round(contentLayout.implicitHeight + Style.marginL * 2 * scaling)
radius: Style.radiusL * scaling
visible: false
opacity: 0
scale: initialScale
// Clean surface background like NToast
color: Color.mSurface
// Colored border based on type
@@ -67,6 +65,12 @@ Rectangle {
}
}
// Cleanup on destruction
Component.onDestruction: {
hideTimer.stop()
hideAnimation.stop()
}
RowLayout {
id: contentLayout
anchors.fill: parent
@@ -125,24 +129,9 @@ Rectangle {
visible: text.length > 0
}
}
// Close button
NIconButton {
id: closeButton
icon: "close"
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.mOutline
baseSize: Style.baseWidgetSize * 0.8
Layout.alignment: Qt.AlignTop
onClicked: root.hide()
}
}
// Click anywhere dismiss the toast
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
@@ -151,6 +140,10 @@ Rectangle {
}
function show(msg, desc, msgType, msgDuration) {
// Stop all timers first
hideTimer.stop()
hideAnimation.stop()
message = msg
description = desc || ""
type = msgType || "notice"
@@ -167,10 +160,12 @@ Rectangle {
hideTimer.stop()
opacity = 0
scale = initialScale
hideAnimation.start()
hideAnimation.restart()
}
function hideImmediately() {
hideTimer.stop()
hideAnimation.stop()
opacity = 0
scale = initialScale
root.visible = false

View File

@@ -6,12 +6,12 @@ import qs.Commons
import qs.Services
import qs.Widgets
Loader {
Item {
id: root
required property ShellScreen screen
required property real scaling
required property bool active
property bool active: false
// Local queue for this screen only
property var messageQueue: []
@@ -44,16 +44,26 @@ Loader {
}
}
// Clear queue on component destruction to prevent orphaned toasts
Component.onDestruction: {
messageQueue = []
isShowingToast = false
hideTimer.stop()
quickSwitchTimer.stop()
}
function enqueueToast(toastData) {
Logger.log("ToastScreen", "Queuing:", toastData.message, toastData.description, toastData.type)
if (replaceOnNew && isShowingToast) {
// Cancel current toast and clear queue for latest toast
messageQueue = [] // Clear existing queue
messageQueue.push(toastData)
// Hide current toast immediately
if (item) {
if (windowLoader.item) {
hideTimer.stop()
item.hideToast() // Need to add this method to PanelWindow
windowLoader.item.hideToast()
}
// Process new toast after a brief delay
@@ -73,20 +83,30 @@ Loader {
}
function processQueue() {
if (!active || !item || messageQueue.length === 0 || isShowingToast) {
if (!active || messageQueue.length === 0 || isShowingToast) {
return
}
var data = messageQueue.shift()
isShowingToast = true
// Show the toast
item.showToast(data.message, data.description, data.type, data.duration)
// Activate the loader and show toast
windowLoader.active = true
// Need a small delay to ensure the window is created
Qt.callLater(function () {
if (windowLoader.item) {
windowLoader.item.showToast(data.message, data.description, data.type, data.duration)
}
})
}
function onToastHidden() {
isShowingToast = false
// Small delay before next toast
// Deactivate the loader to completely remove the window
windowLoader.active = false
// Small delay before processing next toast
hideTimer.restart()
}
@@ -96,48 +116,55 @@ Loader {
onTriggered: root.processQueue()
}
sourceComponent: PanelWindow {
id: panel
// The loader that creates/destroys the PanelWindow as needed
Loader {
id: windowLoader
active: false // Only active when showing a toast
screen: root.screen
sourceComponent: PanelWindow {
id: panel
anchors {
top: true
}
property alias toastItem: toastItem
implicitWidth: 500 * root.scaling
implicitHeight: Math.round(toastItem.visible ? toastItem.height + Style.marginM * root.scaling : 1)
screen: root.screen
// Set margins based on bar position
margins.top: {
switch (Settings.data.bar.position) {
case "top":
return (Style.barHeight + Style.marginS) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0)
default:
return Style.marginL * scaling
anchors {
top: true
}
}
color: Color.transparent
implicitWidth: 420 * root.scaling
implicitHeight: toastItem.height
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
exclusionMode: PanelWindow.ExclusionMode.Ignore
// Set margins based on bar position
margins.top: {
switch (Settings.data.bar.position) {
case "top":
return (Style.barHeight + Style.marginS) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0)
default:
return Style.marginL * scaling
}
}
function showToast(message, description, type, duration) {
toastItem.show(message, description, type, duration)
}
color: Color.transparent
// Add method to immediately hide toast
function hideToast() {
toastItem.hideImmediately()
}
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
exclusionMode: PanelWindow.ExclusionMode.Ignore
SimpleToast {
id: toastItem
function showToast(message, description, type, duration) {
toastItem.show(message, description, type, duration)
}
anchors.horizontalCenter: parent.horizontalCenter
onHidden: root.onToastHidden()
function hideToast() {
toastItem.hideImmediately()
}
SimpleToast {
id: toastItem
anchors.horizontalCenter: parent.horizontalCenter
onHidden: root.onToastHidden()
}
}
}
}

View File

@@ -0,0 +1,353 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
import "../../Helpers/FuzzySort.js" as FuzzySort
NPanel {
id: root
preferredWidth: 640
preferredHeight: 480
preferredWidthRatio: 0.4
preferredHeightRatio: 0.52
panelAnchorHorizontalCenter: true
panelAnchorVerticalCenter: true
panelKeyboardFocus: true
draggable: true
panelContent: Rectangle {
// Local reactive state
property list<string> wallpapersList: []
property string currentWallpaper: ""
property string filterText: ""
property list<string> filteredWallpapers: []
Component.onCompleted: {
refreshWallpaperScreenData()
}
Connections {
target: WallpaperService
function onWallpaperChanged(screenName, path) {
if (screen !== null && screenName === screen.name) {
currentWallpaper = WallpaperService.getWallpaper(screen.name)
}
}
function onWallpaperDirectoryChanged(screenName, directory) {
if (screen !== null && screenName === screen.name) {
refreshWallpaperScreenData()
}
}
function onWallpaperListChanged(screenName, count) {
if (screen !== null && screenName === screen.name) {
refreshWallpaperScreenData()
}
}
}
function refreshWallpaperScreenData() {
if (screen === null) {
return
}
wallpapersList = WallpaperService.getWallpapersList(screen.name)
currentWallpaper = WallpaperService.getWallpaper(screen.name)
updateFiltered()
}
function updateFiltered() {
if (!filterText || filterText.trim().length === 0) {
filteredWallpapers = wallpapersList
return
}
// Build objects with basename for ranking
const items = wallpapersList.map(function (p) {
return {
"path": p,
"name": p.split('/').pop()
}
})
const results = FuzzySort.go(filterText.trim(), items, {
"key": 'name',
"limit": 200
})
// Map back to path list
filteredWallpapers = results.map(function (r) {
return r.obj.path
})
}
color: Color.transparent
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginM * scaling
// Header
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NIcon {
icon: "settings-wallpaper-selector"
font.pointSize: Style.fontSizeXXL * scaling
color: Color.mPrimary
}
NText {
text: "Wallpaper Selector"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
NIconButton {
icon: "refresh"
tooltipText: "Refresh wallpaper list."
baseSize: Style.baseWidgetSize * 0.8
onClicked: WallpaperService.refreshWallpapersList()
}
NIconButton {
icon: "close"
tooltipText: "Close."
baseSize: Style.baseWidgetSize * 0.8
onClicked: root.close()
}
}
NDivider {
Layout.fillWidth: true
}
NToggle {
label: "Apply to all monitors"
description: "Apply selected wallpaper to all monitors at once."
checked: Settings.data.wallpaper.setWallpaperOnAllMonitors
onToggled: checked => Settings.data.wallpaper.setWallpaperOnAllMonitors = checked
visible: (wallpapersList.length > 0)
Layout.fillWidth: true
}
// Filter input
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NText {
text: "Search:"
color: Color.mOnSurface
font.pointSize: Style.fontSizeM * scaling
Layout.preferredWidth: implicitWidth
}
NTextInput {
id: searchInput
placeholderText: "Type to filter wallpapers..."
text: filterText
onTextChanged: {
filterText = text
updateFiltered()
}
Layout.fillWidth: true
Component.onCompleted: {
if (searchInput.inputItem && searchInput.inputItem.visible) {
searchInput.inputItem.forceActiveFocus()
}
}
}
}
// Scroll container for wallpaper grid only
Flickable {
Layout.fillWidth: true
Layout.fillHeight: true
pressDelay: 200
NScrollView {
id: scrollView
anchors.fill: parent
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
padding: Style.marginL * 0 * scaling
clip: true
ColumnLayout {
width: scrollView.availableWidth
spacing: Style.marginM * scaling
// Grid container
Item {
visible: !WallpaperService.scanning
Layout.fillWidth: true
Layout.preferredHeight: Math.ceil(filteredWallpapers.length / wallpaperGridView.columns) * wallpaperGridView.cellHeight
GridView {
id: wallpaperGridView
anchors.fill: parent
model: filteredWallpapers
interactive: false
property int columns: 4
property int itemSize: Math.floor((width - leftMargin - rightMargin - (columns * Style.marginS * scaling)) / columns)
cellWidth: Math.floor((width - leftMargin - rightMargin) / columns)
cellHeight: Math.floor(itemSize * 0.7) + Style.marginXS * scaling + Style.fontSizeXS * scaling + Style.marginM * scaling
leftMargin: Style.marginS * scaling
rightMargin: Style.marginS * scaling
topMargin: Style.marginS * scaling
bottomMargin: Style.marginS * scaling
delegate: ColumnLayout {
id: wallpaperItem
property string wallpaperPath: modelData
property bool isSelected: (wallpaperPath === currentWallpaper)
property string filename: wallpaperPath.split('/').pop()
width: wallpaperGridView.itemSize
spacing: Style.marginXS * scaling
Rectangle {
id: imageContainer
Layout.fillWidth: true
Layout.preferredHeight: Math.round(wallpaperGridView.itemSize * 0.67)
color: Color.transparent
NImageCached {
id: img
imagePath: wallpaperPath
anchors.fill: parent
}
Rectangle {
anchors.fill: parent
color: Color.transparent
border.color: isSelected ? Color.mSecondary : Color.mSurface
border.width: Math.max(1, Style.borderL * 1.5 * scaling)
}
Rectangle {
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Style.marginS * scaling
width: 28 * scaling
height: 28 * scaling
radius: width / 2
color: Color.mSecondary
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
visible: isSelected
NIcon {
icon: "check"
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSecondary
anchors.centerIn: parent
}
}
Rectangle {
anchors.fill: parent
color: Color.mSurface
opacity: (mouseArea.containsMouse || isSelected) ? 0 : 0.3
radius: parent.radius
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton
hoverEnabled: true
onPressed: {
if (Settings.data.wallpaper.setWallpaperOnAllMonitors) {
WallpaperService.changeWallpaper(wallpaperPath, undefined)
} else {
WallpaperService.changeWallpaper(wallpaperPath, Screen.name)
}
}
}
}
NText {
text: filename
color: Color.mOnSurfaceVariant
opacity: 0.5
font.pointSize: Style.fontSizeXS * scaling
Layout.fillWidth: true
Layout.leftMargin: Style.marginS * scaling
Layout.rightMargin: Style.marginS * scaling
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
}
}
}
}
// Empty / scanning state
Rectangle {
color: Color.mSurface
radius: Style.radiusM * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
visible: (filteredWallpapers.length === 0 && !WallpaperService.scanning) || WallpaperService.scanning
Layout.fillWidth: true
Layout.preferredHeight: 130 * scaling
ColumnLayout {
anchors.fill: parent
visible: WallpaperService.scanning
NBusyIndicator {
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
}
ColumnLayout {
anchors.fill: parent
visible: filteredWallpapers.length === 0 && !WallpaperService.scanning
Item {
Layout.fillHeight: true
}
NIcon {
icon: "folder-open"
font.pointSize: Style.fontSizeXXL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: (filterText && filterText.length > 0) ? "No match found." : "No wallpaper found."
color: Color.mOnSurface
font.weight: Style.fontWeightBold
Layout.alignment: Qt.AlignHCenter
}
NText {
text: (filterText && filterText.length > 0) ? "Try a different search query." : "Configure your wallpaper directory with images."
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.alignment: Qt.AlignHCenter
}
Item {
Layout.fillHeight: true
}
}
}
}
}
}
}
}
}

337
README.md
View File

@@ -1,31 +1,29 @@
<p align="center">
<img src="https://assets.noctalia.dev/noctalia-logo.png" alt="Noctalia Logo" width="124" />
</p>
# Noctalia
**_quiet by design_**
<p align="center">
<img src="https://assets.noctalia.dev/noctalia-logo.png" alt="Noctalia Logo" width="124" />
</p>
<p align="center">
<a href="https://github.com/noctalia-dev/noctalia-shell/commits">
<img src="https://img.shields.io/github/last-commit/noctalia-dev/noctalia-shell?style=for-the-badge&labelColor=0C0D11&color=A8AEFF" alt="Last commit" />
<img src="https://img.shields.io/github/last-commit/noctalia-dev/noctalia-shell?style=for-the-badge&labelColor=0C0D11&color=A8AEFF&logo=git&logoColor=FFFFFF&label=commit" alt="Last commit" />
</a>
<a href="https://github.com/noctalia-dev/noctalia-shell/stargazers">
<img src="https://img.shields.io/github/stars/noctalia-dev/noctalia-shell?style=for-the-badge&labelColor=0C0D11&color=A8AEFF" alt="GitHub stars" />
<img src="https://img.shields.io/github/stars/noctalia-dev/noctalia-shell?style=for-the-badge&labelColor=0C0D11&color=A8AEFF&logo=github&logoColor=FFFFFF" alt="GitHub stars" />
</a>
<a href="https://github.com/noctalia-dev/noctalia-shell/graphs/contributors">
<img src="https://img.shields.io/github/contributors/noctalia-dev/noctalia-shell?style=for-the-badge&labelColor=0C0D11&color=A8AEFF" alt="GitHub contributors" />
<a href="https://docs.noctalia.dev">
<img src="https://img.shields.io/badge/docs-A8AEFF?style=for-the-badge&logo=gitbook&logoColor=FFFFFF&labelColor=0C0D11" alt="Documentation" />
</a>
<a href="https://discord.noctalia.dev">
<img src="https://img.shields.io/badge/Discord-5865F2?style=for-the-badge&labelColor=0C0D11&color=A8AEFF&logo=discord&logoColor=white" alt="Discord" />
<img src="https://img.shields.io/badge/discord-A8AEFF?style=for-the-badge&labelColor=0C0D11&logo=discord&logoColor=FFFFFF" alt="Discord" />
</a>
</p>
---
A sleek and minimal desktop shell thoughtfully crafted for Wayland, built with Quickshell.
Features a modern modular architecture with a status bar, notification system, control panel, comprehensive system integration, and more — all styled with a warm lavender palette, or your favorite color scheme!
A beautiful, minimal desktop shell for Wayland that actually gets out of your way. Built on Quickshell with a warm lavender aesthetic that you can easily customize to match your vibe.
## Preview
@@ -46,298 +44,34 @@ https://github.com/user-attachments/assets/72c6d6dc-48b0-48a0-bd8b-c7e70990edc4
---
> ⚠️ **Note:**
> This shell currently supports **Niri** and **Hyprland** compositors. For other compositors, you will need to implement custom workspace logic in the CompositorService.
## 🚀 Getting Started
**New to Noctalia?**
Check out our documentation & installation guide to get started!
<a href="https://docs.noctalia.dev/getting-started/installation">
<img src="https://img.shields.io/badge/⚡%20Quick%20Install-Get%20Started-A8AEFF?style=for-the-badge&logoColor=FFFFFF&labelColor=0C0D11" alt="Quick Install" />
</a>
**Need help?** Join our [Discord community](https://discord.noctalia.dev) or browse the [FAQ](https://docs.noctalia.dev/getting-started/faq/).
---
## Features
## 🖥️ Wayland Compositors
- **Status Bar:** Modular bar with workspace indicators, system monitors, clock, and quick access controls.
- **Workspace Management:** Dynamic workspace switching with visual indicators and active window tracking.
- **Notifications:** Rich notification system with history panel.
- **Application Launcher:** Stylized launcher with favorites, recent apps, and special commands (calc, clipboard).
- **Side Panel:** Quick access panel with media controls, weather, power profiles, and system utilities.
- **Settings Panel:** Comprehensive configuration interface for all shell components and preferences.
- **Lock Screen:** Secure lock experience with PAM authentication, time display, and animated background.
- **Audio Integration:** Volume controls, media playback, and audio visualizer (cava-based).
- **Connectivity:** WiFi and Bluetooth management with device pairing and network status.
- **Power Management:** Battery monitoring, brightness control, power profile switching, power menu, and idle inhibition.
- **System Monitoring:** CPU, memory, and network usage monitoring with visual indicators.
- **Tray System:** Application tray with menu support and system integration.
- **Background Management:** Wallpaper management with effects and dynamic theming support.
- **Color Schemes:** Catppuccin, Dracula, Gruvbox, Noctalia, Nord, Rosépine, Solarized, Tokyo night or generated from your wallpaper.
- **Scaling:** Per monitor scaling for maximum control.
---
## Dependencies
### Required
- `quickshell-git` - Core shell framework
- `ttf-roboto` - The default font used for most of the UI
- `inter-font` - The default font used for Headers (ex: clock on the LockScreen)
- `gpu-screen-recorder` - Screen recording functionality (Flatpak also supported)
- `brightnessctl` - For internal/laptop monitor brightness
- `ddcutil` - For desktop monitor brightness (might introduce some system instability with certain monitors)
### Optional
- `cliphist` - For clipboard history support
- `matugen` - Material You color scheme generation
- `cava` - Audio visualizer component
- `wlsunset` - To be able to use NightLight
> There is one more optional dependency.
> `xdg-desktop-portal` to be able to use the "Portal" option from the screenRecorder.
Noctalia provides native support for **Niri** and **Hyprland**. Other Wayland compositors will work but may require additional workspace logic configuration.
---
## Quick Start
## 🤝 Contributing
### Installation
We welcome contributions of any size - bug fixes, new features, documentation improvements, or custom themes and configs.
#### Arch Linux
<details>
<summary><strong>AUR</strong></summary>
You can install Noctalia from the [AUR](https://aur.archlinux.org/packages/noctalia-shell). This method will install the shell system-wide.
```bash
paru -S noctalia-shell
```
If you want the latest development version directly from the git repository, you can use the `noctalia-shell-git` package:
```bash
paru -S noctalia-shell-git
```
This will always pull the most recent commit from the Noctalia repository. Note that it may be less stable than the release version.
</details>
<details>
<summary><strong>Manual Installation</strong></summary>
This method installs the shell to your local user configuration.
Make sure you have Quickshell installed:
```bash
paru -S quickshell-git
```
Download and install Noctalia (latest release):
```bash
mkdir -p ~/.config/quickshell/noctalia-shell && curl -sL https://github.com/noctalia-dev/noctalia-shell/releases/latest/download/noctalia-latest.tar.gz | tar -xz --strip-components=1 -C ~/.config/quickshell/noctalia-shell
```
</details>
#### Nix
<details>
<summary><strong>Nix Installation</strong></summary>
You can run Noctalia directly using the `nix run` command:
```bash
nix run github:noctalia-dev/noctalia-shell
```
Alternatively, you can add it to your NixOS configuration or flake:
**Step 1**: Add Quickshell and Noctalia flakes to your `flake.nix`:
```nix
{
description = "Example Nix flake with Noctalia + Quickshell";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
# you need nixpkgs unstable
quickshell = {
url = "github:outfoxxed/quickshell";
inputs.nixpkgs.follows = "nixpkgs";
};
noctalia = {
url = "github:noctalia-dev/noctalia-shell";
inputs.nixpkgs.follows = "nixpkgs";
inputs.quickshell.follows = "quickshell";
};
};
outputs = { self, nixpkgs, noctalia, quickshell, ... }:
{
nixosConfigurations.my-host = nixpkgs.lib.nixosSystem {
modules = [
./configuration.nix
];
};
};
}
```
**Step 2**: Add the packages to your `configuration.nix`:
```nix
{
environment.systemPackages = with pkgs; [
inputs.noctalia.packages.${system}.default
inputs.quickshell.packages.${system}.default
];
}
```
</details>
### Usage
Start the Shell with: `qs -c noctalia-shell`
`noctalia-shell` offers many IPC calls for your convenience, so you can add them to your favorite keybinds or scripts.
*If you're using the Flake installation on NixOS, replace `qs -c noctalia-shell` with `noctalia-shell`*
*If you're using the manual install (`git clone...` and have it in `~/.config/quickshell/`) you can just use `qs ipc call...`*
| Action | Command* |
| --------------------------- | -------------------------------------------------------------- |
| Start the Shell | `qs -c noctalia-shell` |
| Toggle Application Launcher | `qs -c noctalia-shell ipc call launcher toggle` |
| Toggle Side Panel | `qs -c noctalia-shell ipc call sidePanel toggle` |
| Open Clipboard History | `qs -c noctalia-shell ipc call launcher clipboard` |
| Open Calculator | `qs -c noctalia-shell ipc call launcher calculator` |
| Increase Brightness | `qs -c noctalia-shell ipc call brightness increase` |
| Decrease Brightness | `qs -c noctalia-shell ipc call brightness decrease` |
| Increase Output Volume | `qs -c noctalia-shell ipc call volume increase` |
| Decrease Output Volume | `qs -c noctalia-shell ipc call volume decrease` |
| Toggle Mute Audio Output | `qs -c noctalia-shell ipc call volume muteOutput` |
| Toggle Mute Audio Input | `qs -c noctalia-shell ipc call volume muteInput` |
| Toggle Power Panel | `qs -c noctalia-shell ipc call powerPanel toggle` |
| Toggle Idle Inhibitor | `qs -c noctalia-shell ipc call idleInhibitor toggle` |
| Toggle Settings Window | `qs -c noctalia-shell ipc call settings toggle` |
| Toggle Lock Screen | `qs -c noctalia-shell ipc call lockScreen toggle` |
| Toggle Notification History | `qs -c noctalia-shell ipc call notifications toggleHistory` |
| Toggle Notification DND | `qs -c noctalia-shell ipc call notifications toggleDND` |
| Change Wallpaper | `qs -c noctalia-shell ipc call wallpaper set $path $monitor` |
| Assign a Random Wallpaper | `qs -c noctalia-shell ipc call wallpaper random` |
| Toggle Dark Mode | `qs -c noctalia-shell ipc call darkMode toggle` |
| Set Dark Mode | `qs -c noctalia-shell ipc call darkMode setDark` |
| Set Light Mode | `qs -c noctalia-shell ipc call darkMode setLight` |
### Configuration
Access settings through the side panel (top right button) to configure weather, wallpapers, screen recording, audio, network, and theme options.
Configuration is usually stored in ~/.config/noctalia.
### Some of my app icons are missing!
The issue is most likely that you did not set up your environment variables properly.
Example environment variables that you can use one of the following:
If you already have an icon theme set for GTK then you can use this one:
- `QT_QPA_PLATFORMTHEME=gtk3`
You can also use Qt6ct to set your icon theme, for that you can use:
- `QT_QPA_PLATFORMTHEME=qt6ct`
If you don't have either of those set then you can just use:
- `QS_ICON_THEME="youricontheme"`
**Any of these environment variables should go into `/etc/environment` (you need to reboot afterwards). For NixOS you can use `environment.variables` or `home.sessionVariables`.**
### Application Launcher
The launcher supports special commands for enhanced functionality:
- `>calc` - Simple mathematical calculations
- `>clip` - Clipboard history management
---
<details>
<summary><strong>Theme Colors</strong></summary>
| Color Role | Color | Description |
| -------------------- | ----------- | -------------------------- |
| Primary | `#c7a1d8` | Soft lavender purple |
| On Primary | `#1a151f` | Dark text on primary |
| Secondary | `#a984c4` | Muted lavender |
| On Secondary | `#f3edf7` | Light text on secondary |
| Tertiary | `#e0b7c9` | Warm pink-lavender |
| On Tertiary | `#20161f` | Dark text on tertiary |
| Surface | `#1c1822` | Dark purple-tinted surface |
| On Surface | `#e9e4f0` | Light text on surface |
| Surface Variant | `#262130` | Elevated surface variant |
| On Surface Variant | `#a79ab0` | Muted text on surface variant |
| Error | `#e9899d` | Soft rose red |
| On Error | `#1e1418` | Dark text on error |
| Outline | `#4d445a` | Purple-tinted outline |
| Shadow | `#120f18` | Deep purple-tinted shadow |
</details>
---
## Advanced Configuration
### Recommended Compositor Settings
For Niri:
```
debug {
honor-xdg-activation-with-invalid-serial
}
window-rule {
geometry-corner-radius 20
clip-to-geometry true
}
layer-rule {
match namespace="^quickshell-overview$"
place-within-backdrop true
}
```
`honor-xdg-activation-with-invalid-serial` allows notification actions (like view etc) to work.
---
## Development
### Project Structure
```
Noctalia/
├── shell.qml # Main shell entry point
├── Modules/ # UI components
│ ├── Bar/ # Status bar components
│ ├── Dock/ # Application launcher
│ ├── SidePanel/ # Quick access panel
│ ├── SettingsPanel/ # Configuration interface
│ └── ...
├── Services/ # Backend services
│ ├── CompositorService.qml
│ ├── WorkspacesService.qml
│ ├── AudioService.qml
│ └── ...
├── Widgets/ # Reusable UI components
├── Commons/ # Shared utilities
├── Assets/ # Static assets
└── Bin/ # Utility scripts
```
### Contributing
1. Follow the existing code style and patterns
2. Use the modular architecture for new features
3. Implement proper error handling and logging
4. Test with both Hyprland and Niri compositors (if applicable)
Contributions are welcome! Don't worry about being perfect - every contribution helps! Whether it's fixing a small bug, adding a new feature, or improving documentation, we welcome all contributions. Feel free to open an issue to discuss ideas or ask questions before diving in. For feature requests and ideas, you can also use our discussions page.
**Get involved:**
- **Found a bug?** [Open an issue](https://github.com/noctalia-dev/noctalia-shell/issues/new)
- **Want to code?** Check out our [development guidelines](https://docs.noctalia.dev/development/guideline)
- **Need help?** Join our [Discord](https://discord.noctalia.dev)
---
@@ -347,13 +81,13 @@ A heartfelt thank you to our incredible community of [**contributors**](https://
---
## Acknowledgment
## 🙏 Acknowledgment
Special thanks to the creators of [**Caelestia**](https://github.com/caelestia-dots/shell) and [**DankMaterialShell**](https://github.com/AvengeMedia/DankMaterialShell) for their inspirational designs and clever implementation techniques.
---
#### Donation
## Donation
While all donations are greatly appreciated, they are completely voluntary.
@@ -361,13 +95,14 @@ While all donations are greatly appreciated, they are completely voluntary.
<img src="https://img.shields.io/badge/donate-ko--fi-A8AEFF?style=for-the-badge&logo=kofi&logoColor=FFFFFF&labelColor=0C0D11" alt="Ko-Fi" />
</a>
#### Thank you to everyone who supports the project 💜!
### Thank you to everyone who supports the project 💜!
* Gohma
* <a href="https://pika-os.com/" target="_blank">PikaOS</a>
* DiscoCevapi
* <a href="https://pika-os.com/" target="_blank">PikaOS</a>
---
## License
## 📄 License
This project is licensed under the terms of the [MIT License](./LICENSE).
MIT License - see [LICENSE](./LICENSE) for details.

171
Services/BarService.qml Normal file
View File

@@ -0,0 +1,171 @@
pragma Singleton
import Quickshell
import qs.Commons
Singleton {
id: root
// Registry to store actual widget instances
// Key format: "screenName|section|widgetId|index"
property var widgetInstances: ({})
// Register a widget instance
function registerWidget(screenName, section, widgetId, index, instance) {
const key = [screenName, section, widgetId, index].join("|")
widgetInstances[key] = {
"key": key,
"screenName": screenName,
"section": section,
"widgetId": widgetId,
"index": index,
"instance": instance
}
Logger.log("BarService", "Registered widget:", key)
}
// Unregister a widget instance
function unregisterWidget(screenName, section, widgetId, index) {
const key = [screenName, section, widgetId, index].join("|")
delete widgetInstances[key]
Logger.log("BarService", "Unregistered widget:", key)
}
// Lookup a specific widget instance (returns the actual QML instance)
function lookupWidget(widgetId, screenName = null, section = null) {
// If looking for a specific instance
if (screenName && section !== null) {
for (var key in widgetInstances) {
var widget = widgetInstances[key]
if (widget.widgetId === widgetId && widget.screenName === screenName && widget.section === section) {
return widget.instance
}
}
}
// Return first match if no specific screen/section specified
for (var key in widgetInstances) {
var widget = widgetInstances[key]
if (widget.widgetId === widgetId) {
if (!screenName || widget.screenName === screenName) {
if (section === null || widget.section === section) {
return widget.instance
}
}
}
}
return undefined
}
// Get all instances of a widget type
function getAllWidgetInstances(widgetId = null, screenName = null, section = null) {
var instances = []
for (var key in widgetInstances) {
var widget = widgetInstances[key]
var matches = true
if (widgetId && widget.widgetId !== widgetId)
matches = false
if (screenName && widget.screenName !== screenName)
matches = false
if (section !== null && widget.section !== section)
matches = false
if (matches) {
instances.push(widget.instance)
}
}
return instances
}
// Get widget with full metadata
function getWidgetWithMetadata(widgetId, screenName = null, section = null) {
for (var key in widgetInstances) {
var widget = widgetInstances[key]
if (widget.widgetId === widgetId) {
if (!screenName || widget.screenName === screenName) {
if (section === null || widget.section === section) {
return widget
}
}
}
}
return undefined
}
// Get all widgets in a specific section
function getWidgetsBySection(section, screenName = null) {
var widgets = []
for (var key in widgetInstances) {
var widget = widgetInstances[key]
if (widget.section === section) {
if (!screenName || widget.screenName === screenName) {
widgets.push(widget.instance)
}
}
}
// Sort by index to maintain order
widgets.sort(function (a, b) {
var aWidget = getWidgetWithMetadata(a.widgetId, a.screen?.name, a.section)
var bWidget = getWidgetWithMetadata(b.widgetId, b.screen?.name, b.section)
return (aWidget?.index || 0) - (bWidget?.index || 0)
})
return widgets
}
// Get all registered widgets (for debugging)
function getAllRegisteredWidgets() {
var result = []
for (var key in widgetInstances) {
result.push({
"key": key,
"widgetId": widgetInstances[key].widgetId,
"section": widgetInstances[key].section,
"screenName": widgetInstances[key].screenName,
"index": widgetInstances[key].index
})
}
return result
}
// Check if a widget type exists in a section
function hasWidget(widgetId, section = null, screenName = null) {
for (var key in widgetInstances) {
var widget = widgetInstances[key]
if (widget.widgetId === widgetId) {
if (section === null || widget.section === section) {
if (!screenName || widget.screenName === screenName) {
return true
}
}
}
}
return false
}
// Get pill direction for a widget instance
function getPillDirection(widgetInstance) {
try {
if (widgetInstance.section === "left") {
return true
} else if (widgetInstance.section === "right") {
return false
} else {
// middle section
if (widgetInstance.sectionWidgetIndex < widgetInstance.sectionWidgetsCount / 2) {
return false
} else {
return true
}
}
} catch (e) {
Logger.error(e)
}
return false
}
}

View File

@@ -52,9 +52,7 @@ Singleton {
},
"Clock": {
"allowUserSettings": true,
"displayFormat": "time-date-short",
"use12HourClock": false,
"reverseDayMonth": true
"displayFormat": "time-date-short"
},
"CustomButton": {
"allowUserSettings": true,
@@ -206,24 +204,4 @@ Singleton {
function widgetHasUserSettings(id) {
return (widgetMetadata[id] !== undefined) && (widgetMetadata[id].allowUserSettings === true)
}
function getPillDirection(widget) {
try {
if (widget.section === "left") {
return true
} else if (widget.section === "right") {
return false
} else {
// middle section
if (widget.sectionWidgetIndex < widget.sectionWidgetsCount / 2) {
return false
} else {
return true
}
}
} catch (e) {
Logger.error(e)
}
return false
}
}

View File

@@ -9,8 +9,7 @@ Singleton {
id: root
readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter
readonly property bool available: adapter !== null
readonly property bool enabled: (adapter && adapter.enabled) ?? false
readonly property bool available: (adapter !== null)
readonly property bool discovering: (adapter && adapter.discovering) ?? false
readonly property var devices: adapter ? adapter.devices : null
readonly property var pairedDevices: {
@@ -30,37 +29,64 @@ Singleton {
})
}
property bool lastAdapterState: false
function init() {
Logger.log("Bluetooth", "Service initialized")
delaySyncState.running = true
syncStateTimer.running = true
}
Timer {
id: delaySyncState
id: syncStateTimer
interval: 1000
repeat: false
onTriggered: {
Settings.data.network.bluetoothEnabled = adapter.enabled
lastAdapterState = Settings.data.network.bluetoothEnabled = adapter.enabled
}
}
Timer {
id: delayDiscovery
id: discoveryTimer
interval: 1000
repeat: false
onTriggered: adapter.discovering = true
}
Timer {
id: stateDebounceTimer
interval: 200
repeat: false
onTriggered: {
if (!adapter) {
Logger.warn("Bluetooth", "State debouncer", "No adapter available")
return
}
if (lastAdapterState === adapter.enabled) {
return
}
lastAdapterState = adapter.enabled
if (adapter.enabled) {
ToastService.showNotice("Bluetooth", "Enabled")
} else {
ToastService.showNotice("Bluetooth", "Disabled")
}
}
}
Connections {
target: adapter
function onEnabledChanged() {
if (!adapter) {
Logger.warn("Bluetooth", "onEnabledChanged", "No adapter available")
return
}
Logger.log("Bluetooth", "onEnableChanged", adapter.enabled)
Settings.data.network.bluetoothEnabled = adapter.enabled
stateDebounceTimer.restart()
if (adapter.enabled) {
ToastService.showNotice("Bluetooth", "Enabled")
// Using a timer to give a little time so the adapter is really enabled
delayDiscovery.running = true
} else {
ToastService.showNotice("Bluetooth", "Disabled")
discoveryTimer.running = true
}
}
}
@@ -231,12 +257,13 @@ Singleton {
device.forget()
}
function setBluetoothEnabled(enabled) {
function setBluetoothEnabled(state) {
if (!adapter) {
Logger.warn("Bluetooth", "No adapter available")
return
}
adapter.enabled = enabled
Logger.log("Bluetooth", "SetBluetoothEnabled", state)
adapter.enabled = state
}
}

View File

@@ -197,9 +197,11 @@ Singleton {
}
}
readonly property real stepSize: Settings.data.brightness.brightnessStep / 100.0
// Timer for debouncing rapid changes
readonly property Timer timer: Timer {
interval: 200
interval: 100
onTriggered: {
if (!isNaN(monitor.queuedBrightness)) {
monitor.setBrightness(monitor.queuedBrightness)
@@ -208,14 +210,19 @@ Singleton {
}
}
function setBrightnessDebounced(value: real): void {
monitor.queuedBrightness = value
timer.start()
}
function increaseBrightness(): void {
var stepSize = Settings.data.brightness.brightnessStep / 100.0
setBrightnessDebounced(monitor.brightness + stepSize)
const value = !isNaN(monitor.queuedBrightness) ? monitor.queuedBrightness : monitor.brightness
setBrightnessDebounced(value + stepSize)
}
function decreaseBrightness(): void {
var stepSize = Settings.data.brightness.brightnessStep / 100.0
setBrightnessDebounced(monitor.brightness - stepSize)
const value = !isNaN(monitor.queuedBrightness) ? monitor.queuedBrightness : monitor.brightness
setBrightnessDebounced(value - stepSize)
}
function setBrightness(value: real): void {
@@ -225,7 +232,7 @@ Singleton {
if (Math.round(monitor.brightness * 100) === rounded)
return
if (isDdc && timer.running) {
if (timer.running) {
monitor.queuedBrightness = value
return
}
@@ -247,11 +254,6 @@ Singleton {
}
}
function setBrightnessDebounced(value: real): void {
monitor.queuedBrightness = value
timer.restart()
}
function initBrightness(): void {
if (isAppleDisplay) {
initProc.command = ["asdbctl", "get"]

View File

@@ -163,13 +163,13 @@ Singleton {
if (activeInhibitors.includes("manual")) {
removeInhibitor("manual")
Settings.data.ui.idleInhibitorEnabled = false
ToastService.showNotice("Keep Awake", "Disabled", false, 3000)
ToastService.showNotice("Keep Awake", "Disabled")
Logger.log("IdleInhibitor", "Manual inhibition disabled and saved to settings")
return false
} else {
addInhibitor("manual", "Manually activated by user")
Settings.data.ui.idleInhibitorEnabled = true
ToastService.showNotice("Keep Awake", "Enabled", false, 3000)
ToastService.showNotice("Keep Awake", "Enabled")
Logger.log("IdleInhibitor", "Manual inhibition enabled and saved to settings")
return true
}

View File

@@ -47,6 +47,11 @@ Singleton {
// Generate colors using current wallpaper and settings
function generateFromWallpaper() {
if (!Settings.isLoaded) {
Logger.log("Matugen", "Settings not loaded yet, skipping wallpaper color generation")
return
}
Logger.log("Matugen", "Generating from wallpaper on screen:", Screen.name)
var wp = WallpaperService.getWallpaper(Screen.name).replace(/'/g, "'\\''")
if (wp === "") {

View File

@@ -13,17 +13,40 @@ Singleton {
readonly property bool available: powerProfiles && powerProfiles.hasPerformanceProfile
property int profile: powerProfiles ? powerProfiles.profile : PowerProfile.Balanced
function profileName(p) {
const prof = (p !== undefined) ? p : profile
function getName(p) {
if (!available)
return "Unknown"
if (prof === PowerProfile.Performance)
const prof = (p !== undefined) ? p : profile
switch (prof) {
case PowerProfile.Performance:
return "Performance"
if (prof === PowerProfile.Balanced)
case PowerProfile.Balanced:
return "Balanced"
if (prof === PowerProfile.PowerSaver)
case PowerProfile.PowerSaver:
return "Power Saver"
return "Unknown"
default:
return "Unknown"
}
}
function getIcon(p) {
if (!available)
return "balanced"
const prof = (p !== undefined) ? p : profile
switch (prof) {
case PowerProfile.Performance:
return "performance"
case PowerProfile.Balanced:
return "balanced"
case PowerProfile.PowerSaver:
return "powersaver"
default:
return "balanced"
}
}
function setProfile(p) {
@@ -53,9 +76,9 @@ Singleton {
function onProfileChanged() {
root.profile = powerProfiles.profile
// Only show toast if we have a valid profile name (not "Unknown")
const profileName = root.profileName()
const profileName = root.getName()
if (profileName !== "Unknown") {
ToastService.showNotice("Power Profile", profileName)
ToastService.showNotice("Power Profile Changed", `"${profileName}"`)
}
}
}

View File

@@ -8,8 +8,8 @@ Singleton {
id: root
// Public properties
property string baseVersion: "2.10.0"
property bool isDevelopment: false
property string baseVersion: "2.12.0"
property bool isDevelopment: true
property string currentVersion: `v${!isDevelopment ? baseVersion : baseVersion + "-dev"}`

View File

@@ -9,6 +9,20 @@ import qs.Commons
Singleton {
id: root
// Public init to rehydrate cache after Settings load
function init() {
// Rebuild cache from persisted settings
var monitors = Settings.data.wallpaper.monitors || []
currentWallpapers = ({})
for (var i = 0; i < monitors.length; i++) {
if (monitors[i].name && monitors[i].wallpaper) {
currentWallpapers[monitors[i].name] = monitors[i].wallpaper
// Notify listeners so Background updates immediately after settings load
root.wallpaperChanged(monitors[i].name, monitors[i].wallpaper)
}
}
}
Component.onCompleted: {
Logger.log("Wallpaper", "Service started")

View File

@@ -16,7 +16,6 @@ ColumnLayout {
text: root.label
font.pointSize: Style.fontSizeXL * scaling
font.weight: Style.fontWeightBold
font.capitalization: Font.Capitalize
color: Color.mSecondary
visible: root.title !== ""
}

View File

@@ -17,7 +17,6 @@ ColumnLayout {
text: label
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
font.capitalization: Font.Capitalize
color: labelColor
visible: label !== ""
Layout.fillWidth: true

View File

@@ -16,6 +16,7 @@ Loader {
property real preferredWidthRatio
property real preferredHeightRatio
property color panelBackgroundColor: Color.mSurface
property bool draggable: false
property bool panelAnchorHorizontalCenter: false
property bool panelAnchorVerticalCenter: false
@@ -38,6 +39,7 @@ Loader {
readonly property real originalOpacity: 0.0
property real scaleValue: originalScale
property real opacityValue: originalOpacity
property real dimmingOpacity: 0
property alias isClosing: hideTimer.running
@@ -108,6 +110,7 @@ Loader {
// -----------------------------------------
function close() {
dimmingOpacity = 0
scaleValue = originalScale
opacityValue = originalOpacity
hideTimer.start()
@@ -149,6 +152,7 @@ Loader {
Component.onCompleted: {
Logger.log("NPanel", "Opened", root.objectName)
dimmingOpacity = Style.opacityHeavy
}
Connections {
@@ -174,17 +178,14 @@ Loader {
visible: true
// No dimming here
color: Color.transparent
WlrLayershell.layer: Settings.data.general.dimDesktop ? WlrLayer.Overlay : WlrLayer.Top
color: Settings.data.general.dimDesktop ? Qt.alpha(Color.mShadow, dimmingOpacity) : Color.transparent
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "noctalia-panel"
WlrLayershell.keyboardFocus: root.panelKeyboardFocus ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
Behavior on color {
ColorAnimation {
duration: Style.animationNormal
duration: Style.animationSlow
}
}
@@ -208,12 +209,18 @@ Loader {
onClicked: root.close()
}
// The actual panel's content
Rectangle {
id: panelBackground
color: panelBackgroundColor
radius: Style.radiusL * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
// Dragging support
property bool draggable: root.draggable
property bool isDragged: false
property real manualX: 0
property real manualY: 0
width: {
var w
if (preferredWidthRatio !== undefined) {
@@ -238,8 +245,8 @@ Loader {
scale: root.scaleValue
opacity: root.opacityValue
x: calculatedX
y: calculatedY
x: isDragged ? manualX : calculatedX
y: isDragged ? manualY : calculatedY
// ---------------------------------------------
// Does not account for corners are they are negligible and helps keep the code clean.
@@ -372,6 +379,14 @@ Loader {
root.opacityValue = 1.0
}
// Reset drag position when panel closes
Connections {
target: root
function onClosed() {
panelBackground.isDragged = false
}
}
// Prevent closing when clicking in the panel bg
MouseArea {
anchors.fill: parent
@@ -397,6 +412,79 @@ Loader {
anchors.fill: parent
sourceComponent: root.panelContent
}
// Handle drag move on the whole panel area
DragHandler {
id: dragHandler
target: null
enabled: panelBackground.draggable
property real dragStartX: 0
property real dragStartY: 0
onActiveChanged: {
if (active) {
// Capture current position into manual coordinates BEFORE toggling isDragged
panelBackground.manualX = panelBackground.x
panelBackground.manualY = panelBackground.y
dragStartX = panelBackground.x
dragStartY = panelBackground.y
panelBackground.isDragged = true
if (root.enableBackgroundClick)
root.disableBackgroundClick()
} else {
// Keep isDragged true so we continue using the manual x/y after release
if (root.enableBackgroundClick)
root.enableBackgroundClick()
}
}
onTranslationChanged: {
// Proposed new coordinates from fixed drag origin
var nx = dragStartX + translation.x
var ny = dragStartY + translation.y
// Calculate gaps so we never overlap the bar on any side
var baseGap = Style.marginS * scaling
var floatExtraH = Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * 2 * Style.marginXL * scaling : 0
var floatExtraV = Settings.data.bar.floating ? Settings.data.bar.marginVertical * 2 * Style.marginXL * scaling : 0
var insetLeft = baseGap + ((barIsVisible && barPosition === "left") ? (Style.barHeight * scaling + floatExtraH) : 0)
var insetRight = baseGap + ((barIsVisible && barPosition === "right") ? (Style.barHeight * scaling + floatExtraH) : 0)
var insetTop = baseGap + ((barIsVisible && barPosition === "top") ? (Style.barHeight * scaling + floatExtraV) : 0)
var insetBottom = baseGap + ((barIsVisible && barPosition === "bottom") ? (Style.barHeight * scaling + floatExtraV) : 0)
// Clamp within screen bounds accounting for insets
var maxX = panelWindow.width - panelBackground.width - insetRight
var minX = insetLeft
var maxY = panelWindow.height - panelBackground.height - insetBottom
var minY = insetTop
panelBackground.manualX = Math.round(Math.max(minX, Math.min(nx, maxX)))
panelBackground.manualY = Math.round(Math.max(minY, Math.min(ny, maxY)))
}
}
// Drag indicator border
Rectangle {
anchors.fill: parent
anchors.margins: 0
color: Color.transparent
border.color: Color.mPrimary
border.width: Math.max(2, Style.borderL * scaling)
radius: parent.radius
visible: panelBackground.isDragged && dragHandler.active
opacity: 0.8
z: 3000
// Subtle glow effect
Rectangle {
anchors.fill: parent
anchors.margins: 0
color: Color.transparent
border.color: Color.mPrimary
border.width: Math.max(1, Style.borderS * scaling)
radius: parent.radius
opacity: 0.3
}
}
}
}
}

View File

@@ -40,6 +40,7 @@
libnotify
matugen
networkmanager
wlsunset
wl-clipboard
] ++ lib.optionals (pkgs.stdenv.hostPlatform.isx86_64)
[ gpu-screen-recorder ];
@@ -79,6 +80,17 @@
});
defaultPackage = eachSystem (system: self.packages.${system}.default);
nixosModules = {
noctalia = { pkgs, lib, ... }: {
environment.systemPackages = [
self.packages.${pkgs.system}.default
];
# Required services for proper functionality
services.power-profiles-daemon.enable = true; # Power profile switching support
services.upower.enable = true; # Battery monitoring & power management
};
};
};
}

View File

@@ -28,6 +28,7 @@ import qs.Modules.PowerPanel
import qs.Modules.SidePanel
import qs.Modules.Toast
import qs.Modules.WiFiPanel
import qs.Modules.WallpaperSelector
import qs.Services
import qs.Widgets
@@ -37,7 +38,6 @@ ShellRoot {
Background {}
Overview {}
ScreenCorners {}
Dimmer {}
Bar {}
Dock {}
@@ -95,6 +95,11 @@ ShellRoot {
objectName: "bluetoothPanel"
}
WallpaperSelector {
id: wallpaperSelector
objectName: "wallpaperSelector"
}
Component.onCompleted: {
// Save a ref. to our lockScreen so we can access it easily
PanelService.lockScreen = lockScreen