Compare commits

...

61 commits

Author SHA1 Message Date
6e009d3551 Merge branch 'github-main' 2025-09-08 13:32:06 +02:00
Ly-sec
66a4618d09 switch to dev version 2025-09-08 12:33:17 +02:00
Ly-sec
983e3c5cbe Release v2.7.0
Network: Even more improvements
SysStat: Remove bash script
Notification: Pore image support
NotificationHistory: Proper unread count
Settings: Migrate Bar widgets to new settings
BarWidgets: Easier to access, edit
Background: add default wallpaper (if none is set)
SystemMonitor: add % support for RAM
BarTab:
- remove global settings for widgets
- add settings button per bar widget, this makes it possible to have separate settings of the same kind with different settings. This also makes it way easier to configure.

A decent amount of QoL changes & fixes
2025-09-08 12:28:35 +02:00
Ly-sec
c02d3e3d22 Merge branch 'bartab-overhaul' 2025-09-08 12:21:18 +02:00
Ly-sec
c0900b105b Background: add default wallpaper 2025-09-08 08:46:10 +02:00
Ly-sec
b6166a2a7c SystemMonitor: add % support for RAM usage 2025-09-08 08:04:18 +02:00
Ly-sec
38928abab7 Fix first start noctalia settings & color creation 2025-09-08 07:51:49 +02:00
LemmyCook
849f3c52d7 Notifications badge: hidden by default 2025-09-08 01:10:48 -04:00
LemmyCook
f9e55c8f8d Workspace: removed extra transparent padding around. 2025-09-08 01:03:58 -04:00
LemmyCook
993a7965fd NPill: fixed look at high scaling 2025-09-08 01:00:38 -04:00
LemmyCook
d4f6462e8a Battery: deactivated test mode 2025-09-08 00:40:12 -04:00
LemmyCook
8bfde2f6d8 NPill: fixed, finally! 2025-09-08 00:39:07 -04:00
LemmyCook
b3eea2215d Bar Add Widget: taller NComboBox 2025-09-08 00:05:58 -04:00
LemmyCook
4d7bc811c4 Widget Settings: load settings before triggering the loader to avoid async loading. 2025-09-08 00:02:15 -04:00
LemmyCook
74ec5ea606 Cava: running at all time as its getting to know if a widget needs it. 2025-09-07 23:59:22 -04:00
LemmyCook
dda0266798 Autoformatting 2025-09-07 23:51:31 -04:00
LemmyCook
99d9dbe218 WidgetSettings: replaced all checkboxes by the usual toggles. 2025-09-07 23:51:09 -04:00
LemmyCook
89c7f05782 NLabel: always full width even when there is no description 2025-09-07 23:45:13 -04:00
LemmyCook
d9c36a81c4 NightLight: fixed rightclick to open settings 2025-09-07 23:18:27 -04:00
LemmyCook
91747c71f2 Main Settings: cleaned tabs since we removed many settings 2025-09-07 23:18:10 -04:00
LemmyCook
5a1231a17e Settings: completed migration of old settings on startup 2025-09-07 22:55:28 -04:00
LemmyCook
517c7c97d4 Bar Widgets FrontEnd: Simplified access to editable widget settings 2025-09-07 22:23:45 -04:00
LemmyCook
45af873a6f Bar Widget Settings: One file per Widget settings, refactor - wip 2025-09-07 21:45:28 -04:00
LemmyCook
c01167c9da Settings tabs: adapt to new sizing of NComboBox 2025-09-07 21:24:53 -04:00
LemmyCook
a68b3f49b0 NComboBox: better sizing 2025-09-07 21:13:45 -04:00
LemmyCook
e03042c411 NCheckBox: fast animation speed like the others 2025-09-07 21:13:31 -04:00
LemmyCook
3065bec6c9 BarSectionEditor: Buttons are now easier to click + reverted back to 5 basic colors 2025-09-07 20:03:14 -04:00
LemmyCook
dae1d12b6f NPill: smoother animation when opening and closing (no instant width jump) 2025-09-07 18:50:21 -04:00
LemmyCook
c4846cd977 NPill: improved text centering 2025-09-07 18:42:39 -04:00
LemmyCook
f95c9b76d4 Clock fully migrated to new user settings 2025-09-07 14:40:33 -04:00
LemmyCook
fb01392bc3 Settings: cleanup 2025-09-07 14:29:14 -04:00
LemmyCook
498ee478e7 Settings: centralized migration to user settings. wip 2025-09-07 14:28:50 -04:00
LemmyCook
ba33451957 Network/Wi-Fi: many fixes and robustness improvements
- proper detection when password is wrong
- prevent a new connection while already connecting to a network
- new mechanism to skip scan results if a new scan is incoming (avoid UI
discrepancies)
2025-09-07 13:02:13 -04:00
Ly-sec
d6e253fe7f Replace some double with real 2025-09-07 16:25:11 +02:00
Ly-sec
c32a8a863a WeatherTab: remove useless divider 2025-09-07 16:22:07 +02:00
LemmyCook
4ba0f8d958 Network: Scanning use a more reliable backward parsing + added logs to figure potential bug. 2025-09-07 10:06:53 -04:00
Ly-sec
e4e2ed41b4 Rename TimeWeatherTab to WeatherTab, remove Time settings from said tab
WeatherTab: renamed from TimeWeatherTab, remove Time settings
Time: Time/Date is now widget driven
2025-09-07 15:48:16 +02:00
Ly-sec
888ba108e0 Edit NButton alignment 2025-09-07 15:33:47 +02:00
Ly-sec
c14eb95dba BarWidgetSettingsDialog: remove DND, rename Save to Apply 2025-09-07 15:20:24 +02:00
Ly-sec
dc0ef93680 Notification: DND just uses Settings.data.notifications.doNotDisturb now 2025-09-07 15:18:12 +02:00
Ly-sec
a2ea3c116d NotificationHistory: better display for unread notifications 2025-09-07 15:09:30 +02:00
Ly-sec
4578aad0bc NotificationHistory: properly hook up the unread counter 2025-09-07 14:57:09 +02:00
Ly-sec
57448f100c bartab-overhaul: initial commit 2025-09-07 14:48:20 +02:00
Ly-sec
835f88d71e Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-07 12:51:15 +02:00
Ly-sec
291d919b9f Notification: add -i support 2025-09-07 12:51:13 +02:00
LemmyCook
adac96ee84 SidePanel: proper height computation 2025-09-07 00:49:59 -04:00
LemmyCook
9010a1668b SysStat: fixed warning. cant assign undefined to real 2025-09-07 00:43:57 -04:00
LemmyCook
f27608947c Settings: slightly more compact tabs 2025-09-07 00:06:58 -04:00
LemmyCook
fb2d42da57 SysStat Service: less log on intel CPU 2025-09-06 23:47:17 -04:00
LemmyCook
2bc1d53b18 SysStat Service: Porting code to JS/QML instead of an external bash 2025-09-06 23:43:00 -04:00
LemmyCook
36d3a50f21 Brightness: brings back realtime brightness monitoring for internal(laptop) display.
The pill will open and show the change in real time
2025-09-06 19:27:32 -04:00
LemmyCook
9bc6479c92 NPill: for battery use a very light outline around the icon 2025-09-06 18:34:44 -04:00
LemmyCook
56993d3c00 Battery: Minimal BatteryService which only serve an appropriate icon. Trying different icons rotated 90 degrees to the left. 2025-09-06 18:16:59 -04:00
LemmyCook
86c6135def Network/Wi-Fi: improvements
- Always check for ethernet status every 30s. Should not affect battery
life.
- Less aggressive scan intervals to give more times for slow adapters.
2025-09-06 16:11:16 -04:00
1311192235 make NSpinBox border width not hardcoded 2025-09-06 22:00:38 +02:00
b03f877c27 Merge branch 'main' into never 2025-09-06 21:56:07 +02:00
LemmyCook
1bb1015fdf Dock: one tooltip per app instead of a shared tooltip. avoid a few glitches when hovering. 2025-09-06 15:25:57 -04:00
LemmyCook
ac43b6d78a Dock: autoformatting 2025-09-06 15:19:06 -04:00
LemmyCook
809f16c27e Dock: improvements, new animations, always float, better look. 2025-09-06 15:18:53 -04:00
a4a19f942c make all borders the same width 2025-09-05 19:58:33 +02:00
eeb67364e6 Add new color theme 2025-09-05 19:57:12 +02:00
74 changed files with 2548 additions and 1140 deletions

View file

@ -0,0 +1,35 @@
{
"dark": {
"mPrimary": "#7fccff",
"mOnPrimary": "#000000",
"mSecondary": "#4c4c4c",
"mOnSecondary": "#ffffff",
"mTertiary": "#2c2c2c",
"mOnTertiary": "#ffffff",
"mError": "#ff4c4c",
"mOnError": "#ffffff",
"mSurface": "#191919",
"mOnSurface": "#ffffff",
"mSurfaceVariant": "#191919",
"mOnSurfaceVariant": "#ffffff",
"mOutline": "#4c4c4c",
"mShadow": "#000000"
},
"light": {
"mPrimary": "#7fccff",
"mOnPrimary": "#000000",
"mSecondary": "#4c4c4c",
"mOnSecondary": "#ffffff",
"mTertiary": "#2c2c2c",
"mOnTertiary": "#ffffff",
"mError": "#ff4c4c",
"mOnError": "#ffffff",
"mSurface": "#191919",
"mOnSurface": "#ffffff",
"mSurfaceVariant": "#191919",
"mOnSurfaceVariant": "#ffffff",
"mOutline": "#4c4c4c",
"mShadow": "#000000"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View file

@ -9,3 +9,24 @@ for i in {1..8}; do
done done
echo "All notifications sent!" echo "All notifications sent!"
# Additional tests for icon/image handling
if command -v notify-send >/dev/null 2>&1; then
echo "Sending icon/image tests..."
# 1) Themed icon name
notify-send -i dialog-information "Icon name test" "Should resolve from theme (dialog-information)"
# 2) Absolute path if a sample image exists
SAMPLE_IMG="/usr/share/pixmaps/debian-logo.png"
if [ -f "$SAMPLE_IMG" ]; then
notify-send -i "$SAMPLE_IMG" "Absolute path test" "Should show the provided image path"
fi
# 3) file:// URL form
if [ -f "$SAMPLE_IMG" ]; then
notify-send -i "file://$SAMPLE_IMG" "file:// URL test" "Should display after stripping scheme"
fi
echo "Icon/image tests sent!"
fi

View file

@ -102,7 +102,7 @@ Singleton {
// FileView to load custom colors data from colors.json // FileView to load custom colors data from colors.json
FileView { FileView {
id: customColorsFile id: customColorsFile
path: Settings.configDir + "colors.json" path: Settings.directoriesCreated ? (Settings.configDir + "colors.json") : ""
watchChanges: true watchChanges: true
onFileChanged: { onFileChanged: {
Logger.log("Color", "Reloading colors from disk") Logger.log("Color", "Reloading colors from disk")
@ -112,6 +112,13 @@ Singleton {
Logger.log("Color", "Writing colors to disk") Logger.log("Color", "Writing colors to disk")
writeAdapter() writeAdapter()
} }
// Trigger initial load when path changes from empty to actual path
onPathChanged: {
if (path === Settings.configDir + "colors.json") {
reload()
}
}
onLoadFailed: function (error) { onLoadFailed: function (error) {
if (error.toString().includes("No such file") || error === 2) { if (error.toString().includes("No such file") || error === 2) {
// File doesn't exist, create it with default values // File doesn't exist, create it with default values

View file

@ -26,11 +26,13 @@ Singleton {
property string defaultWallpapersDirectory: Quickshell.env("HOME") + "/Pictures/Wallpapers" property string defaultWallpapersDirectory: Quickshell.env("HOME") + "/Pictures/Wallpapers"
property string defaultVideosDirectory: Quickshell.env("HOME") + "/Videos" property string defaultVideosDirectory: Quickshell.env("HOME") + "/Videos"
property string defaultLocation: "Tokyo" property string defaultLocation: "Tokyo"
property string defaultWallpaper: Quickshell.shellDir + "/Assets/Wallpaper/noctalia.png"
// Used to access via Settings.data.xxx.yyy // Used to access via Settings.data.xxx.yyy
readonly property alias data: adapter readonly property alias data: adapter
property bool isLoaded: false property bool isLoaded: false
property bool directoriesCreated: false
// Signal emitted when settings are loaded after startupcale changes // Signal emitted when settings are loaded after startupcale changes
signal settingsLoaded signal settingsLoaded
@ -71,34 +73,93 @@ Singleton {
// ----------------------------------------------------- // -----------------------------------------------------
// If the settings structure has changed, ensure // If the settings structure has changed, ensure
// backward compatibility // backward compatibility by upgrading the settings
function upgradeSettingsData() { function upgradeSettingsData() {
for (var i = 0; i < adapter.bar.widgets.left.length; i++) {
var obj = adapter.bar.widgets.left[i] const sections = ["left", "center", "right"]
if (typeof obj === "string") {
adapter.bar.widgets.left[i] = { // -----------------
"id": obj // 1st. check our settings are not super old, when we only had the widget type as a plain string
for (var s = 0; s < sections.length; s++) {
const sectionName = sections[s]
for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) {
var widget = adapter.bar.widgets[sectionName][i]
if (typeof widget === "string") {
adapter.bar.widgets[sectionName][i] = {
"id": widget
}
} }
} }
} }
for (var i = 0; i < adapter.bar.widgets.center.length; i++) {
var obj = adapter.bar.widgets.center[i] // -----------------
if (typeof obj === "string") { // 2nd. migrate global settings to user settings
adapter.bar.widgets.center[i] = { for (var s = 0; s < sections.length; s++) {
"id": obj const sectionName = sections[s]
for (var i = 0; i < adapter.bar.widgets[sectionName].length; i++) {
var widget = adapter.bar.widgets[sectionName][i]
// Check if widget registry supports user settings, if it does not, then there is nothing to do
const reg = BarWidgetRegistry.widgetMetadata[widget.id]
if ((reg === undefined) || (reg.allowUserSettings === undefined) || !reg.allowUserSettings) {
continue
} }
}
} // Check that the widget was not previously migrated and skip if necessary
for (var i = 0; i < adapter.bar.widgets.right.length; i++) { const keys = Object.keys(widget)
var obj = adapter.bar.widgets.right[i] if (keys.length > 1) {
if (typeof obj === "string") { continue
adapter.bar.widgets.right[i] = {
"id": obj
} }
migrateWidget(widget)
Logger.log("Settings", JSON.stringify(widget))
} }
} }
} }
// -----------------------------------------------------
function migrateWidget(widget) {
Logger.log("Settings", `Migrating '${widget.id}' widget`)
switch (widget.id) {
case "ActiveWindow":
widget.showIcon = adapter.bar.showActiveWindowIcon
break
case "Battery":
widget.alwaysShowPercentage = adapter.bar.alwaysShowBatteryPercentage
break
case "Brightness":
widget.alwaysShowPercentage = BarWidgetRegistry.widgetMetadata[widget.id].alwaysShowPercentage
break
case "Clock":
widget.showDate = adapter.location.showDateWithClock
widget.use12HourClock = adapter.location.use12HourClock
widget.reverseDayMonth = adapter.location.reverseDayMonth
widget.showSeconds = BarWidgetRegistry.widgetMetadata[widget.id].showSeconds
break
case "MediaMini":
widget.showAlbumArt = adapter.audio.showMiniplayerAlbumArt
widget.showVisualizer = adapter.audio.showMiniplayerCava
widget.visualizerType = BarWidgetRegistry.widgetMetadata[widget.id].visualizerType
break
case "NotificationHistory":
widget.showUnreadBadge = BarWidgetRegistry.widgetMetadata[widget.id].showUnreadBadge
widget.hideWhenZero = BarWidgetRegistry.widgetMetadata[widget.id].hideWhenZero
break
case "SidePanelToggle":
widget.useDistroLogo = adapter.bar.useDistroLogo
break
case "SystemMonitor":
widget.showNetworkStats = adapter.bar.showNetworkStats
break
case "Volume":
widget.alwaysShowPercentage = BarWidgetRegistry.widgetMetadata[widget.id].alwaysShowPercentage
break
case "Workspace":
widget.labelMode = adapter.bar.showWorkspaceLabel
break
}
}
// ----------------------------------------------------- // -----------------------------------------------------
// Kickoff essential services // Kickoff essential services
function kickOffServices() { function kickOffServices() {
@ -117,14 +178,15 @@ Singleton {
} }
// ----------------------------------------------------- // -----------------------------------------------------
Item { // Ensure directories exist before FileView tries to read files
Component.onCompleted: { Component.onCompleted: {
// ensure settings dir exists
Quickshell.execDetached(["mkdir", "-p", configDir])
Quickshell.execDetached(["mkdir", "-p", cacheDir])
Quickshell.execDetached(["mkdir", "-p", cacheDirImages])
// ensure settings dir exists // Mark directories as created and trigger file loading
Quickshell.execDetached(["mkdir", "-p", configDir]) directoriesCreated = true
Quickshell.execDetached(["mkdir", "-p", cacheDir])
Quickshell.execDetached(["mkdir", "-p", cacheDirImages])
}
} }
// Don't write settings to disk immediately // Don't write settings to disk immediately
@ -138,12 +200,16 @@ Singleton {
FileView { FileView {
id: settingsFileView id: settingsFileView
path: settingsFile path: directoriesCreated ? settingsFile : ""
watchChanges: true watchChanges: true
onFileChanged: reload() onFileChanged: reload()
onAdapterUpdated: saveTimer.start() onAdapterUpdated: saveTimer.start()
Component.onCompleted: function () {
reload() // Trigger initial load when path changes from empty to actual path
onPathChanged: {
if (path === settingsFile) {
reload()
}
} }
onLoaded: function () { onLoaded: function () {
if (!isLoaded) { if (!isLoaded) {
@ -174,15 +240,16 @@ Singleton {
// bar // bar
property JsonObject bar: JsonObject { property JsonObject bar: JsonObject {
property string position: "top" // Possible values: "top", "bottom" property string position: "top" // "top" or "bottom"
property bool showActiveWindowIcon: true
property bool alwaysShowBatteryPercentage: false
property bool showNetworkStats: false
property real backgroundOpacity: 1.0 property real backgroundOpacity: 1.0
property bool useDistroLogo: false
property string showWorkspaceLabel: "none"
property list<string> monitors: [] property list<string> monitors: []
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 // Widget configuration for modular bar system
property JsonObject widgets property JsonObject widgets
widgets: JsonObject { widgets: JsonObject {
@ -236,9 +303,10 @@ Singleton {
property JsonObject location: JsonObject { property JsonObject location: JsonObject {
property string name: defaultLocation property string name: defaultLocation
property bool useFahrenheit: false property bool useFahrenheit: false
property bool reverseDayMonth: false
property bool use12HourClock: false property bool reverseDayMonth: false // TODO: delete
property bool showDateWithClock: false property bool use12HourClock: false // TODO: delete
property bool showDateWithClock: false // TODO: delete
} }
// screen recorder // screen recorder
@ -267,6 +335,7 @@ Singleton {
property int transitionDuration: 1500 // 1500 ms property int transitionDuration: 1500 // 1500 ms
property string transitionType: "random" property string transitionType: "random"
property real transitionEdgeSmoothness: 0.05 property real transitionEdgeSmoothness: 0.05
property string defaultWallpaper: root.defaultWallpaper
property list<var> monitors: [] property list<var> monitors: []
} }
@ -299,25 +368,27 @@ Singleton {
property JsonObject notifications: JsonObject { property JsonObject notifications: JsonObject {
property bool doNotDisturb: false property bool doNotDisturb: false
property list<string> monitors: [] property list<string> monitors: []
// Last time the user opened the notification history (ms since epoch)
property real lastSeenTs: 0
} }
// audio // audio
property JsonObject audio: JsonObject { property JsonObject audio: JsonObject {
property bool showMiniplayerAlbumArt: false
property bool showMiniplayerCava: false
property string visualizerType: "linear"
property int volumeStep: 5 property int volumeStep: 5
property int cavaFrameRate: 60 property int cavaFrameRate: 60
// MPRIS controls property string visualizerType: "linear"
property list<string> mprisBlacklist: [] property list<string> mprisBlacklist: []
property string preferredPlayer: "" property string preferredPlayer: ""
property bool showMiniplayerAlbumArt: false // TODO: delete
property bool showMiniplayerCava: false // TODO: delete
} }
// ui // ui
property JsonObject ui: JsonObject { property JsonObject ui: JsonObject {
property string fontDefault: "Roboto" // Default font for all text property string fontDefault: "Roboto"
property string fontFixed: "DejaVu Sans Mono" // Fixed width font for terminal property string fontFixed: "DejaVu Sans Mono"
property string fontBillboard: "Inter" // Large bold font for clocks and prominent displays property string fontBillboard: "Inter"
property list<var> monitorsScaling: [] property list<var> monitorsScaling: []
property bool idleInhibitorEnabled: false property bool idleInhibitorEnabled: false
} }

View file

@ -36,8 +36,8 @@ Singleton {
property int radiusL: 20 * Settings.data.general.radiusRatio property int radiusL: 20 * Settings.data.general.radiusRatio
// Border // Border
property int borderS: 1 property int borderS: 3
property int borderM: 2 property int borderM: 3
property int borderL: 3 property int borderL: 3
// Margins (for margins and spacing) // Margins (for margins and spacing)
@ -60,6 +60,7 @@ Singleton {
property int animationFast: Math.round(150 * Settings.data.general.animationSpeed) property int animationFast: Math.round(150 * Settings.data.general.animationSpeed)
property int animationNormal: Math.round(300 * Settings.data.general.animationSpeed) property int animationNormal: Math.round(300 * Settings.data.general.animationSpeed)
property int animationSlow: Math.round(450 * Settings.data.general.animationSpeed) property int animationSlow: Math.round(450 * Settings.data.general.animationSpeed)
property int animationSlowest: Math.round(750 * Settings.data.general.animationSpeed)
// Dimensions // Dimensions
property int barHeight: 36 property int barHeight: 36

View file

@ -9,52 +9,38 @@ Singleton {
id: root id: root
property var date: new Date() property var date: new Date()
property string time: {
let timeFormat = Settings.data.location.use12HourClock ? "h:mm AP" : "HH:mm"
let timeString = Qt.formatDateTime(date, timeFormat)
if (Settings.data.location.showDateWithClock) { // Returns a Unix Timestamp (in seconds)
let dayName = date.toLocaleDateString(Qt.locale(), "ddd") readonly property int timestamp: {
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1) return Math.floor(date / 1000)
let day = date.getDate()
let month = date.toLocaleDateString(Qt.locale(), "MMM")
return timeString + " - " + (Settings.data.location.reverseDayMonth ? `${dayName}, ${month} ${day}` : `${dayName}, ${day} ${month}`)
}
return timeString
} }
readonly property string dateString: {
function formatDate(reverseDayMonth = true) {
let now = date let now = date
let dayName = now.toLocaleDateString(Qt.locale(), "ddd") let dayName = now.toLocaleDateString(Qt.locale(), "ddd")
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1) dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
let day = now.getDate() let day = now.getDate()
let suffix let suffix
if (day > 3 && day < 21) if (day > 3 && day < 21)
suffix = 'th' suffix = 'th'
else else
switch (day % 10) { switch (day % 10) {
case 1: case 1:
suffix = "st" suffix = "st"
break break
case 2: case 2:
suffix = "nd" suffix = "nd"
break break
case 3: case 3:
suffix = "rd" suffix = "rd"
break break
default: default:
suffix = "th" suffix = "th"
} }
let month = now.toLocaleDateString(Qt.locale(), "MMMM") let month = now.toLocaleDateString(Qt.locale(), "MMMM")
let year = now.toLocaleDateString(Qt.locale(), "yyyy") let year = now.toLocaleDateString(Qt.locale(), "yyyy")
return `${dayName}, `
+ (Settings.data.location.reverseDayMonth ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`)
}
// Returns a Unix Timestamp (in seconds) return `${dayName}, ` + (reverseDayMonth ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`)
readonly property int timestamp: {
return Math.floor(date / 1000)
} }

View file

@ -3,6 +3,8 @@ import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import qs.Commons import qs.Commons
import qs.Services import qs.Services
import qs.Modules.SettingsPanel
import qs.Widgets
Variants { Variants {
id: backgroundVariants id: backgroundVariants
@ -20,6 +22,8 @@ Variants {
// Internal state management // Internal state management
property string transitionType: "fade" property string transitionType: "fade"
property real transitionProgress: 0 property real transitionProgress: 0
// Scaling support for widgets that rely on it
property real scaling: ScalingService.getScreenScale(screen)
readonly property real edgeSmoothness: Settings.data.wallpaper.transitionEdgeSmoothness readonly property real edgeSmoothness: Settings.data.wallpaper.transitionEdgeSmoothness
readonly property var allTransitions: WallpaperService.allTransitions readonly property var allTransitions: WallpaperService.allTransitions
@ -87,6 +91,15 @@ Variants {
left: true left: true
} }
Connections {
target: ScalingService
function onScaleChanged(screenName, scale) {
if ((screen !== null) && (screenName === screen.name)) {
scaling = scale
}
}
}
Timer { Timer {
id: debounceTimer id: debounceTimer
interval: 333 interval: 333

View file

@ -76,6 +76,7 @@ Variants {
widgetProps: { widgetProps: {
"screen": root.modelData || null, "screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen), "scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"barSection": parent.objectName, "barSection": parent.objectName,
"sectionWidgetIndex": index, "sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.left.length "sectionWidgetsCount": Settings.data.bar.widgets.left.length
@ -103,6 +104,7 @@ Variants {
widgetProps: { widgetProps: {
"screen": root.modelData || null, "screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen), "scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"barSection": parent.objectName, "barSection": parent.objectName,
"sectionWidgetIndex": index, "sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.center.length "sectionWidgetsCount": Settings.data.bar.widgets.center.length
@ -131,6 +133,7 @@ Variants {
widgetProps: { widgetProps: {
"screen": root.modelData || null, "screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen), "scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"barSection": parent.objectName, "barSection": parent.objectName,
"sectionWidgetIndex": index, "sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.right.length "sectionWidgetsCount": Settings.data.bar.widgets.right.length

View file

@ -12,6 +12,27 @@ RowLayout {
id: root id: root
property ShellScreen screen property ShellScreen screen
property real scaling: 1.0 property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string barSection: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property bool showIcon: (widgetSettings.showIcon !== undefined) ? widgetSettings.showIcon : widgetMetadata.showIcon
readonly property real minWidth: 160 readonly property real minWidth: 160
readonly property real maxWidth: 400 readonly property real maxWidth: 400
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
@ -74,7 +95,7 @@ RowLayout {
Layout.preferredWidth: Style.fontSizeL * scaling * 1.2 Layout.preferredWidth: Style.fontSizeL * scaling * 1.2
Layout.preferredHeight: Style.fontSizeL * scaling * 1.2 Layout.preferredHeight: Style.fontSizeL * scaling * 1.2
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
visible: getTitle() !== "" && Settings.data.bar.showActiveWindowIcon visible: getTitle() !== "" && showIcon
IconImage { IconImage {
id: windowIcon id: windowIcon

View file

@ -11,11 +11,42 @@ Item {
property ShellScreen screen property ShellScreen screen
property real scaling: 1.0 property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string barSection: "" property string barSection: ""
property int sectionWidgetIndex: 0 property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0 property int sectionWidgetsCount: 0
// Track if we've already notified to avoid spam property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
// Resolve settings: try user settings or defaults from BarWidgetRegistry
readonly property bool alwaysShowPercentage: widgetSettings.alwaysShowPercentage
!== undefined ? widgetSettings.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage
readonly property real warningThreshold: widgetSettings.warningThreshold
!== undefined ? widgetSettings.warningThreshold : widgetMetadata.warningThreshold
// Test mode
readonly property bool testMode: false
readonly property int testPercent: 50
readonly property bool testCharging: true
// Main properties
readonly property var battery: UPower.displayDevice
readonly property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery
&& battery.isPresent)
readonly property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0)
readonly property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false)
property bool hasNotifiedLowBattery: false property bool hasNotifiedLowBattery: false
implicitWidth: pill.width implicitWidth: pill.width
@ -23,15 +54,14 @@ Item {
// Helper to evaluate and possibly notify // Helper to evaluate and possibly notify
function maybeNotify(percent, charging) { function maybeNotify(percent, charging) {
const p = Math.round(percent) // Only notify once we are a below threshold
// Only notify exactly at 15%, not at 0% or any other percentage if (!charging && !root.hasNotifiedLowBattery && percent <= warningThreshold) {
if (!charging && p === 15 && !root.hasNotifiedLowBattery) { root.hasNotifiedLowBattery = true
// Maybe go with toast ?
Quickshell.execDetached( Quickshell.execDetached(
["notify-send", "-u", "critical", "-i", "battery-caution", "Low Battery", `Battery is at ${p}%. Please connect charger.`]) ["notify-send", "-u", "critical", "-i", "battery-caution", "Low Battery", `Battery is at ${p}%. Please connect charger.`])
root.hasNotifiedLowBattery = true } else if (root.hasNotifiedLowBattery && (charging || percent > warningThreshold + 5)) {
} // Reset when charging starts or when battery recovers 5% above threshold
// Reset when charging starts or when battery recovers above 20%
if (charging || p > 20) {
root.hasNotifiedLowBattery = false root.hasNotifiedLowBattery = false
} }
} }
@ -40,19 +70,10 @@ Item {
Connections { Connections {
target: UPower.displayDevice target: UPower.displayDevice
function onPercentageChanged() { function onPercentageChanged() {
let battery = UPower.displayDevice
let isReady = battery && battery.ready && battery.isLaptopBattery && battery.isPresent
let percent = isReady ? (battery.percentage * 100) : 0
let charging = isReady ? battery.state === UPowerDeviceState.Charging : false
root.maybeNotify(percent, charging) root.maybeNotify(percent, charging)
} }
function onStateChanged() { function onStateChanged() {
let battery = UPower.displayDevice
let isReady = battery && battery.ready && battery.isLaptopBattery && battery.isPresent
let charging = isReady ? battery.state === UPowerDeviceState.Charging : false
// Reset notification flag when charging starts // Reset notification flag when charging starts
if (charging) { if (charging) {
root.hasNotifiedLowBattery = false root.hasNotifiedLowBattery = false
@ -63,76 +84,44 @@ Item {
NPill { NPill {
id: pill id: pill
// Test mode
property bool testMode: false
property int testPercent: 20
property bool testCharging: false
property var battery: UPower.displayDevice
property bool isReady: testMode ? true : (battery && battery.ready && battery.isLaptopBattery && battery.isPresent)
property real percent: testMode ? testPercent : (isReady ? (battery.percentage * 100) : 0)
property bool charging: testMode ? testCharging : (isReady ? battery.state === UPowerDeviceState.Charging : false)
// Choose icon based on charge and charging state
function batteryIcon() {
if (!isReady || !battery.isLaptopBattery)
return "battery_android_alert"
if (charging)
return "battery_android_bolt"
if (percent >= 95)
return "battery_android_full"
// Hardcoded battery symbols
if (percent >= 85)
return "battery_android_6"
if (percent >= 70)
return "battery_android_5"
if (percent >= 55)
return "battery_android_4"
if (percent >= 40)
return "battery_android_3"
if (percent >= 25)
return "battery_android_2"
if (percent >= 10)
return "battery_android_1"
if (percent >= 0)
return "battery_android_0"
}
rightOpen: BarWidgetRegistry.getNPillDirection(root) rightOpen: BarWidgetRegistry.getNPillDirection(root)
icon: batteryIcon() icon: testMode ? BatteryService.getIcon(testPercent, testCharging, true) : BatteryService.getIcon(percent,
charging, isReady)
iconRotation: -90
text: ((isReady && battery.isLaptopBattery) || testMode) ? Math.round(percent) + "%" : "-" text: ((isReady && battery.isLaptopBattery) || testMode) ? Math.round(percent) + "%" : "-"
textColor: charging ? Color.mPrimary : Color.mOnSurface textColor: charging ? Color.mPrimary : Color.mOnSurface
iconCircleColor: Color.mPrimary iconCircleColor: Color.mPrimary
collapsedIconColor: Color.mOnSurface collapsedIconColor: Color.mOnSurface
autoHide: false autoHide: false
forceOpen: isReady && (testMode || battery.isLaptopBattery) && Settings.data.bar.alwaysShowBatteryPercentage forceOpen: isReady && (testMode || battery.isLaptopBattery) && alwaysShowPercentage
disableOpen: (!isReady || (!testMode && !battery.isLaptopBattery)) disableOpen: (!isReady || (!testMode && !battery.isLaptopBattery))
tooltipText: { tooltipText: {
let lines = [] let lines = []
if (testMode) { if (testMode) {
lines.push("Time left: " + Time.formatVagueHumanReadableDuration(12345)) lines.push(`Time left: ${Time.formatVagueHumanReadableDuration(12345)}.`)
return lines.join("\n") return lines.join("\n")
} }
if (!isReady || !battery.isLaptopBattery) { if (!isReady || !battery.isLaptopBattery) {
return "No battery detected" return "No battery detected."
} }
if (battery.timeToEmpty > 0) { if (battery.timeToEmpty > 0) {
lines.push("Time left: " + Time.formatVagueHumanReadableDuration(battery.timeToEmpty)) lines.push(`Time left: ${Time.formatVagueHumanReadableDuration(battery.timeToEmpty)}.`)
} }
if (battery.timeToFull > 0) { if (battery.timeToFull > 0) {
lines.push("Time until full: " + Time.formatVagueHumanReadableDuration(battery.timeToFull)) lines.push(`Time until full: ${Time.formatVagueHumanReadableDuration(battery.timeToFull)}.`)
} }
if (battery.changeRate !== undefined) { if (battery.changeRate !== undefined) {
const rate = battery.changeRate const rate = battery.changeRate
if (rate > 0) { if (rate > 0) {
lines.push(charging ? "Charging rate: " + rate.toFixed(2) + " W" : "Discharging rate: " + rate.toFixed( lines.push(charging ? "Charging rate: " + rate.toFixed(2) + " W." : "Discharging rate: " + rate.toFixed(
2) + " W") 2) + " W.")
} else if (rate < 0) { } else if (rate < 0) {
lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W") lines.push("Discharging rate: " + Math.abs(rate).toFixed(2) + " W.")
} else { } else {
lines.push("Estimating...") lines.push("Estimating...")
} }
} else { } else {
lines.push(charging ? "Charging" : "Discharging") lines.push(charging ? "Charging." : "Discharging.")
} }
if (battery.healthPercentage !== undefined && battery.healthPercentage > 0) { if (battery.healthPercentage !== undefined && battery.healthPercentage > 0) {
lines.push("Health: " + Math.round(battery.healthPercentage) + "%") lines.push("Health: " + Math.round(battery.healthPercentage) + "%")

View file

@ -10,10 +10,28 @@ Item {
property ShellScreen screen property ShellScreen screen
property real scaling: 1.0 property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string barSection: "" property string barSection: ""
property int sectionWidgetIndex: 0 property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0 property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property bool userAlwaysShowPercentage: (widgetSettings.alwaysShowPercentage
!== undefined) ? widgetSettings.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage
// Used to avoid opening the pill on Quickshell startup // Used to avoid opening the pill on Quickshell startup
property bool firstBrightnessReceived: false property bool firstBrightnessReceived: false
@ -37,28 +55,26 @@ Item {
target: getMonitor() target: getMonitor()
ignoreUnknownSignals: true ignoreUnknownSignals: true
function onBrightnessUpdated() { function onBrightnessUpdated() {
Logger.log("Bar-Brightness", "OnBrightnessUpdated") // Ignore if this is the first time we receive an update.
var monitor = getMonitor() // Most likely service just kicked off.
if (!monitor)
return
var currentBrightness = monitor.brightness
// Ignore if this is the first time or if brightness hasn't actually changed
if (!firstBrightnessReceived) { if (!firstBrightnessReceived) {
firstBrightnessReceived = true firstBrightnessReceived = true
monitor.lastBrightness = currentBrightness
return return
} }
// Only show pill if brightness actually changed (not just loaded from settings) pill.show()
if (Math.abs(currentBrightness - monitor.lastBrightness) > 0.1) { hideTimerAfterChange.restart()
pill.show()
}
monitor.lastBrightness = currentBrightness
} }
} }
Timer {
id: hideTimerAfterChange
interval: 2500
running: false
repeat: false
onTriggered: pill.hide()
}
NPill { NPill {
id: pill id: pill
@ -71,6 +87,7 @@ Item {
var monitor = getMonitor() var monitor = getMonitor()
return monitor ? (Math.round(monitor.brightness * 100) + "%") : "" return monitor ? (Math.round(monitor.brightness * 100) + "%") : ""
} }
forceOpen: userAlwaysShowPercentage
tooltipText: { tooltipText: {
var monitor = getMonitor() var monitor = getMonitor()
if (!monitor) if (!monitor)

View file

@ -10,24 +10,70 @@ Rectangle {
property ShellScreen screen property ShellScreen screen
property real scaling: 1.0 property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string barSection: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
// Resolve settings: try user settings or defaults from BarWidgetRegistry
readonly property bool showDate: widgetSettings.showDate !== undefined ? widgetSettings.showDate : widgetMetadata.showDate
readonly property bool use12h: widgetSettings.use12HourClock !== undefined ? widgetSettings.use12HourClock : widgetMetadata.use12HourClock
readonly property bool showSeconds: widgetSettings.showSeconds !== undefined ? widgetSettings.showSeconds : widgetMetadata.showSeconds
readonly property bool reverseDayMonth: widgetSettings.reverseDayMonth
!== undefined ? widgetSettings.reverseDayMonth : widgetMetadata.reverseDayMonth
implicitWidth: clock.width + Style.marginM * 2 * scaling implicitWidth: clock.width + Style.marginM * 2 * scaling
implicitHeight: Math.round(Style.capsuleHeight * scaling) implicitHeight: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling) radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant color: Color.mSurfaceVariant
// Clock Icon with attached calendar // Clock Icon with attached calendar
NClock { NText {
id: clock id: clock
anchors.verticalCenter: parent.verticalCenter text: {
anchors.horizontalCenter: parent.horizontalCenter const now = Time.date
const timeFormat = use12h ? (showSeconds ? "h:mm:ss AP" : "h:mm AP") : (showSeconds ? "HH:mm:ss" : "HH:mm")
const timeString = Qt.formatDateTime(now, timeFormat)
NTooltip { if (showDate) {
id: tooltip let dayName = now.toLocaleDateString(Qt.locale(), "ddd")
text: `${Time.dateString}.` dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
target: clock let day = now.getDate()
positionAbove: Settings.data.bar.position === "bottom" let month = now.toLocaleDateString(Qt.locale(), "MMM")
return timeString + " - " + (reverseDayMonth ? `${dayName}, ${month} ${day}` : `${dayName}, ${day} ${month}`)
}
return timeString
} }
anchors.centerIn: parent
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightBold
}
NTooltip {
id: tooltip
text: `${Time.formatDate(reverseDayMonth)}.`
target: clock
positionAbove: Settings.data.bar.position === "bottom"
}
MouseArea {
id: clockMouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: { onEntered: {
if (!PanelService.getPanel("calendarPanel")?.active) { if (!PanelService.getPanel("calendarPanel")?.active) {
tooltip.show() tooltip.show()

View file

@ -13,11 +13,13 @@ NIconButton {
property var screen property var screen
property real scaling: 1.0 property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string barSection: "" property string barSection: ""
property int sectionWidgetIndex: -1 property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0 property int sectionWidgetsCount: 0
// Get user settings from Settings data property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: { property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase() var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) { if (section && sectionWidgetIndex >= 0) {
@ -30,30 +32,27 @@ NIconButton {
} }
// Use settings or defaults from BarWidgetRegistry // Use settings or defaults from BarWidgetRegistry
readonly property string userIcon: widgetSettings.icon || BarWidgetRegistry.widgetMetadata["CustomButton"].icon readonly property string customIcon: widgetSettings.icon || widgetMetadata.icon
readonly property string userLeftClickExec: widgetSettings.leftClickExec readonly property string leftClickExec: widgetSettings.leftClickExec || widgetMetadata.leftClickExec
|| BarWidgetRegistry.widgetMetadata["CustomButton"].leftClickExec readonly property string rightClickExec: widgetSettings.rightClickExec || widgetMetadata.rightClickExec
readonly property string userRightClickExec: widgetSettings.rightClickExec readonly property string middleClickExec: widgetSettings.middleClickExec || widgetMetadata.middleClickExec
|| BarWidgetRegistry.widgetMetadata["CustomButton"].rightClickExec readonly property bool hasExec: (leftClickExec || rightClickExec || middleClickExec)
readonly property string userMiddleClickExec: widgetSettings.middleClickExec
|| BarWidgetRegistry.widgetMetadata["CustomButton"].middleClickExec
readonly property bool hasExec: (userLeftClickExec || userRightClickExec || userMiddleClickExec)
sizeRatio: 0.8 sizeRatio: 0.8
icon: userIcon icon: customIcon
tooltipText: { tooltipText: {
if (!hasExec) { if (!hasExec) {
return "Custom Button - Configure in settings" return "Custom Button - Configure in settings"
} else { } else {
var lines = [] var lines = []
if (userLeftClickExec !== "") { if (leftClickExec !== "") {
lines.push(`Left click: <i>${userLeftClickExec}</i>.`) lines.push(`Left click: <i>${leftClickExec}</i>.`)
} }
if (userRightClickExec !== "") { if (rightClickExec !== "") {
lines.push(`Right click: <i>${userRightClickExec}</i>.`) lines.push(`Right click: <i>${rightClickExec}</i>.`)
} }
if (userMiddleClickExec !== "") { if (middleClickExec !== "") {
lines.push(`Middle click: <i>${userMiddleClickExec}</i>.`) lines.push(`Middle click: <i>${middleClickExec}</i>.`)
} }
return lines.join("<br/>") return lines.join("<br/>")
} }
@ -61,9 +60,9 @@ NIconButton {
opacity: hasExec ? Style.opacityFull : Style.opacityMedium opacity: hasExec ? Style.opacityFull : Style.opacityMedium
onClicked: { onClicked: {
if (userLeftClickExec) { if (leftClickExec) {
Quickshell.execDetached(["sh", "-c", userLeftClickExec]) Quickshell.execDetached(["sh", "-c", leftClickExec])
Logger.log("CustomButton", `Executing command: ${userLeftClickExec}`) Logger.log("CustomButton", `Executing command: ${leftClickExec}`)
} else if (!hasExec) { } else if (!hasExec) {
// No script was defined, open settings // No script was defined, open settings
var settingsPanel = PanelService.getPanel("settingsPanel") var settingsPanel = PanelService.getPanel("settingsPanel")
@ -73,16 +72,16 @@ NIconButton {
} }
onRightClicked: { onRightClicked: {
if (userRightClickExec) { if (rightClickExec) {
Quickshell.execDetached(["sh", "-c", userRightClickExec]) Quickshell.execDetached(["sh", "-c", rightClickExec])
Logger.log("CustomButton", `Executing command: ${userRightClickExec}`) Logger.log("CustomButton", `Executing command: ${rightClickExec}`)
} }
} }
onMiddleClicked: { onMiddleClicked: {
if (userMiddleClickExec) { if (middleClickExec) {
Quickshell.execDetached(["sh", "-c", userMiddleClickExec]) Quickshell.execDetached(["sh", "-c", middleClickExec])
Logger.log("CustomButton", `Executing command: ${userMiddleClickExec}`) Logger.log("CustomButton", `Executing command: ${middleClickExec}`)
} }
} }
} }

View file

@ -12,9 +12,6 @@ Item {
property ShellScreen screen property ShellScreen screen
property real scaling: 1.0 property real scaling: 1.0
property string barSection: ""
property int sectionWidgetIndex: 0
property int sectionWidgetsCount: 0
// Use the shared service for keyboard layout // Use the shared service for keyboard layout
property string currentLayout: KeyboardLayoutService.currentLayout property string currentLayout: KeyboardLayoutService.currentLayout

View file

@ -12,18 +12,44 @@ RowLayout {
property ShellScreen screen property ShellScreen screen
property real scaling: 1.0 property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string barSection: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property bool showAlbumArt: (widgetSettings.showAlbumArt
!== undefined) ? widgetSettings.showAlbumArt : widgetMetadata.showAlbumArt
readonly property bool showVisualizer: (widgetSettings.showVisualizer
!== undefined) ? widgetSettings.showVisualizer : widgetMetadata.showVisualizer
readonly property string visualizerType: (widgetSettings.visualizerType !== undefined && widgetSettings.visualizerType
!== "") ? widgetSettings.visualizerType : widgetMetadata.visualizerType
readonly property real minWidth: 160 readonly property real minWidth: 160
readonly property real maxWidth: 400 readonly property real maxWidth: 400
function getTitle() {
return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "")
}
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
visible: MediaService.currentPlayer !== null && MediaService.canPlay visible: MediaService.currentPlayer !== null && MediaService.canPlay
Layout.preferredWidth: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0 Layout.preferredWidth: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0
function getTitle() {
return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "")
}
// A hidden text element to safely measure the full title width // A hidden text element to safely measure the full title width
NText { NText {
id: fullTitleMetrics id: fullTitleMetrics
@ -58,8 +84,7 @@ RowLayout {
Loader { Loader {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "linear" active: showVisualizer && visualizerType == "linear" && MediaService.isPlaying
&& MediaService.isPlaying
z: 0 z: 0
sourceComponent: LinearSpectrum { sourceComponent: LinearSpectrum {
@ -74,8 +99,7 @@ RowLayout {
Loader { Loader {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "mirrored" active: showVisualizer && visualizerType == "mirrored" && MediaService.isPlaying
&& MediaService.isPlaying
z: 0 z: 0
sourceComponent: MirroredSpectrum { sourceComponent: MirroredSpectrum {
@ -90,8 +114,7 @@ RowLayout {
Loader { Loader {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "wave" active: showVisualizer && visualizerType == "wave" && MediaService.isPlaying
&& MediaService.isPlaying
z: 0 z: 0
sourceComponent: WaveSpectrum { sourceComponent: WaveSpectrum {
@ -115,12 +138,12 @@ RowLayout {
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
visible: !Settings.data.audio.showMiniplayerAlbumArt && getTitle() !== "" && !trackArt.visible visible: !showAlbumArt && getTitle() !== "" && !trackArt.visible
} }
ColumnLayout { ColumnLayout {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
visible: Settings.data.audio.showMiniplayerAlbumArt visible: showAlbumArt
spacing: 0 spacing: 0
Item { Item {

View file

@ -12,10 +12,28 @@ Item {
property ShellScreen screen property ShellScreen screen
property real scaling: 1.0 property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string barSection: "" property string barSection: ""
property int sectionWidgetIndex: 0 property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0 property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property bool alwaysShowPercentage: (widgetSettings.alwaysShowPercentage
!== undefined) ? widgetSettings.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage
// Used to avoid opening the pill on Quickshell startup // Used to avoid opening the pill on Quickshell startup
property bool firstInputVolumeReceived: false property bool firstInputVolumeReceived: false
property int wheelAccumulator: 0 property int wheelAccumulator: 0
@ -78,6 +96,7 @@ Item {
collapsedIconColor: Color.mOnSurface collapsedIconColor: Color.mOnSurface
autoHide: false // Important to be false so we can hover as long as we want autoHide: false // Important to be false so we can hover as long as we want
text: Math.floor(AudioService.inputVolume * 100) + "%" text: Math.floor(AudioService.inputVolume * 100) + "%"
forceOpen: alwaysShowPercentage
tooltipText: "Microphone: " + Math.round(AudioService.inputVolume * 100) tooltipText: "Microphone: " + Math.round(AudioService.inputVolume * 100)
+ "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute." + "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute."

View file

@ -4,6 +4,7 @@ import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import qs.Commons import qs.Commons
import qs.Modules.SettingsPanel
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@ -14,7 +15,6 @@ NIconButton {
property real scaling: 1.0 property real scaling: 1.0
sizeRatio: 0.8 sizeRatio: 0.8
colorBg: Color.mSurfaceVariant colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface colorFg: Color.mOnSurface
colorBorder: Color.transparent colorBorder: Color.transparent
@ -26,7 +26,7 @@ NIconButton {
onRightClicked: { onRightClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel") var settingsPanel = PanelService.getPanel("settingsPanel")
settingsPanel.requestedTab = SettingsPanel.Tab.Display settingsPanel.requestedTab = SettingsPanel.Tab.Brightness
settingsPanel.open(screen) settingsPanel.open(screen)
} }
} }

View file

@ -13,6 +13,45 @@ NIconButton {
property ShellScreen screen property ShellScreen screen
property real scaling: 1.0 property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string barSection: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property bool showUnreadBadge: (widgetSettings.showUnreadBadge
!== undefined) ? widgetSettings.showUnreadBadge : widgetMetadata.showUnreadBadge
readonly property bool hideWhenZero: (widgetSettings.hideWhenZero
!== undefined) ? widgetSettings.hideWhenZero : widgetMetadata.hideWhenZero
function lastSeenTs() {
return Settings.data.notifications?.lastSeenTs || 0
}
function computeUnreadCount() {
var since = lastSeenTs()
var count = 0
var model = NotificationService.historyModel
for (var i = 0; i < model.count; i++) {
var item = model.get(i)
var ts = item.timestamp instanceof Date ? item.timestamp.getTime() : item.timestamp
if (ts > since)
count++
}
return count
}
sizeRatio: 0.8 sizeRatio: 0.8
icon: Settings.data.notifications.doNotDisturb ? "notifications_off" : "notifications" icon: Settings.data.notifications.doNotDisturb ? "notifications_off" : "notifications"
tooltipText: Settings.data.notifications.doNotDisturb ? "Notification history.\nRight-click to disable 'Do Not Disturb'." : "Notification history.\nRight-click to enable 'Do Not Disturb'." tooltipText: Settings.data.notifications.doNotDisturb ? "Notification history.\nRight-click to disable 'Do Not Disturb'." : "Notification history.\nRight-click to enable 'Do Not Disturb'."
@ -21,7 +60,40 @@ NIconButton {
colorBorder: Color.transparent colorBorder: Color.transparent
colorBorderHover: Color.transparent colorBorderHover: Color.transparent
onClicked: PanelService.getPanel("notificationHistoryPanel")?.toggle(screen, this) onClicked: {
var panel = PanelService.getPanel("notificationHistoryPanel")
panel?.toggle(screen, this)
Settings.data.notifications.lastSeenTs = Time.timestamp * 1000
}
onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
Loader {
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: -4 * scaling
anchors.topMargin: -4 * scaling
z: 2
active: showUnreadBadge && (!hideWhenZero || computeUnreadCount() > 0)
sourceComponent: Rectangle {
id: badge
readonly property int count: computeUnreadCount()
readonly property string label: count <= 99 ? String(count) : "99+"
readonly property real pad: 8 * scaling
height: 16 * scaling
width: Math.max(height, textNode.implicitWidth + pad)
radius: height / 2
color: Color.mError
border.color: Color.mSurface
border.width: 1
visible: count > 0 || !hideWhenZero
NText {
id: textNode
anchors.centerIn: parent
text: badge.label
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnError
}
}
}
} }

View file

@ -1,3 +1,4 @@
import QtQuick
import Quickshell import Quickshell
import Quickshell.Widgets import Quickshell.Widgets
import QtQuick.Effects import QtQuick.Effects
@ -11,7 +12,28 @@ NIconButton {
property ShellScreen screen property ShellScreen screen
property real scaling: 1.0 property real scaling: 1.0
icon: Settings.data.bar.useDistroLogo ? "" : "widgets" // Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string barSection: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property bool useDistroLogo: (widgetSettings.useDistroLogo
!== undefined) ? widgetSettings.useDistroLogo : widgetMetadata.useDistroLogo
icon: useDistroLogo ? "" : "widgets"
tooltipText: "Open side panel." tooltipText: "Open side panel."
sizeRatio: 0.8 sizeRatio: 0.8
@ -24,14 +46,13 @@ NIconButton {
onClicked: PanelService.getPanel("sidePanel")?.toggle(screen, this) onClicked: PanelService.getPanel("sidePanel")?.toggle(screen, this)
onRightClicked: PanelService.getPanel("settingsPanel")?.toggle(screen) onRightClicked: PanelService.getPanel("settingsPanel")?.toggle(screen)
// When enabled, draw the distro logo instead of the icon glyph
IconImage { IconImage {
id: logo id: logo
anchors.centerIn: parent anchors.centerIn: parent
width: root.width * 0.6 width: root.width * 0.6
height: width height: width
source: Settings.data.bar.useDistroLogo ? DistroLogoService.osLogo : "" source: useDistroLogo ? DistroLogoService.osLogo : ""
visible: false //Settings.data.bar.useDistroLogo && source !== "" visible: useDistroLogo && source !== ""
smooth: true smooth: true
} }

View file

@ -12,11 +12,13 @@ Item {
property var screen property var screen
property real scaling: 1.0 property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string barSection: "" property string barSection: ""
property int sectionWidgetIndex: -1 property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0 property int sectionWidgetsCount: 0
// Get user settings from Settings data - make it reactive property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: { property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase() var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) { if (section && sectionWidgetIndex >= 0) {
@ -29,19 +31,10 @@ Item {
} }
// Use settings or defaults from BarWidgetRegistry // Use settings or defaults from BarWidgetRegistry
readonly property int userWidth: { readonly property int spacerWidth: widgetSettings.width !== undefined ? widgetSettings.width : widgetMetadata.width
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex].width || BarWidgetRegistry.widgetMetadata["Spacer"].width
}
}
return BarWidgetRegistry.widgetMetadata["Spacer"].width
}
// Set the width based on user settings // Set the width based on user settings
implicitWidth: userWidth * scaling implicitWidth: spacerWidth * scaling
implicitHeight: Style.barHeight * scaling implicitHeight: Style.barHeight * scaling
width: implicitWidth width: implicitWidth
height: implicitHeight height: implicitHeight
@ -51,6 +44,6 @@ Item {
anchors.fill: parent anchors.fill: parent
color: Qt.rgba(1, 0, 0, 0.1) // Very subtle red tint color: Qt.rgba(1, 0, 0, 0.1) // Very subtle red tint
visible: Settings.data.general.debugMode || false visible: Settings.data.general.debugMode || false
radius: 2 * scaling radius: Style.radiusXXS * scaling
} }
} }

View file

@ -11,6 +11,34 @@ RowLayout {
property ShellScreen screen property ShellScreen screen
property real scaling: 1.0 property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string barSection: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property bool showCpuUsage: (widgetSettings.showCpuUsage
!== undefined) ? widgetSettings.showCpuUsage : widgetMetadata.showCpuUsage
readonly property bool showCpuTemp: (widgetSettings.showCpuTemp !== undefined) ? widgetSettings.showCpuTemp : widgetMetadata.showCpuTemp
readonly property bool showMemoryUsage: (widgetSettings.showMemoryUsage
!== undefined) ? widgetSettings.showMemoryUsage : widgetMetadata.showMemoryUsage
readonly property bool showMemoryAsPercent: (widgetSettings.showMemoryAsPercent
!== undefined) ? widgetSettings.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent
readonly property bool showNetworkStats: (widgetSettings.showNetworkStats
!== undefined) ? widgetSettings.showNetworkStats : widgetMetadata.showNetworkStats
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
@ -34,6 +62,7 @@ RowLayout {
id: cpuUsageLayout id: cpuUsageLayout
spacing: Style.marginXS * scaling spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
visible: showCpuUsage
NIcon { NIcon {
id: cpuUsageIcon id: cpuUsageIcon
@ -59,6 +88,7 @@ RowLayout {
// spacing is thin here to compensate for the vertical thermometer icon // spacing is thin here to compensate for the vertical thermometer icon
spacing: Style.marginXXS * scaling spacing: Style.marginXXS * scaling
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
visible: showCpuTemp
NIcon { NIcon {
text: "thermometer" text: "thermometer"
@ -81,6 +111,7 @@ RowLayout {
id: memoryUsageLayout id: memoryUsageLayout
spacing: Style.marginXS * scaling spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
visible: showMemoryUsage
NIcon { NIcon {
text: "memory" text: "memory"
@ -88,7 +119,7 @@ RowLayout {
} }
NText { NText {
text: `${SystemStatService.memoryUsageGb}G` text: showMemoryAsPercent ? `${SystemStatService.memPercent}%` : `${SystemStatService.memGb}G`
font.family: Settings.data.ui.fontFixed font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium font.weight: Style.fontWeightMedium
@ -103,7 +134,7 @@ RowLayout {
id: networkDownloadLayout id: networkDownloadLayout
spacing: Style.marginXS * scaling spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
visible: Settings.data.bar.showNetworkStats visible: showNetworkStats
NIcon { NIcon {
text: "download" text: "download"
@ -126,7 +157,7 @@ RowLayout {
id: networkUploadLayout id: networkUploadLayout
spacing: Style.marginXS * scaling spacing: Style.marginXS * scaling
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
visible: Settings.data.bar.showNetworkStats visible: showNetworkStats
NIcon { NIcon {
text: "upload" text: "upload"

View file

@ -15,6 +15,7 @@ Rectangle {
property ShellScreen screen property ShellScreen screen
property real scaling: 1.0 property real scaling: 1.0
readonly property real itemSize: 24 * scaling readonly property real itemSize: 24 * scaling
function onLoaded() { function onLoaded() {

View file

@ -12,10 +12,28 @@ Item {
property ShellScreen screen property ShellScreen screen
property real scaling: 1.0 property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string barSection: "" property string barSection: ""
property int sectionWidgetIndex: 0 property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0 property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property bool alwaysShowPercentage: (widgetSettings.alwaysShowPercentage
!== undefined) ? widgetSettings.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage
// Used to avoid opening the pill on Quickshell startup // Used to avoid opening the pill on Quickshell startup
property bool firstVolumeReceived: false property bool firstVolumeReceived: false
property int wheelAccumulator: 0 property int wheelAccumulator: 0
@ -63,6 +81,7 @@ Item {
collapsedIconColor: Color.mOnSurface collapsedIconColor: Color.mOnSurface
autoHide: false // Important to be false so we can hover as long as we want autoHide: false // Important to be false so we can hover as long as we want
text: Math.floor(AudioService.volume * 100) + "%" text: Math.floor(AudioService.volume * 100) + "%"
forceOpen: alwaysShowPercentage
tooltipText: "Volume: " + Math.round(AudioService.volume * 100) tooltipText: "Volume: " + Math.round(AudioService.volume * 100)
+ "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute." + "%\nLeft click for advanced settings.\nScroll up/down to change volume.\nRight click to toggle mute."

View file

@ -40,6 +40,6 @@ NIconButton {
return "signal_wifi_bad" return "signal_wifi_bad"
} }
} }
tooltipText: "Network / Wi-Fi." tooltipText: "Manage Wi-Fi."
onClicked: PanelService.getPanel("wifiPanel")?.toggle(screen, this) onClicked: PanelService.getPanel("wifiPanel")?.toggle(screen, this)
} }

View file

@ -14,6 +14,26 @@ Item {
property ShellScreen screen property ShellScreen screen
property real scaling: 1.0 property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string barSection: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
var section = barSection.replace("Section", "").toLowerCase()
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property string labelMode: (widgetSettings.labelMode !== undefined) ? widgetSettings.labelMode : widgetMetadata.labelMode
property bool isDestroying: false property bool isDestroying: false
property bool hovered: false property bool hovered: false
@ -22,8 +42,8 @@ Item {
property bool effectsActive: false property bool effectsActive: false
property color effectColor: Color.mPrimary property color effectColor: Color.mPrimary
property int horizontalPadding: Math.round(16 * scaling) property int horizontalPadding: Math.round(Style.marginS * scaling)
property int spacingBetweenPills: Math.round(8 * scaling) property int spacingBetweenPills: Math.round(Style.marginXS * scaling)
signal workspaceChanged(int workspaceId, color accentColor) signal workspaceChanged(int workspaceId, color accentColor)
@ -124,7 +144,7 @@ Item {
Rectangle { Rectangle {
id: workspaceBackground id: workspaceBackground
width: parent.width - Style.marginS * scaling * 2 width: parent.width
height: Math.round(Style.capsuleHeight * scaling) height: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling) radius: Math.round(Style.radiusM * scaling)
@ -145,7 +165,7 @@ Item {
model: localWorkspaces model: localWorkspaces
Item { Item {
id: workspacePillContainer id: workspacePillContainer
height: (Settings.data.bar.showWorkspaceLabel !== "none") ? Math.round(18 * scaling) : Math.round(14 * scaling) height: (labelMode !== "none") ? Math.round(18 * scaling) : Math.round(14 * scaling)
width: root.calculatedWsWidth(model) width: root.calculatedWsWidth(model)
Rectangle { Rectangle {
@ -153,15 +173,13 @@ Item {
anchors.fill: parent anchors.fill: parent
Loader { Loader {
active: (Settings.data.bar.showWorkspaceLabel !== "none") active: (labelMode !== "none")
sourceComponent: Component { sourceComponent: Component {
Text { Text {
// Center horizontally
x: (pill.width - width) / 2 x: (pill.width - width) / 2
// Center vertically accounting for font metrics
y: (pill.height - height) / 2 + (height - contentHeight) / 2 y: (pill.height - height) / 2 + (height - contentHeight) / 2
text: { text: {
if (Settings.data.bar.showWorkspaceLabel === "name" && model.name && model.name.length > 0) { if (labelMode === "name" && model.name && model.name.length > 0) {
return model.name.substring(0, 2) return model.name.substring(0, 2)
} else { } else {
return model.idx.toString() return model.idx.toString()

View file

@ -34,24 +34,28 @@ Variants {
WlrLayershell.namespace: "noctalia-dock" WlrLayershell.namespace: "noctalia-dock"
property bool autoHide: Settings.data.dock.autoHide readonly property bool autoHide: Settings.data.dock.autoHide
property bool hidden: autoHide readonly property int hideDelay: 500
property int hideDelay: 500 readonly property int showDelay: 100
property int showDelay: 100 readonly property int hideAnimationDuration: Style.animationFast
property int hideAnimationDuration: Style.animationFast readonly property int showAnimationDuration: Style.animationFast
property int showAnimationDuration: Style.animationFast readonly property int peekHeight: 7 * scaling
property int peekHeight: 7 * scaling readonly property int fullHeight: dockContainer.height
property int fullHeight: dockContainer.height readonly property int iconSize: 36 * scaling
property int iconSize: 36 readonly property int floatingMargin: 12 * scaling // Margin to make dock float
// Bar positioning properties // Bar detection and positioning properties
property bool barAtBottom: Settings.data.bar.position === "bottom" readonly property bool hasBar: modelData.name ? (Settings.data.bar.monitors.includes(modelData.name)
property int barHeight: barAtBottom ? (Settings.data.bar.height || 30) * scaling : 0 || (Settings.data.bar.monitors.length === 0)) : false
property int dockSpacing: 4 * scaling // Space between dock and bar readonly property bool barAtBottom: hasBar && Settings.data.bar.position === "bottom"
readonly property bool barAtTop: hasBar && Settings.data.bar.position === "top"
readonly property int barHeight: (barAtBottom || barAtTop) ? (Settings.data.bar.height || 30) * scaling : 0
readonly property int dockSpacing: 8 * scaling // Space between dock and bar/edge
// Track hover state // Track hover state
property bool dockHovered: false property bool dockHovered: false
property bool anyAppHovered: false property bool anyAppHovered: false
property bool hidden: autoHide
// Dock is positioned at the bottom // Dock is positioned at the bottom
anchors.bottom: true anchors.bottom: true
@ -63,11 +67,11 @@ Variants {
// Make the window transparent // Make the window transparent
color: Color.transparent color: Color.transparent
// Set the window size - always include space for peek area when auto-hide is enabled // Set the window size - include extra height only if bar is at bottom
implicitWidth: dockContainer.width implicitWidth: dockContainer.width + (floatingMargin * 2)
implicitHeight: fullHeight + (barAtBottom ? barHeight + dockSpacing : 0) implicitHeight: fullHeight + floatingMargin + (barAtBottom ? barHeight + dockSpacing : 0)
// Position the entire window above the bar when bar is at bottom // Position the entire window above the bar only when bar is at bottom
margins.bottom: barAtBottom ? barHeight : 0 margins.bottom: barAtBottom ? barHeight : 0
// Watch for autoHide setting changes // Watch for autoHide setting changes
@ -111,7 +115,7 @@ Variants {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
height: peekHeight + dockSpacing height: peekHeight + floatingMargin + (barAtBottom ? dockSpacing : 0)
hoverEnabled: autoHide hoverEnabled: autoHide
visible: autoHide visible: autoHide
@ -130,24 +134,32 @@ Variants {
Rectangle { Rectangle {
id: dockContainer id: dockContainer
width: dockLayout.implicitWidth + 48 * scaling width: dockLayout.implicitWidth + Style.marginL * scaling * 2
height: iconSize * 1.4 * scaling height: Math.round(iconSize * 1.6)
color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity) color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity)
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: dockSpacing anchors.bottomMargin: floatingMargin + (barAtBottom ? dockSpacing : 0)
topLeftRadius: Style.radiusL * scaling radius: Style.radiusL * scaling
topRightRadius: Style.radiusL * scaling border.width: Math.max(1, Style.borderS * scaling)
border.color: Color.mOutline
// Animate the dock sliding up and down // Fade and zoom animation properties
transform: Translate { opacity: hidden ? 0 : 1
y: hidden ? (fullHeight - peekHeight) : 0 scale: hidden ? 0.85 : 1
Behavior on y { Behavior on opacity {
NumberAnimation { NumberAnimation {
duration: hidden ? hideAnimationDuration : showAnimationDuration duration: hidden ? hideAnimationDuration : showAnimationDuration
easing.type: Easing.InOutQuad easing.type: Easing.InOutQuad
} }
}
Behavior on scale {
NumberAnimation {
duration: hidden ? hideAnimationDuration : showAnimationDuration
easing.type: hidden ? Easing.InQuad : Easing.OutBack
easing.overshoot: hidden ? 0 : 1.05
} }
} }
@ -179,15 +191,9 @@ Variants {
Item { Item {
id: dock id: dock
width: dockLayout.implicitWidth width: dockLayout.implicitWidth
height: parent.height - (20 * scaling) height: parent.height - (Style.marginM * 2 * scaling)
anchors.centerIn: parent anchors.centerIn: parent
NTooltip {
id: appTooltip
visible: false
positionAbove: true
}
function getAppIcon(toplevel: Toplevel): string { function getAppIcon(toplevel: Toplevel): string {
if (!toplevel) if (!toplevel)
return "" return ""
@ -203,39 +209,48 @@ Variants {
Repeater { Repeater {
model: ToplevelManager ? ToplevelManager.toplevels : null model: ToplevelManager ? ToplevelManager.toplevels : null
delegate: Rectangle { delegate: Item {
id: appButton id: appButton
Layout.preferredWidth: iconSize * scaling Layout.preferredWidth: iconSize
Layout.preferredHeight: iconSize * scaling Layout.preferredHeight: iconSize
Layout.alignment: Qt.AlignCenter Layout.alignment: Qt.AlignCenter
color: Color.transparent
radius: Style.radiusM * scaling
property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData
property bool hovered: appMouseArea.containsMouse property bool hovered: appMouseArea.containsMouse
property string appId: modelData ? modelData.appId : "" property string appId: modelData ? modelData.appId : ""
property string appTitle: modelData ? modelData.title : "" property string appTitle: modelData ? modelData.title : ""
// The icon // Individual tooltip for this app
NTooltip {
id: appTooltip
target: appButton
positionAbove: true
visible: false
}
// The icon with better quality settings
Image { Image {
id: appIcon id: appIcon
width: iconSize * scaling width: iconSize
height: iconSize * scaling height: iconSize
anchors.centerIn: parent anchors.centerIn: parent
source: dock.getAppIcon(modelData) source: dock.getAppIcon(modelData)
visible: source.toString() !== "" visible: source.toString() !== ""
sourceSize.width: iconSize * 2
sourceSize.height: iconSize * 2
smooth: true smooth: true
mipmap: false mipmap: true
antialiasing: false antialiasing: true
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
cache: true
scale: appButton.hovered ? 1.1 : 1.0 scale: appButton.hovered ? 1.15 : 1.0
Behavior on scale { Behavior on scale {
NumberAnimation { NumberAnimation {
duration: Style.animationFast duration: Style.animationNormal
easing.type: Easing.OutBack easing.type: Easing.OutBack
easing.overshoot: 1.2
} }
} }
} }
@ -246,15 +261,15 @@ Variants {
visible: !appIcon.visible visible: !appIcon.visible
text: "question_mark" text: "question_mark"
font.family: "Material Symbols Rounded" font.family: "Material Symbols Rounded"
font.pointSize: iconSize * 0.7 * scaling font.pointSize: iconSize * 0.7
color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant
scale: appButton.hovered ? 1.15 : 1.0
scale: appButton.hovered ? 1.1 : 1.0
Behavior on scale { Behavior on scale {
NumberAnimation { NumberAnimation {
duration: Style.animationFast duration: Style.animationFast
easing.type: Easing.OutBack easing.type: Easing.OutBack
easing.overshoot: 1.2
} }
} }
} }
@ -270,7 +285,6 @@ Variants {
anyAppHovered = true anyAppHovered = true
const appName = appButton.appTitle || appButton.appId || "Unknown" const appName = appButton.appTitle || appButton.appId || "Unknown"
appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName
appTooltip.target = appButton
appTooltip.isVisible = true appTooltip.isVisible = true
if (autoHide) { if (autoHide) {
showTimer.stop() showTimer.stop()
@ -300,15 +314,32 @@ Variants {
} }
} }
// Active indicator
Rectangle { Rectangle {
visible: isActive visible: isActive
width: iconSize * 0.25 width: iconSize * 0.2
height: 4 * scaling height: iconSize * 0.1
color: Color.mPrimary color: Color.mPrimary
radius: Style.radiusXS radius: Style.radiusXS * scaling
anchors.top: parent.bottom anchors.top: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: Style.marginXXS * scaling anchors.topMargin: Style.marginXXS * 1.5 * scaling
// Pulse animation for active indicator
SequentialAnimation on opacity {
running: isActive
loops: Animation.Infinite
NumberAnimation {
to: 0.6
duration: Style.animationSlowest
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 1.0
duration: Style.animationSlowest
easing.type: Easing.InOutQuad
}
}
} }
} }
} }

View file

@ -58,29 +58,6 @@ Loader {
property real percent: isReady ? (battery.percentage * 100) : 0 property real percent: isReady ? (battery.percentage * 100) : 0
property bool charging: isReady ? battery.state === UPowerDeviceState.Charging : false property bool charging: isReady ? battery.state === UPowerDeviceState.Charging : false
property bool batteryVisible: isReady && percent > 0 property bool batteryVisible: isReady && percent > 0
function getIcon() {
if (!batteryVisible)
return ""
if (charging)
return "battery_android_bolt"
if (percent >= 95)
return "battery_android_full"
if (percent >= 85)
return "battery_android_6"
if (percent >= 70)
return "battery_android_5"
if (percent >= 55)
return "battery_android_4"
if (percent >= 40)
return "battery_android_3"
if (percent >= 25)
return "battery_android_2"
if (percent >= 10)
return "battery_android_1"
if (percent >= 0)
return "battery_android_0"
}
} }
Item { Item {
@ -420,7 +397,7 @@ Loader {
anchors.bottomMargin: Style.marginM * scaling anchors.bottomMargin: Style.marginM * scaling
anchors.leftMargin: Style.marginL * scaling anchors.leftMargin: Style.marginL * scaling
anchors.rightMargin: Style.marginL * scaling anchors.rightMargin: Style.marginL * scaling
spacing: Style.marginM * scaling spacing: Style.marginL * scaling
NText { NText {
text: "SECURE TERMINAL" text: "SECURE TERMINAL"
@ -431,23 +408,6 @@ Loader {
Layout.fillWidth: true Layout.fillWidth: true
} }
RowLayout {
spacing: Style.marginS * scaling
visible: batteryIndicator.batteryVisible
NIcon {
text: batteryIndicator.getIcon()
font.pointSize: Style.fontSizeM * scaling
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface
}
NText {
text: Math.round(batteryIndicator.percent) + "%"
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
}
}
RowLayout { RowLayout {
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
NText { NText {
@ -463,6 +423,25 @@ Loader {
color: Color.mOnSurface color: Color.mOnSurface
} }
} }
RowLayout {
spacing: Style.marginS * scaling
visible: batteryIndicator.batteryVisible
NIcon {
text: BatteryService.getIcon(batteryIndicator.percent, batteryIndicator.charging,
batteryIndicator.isReady)
font.pointSize: Style.fontSizeM * scaling
color: batteryIndicator.charging ? Color.mPrimary : Color.mOnSurface
rotation: -90
}
NText {
text: Math.round(batteryIndicator.percent) + "%"
color: Color.mOnSurface
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
}
}
} }
} }

View file

@ -39,7 +39,7 @@ NBox {
const totalSum = JSON.stringify(widget).split('').reduce((acc, character) => { const totalSum = JSON.stringify(widget).split('').reduce((acc, character) => {
return acc + character.charCodeAt(0) return acc + character.charCodeAt(0)
}, 0) }, 0)
switch (totalSum % 10) { switch (totalSum % 5) {
case 0: case 0:
return Color.mPrimary return Color.mPrimary
case 1: case 1:
@ -50,16 +50,6 @@ NBox {
return Color.mError return Color.mError
case 4: case 4:
return Color.mOnSurface return Color.mOnSurface
case 5:
return Qt.darker(Color.mPrimary, 1.3)
case 6:
return Qt.darker(Color.mSecondary, 1.3)
case 7:
return Qt.darker(Color.mTertiary, 1.3)
case 8:
return Qt.darker(Color.mError, 1.3)
case 9:
return Qt.darker(Color.mOnSurface, 1.3)
} }
} }
@ -75,7 +65,7 @@ NBox {
text: sectionName + " Section" text: sectionName + " Section"
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold font.weight: Style.fontWeightBold
color: Color.mSecondary color: Color.mOnSurface
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
@ -89,7 +79,7 @@ NBox {
description: "" description: ""
placeholder: "Select a widget to add..." placeholder: "Select a widget to add..."
onSelected: key => comboBox.currentKey = key onSelected: key => comboBox.currentKey = key
popupHeight: 240 * scaling popupHeight: 340 * scaling
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
@ -188,13 +178,33 @@ NBox {
colorBgHover: Qt.alpha(Color.mOnPrimary, Style.opacityLight) colorBgHover: Qt.alpha(Color.mOnPrimary, Style.opacityLight)
colorFgHover: Color.mOnPrimary colorFgHover: Color.mOnPrimary
onClicked: { onClicked: {
var dialog = Qt.createComponent("BarWidgetSettingsDialog.qml").createObject(root, { var component = Qt.createComponent(Qt.resolvedUrl("BarWidgetSettingsDialog.qml"))
"widgetIndex": index, function instantiateAndOpen() {
"widgetData": modelData, var dialog = component.createObject(root, {
"widgetId": modelData.id, "widgetIndex": index,
"parent": Overlay.overlay "widgetData": modelData,
}) "widgetId": modelData.id,
dialog.open() "parent": Overlay.overlay
})
if (dialog) {
dialog.open()
} else {
Logger.error("BarSectionEditor", "Failed to create settings dialog instance")
}
}
if (component.status === Component.Ready) {
instantiateAndOpen()
} else if (component.status === Component.Error) {
Logger.error("BarSectionEditor", component.errorString())
} else {
component.statusChanged.connect(function () {
if (component.status === Component.Ready) {
instantiateAndOpen()
} else if (component.status === Component.Error) {
Logger.error("BarSectionEditor", component.errorString())
}
})
}
} }
} }
} }
@ -221,7 +231,7 @@ NBox {
MouseArea { MouseArea {
id: flowDragArea id: flowDragArea
anchors.fill: parent anchors.fill: parent
z: 999 // Above all widgets to ensure it gets events first z: -1 // Ensure this mouse area is below the Settings and Close buttons
// Critical properties for proper event handling // Critical properties for proper event handling
acceptedButtons: Qt.LeftButton acceptedButtons: Qt.LeftButton

View file

@ -0,0 +1,134 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
import "./WidgetSettings" as WidgetSettings
// Widget Settings Dialog Component
Popup {
id: settingsPopup
property int widgetIndex: -1
property var widgetData: null
property string widgetId: ""
// Center popup in parent
x: (parent.width - width) * 0.5
y: (parent.height - height) * 0.5
width: 420 * scaling
height: content.implicitHeight + padding * 2
padding: Style.marginXL * scaling
modal: true
background: Rectangle {
id: bgRect
color: Color.mSurface
radius: Style.radiusL * scaling
border.color: Color.mPrimary
border.width: Style.borderM * scaling
}
// Load settings when popup opens with data
onOpened: {
if (widgetData && widgetId) {
loadWidgetSettings()
}
}
function loadWidgetSettings() {
const widgetSettingsMap = {
"ActiveWindow": "WidgetSettings/ActiveWindowSettings.qml",
"Battery": "WidgetSettings/BatterySettings.qml",
"Brightness": "WidgetSettings/BrightnessSettings.qml",
"Clock": "WidgetSettings/ClockSettings.qml",
"CustomButton": "WidgetSettings/CustomButtonSettings.qml",
"MediaMini": "WidgetSettings/MediaMiniSettings.qml",
"Microphone": "WidgetSettings/MicrophoneSettings.qml",
"NotificationHistory": "WidgetSettings/NotificationHistorySettings.qml",
"Workspace": "WidgetSettings/WorkspaceSettings.qml",
"SidePanelToggle": "WidgetSettings/SidePanelToggleSettings.qml",
"Spacer": "WidgetSettings/SpacerSettings.qml",
"SystemMonitor": "WidgetSettings/SystemMonitorSettings.qml",
"Volume": "WidgetSettings/VolumeSettings.qml"
}
const source = widgetSettingsMap[widgetId]
if (source) {
// Use setSource to pass properties at creation time
settingsLoader.setSource(source, {
"widgetData": widgetData,
"widgetMetadata": BarWidgetRegistry.widgetMetadata[widgetId]
})
}
}
ColumnLayout {
id: content
width: parent.width
spacing: Style.marginM * scaling
// Title
RowLayout {
Layout.fillWidth: true
NText {
text: `${settingsPopup.widgetId} Settings`
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.fillWidth: true
}
NIconButton {
icon: "close"
onClicked: settingsPopup.close()
}
}
// Separator
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: Color.mOutline
}
// Settings based on widget type
// Will be triggered via settingsLoader.setSource()
Loader {
id: settingsLoader
Layout.fillWidth: true
}
// Action buttons
RowLayout {
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
Item {
Layout.fillWidth: true
}
NButton {
text: "Cancel"
outlined: true
onClicked: settingsPopup.close()
}
NButton {
text: "Apply"
icon: "check"
onClicked: {
if (settingsLoader.item && settingsLoader.item.saveSettings) {
var newSettings = settingsLoader.item.saveSettings()
root.updateWidgetSettings(sectionId, settingsPopup.widgetIndex, newSettings)
settingsPopup.close()
}
}
}
}
}
}

View file

@ -0,0 +1,32 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueShowIcon: widgetData.showIcon !== undefined ? widgetData.showIcon : widgetMetadata.showIcon
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.showIcon = valueShowIcon
return settings
}
NToggle {
id: showIcon
Layout.fillWidth: true
label: "Show app icon"
checked: root.valueShowIcon
onToggled: checked => root.valueShowIcon = checked
}
}

View file

@ -0,0 +1,31 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueAlwaysShowPercentage: widgetData.alwaysShowPercentage
!== undefined ? widgetData.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.alwaysShowPercentage = valueAlwaysShowPercentage
return settings
}
NToggle {
label: "Always show percentage"
checked: root.valueAlwaysShowPercentage
onToggled: checked => root.valueAlwaysShowPercentage = checked
}
}

View file

@ -0,0 +1,31 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueAlwaysShowPercentage: widgetData.alwaysShowPercentage
!== undefined ? widgetData.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.alwaysShowPercentage = valueAlwaysShowPercentage
return settings
}
NToggle {
label: "Always show percentage"
checked: valueAlwaysShowPercentage
onToggled: checked => valueAlwaysShowPercentage = checked
}
}

View file

@ -0,0 +1,54 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueShowDate: widgetData.showDate !== undefined ? widgetData.showDate : widgetMetadata.showDate
property bool valueUse12h: widgetData.use12HourClock !== undefined ? widgetData.use12HourClock : widgetMetadata.use12HourClock
property bool valueShowSeconds: widgetData.showSeconds !== undefined ? widgetData.showSeconds : widgetMetadata.showSeconds
property bool valueReverseDayMonth: widgetData.reverseDayMonth !== undefined ? widgetData.reverseDayMonth : widgetMetadata.reverseDayMonth
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.showDate = valueShowDate
settings.use12HourClock = valueUse12h
settings.showSeconds = valueShowSeconds
settings.reverseDayMonth = valueReverseDayMonth
return settings
}
NToggle {
label: "Show date"
checked: valueShowDate
onToggled: checked => valueShowDate = checked
}
NToggle {
label: "Use 12-hour clock"
checked: valueUse12h
onToggled: checked => valueUse12h = checked
}
NToggle {
label: "Show seconds"
checked: valueShowSeconds
onToggled: checked => valueShowSeconds = checked
}
NToggle {
label: "Reverse day and month"
checked: valueReverseDayMonth
onToggled: checked => valueReverseDayMonth = checked
}
}

View file

@ -0,0 +1,58 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.icon = iconInput.text
settings.leftClickExec = leftClickExecInput.text
settings.rightClickExec = rightClickExecInput.text
settings.middleClickExec = middleClickExecInput.text
return settings
}
// Icon setting
NTextInput {
id: iconInput
Layout.fillWidth: true
label: "Icon Name"
description: "Choose a name from the Material Icon set."
placeholderText: "Enter icon name (e.g., favorite, home, settings)"
text: widgetData?.icon || widgetMetadata.icon
}
NTextInput {
id: leftClickExecInput
Layout.fillWidth: true
label: "Left Click Command"
placeholderText: "Enter command to execute (app or custom script)"
text: widgetData?.leftClickExec || widgetMetadata.leftClickExec
}
NTextInput {
id: rightClickExecInput
Layout.fillWidth: true
label: "Right Click Command"
placeholderText: "Enter command to execute (app or custom script)"
text: widgetData?.rightClickExec || widgetMetadata.rightClickExec
}
NTextInput {
id: middleClickExecInput
Layout.fillWidth: true
label: "Middle Click Command"
placeholderText: "Enter command to execute (app or custom script)"
text: widgetData.middleClickExec || widgetMetadata.middleClickExec
}
}

View file

@ -0,0 +1,62 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueShowAlbumArt: widgetData.showAlbumArt !== undefined ? widgetData.showAlbumArt : widgetMetadata.showAlbumArt
property bool valueShowVisualizer: widgetData.showVisualizer !== undefined ? widgetData.showVisualizer : widgetMetadata.showVisualizer
property string valueVisualizerType: widgetData.visualizerType || widgetMetadata.visualizerType
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.showAlbumArt = valueShowAlbumArt
settings.showVisualizer = valueShowVisualizer
settings.visualizerType = valueVisualizerType
return settings
}
NToggle {
label: "Show album art"
checked: valueShowAlbumArt
onToggled: checked => valueShowAlbumArt = checked
}
NToggle {
label: "Show visualizer"
checked: valueShowVisualizer
onToggled: checked => valueShowVisualizer = checked
}
NComboBox {
visible: valueShowVisualizer
label: "Visualizer type"
model: ListModel {
ListElement {
key: "linear"
name: "Linear"
}
ListElement {
key: "mirrored"
name: "Mirrored"
}
ListElement {
key: "wave"
name: "Wave"
}
}
currentKey: valueVisualizerType
onSelected: key => valueVisualizerType = key
minimumWidth: 200 * scaling
}
}

View file

@ -0,0 +1,31 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueAlwaysShowPercentage: widgetData.alwaysShowPercentage
!== undefined ? widgetData.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.alwaysShowPercentage = valueAlwaysShowPercentage
return settings
}
NToggle {
label: "Always show percentage"
checked: valueAlwaysShowPercentage
onToggled: checked => valueAlwaysShowPercentage = checked
}
}

View file

@ -0,0 +1,38 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueShowUnreadBadge: widgetData.showUnreadBadge !== undefined ? widgetData.showUnreadBadge : widgetMetadata.showUnreadBadge
property bool valueHideWhenZero: widgetData.hideWhenZero !== undefined ? widgetData.hideWhenZero : widgetMetadata.hideWhenZero
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.showUnreadBadge = valueShowUnreadBadge
settings.hideWhenZero = valueHideWhenZero
return settings
}
NToggle {
label: "Show unread badge"
checked: valueShowUnreadBadge
onToggled: checked => valueShowUnreadBadge = checked
}
NToggle {
label: "Hide badge when zero"
checked: valueHideWhenZero
onToggled: checked => valueHideWhenZero = checked
}
}

View file

@ -0,0 +1,30 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueUseDistroLogo: widgetData.useDistroLogo !== undefined ? widgetData.useDistroLogo : widgetMetadata.useDistroLogo
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.useDistroLogo = valueUseDistroLogo
return settings
}
NToggle {
label: "Use distro logo instead of icon"
checked: valueUseDistroLogo
onToggled: checked => valueUseDistroLogo = checked
}
}

View file

@ -0,0 +1,30 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.width = parseInt(widthInput.text) || widgetMetadata.width
return settings
}
NTextInput {
id: widthInput
Layout.fillWidth: true
label: "Width"
description: "Spacing width in pixels"
text: widgetData.width || widgetMetadata.width
placeholderText: "Enter width in pixels"
}
}

View file

@ -0,0 +1,74 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local, editable state for checkboxes
property bool valueShowCpuUsage: widgetData.showCpuUsage !== undefined ? widgetData.showCpuUsage : widgetMetadata.showCpuUsage
property bool valueShowCpuTemp: widgetData.showCpuTemp !== undefined ? widgetData.showCpuTemp : widgetMetadata.showCpuTemp
property bool valueShowMemoryUsage: widgetData.showMemoryUsage !== undefined ? widgetData.showMemoryUsage : widgetMetadata.showMemoryUsage
property bool valueShowMemoryAsPercent: widgetData.showMemoryAsPercent
!== undefined ? widgetData.showMemoryAsPercent : widgetMetadata.showMemoryAsPercent
property bool valueShowNetworkStats: widgetData.showNetworkStats
!== undefined ? widgetData.showNetworkStats : widgetMetadata.showNetworkStats
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.showCpuUsage = valueShowCpuUsage
settings.showCpuTemp = valueShowCpuTemp
settings.showMemoryUsage = valueShowMemoryUsage
settings.showMemoryAsPercent = valueShowMemoryAsPercent
settings.showNetworkStats = valueShowNetworkStats
return settings
}
NToggle {
id: showCpuUsage
Layout.fillWidth: true
label: "CPU usage"
checked: valueShowCpuUsage
onToggled: checked => valueShowCpuUsage = checked
}
NToggle {
id: showCpuTemp
Layout.fillWidth: true
label: "CPU temperature"
checked: valueShowCpuTemp
onToggled: checked => valueShowCpuTemp = checked
}
NToggle {
id: showMemoryUsage
Layout.fillWidth: true
label: "Memory usage"
checked: valueShowMemoryUsage
onToggled: checked => valueShowMemoryUsage = checked
}
NToggle {
id: showMemoryAsPercent
Layout.fillWidth: true
label: "Show memory as percentage"
checked: valueShowMemoryAsPercent
onToggled: checked => valueShowMemoryAsPercent = checked
}
NToggle {
id: showNetworkStats
Layout.fillWidth: true
label: "Network traffic"
checked: valueShowNetworkStats
onToggled: checked => valueShowNetworkStats = checked
}
}

View file

@ -0,0 +1,31 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
// Local state
property bool valueAlwaysShowPercentage: widgetData.alwaysShowPercentage
!== undefined ? widgetData.alwaysShowPercentage : widgetMetadata.alwaysShowPercentage
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.alwaysShowPercentage = valueAlwaysShowPercentage
return settings
}
NToggle {
label: "Always show percentage"
checked: valueAlwaysShowPercentage
onToggled: checked => valueAlwaysShowPercentage = checked
}
}

View file

@ -0,0 +1,44 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
// Properties to receive data from parent
property var widgetData: null
property var widgetMetadata: null
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.labelMode = labelModeCombo.currentKey
return settings
}
NComboBox {
id: labelModeCombo
label: "Label Mode"
model: ListModel {
ListElement {
key: "none"
name: "None"
}
ListElement {
key: "index"
name: "Index"
}
ListElement {
key: "name"
name: "Name"
}
}
currentKey: widgetData.labelMode || widgetMetadata.labelMode
onSelected: key => labelModeCombo.currentKey = key
minimumWidth: 200 * scaling
}
}

View file

@ -1,186 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
// Widget Settings Dialog Component
Popup {
id: settingsPopup
property int widgetIndex: -1
property var widgetData: null
property string widgetId: ""
// Center popup in parent
x: (parent.width - width) * 0.5
y: (parent.height - height) * 0.5
width: 420 * scaling
height: content.implicitHeight + padding * 2
padding: Style.marginXL * scaling
modal: true
background: Rectangle {
id: bgRect
color: Color.mSurface
radius: Style.radiusL * scaling
border.color: Color.mPrimary
border.width: Style.borderM * scaling
}
ColumnLayout {
id: content
width: parent.width
spacing: Style.marginM * scaling
// Title
RowLayout {
Layout.fillWidth: true
NText {
text: "Widget Settings: " + settingsPopup.widgetId
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.fillWidth: true
}
NIconButton {
icon: "close"
onClicked: settingsPopup.close()
}
}
// Separator
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: Color.mOutline
}
// Settings based on widget type
Loader {
id: settingsLoader
Layout.fillWidth: true
sourceComponent: {
if (settingsPopup.widgetId === "CustomButton") {
return customButtonSettings
} else if (settingsPopup.widgetId === "Spacer") {
return spacerSettings
}
// Add more widget settings components here as needed
return null
}
}
// Action buttons
RowLayout {
Layout.fillWidth: true
Layout.topMargin: Style.marginM * scaling
Item {
Layout.fillWidth: true
}
NButton {
text: "Cancel"
outlined: true
onClicked: settingsPopup.close()
}
NButton {
text: "Save"
onClicked: {
if (settingsLoader.item && settingsLoader.item.saveSettings) {
var newSettings = settingsLoader.item.saveSettings()
root.updateWidgetSettings(sectionId, settingsPopup.widgetIndex, newSettings)
settingsPopup.close()
}
}
}
}
}
// CustomButton settings component
Component {
id: customButtonSettings
ColumnLayout {
spacing: Style.marginM * scaling
function saveSettings() {
var settings = Object.assign({}, settingsPopup.widgetData)
settings.icon = iconInput.text
settings.leftClickExec = leftClickExecInput.text
settings.rightClickExec = rightClickExecInput.text
settings.middleClickExec = middleClickExecInput.text
return settings
}
// Icon setting
NTextInput {
id: iconInput
Layout.fillWidth: true
Layout.bottomMargin: Style.marginXL * scaling
label: "Icon Name"
description: "Use Material Icon names from the icon set."
text: settingsPopup.widgetData.icon || ""
placeholderText: "Enter icon name (e.g., favorite, home, settings)"
}
NTextInput {
id: leftClickExecInput
Layout.fillWidth: true
label: "Left Click Command"
description: "Command or application to run when left clicked."
text: settingsPopup.widgetData.leftClickExec || ""
placeholderText: "Enter command to execute (app or custom script)"
}
NTextInput {
id: rightClickExecInput
Layout.fillWidth: true
label: "Right Click Command"
description: "Command or application to run when right clicked."
text: settingsPopup.widgetData.rightClickExec || ""
placeholderText: "Enter command to execute (app or custom script)"
}
NTextInput {
id: middleClickExecInput
Layout.fillWidth: true
label: "Middle Click Command"
description: "Command or application to run when middle clicked."
text: settingsPopup.widgetData.middleClickExec || ""
placeholderText: "Enter command to execute (app or custom script)"
}
}
}
// Spacer settings component
Component {
id: spacerSettings
ColumnLayout {
spacing: Style.marginM * scaling
function saveSettings() {
var settings = Object.assign({}, settingsPopup.widgetData)
settings.width = parseInt(widthInput.text) || 20
return settings
}
NTextInput {
id: widthInput
Layout.fillWidth: true
label: "Width (pixels)"
description: "Width of the spacer in pixels."
text: settingsPopup.widgetData.width || "20"
placeholderText: "Enter width in pixels"
}
}
}
}

View file

@ -39,7 +39,7 @@ NPanel {
General, General,
Network, Network,
ScreenRecorder, ScreenRecorder,
TimeWeather, Weather,
Wallpaper, Wallpaper,
WallpaperSelector WallpaperSelector
} }
@ -90,8 +90,8 @@ NPanel {
Tabs.NetworkTab {} Tabs.NetworkTab {}
} }
Component { Component {
id: timeWeatherTab id: weatherTab
Tabs.TimeWeatherTab {} Tabs.WeatherTab {}
} }
Component { Component {
id: colorSchemeTab id: colorSchemeTab
@ -156,10 +156,10 @@ NPanel {
"icon": "brightness_6", "icon": "brightness_6",
"source": brightnessTab "source": brightnessTab
}, { }, {
"id": SettingsPanel.Tab.TimeWeather, "id": SettingsPanel.Tab.Weather,
"label": "Time & Weather", "label": "Weather",
"icon": "schedule", "icon": "partly_cloudy_day",
"source": timeWeatherTab "source": weatherTab
}, { }, {
"id": SettingsPanel.Tab.ColorScheme, "id": SettingsPanel.Tab.ColorScheme,
"label": "Color Scheme", "label": "Color Scheme",
@ -368,7 +368,7 @@ NPanel {
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginS * scaling anchors.margins: Style.marginS * scaling
spacing: Style.marginXS * 1.5 * scaling spacing: Style.marginXS * scaling
Repeater { Repeater {
id: sections id: sections
@ -398,7 +398,8 @@ NPanel {
RowLayout { RowLayout {
id: tabEntryRow id: tabEntryRow
anchors.fill: parent anchors.fill: parent
anchors.margins: Style.marginS * scaling anchors.leftMargin: Style.marginS * scaling
anchors.rightMargin: Style.marginS * scaling
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
// Tab icon // Tab icon

View file

@ -242,21 +242,7 @@ ColumnLayout {
Layout.bottomMargin: Style.marginS * scaling Layout.bottomMargin: Style.marginS * scaling
} }
// Miniplayer section // Preferred player
NToggle {
label: "Show Album Art In Bar Media Player"
description: "Show the album art of the currently playing song next to the title."
checked: Settings.data.audio.showMiniplayerAlbumArt
onToggled: checked => Settings.data.audio.showMiniplayerAlbumArt = checked
}
NToggle {
label: "Show Audio Visualizer In Bar Media Player"
description: "Shows an audio visualizer in the background of the miniplayer."
checked: Settings.data.audio.showMiniplayerCava
onToggled: checked => Settings.data.audio.showMiniplayerCava = checked
}
// Preferred player (persistent)
NTextInput { NTextInput {
label: "Preferred Player" label: "Preferred Player"
description: "Substring to match MPRIS player (identity/bus/desktop)." description: "Substring to match MPRIS player (identity/bus/desktop)."

View file

@ -4,7 +4,7 @@ import QtQuick.Layouts
import qs.Commons import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
import qs.Modules.SettingsPanel.Extras import qs.Modules.SettingsPanel.Bar
ColumnLayout { ColumnLayout {
id: root id: root
@ -70,57 +70,6 @@ ColumnLayout {
} }
} }
} }
NToggle {
label: "Show Active Window's Icon"
description: "Display the app icon next to the title of the currently focused window."
checked: Settings.data.bar.showActiveWindowIcon
onToggled: checked => Settings.data.bar.showActiveWindowIcon = checked
}
NToggle {
label: "Show Battery Percentage"
description: "Display battery percentage at all times."
checked: Settings.data.bar.alwaysShowBatteryPercentage
onToggled: checked => Settings.data.bar.alwaysShowBatteryPercentage = checked
}
NToggle {
label: "Show Network Statistics"
description: "Display network upload and download speeds in the system monitor."
checked: Settings.data.bar.showNetworkStats
onToggled: checked => Settings.data.bar.showNetworkStats = checked
}
NToggle {
label: "Replace SidePanel toggle with distro logo"
description: "Show distro logo instead of the SidePanel toggle button in the bar."
checked: Settings.data.bar.useDistroLogo
onToggled: checked => {
Settings.data.bar.useDistroLogo = checked
}
}
NComboBox {
label: "Show Workspaces Labels"
description: "Show the workspace name or index within the workspace indicator."
model: ListModel {
ListElement {
key: "none"
name: "None"
}
ListElement {
key: "index"
name: "Index"
}
ListElement {
key: "name"
name: "Name"
}
}
currentKey: Settings.data.bar.showWorkspaceLabel
onSelected: key => Settings.data.bar.showWorkspaceLabel = key
}
} }
NDivider { NDivider {
@ -138,7 +87,7 @@ ColumnLayout {
text: "Widgets Positioning" text: "Widgets Positioning"
font.pointSize: Style.fontSizeXXL * scaling font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold font.weight: Style.fontWeightBold
color: Color.mOnSurface color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling Layout.bottomMargin: Style.marginS * scaling
} }

View file

@ -299,8 +299,7 @@ ColumnLayout {
currentKey: Settings.data.nightLight.manualSunrise currentKey: Settings.data.nightLight.manualSunrise
placeholder: "Select start time" placeholder: "Select start time"
onSelected: key => Settings.data.nightLight.manualSunrise = key onSelected: key => Settings.data.nightLight.manualSunrise = key
minimumWidth: 120 * scaling
preferredWidth: 120 * scaling
} }
Item {// add a little more spacing Item {// add a little more spacing
@ -316,8 +315,7 @@ ColumnLayout {
currentKey: Settings.data.nightLight.manualSunset currentKey: Settings.data.nightLight.manualSunset
placeholder: "Select stop time" placeholder: "Select stop time"
onSelected: key => Settings.data.nightLight.manualSunset = key onSelected: key => Settings.data.nightLight.manualSunset = key
minimumWidth: 120 * scaling
preferredWidth: 120 * scaling
} }
} }
} }

View file

@ -8,6 +8,7 @@ import qs.Widgets
ColumnLayout { ColumnLayout {
id: root id: root
spacing: 0
// Cache for scheme JSON (can be flat or {dark, light}) // Cache for scheme JSON (can be flat or {dark, light})
property var schemeColorsCache: ({}) property var schemeColorsCache: ({})
@ -103,225 +104,218 @@ ColumnLayout {
} }
} }
// Main Toggles - Dark Mode / Matugen
ColumnLayout { ColumnLayout {
spacing: 0 spacing: Style.marginL * scaling
Layout.fillWidth: true
Item { // Dark Mode Toggle (affects both Matugen and predefined schemes that provide variants)
Layout.fillWidth: true NToggle {
Layout.preferredHeight: 0 label: "Dark Mode"
description: Settings.data.colorSchemes.useWallpaperColors ? "Generate dark theme colors when using Matugen." : "Use a dark variant if available."
checked: Settings.data.colorSchemes.darkMode
enabled: true
onToggled: checked => Settings.data.colorSchemes.darkMode = checked
} }
ColumnLayout { // Use Matugen
spacing: Style.marginL * scaling NToggle {
Layout.fillWidth: true label: "Enable Matugen"
description: "Automatically generate colors based on your active wallpaper."
checked: Settings.data.colorSchemes.useWallpaperColors
onToggled: checked => {
if (checked) {
// Check if matugen is installed
matugenCheck.running = true
} else {
Settings.data.colorSchemes.useWallpaperColors = false
ToastService.showNotice("Matugen", "Disabled")
// Dark Mode Toggle (affects both Matugen and predefined schemes that provide variants) if (Settings.data.colorSchemes.predefinedScheme) {
NToggle {
label: "Dark Mode"
description: Settings.data.colorSchemes.useWallpaperColors ? "Generate dark theme colors when using Matugen." : "Use a dark variant if available."
checked: Settings.data.colorSchemes.darkMode
enabled: true
onToggled: checked => Settings.data.colorSchemes.darkMode = checked
}
// Use Matugen ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme)
NToggle {
label: "Enable Matugen"
description: "Automatically generate colors based on your active wallpaper."
checked: Settings.data.colorSchemes.useWallpaperColors
onToggled: checked => {
if (checked) {
// Check if matugen is installed
matugenCheck.running = true
} else {
Settings.data.colorSchemes.useWallpaperColors = false
ToastService.showNotice("Matugen", "Disabled")
if (Settings.data.colorSchemes.predefinedScheme) {
ColorSchemeService.applyScheme(Settings.data.colorSchemes.predefinedScheme)
}
} }
} }
} }
}
}
NDivider { NDivider {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling Layout.bottomMargin: Style.marginXL * scaling
} }
ColumnLayout { // Predefined Color Schemes
spacing: Style.marginS * scaling ColumnLayout {
Layout.fillWidth: true spacing: Style.marginM * scaling
Layout.fillWidth: true
NText { NText {
text: "Predefined Color Schemes" text: "Predefined Color Schemes"
font.pointSize: Style.fontSizeXXL * scaling font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold font.weight: Style.fontWeightBold
color: Color.mSecondary color: Color.mSecondary
} }
NText {
text: "To use these color schemes, you must turn off Matugen. With Matugen enabled, colors are automatically generated from your wallpaper."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
// Color Schemes Grid
GridLayout {
columns: 3
rowSpacing: Style.marginM * scaling
columnSpacing: Style.marginM * scaling
Layout.fillWidth: true
Repeater {
model: ColorSchemeService.schemes
Rectangle {
id: schemeCard
property string schemePath: modelData
NText {
text: "These color schemes are only active when 'Use Matugen' is turned off. With Matugen enabled, colors will be automatically generated from your wallpaper. You can still switch between light and dark themes while using Matugen."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.fillWidth: true Layout.fillWidth: true
wrapMode: Text.WordWrap Layout.preferredHeight: 120 * scaling
} radius: Style.radiusM * scaling
} color: getSchemeColor(modelData, "mSurface")
border.width: Math.max(1, Style.borderL * scaling)
border.color: (!Settings.data.colorSchemes.useWallpaperColors
&& (Settings.data.colorSchemes.predefinedScheme === modelData)) ? Color.mPrimary : Color.mOutline
scale: root.cardScaleLow
// Color Schemes Grid // Mouse area for selection
GridLayout { MouseArea {
columns: 3 anchors.fill: parent
rowSpacing: Style.marginM * scaling onClicked: {
columnSpacing: Style.marginM * scaling // Disable useWallpaperColors when picking a predefined color scheme
Layout.fillWidth: true Settings.data.colorSchemes.useWallpaperColors = false
Logger.log("ColorSchemeTab", "Disabled matugen setting")
Repeater { Settings.data.colorSchemes.predefinedScheme = schemePath
model: ColorSchemeService.schemes ColorSchemeService.applyScheme(schemePath)
}
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
Rectangle { onEntered: {
id: schemeCard schemeCard.scale = root.cardScaleHigh
property string schemePath: modelData
Layout.fillWidth: true
Layout.preferredHeight: 120 * scaling
radius: Style.radiusM * scaling
color: getSchemeColor(modelData, "mSurface")
border.width: Math.max(1, Style.borderL * scaling)
border.color: (!Settings.data.colorSchemes.useWallpaperColors
&& (Settings.data.colorSchemes.predefinedScheme === modelData)) ? Color.mPrimary : Color.mOutline
scale: root.cardScaleLow
// Mouse area for selection
MouseArea {
anchors.fill: parent
onClicked: {
// Disable useWallpaperColors when picking a predefined color scheme
Settings.data.colorSchemes.useWallpaperColors = false
Logger.log("ColorSchemeTab", "Disabled matugen setting")
Settings.data.colorSchemes.predefinedScheme = schemePath
ColorSchemeService.applyScheme(schemePath)
}
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
schemeCard.scale = root.cardScaleHigh
}
onExited: {
schemeCard.scale = root.cardScaleLow
}
} }
// Card content onExited: {
ColumnLayout { schemeCard.scale = root.cardScaleLow
anchors.fill: parent }
anchors.margins: Style.marginXL * scaling }
// Card content
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginXL * scaling
spacing: Style.marginS * scaling
// Scheme name
NText {
text: {
// Remove json and the full path
var chunks = schemePath.replace(".json", "").split("/")
return chunks[chunks.length - 1]
}
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
color: getSchemeColor(modelData, "mOnSurface")
Layout.fillWidth: true
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
}
// Color swatches
RowLayout {
id: swatches
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
// Scheme name readonly property int swatchSize: 20 * scaling
NText {
text: { // Primary color swatch
// Remove json and the full path Rectangle {
var chunks = schemePath.replace(".json", "").split("/") width: swatches.swatchSize
return chunks[chunks.length - 1] height: swatches.swatchSize
} radius: width * 0.5
font.pointSize: Style.fontSizeM * scaling color: getSchemeColor(modelData, "mPrimary")
font.weight: Style.fontWeightBold
color: getSchemeColor(modelData, "mOnSurface")
Layout.fillWidth: true
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
} }
// Color swatches // Secondary color swatch
RowLayout { Rectangle {
id: swatches width: swatches.swatchSize
height: swatches.swatchSize
radius: width * 0.5
color: getSchemeColor(modelData, "mSecondary")
}
spacing: Style.marginS * scaling // Tertiary color swatch
Layout.fillWidth: true Rectangle {
Layout.alignment: Qt.AlignHCenter width: swatches.swatchSize
height: swatches.swatchSize
radius: width * 0.5
color: getSchemeColor(modelData, "mTertiary")
}
readonly property int swatchSize: 20 * scaling // Error color swatch
Rectangle {
// Primary color swatch width: swatches.swatchSize
Rectangle { height: swatches.swatchSize
width: swatches.swatchSize radius: width * 0.5
height: swatches.swatchSize color: getSchemeColor(modelData, "mError")
radius: width * 0.5
color: getSchemeColor(modelData, "mPrimary")
}
// Secondary color swatch
Rectangle {
width: swatches.swatchSize
height: swatches.swatchSize
radius: width * 0.5
color: getSchemeColor(modelData, "mSecondary")
}
// Tertiary color swatch
Rectangle {
width: swatches.swatchSize
height: swatches.swatchSize
radius: width * 0.5
color: getSchemeColor(modelData, "mTertiary")
}
// Error color swatch
Rectangle {
width: swatches.swatchSize
height: swatches.swatchSize
radius: width * 0.5
color: getSchemeColor(modelData, "mError")
}
} }
} }
}
// Selection indicator (Checkmark) // Selection indicator (Checkmark)
Rectangle { Rectangle {
visible: !Settings.data.colorSchemes.useWallpaperColors visible: !Settings.data.colorSchemes.useWallpaperColors
&& (Settings.data.colorSchemes.predefinedScheme === schemePath) && (Settings.data.colorSchemes.predefinedScheme === schemePath)
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
anchors.margins: Style.marginS * scaling anchors.margins: Style.marginS * scaling
width: 24 * scaling width: 24 * scaling
height: 24 * scaling height: 24 * scaling
radius: width * 0.5 radius: width * 0.5
color: Color.mPrimary color: Color.mPrimary
NText { NText {
anchors.centerIn: parent anchors.centerIn: parent
text: "✓" text: "✓"
font.pointSize: Style.fontSizeXS * scaling font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightBold font.weight: Style.fontWeightBold
color: Color.mOnPrimary color: Color.mOnPrimary
}
} }
}
// Smooth animations // Smooth animations
Behavior on scale { Behavior on scale {
NumberAnimation { NumberAnimation {
duration: Style.animationNormal duration: Style.animationNormal
easing.type: Easing.OutCubic easing.type: Easing.OutCubic
}
} }
}
Behavior on border.color { Behavior on border.color {
ColorAnimation { ColorAnimation {
duration: Style.animationNormal duration: Style.animationNormal
}
} }
}
Behavior on border.width { Behavior on border.width {
NumberAnimation { NumberAnimation {
duration: Style.animationFast duration: Style.animationFast
}
} }
} }
} }

View file

@ -70,52 +70,6 @@ ColumnLayout {
onToggled: checked => Settings.data.general.dimDesktop = checked onToggled: checked => Settings.data.general.dimDesktop = checked
} }
NToggle {
label: "Auto-hide Dock"
description: "Automatically hide the dock when not in use."
checked: Settings.data.dock.autoHide
onToggled: checked => Settings.data.dock.autoHide = checked
}
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NText {
text: "Dock Background Opacity"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText {
text: "Adjust the background opacity of the dock."
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
RowLayout {
NSlider {
Layout.fillWidth: true
from: 0
to: 1
stepSize: 0.01
value: Settings.data.dock.backgroundOpacity
onMoved: Settings.data.dock.backgroundOpacity = value
cutoutColor: Color.mSurface
}
NText {
text: Math.floor(Settings.data.dock.backgroundOpacity * 100) + "%"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
color: Color.mOnSurface
}
}
}
ColumnLayout { ColumnLayout {
spacing: Style.marginXXS * scaling spacing: Style.marginXXS * scaling
Layout.fillWidth: true Layout.fillWidth: true
@ -175,7 +129,70 @@ ColumnLayout {
} }
} }
} }
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Dock
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Dock"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
Layout.bottomMargin: Style.marginS * scaling
}
NToggle {
label: "Auto-hide Dock"
description: "Automatically hide the dock when not in use."
checked: Settings.data.dock.autoHide
onToggled: checked => Settings.data.dock.autoHide = checked
}
ColumnLayout {
spacing: Style.marginXXS * scaling
Layout.fillWidth: true
NText {
text: "Dock Background Opacity"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
}
NText {
text: "Adjust the background opacity of the dock."
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
RowLayout {
NSlider {
Layout.fillWidth: true
from: 0
to: 1
stepSize: 0.01
value: Settings.data.dock.backgroundOpacity
onMoved: Settings.data.dock.backgroundOpacity = value
cutoutColor: Color.mSurface
}
NText {
text: Math.floor(Settings.data.dock.backgroundOpacity * 100) + "%"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
color: Color.mOnSurface
}
}
}
}
NDivider { NDivider {
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling Layout.topMargin: Style.marginXL * scaling
@ -206,6 +223,7 @@ ColumnLayout {
currentKey: Settings.data.ui.fontDefault currentKey: Settings.data.ui.fontDefault
placeholder: "Select default font..." placeholder: "Select default font..."
popupHeight: 420 * scaling popupHeight: 420 * scaling
minimumWidth: 300 * scaling
onSelected: function (key) { onSelected: function (key) {
Settings.data.ui.fontDefault = key Settings.data.ui.fontDefault = key
} }
@ -218,6 +236,7 @@ ColumnLayout {
currentKey: Settings.data.ui.fontFixed currentKey: Settings.data.ui.fontFixed
placeholder: "Select monospace font..." placeholder: "Select monospace font..."
popupHeight: 320 * scaling popupHeight: 320 * scaling
minimumWidth: 300 * scaling
onSelected: function (key) { onSelected: function (key) {
Settings.data.ui.fontFixed = key Settings.data.ui.fontFixed = key
} }
@ -230,6 +249,7 @@ ColumnLayout {
currentKey: Settings.data.ui.fontBillboard currentKey: Settings.data.ui.fontBillboard
placeholder: "Select display font..." placeholder: "Select display font..."
popupHeight: 320 * scaling popupHeight: 320 * scaling
minimumWidth: 300 * scaling
onSelected: function (key) { onSelected: function (key) {
Settings.data.ui.fontBillboard = key Settings.data.ui.fontBillboard = key
} }

View file

@ -52,20 +52,6 @@ ColumnLayout {
} }
} }
NToggle {
label: "Enable Clipboard History"
description: "Show clipboard history in 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."
checked: Settings.data.appLauncher.useApp2Unit
onToggled: checked => Settings.data.appLauncher.useApp2Unit = checked
}
ColumnLayout { ColumnLayout {
spacing: Style.marginXXS * scaling spacing: Style.marginXXS * scaling
Layout.fillWidth: true Layout.fillWidth: true
@ -105,6 +91,20 @@ ColumnLayout {
} }
} }
} }
NToggle {
label: "Enable Clipboard History"
description: "Show clipboard history in 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."
checked: Settings.data.appLauncher.useApp2Unit
onToggled: checked => Settings.data.appLauncher.useApp2Unit = checked
}
} }
NDivider { NDivider {

View file

@ -52,46 +52,6 @@ ColumnLayout {
Layout.bottomMargin: Style.marginXL * scaling Layout.bottomMargin: Style.marginXL * scaling
} }
// Time section
ColumnLayout {
spacing: Style.marginL * scaling
Layout.fillWidth: true
NText {
text: "Time Format"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mSecondary
}
NToggle {
label: "Use 12-Hour Clock"
description: "Display time in 12-hour format (AM/PM) instead of 24-hour."
checked: Settings.data.location.use12HourClock
onToggled: checked => Settings.data.location.use12HourClock = checked
}
NToggle {
label: "Reverse Day/Month"
description: "Display date as dd/mm instead of mm/dd."
checked: Settings.data.location.reverseDayMonth
onToggled: checked => Settings.data.location.reverseDayMonth = checked
}
NToggle {
label: "Show Date with Clock"
description: "Display date alongside time (e.g., 18:12 - Sat, 23 Aug)."
checked: Settings.data.location.showDateWithClock
onToggled: checked => Settings.data.location.showDateWithClock = checked
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
// Weather section // Weather section
ColumnLayout { ColumnLayout {
spacing: Style.marginM * scaling spacing: Style.marginM * scaling
@ -111,10 +71,4 @@ ColumnLayout {
onToggled: checked => Settings.data.location.useFahrenheit = checked onToggled: checked => Settings.data.location.useFahrenheit = checked
} }
} }
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginXL * scaling
Layout.bottomMargin: Style.marginXL * scaling
}
} }

View file

@ -40,7 +40,7 @@ NBox {
height: 68 * scaling height: 68 * scaling
} }
NCircleStat { NCircleStat {
value: SystemStatService.memoryUsagePer value: SystemStatService.memPercent
icon: "memory" icon: "memory"
flat: true flat: true
contentScale: 0.8 contentScale: 0.8
@ -48,7 +48,7 @@ NBox {
height: 68 * scaling height: 68 * scaling
} }
NCircleStat { NCircleStat {
value: SystemStatService.diskUsage value: SystemStatService.diskPercent
icon: "hard_drive" icon: "hard_drive"
flat: true flat: true
contentScale: 0.8 contentScale: 0.8

View file

@ -11,71 +11,92 @@ NPanel {
id: root id: root
panelWidth: 460 * scaling panelWidth: 460 * scaling
panelHeight: 708 * scaling panelHeight: contentHeight
// Default height, will be modified via binding when the content is fully loaded
property real contentHeight: 720 * scaling
panelContent: Item { panelContent: Item {
id: content id: content
property real cardSpacing: Style.marginL * scaling property real cardSpacing: Style.marginL * scaling
anchors.left: parent.left width: root.panelWidth
anchors.right: parent.right implicitHeight: layout.implicitHeight + (2 * cardSpacing)
anchors.top: parent.top height: implicitHeight
anchors.margins: content.cardSpacing
implicitHeight: layout.implicitHeight
// Layout content (not vertically anchored so implicitHeight is valid) // Update parent's contentHeight whenever our height changes
onHeightChanged: {
root.contentHeight = height
}
onImplicitHeightChanged: {
if (implicitHeight > 0) {
root.contentHeight = implicitHeight
}
}
// Layout content
ColumnLayout { ColumnLayout {
id: layout id: layout
// Use the same spacing value horizontally and vertically x: content.cardSpacing
anchors.left: parent.left y: content.cardSpacing
anchors.right: parent.right width: parent.width - (2 * content.cardSpacing)
anchors.top: parent.top
spacing: content.cardSpacing spacing: content.cardSpacing
// Cards (consistent inter-card spacing via ColumnLayout spacing) // Cards (consistent inter-card spacing via ColumnLayout spacing)
ProfileCard {// Layout.topMargin: 0 ProfileCard {
// Layout.bottomMargin: 0 id: profileCard
Layout.fillWidth: true
} }
WeatherCard {// Layout.topMargin: 0
// Layout.bottomMargin: 0 WeatherCard {
id: weatherCard
Layout.fillWidth: true
} }
// Middle section: media + stats column // Middle section: media + stats column
RowLayout { RowLayout {
id: middleRow
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 0 Layout.minimumHeight: 280 * scaling
Layout.bottomMargin: 0 Layout.preferredHeight: Math.max(280 * scaling, statsCard.implicitHeight)
spacing: content.cardSpacing spacing: content.cardSpacing
// Media card // Media card
MediaCard { MediaCard {
id: mediaCard id: mediaCard
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: statsCard.implicitHeight Layout.fillHeight: true
} }
// System monitors combined in one card // System monitors combined in one card
SystemMonitorCard { SystemMonitorCard {
id: statsCard id: statsCard
Layout.alignment: Qt.AlignTop
} }
} }
// Bottom actions (two grouped rows of round buttons) // Bottom actions (two grouped rows of round buttons)
RowLayout { RowLayout {
id: bottomRow
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 0 Layout.minimumHeight: 60 * scaling
Layout.bottomMargin: 0 Layout.preferredHeight: Math.max(60 * scaling, powerProfilesCard.implicitHeight, utilitiesCard.implicitHeight)
spacing: content.cardSpacing spacing: content.cardSpacing
// Power Profiles switcher // Power Profiles switcher
PowerProfilesCard { PowerProfilesCard {
id: powerProfilesCard
spacing: content.cardSpacing spacing: content.cardSpacing
Layout.fillWidth: true
} }
// Utilities buttons // Utilities buttons
UtilitiesCard { UtilitiesCard {
id: utilitiesCard
spacing: content.cardSpacing spacing: content.cardSpacing
Layout.fillWidth: true
} }
} }
} }

View file

@ -397,6 +397,7 @@ NPanel {
} }
outlined: !hovered outlined: !hovered
fontSize: Style.fontSizeXS * scaling fontSize: Style.fontSizeXS * scaling
enabled: !NetworkService.connecting
onClicked: { onClicked: {
if (modelData.existing || modelData.cached || !NetworkService.isSecured(modelData.security)) { if (modelData.existing || modelData.cached || !NetworkService.isSecured(modelData.security)) {
NetworkService.connect(modelData.ssid) NetworkService.connect(modelData.ssid)
@ -461,7 +462,7 @@ NPanel {
onVisibleChanged: if (visible) onVisibleChanged: if (visible)
forceActiveFocus() forceActiveFocus()
onAccepted: { onAccepted: {
if (text) { if (text && !NetworkService.connecting) {
NetworkService.connect(passwordSsid, text) NetworkService.connect(passwordSsid, text)
passwordSsid = "" passwordSsid = ""
passwordInput = "" passwordInput = ""
@ -481,7 +482,7 @@ NPanel {
NButton { NButton {
text: "Connect" text: "Connect"
fontSize: Style.fontSizeXXS * scaling fontSize: Style.fontSizeXXS * scaling
enabled: passwordInput.length > 0 enabled: passwordInput.length > 0 && !NetworkService.connecting
outlined: true outlined: true
onClicked: { onClicked: {
NetworkService.connect(passwordSsid, passwordInput) NetworkService.connect(passwordSsid, passwordInput)

View file

@ -38,6 +38,26 @@ Singleton {
}) })
property var widgetMetadata: ({ property var widgetMetadata: ({
"ActiveWindow": {
"allowUserSettings": true,
"showIcon": true
},
"Battery": {
"allowUserSettings": true,
"alwaysShowPercentage": false,
"warningThreshold": 30
},
"Brightness": {
"allowUserSettings": true,
"alwaysShowPercentage": false
},
"Clock": {
"allowUserSettings": true,
"showDate": false,
"use12HourClock": false,
"showSeconds": false,
"reverseDayMonth": true
},
"CustomButton": { "CustomButton": {
"allowUserSettings": true, "allowUserSettings": true,
"icon": "favorite", "icon": "favorite",
@ -45,10 +65,44 @@ Singleton {
"rightClickExec": "", "rightClickExec": "",
"middleClickExec": "" "middleClickExec": ""
}, },
"Microphone": {
"allowUserSettings": true,
"alwaysShowPercentage": false
},
"NotificationHistory": {
"allowUserSettings": true,
"showUnreadBadge": true,
"hideWhenZero": true
},
"Spacer": { "Spacer": {
"allowUserSettings": true, "allowUserSettings": true,
"icon": "space_bar",
"width": 20 "width": 20
},
"SystemMonitor": {
"allowUserSettings": true,
"showCpuUsage": true,
"showCpuTemp": true,
"showMemoryUsage": true,
"showMemoryAsPercent": false,
"showNetworkStats": false
},
"Workspace": {
"allowUserSettings": true,
"labelMode": "index"
},
"MediaMini": {
"allowUserSettings": true,
"showAlbumArt": false,
"showVisualizer": false,
"visualizerType": "linear"
},
"SidePanelToggle": {
"allowUserSettings": true,
"useDistroLogo": false
},
"Volume": {
"allowUserSettings": true,
"alwaysShowPercentage": false
} }
}) })

View file

@ -0,0 +1,49 @@
pragma Singleton
import Quickshell
import Quickshell.Services.UPower
Singleton {
id: root
// Choose icon based on charge and charging state
function getIcon(percent, charging, isReady) {
if (!isReady) {
return "battery_error"
}
if (charging) {
if (percent >= 95)
return "battery_full"
if (percent >= 85)
return "battery_charging_90"
if (percent >= 65)
return "battery_charging_80"
if (percent >= 55)
return "battery_charging_60"
if (percent >= 45)
return "battery_charging_50"
if (percent >= 25)
return "battery_charging_30"
if (percent >= 0)
return "battery_charging_20"
} else {
if (percent >= 95)
return "battery_full"
if (percent >= 85)
return "battery_6_bar"
if (percent >= 70)
return "battery_5_bar"
if (percent >= 55)
return "battery_4_bar"
if (percent >= 40)
return "battery_3_bar"
if (percent >= 25)
return "battery_2_bar"
if (percent >= 10)
return "battery_1_bar"
if (percent >= 0)
return "battery_0_bar"
}
}
}

View file

@ -110,9 +110,43 @@ Singleton {
property real lastBrightness: 0 property real lastBrightness: 0
property real queuedBrightness: NaN property real queuedBrightness: NaN
// For internal displays - store the backlight device path
property string backlightDevice: ""
property string brightnessPath: ""
property string maxBrightnessPath: ""
property int maxBrightness: 100
property bool ignoreNextChange: false
// Signal for brightness changes // Signal for brightness changes
signal brightnessUpdated(real newBrightness) signal brightnessUpdated(real newBrightness)
// FileView to watch for external brightness changes (internal displays only)
readonly property FileView brightnessWatcher: FileView {
id: brightnessWatcher
// Only set path for internal displays with a valid brightness path
path: (!monitor.isDdc && !monitor.isAppleDisplay && monitor.brightnessPath !== "") ? monitor.brightnessPath : ""
watchChanges: path !== ""
onFileChanged: {
reload()
if (monitor.ignoreNextChange) {
monitor.ignoreNextChange = false
return
}
if (text() === "")
return
var current = parseInt(text().trim())
if (!isNaN(current) && monitor.maxBrightness > 0) {
var newBrightness = current / monitor.maxBrightness
// Only update if it's actually different (avoid feedback loops)
if (Math.abs(newBrightness - monitor.brightness) > 0.01) {
monitor.brightness = newBrightness
monitor.brightnessUpdated(monitor.brightness)
//Logger.log("Brightness", "External change detected:", monitor.modelData.name, monitor.brightness)
}
}
}
}
// Initialize brightness // Initialize brightness
readonly property Process initProc: Process { readonly property Process initProc: Process {
stdout: StdioCollector { stdout: StdioCollector {
@ -121,8 +155,8 @@ Singleton {
if (dataText === "") { if (dataText === "") {
return return
} }
Logger.log("Brightness", "Raw brightness data for", monitor.modelData.name + ":", dataText)
//Logger.log("Brightness", "Raw brightness data for", monitor.modelData.name + ":", dataText)
if (monitor.isAppleDisplay) { if (monitor.isAppleDisplay) {
var val = parseInt(dataText) var val = parseInt(dataText)
if (!isNaN(val)) { if (!isNaN(val)) {
@ -140,14 +174,20 @@ Singleton {
} }
} }
} else { } else {
// Internal backlight // Internal backlight - parse the response which includes device path
var parts = dataText.split(" ") var lines = dataText.split("\n")
if (parts.length >= 2) { if (lines.length >= 3) {
var current = parseInt(parts[0]) monitor.backlightDevice = lines[0]
var max = parseInt(parts[1]) monitor.brightnessPath = monitor.backlightDevice + "/brightness"
monitor.maxBrightnessPath = monitor.backlightDevice + "/max_brightness"
var current = parseInt(lines[1])
var max = parseInt(lines[2])
if (!isNaN(current) && !isNaN(max) && max > 0) { if (!isNaN(current) && !isNaN(max) && max > 0) {
monitor.maxBrightness = max
monitor.brightness = current / max monitor.brightness = current / max
Logger.log("Brightness", "Internal brightness:", current + "/" + max + " =", monitor.brightness) Logger.log("Brightness", "Internal brightness:", current + "/" + max + " =", monitor.brightness)
Logger.log("Brightness", "Using backlight device:", monitor.backlightDevice)
} }
} }
} }
@ -171,7 +211,7 @@ Singleton {
function increaseBrightness(): void { function increaseBrightness(): void {
var stepSize = Settings.data.brightness.brightnessStep / 100.0 var stepSize = Settings.data.brightness.brightnessStep / 100.0
setBrightnessDebounced(brightness + stepSize) setBrightnessDebounced(monitor.brightness + stepSize)
} }
function decreaseBrightness(): void { function decreaseBrightness(): void {
@ -183,22 +223,23 @@ Singleton {
value = Math.max(0, Math.min(1, value)) value = Math.max(0, Math.min(1, value))
var rounded = Math.round(value * 100) var rounded = Math.round(value * 100)
if (Math.round(brightness * 100) === rounded) if (Math.round(monitor.brightness * 100) === rounded)
return return
if (isDdc && timer.running) { if (isDdc && timer.running) {
queuedBrightness = value monitor.queuedBrightness = value
return return
} }
brightness = value monitor.brightness = value
brightnessUpdated(brightness) brightnessUpdated(monitor.brightness)
if (isAppleDisplay) { if (isAppleDisplay) {
Quickshell.execDetached(["asdbctl", "set", rounded]) Quickshell.execDetached(["asdbctl", "set", rounded])
} else if (isDdc) { } else if (isDdc) {
Quickshell.execDetached(["ddcutil", "-b", busNum, "setvcp", "10", rounded]) Quickshell.execDetached(["ddcutil", "-b", busNum, "setvcp", "10", rounded])
} else { } else {
monitor.ignoreNextChange = true
Quickshell.execDetached(["brightnessctl", "s", rounded + "%"]) Quickshell.execDetached(["brightnessctl", "s", rounded + "%"])
} }
@ -208,7 +249,7 @@ Singleton {
} }
function setBrightnessDebounced(value: real): void { function setBrightnessDebounced(value: real): void {
queuedBrightness = value monitor.queuedBrightness = value
timer.restart() timer.restart()
} }
@ -218,8 +259,11 @@ Singleton {
} else if (isDdc) { } else if (isDdc) {
initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"] initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"]
} else { } else {
// Internal backlight - try to find the first available backlight device // Internal backlight - find the first available backlight device and get its info
initProc.command = ["sh", "-c", "for dev in /sys/class/backlight/*; do if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then echo \"$(cat $dev/brightness) $(cat $dev/max_brightness)\"; break; fi; done"] // This now returns: device_path, current_brightness, max_brightness (on separate lines)
initProc.command = ["sh", "-c", "for dev in /sys/class/backlight/*; do "
+ " if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then " + " echo \"$dev\"; "
+ " cat \"$dev/brightness\"; " + " cat \"$dev/max_brightness\"; " + " break; " + " fi; " + "done"]
} }
initProc.running = true initProc.running = true
} }

View file

@ -37,9 +37,7 @@ Singleton {
Process { Process {
id: process id: process
stdinEnabled: true stdinEnabled: true
running: (Settings.data.audio.visualizerType !== "none") running: true
&& (PanelService.getPanel("sidePanel").active || Settings.data.audio.showMiniplayerCava
|| (PanelService.lockScreen && PanelService.lockScreen.active))
command: ["cava", "-p", "/dev/stdin"] command: ["cava", "-p", "/dev/stdin"]
onExited: { onExited: {
stdinEnabled = true stdinEnabled = true

View file

@ -45,7 +45,7 @@ Singleton {
property string version: "Unknown" property string version: "Unknown"
property var contributors: [] property var contributors: []
property double timestamp: 0 property real timestamp: 0
} }
} }

View file

@ -18,6 +18,9 @@ Singleton {
property string disconnectingFrom: "" property string disconnectingFrom: ""
property string forgettingNetwork: "" property string forgettingNetwork: ""
property bool ignoreScanResults: false
property bool scanPending: false
// Persistent cache // Persistent cache
property string cacheFile: Settings.cacheDir + "network.json" property string cacheFile: Settings.cacheDir + "network.json"
readonly property string cachedLastConnected: cacheAdapter.lastConnected readonly property string cachedLastConnected: cacheAdapter.lastConnected
@ -54,7 +57,7 @@ Singleton {
Component.onCompleted: { Component.onCompleted: {
Logger.log("Network", "Service initialized") Logger.log("Network", "Service initialized")
syncWifiState() syncWifiState()
refresh() scan()
} }
// Save cache with debounce // Save cache with debounce
@ -75,6 +78,16 @@ Singleton {
onTriggered: scan() onTriggered: scan()
} }
// Ethernet check timer
// Always running every 30s
Timer {
id: ethernetCheckTimer
interval: 30000
running: true
repeat: true
onTriggered: ethernetStateProcess.running = true
}
// Core functions // Core functions
function syncWifiState() { function syncWifiState() {
wifiStateProcess.running = true wifiStateProcess.running = true
@ -82,26 +95,26 @@ Singleton {
function setWifiEnabled(enabled) { function setWifiEnabled(enabled) {
Settings.data.network.wifiEnabled = enabled Settings.data.network.wifiEnabled = enabled
wifiToggleProcess.action = enabled ? "on" : "off"
wifiToggleProcess.running = true
}
function refresh() {
ethernetStateProcess.running = true
if (Settings.data.network.wifiEnabled) {
scan()
}
} }
function scan() { function scan() {
if (scanning) if (!Settings.data.network.wifiEnabled)
return return
if (scanning) {
// Mark current scan results to be ignored and schedule a new scan
Logger.log("Network", "Scan already in progress, will ignore results and rescan")
ignoreScanResults = true
scanPending = true
return
}
scanning = true scanning = true
lastError = "" lastError = ""
scanProcess.running = true ignoreScanResults = false
// Get existing profiles first, then scan
profileCheckProcess.running = true
Logger.log("Network", "Wi-Fi scan in progress...") Logger.log("Network", "Wi-Fi scan in progress...")
} }
@ -174,8 +187,7 @@ Singleton {
"ssid": ssid, "ssid": ssid,
"security": "--", "security": "--",
"signal": 100, "signal": 100,
"connected"// Default to good signal until real scan "connected": true,
: true,
"existing": true, "existing": true,
"cached": true "cached": true
} }
@ -206,7 +218,7 @@ Singleton {
// Processes // Processes
Process { Process {
id: ethernetStateProcess id: ethernetStateProcess
running: false running: true
command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"] command: ["nmcli", "-t", "-f", "DEVICE,TYPE,STATE", "device"]
stdout: StdioCollector { stdout: StdioCollector {
@ -239,30 +251,34 @@ Singleton {
} }
} }
// Helper process to get existing profiles
Process { Process {
id: wifiToggleProcess id: profileCheckProcess
property string action: "on"
running: false running: false
command: ["nmcli", "radio", "wifi", action] command: ["nmcli", "-t", "-f", "NAME", "connection", "show"]
onRunningChanged: { stdout: StdioCollector {
if (!running) {
if (action === "on") {
// Clear networks immediately and start delayed scan
root.networks = ({})
delayedScanTimer.interval = 8000
delayedScanTimer.restart()
} else {
root.networks = ({})
}
}
}
stderr: StdioCollector {
onStreamFinished: { onStreamFinished: {
if (text.trim()) { if (root.ignoreScanResults) {
Logger.warn("Network", "WiFi toggle error: " + text) Logger.log("Network", "Ignoring profile check results (new scan requested)")
root.scanning = false
// Check if we need to start a new scan
if (root.scanPending) {
root.scanPending = false
delayedScanTimer.interval = 100
delayedScanTimer.restart()
}
return
} }
const profiles = {}
const lines = text.split("\n").filter(l => l.trim())
for (const line of lines) {
profiles[line.trim()] = true
}
scanProcess.existingProfiles = profiles
scanProcess.running = true
} }
} }
} }
@ -270,74 +286,103 @@ Singleton {
Process { Process {
id: scanProcess id: scanProcess
running: false running: false
command: ["sh", "-c", ` command: ["nmcli", "-t", "-f", "SSID,SECURITY,SIGNAL,IN-USE", "device", "wifi", "list", "--rescan", "yes"]
# Get list of saved connection profiles (just the names)
profiles=$(nmcli -t -f NAME connection show | tr '\n' '|')
# Get WiFi networks property var existingProfiles: ({})
nmcli -t -f SSID,SECURITY,SIGNAL,IN-USE device wifi list --rescan yes | while read line; do
ssid=$(echo "$line" | cut -d: -f1)
security=$(echo "$line" | cut -d: -f2)
signal=$(echo "$line" | cut -d: -f3)
in_use=$(echo "$line" | cut -d: -f4)
# Skip empty SSIDs
if [ -z "$ssid" ]; then
continue
fi
# Check if SSID matches any profile name (simple check)
# This covers most cases where profile name equals or contains the SSID
existing=false
if echo "$profiles" | grep -qF "$ssid|"; then
existing=true
fi
echo "$ssid|$security|$signal|$in_use|$existing"
done
`]
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
const nets = {} if (root.ignoreScanResults) {
const lines = text.split("\n").filter(l => l.trim()) Logger.log("Network", "Ignoring scan results (new scan requested)")
root.scanning = false
for (const line of lines) { // Check if we need to start a new scan
const parts = line.split("|") if (root.scanPending) {
if (parts.length < 5) root.scanPending = false
delayedScanTimer.interval = 100
delayedScanTimer.restart()
}
return
}
// Process the scan results as before...
const lines = text.split("\n")
const networksMap = {}
for (var i = 0; i < lines.length; ++i) {
const line = lines[i].trim()
if (!line)
continue continue
const ssid = parts[0] // Parse from the end to handle SSIDs with colons
if (!ssid || ssid.trim() === "") // Format is SSID:SECURITY:SIGNAL:IN-USE
continue // We know the last 3 fields, so everything else is SSID
const lastColonIdx = line.lastIndexOf(":")
const network = { if (lastColonIdx === -1) {
"ssid": ssid, Logger.warn("Network", "Malformed nmcli output line:", line)
"security": parts[1] || "--", continue
"signal": parseInt(parts[2]) || 0,
"connected": parts[3] === "*",
"existing": parts[4] === "true",
"cached": ssid in cacheAdapter.knownNetworks
} }
// Track connected network const inUse = line.substring(lastColonIdx + 1)
if (network.connected && cacheAdapter.lastConnected !== ssid) { const remainingLine = line.substring(0, lastColonIdx)
cacheAdapter.lastConnected = ssid
saveCache() const secondLastColonIdx = remainingLine.lastIndexOf(":")
if (secondLastColonIdx === -1) {
Logger.warn("Network", "Malformed nmcli output line:", line)
continue
} }
// Keep best signal for duplicate SSIDs const signal = remainingLine.substring(secondLastColonIdx + 1)
if (!nets[ssid] || network.signal > nets[ssid].signal) { const remainingLine2 = remainingLine.substring(0, secondLastColonIdx)
nets[ssid] = network
const thirdLastColonIdx = remainingLine2.lastIndexOf(":")
if (thirdLastColonIdx === -1) {
Logger.warn("Network", "Malformed nmcli output line:", line)
continue
}
const security = remainingLine2.substring(thirdLastColonIdx + 1)
const ssid = remainingLine2.substring(0, thirdLastColonIdx)
if (ssid) {
const signalInt = parseInt(signal) || 0
const connected = inUse === "*"
// Track connected network in cache
if (connected && cacheAdapter.lastConnected !== ssid) {
cacheAdapter.lastConnected = ssid
saveCache()
}
if (!networksMap[ssid]) {
networksMap[ssid] = {
"ssid": ssid,
"security": security || "--",
"signal": signalInt,
"connected": connected,
"existing": ssid in scanProcess.existingProfiles,
"cached": ssid in cacheAdapter.knownNetworks
}
} else {
// Keep the best signal for duplicate SSIDs
const existingNet = networksMap[ssid]
if (connected) {
existingNet.connected = true
}
if (signalInt > existingNet.signal) {
existingNet.signal = signalInt
existingNet.security = security || "--"
}
}
} }
} }
// For logging purpose only // Logging
Logger.log("Network", "Wi-Fi scan completed")
const oldSSIDs = Object.keys(root.networks) const oldSSIDs = Object.keys(root.networks)
const newSSIDs = Object.keys(nets) const newSSIDs = Object.keys(networksMap)
const newNetworks = newSSIDs.filter(ssid => !oldSSIDs.includes(ssid)) const newNetworks = newSSIDs.filter(ssid => !oldSSIDs.includes(ssid))
const lostNetworks = oldSSIDs.filter(ssid => !newSSIDs.includes(ssid)) const lostNetworks = oldSSIDs.filter(ssid => !newSSIDs.includes(ssid))
if (newNetworks.length > 0 || lostNetworks.length > 0) { if (newNetworks.length > 0 || lostNetworks.length > 0) {
if (newNetworks.length > 0) { if (newNetworks.length > 0) {
Logger.log("Network", "New Wi-Fi SSID discovered:", newNetworks.join(", ")) Logger.log("Network", "New Wi-Fi SSID discovered:", newNetworks.join(", "))
@ -345,12 +390,19 @@ Singleton {
if (lostNetworks.length > 0) { if (lostNetworks.length > 0) {
Logger.log("Network", "Wi-Fi SSID disappeared:", lostNetworks.join(", ")) Logger.log("Network", "Wi-Fi SSID disappeared:", lostNetworks.join(", "))
} }
Logger.log("Network", "Total Wi-Fi SSIDs:", Object.keys(nets).length) Logger.log("Network", "Total Wi-Fi SSIDs:", Object.keys(networksMap).length)
} }
// Assign the results Logger.log("Network", "Wi-Fi scan completed")
root.networks = nets root.networks = networksMap
root.scanning = false root.scanning = false
// Check if we need to start a new scan
if (root.scanPending) {
root.scanPending = false
delayedScanTimer.interval = 100
delayedScanTimer.restart()
}
} }
} }
@ -359,16 +411,14 @@ Singleton {
root.scanning = false root.scanning = false
if (text.trim()) { if (text.trim()) {
Logger.warn("Network", "Scan error: " + text) Logger.warn("Network", "Scan error: " + text)
// If scan fails, set a short retry
if (Settings.data.network.wifiEnabled) { // If scan fails, retry
delayedScanTimer.interval = 5000 delayedScanTimer.interval = 5000
delayedScanTimer.restart() delayedScanTimer.restart()
}
} }
} }
} }
} }
Process { Process {
id: connectProcess id: connectProcess
property string mode: "new" property string mode: "new"
@ -390,6 +440,17 @@ Singleton {
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
// Check if the output actually indicates success
// nmcli outputs "Device '...' successfully activated" or "Connection successfully activated"
// on success. Empty output or other messages indicate failure.
const output = text.trim()
if (!output || (!output.includes("successfully activated") && !output.includes("Connection successfully"))) {
// No success message - likely an error occurred
// Don't update anything, let stderr handler deal with it
return
}
// Success - update cache // Success - update cache
let known = cacheAdapter.knownNetworks let known = cacheAdapter.knownNetworks
known[connectProcess.ssid] = { known[connectProcess.ssid] = {
@ -408,7 +469,7 @@ Singleton {
Logger.log("Network", `Connected to network: "${connectProcess.ssid}"`) Logger.log("Network", `Connected to network: "${connectProcess.ssid}"`)
// Still do a scan to get accurate signal and security info // Still do a scan to get accurate signal and security info
delayedScanTimer.interval = 1000 delayedScanTimer.interval = 5000
delayedScanTimer.restart() delayedScanTimer.restart()
} }
} }
@ -464,7 +525,7 @@ Singleton {
Logger.warn("Network", "Disconnect error: " + text) Logger.warn("Network", "Disconnect error: " + text)
} }
// Still trigger a scan even on error // Still trigger a scan even on error
delayedScanTimer.interval = 1000 delayedScanTimer.interval = 5000
delayedScanTimer.restart() delayedScanTimer.restart()
} }
} }
@ -522,8 +583,8 @@ Singleton {
root.forgettingNetwork = "" root.forgettingNetwork = ""
// Quick scan to verify the profile is gone // Scan to verify the profile is gone
delayedScanTimer.interval = 500 delayedScanTimer.interval = 5000
delayedScanTimer.restart() delayedScanTimer.restart()
} }
} }
@ -535,7 +596,7 @@ Singleton {
Logger.warn("Network", "Forget error: " + text) Logger.warn("Network", "Forget error: " + text)
} }
// Still Trigger a scan even on error // Still Trigger a scan even on error
delayedScanTimer.interval = 500 delayedScanTimer.interval = 5000
delayedScanTimer.restart() delayedScanTimer.restart()
} }
} }

View file

@ -80,7 +80,7 @@ Singleton {
JsonAdapter { JsonAdapter {
id: historyAdapter id: historyAdapter
property var history: [] property var history: []
property double timestamp: 0 property real timestamp: 0
} }
} }
@ -118,12 +118,13 @@ Singleton {
// Function to add notification to model // Function to add notification to model
function addNotification(notification) { function addNotification(notification) {
const resolvedImage = resolveNotificationImage(notification)
notificationModel.insert(0, { notificationModel.insert(0, {
"rawNotification": notification, "rawNotification": notification,
"summary": notification.summary, "summary": notification.summary,
"body": notification.body, "body": notification.body,
"appName": notification.appName, "appName": notification.appName,
"image": notification.image, "image": resolvedImage,
"appIcon": notification.appIcon, "appIcon": notification.appIcon,
"urgency": notification.urgency, "urgency": notification.urgency,
"timestamp": new Date() "timestamp": new Date()
@ -139,6 +140,40 @@ Singleton {
} }
} }
// Resolve an image path for a notification, supporting icon names and absolute paths
function resolveNotificationImage(notification) {
try {
// If an explicit image is already provided, prefer it
if (notification && notification.image && notification.image !== "") {
return notification.image
}
// Fallback to appIcon which may be a name or a path (notify-send -i)
const icon = notification ? (notification.appIcon || "") : ""
if (!icon)
return ""
// Accept absolute file paths or file URLs directly
if (icon.startsWith("/")) {
return icon
}
if (icon.startsWith("file://")) {
// Strip the scheme for QML image source compatibility
return icon.substring("file://".length)
}
// Resolve themed icon names to absolute paths
try {
const p = Icons.iconFromName(icon, "")
return p || ""
} catch (e2) {
return ""
}
} catch (e) {
return ""
}
}
// Add a simplified copy into persistent history // Add a simplified copy into persistent history
function addToHistory(notification) { function addToHistory(notification) {
historyModel.insert(0, { historyModel.insert(0, {
@ -166,12 +201,17 @@ Singleton {
const items = historyAdapter.history || [] const items = historyAdapter.history || []
for (var i = 0; i < items.length; i++) { for (var i = 0; i < items.length; i++) {
const it = items[i] const it = items[i]
// Coerce legacy second-based timestamps to milliseconds
var ts = it.timestamp
if (typeof ts === "number" && ts < 1e12) {
ts = ts * 1000
}
historyModel.append({ historyModel.append({
"summary": it.summary || "", "summary": it.summary || "",
"body": it.body || "", "body": it.body || "",
"appName": it.appName || "", "appName": it.appName || "",
"urgency": it.urgency, "urgency": it.urgency,
"timestamp": it.timestamp ? new Date(it.timestamp) : new Date() "timestamp": ts ? new Date(ts) : new Date()
}) })
} }
} catch (e) { } catch (e) {
@ -190,7 +230,10 @@ Singleton {
"body": n.body, "body": n.body,
"appName": n.appName, "appName": n.appName,
"urgency": n.urgency, "urgency": n.urgency,
"timestamp": (n.timestamp instanceof Date) ? n.timestamp.getTime() : n.timestamp "timestamp"// Always persist in milliseconds
: (n.timestamp instanceof Date) ? n.timestamp.getTime(
) : (typeof n.timestamp === "number"
&& n.timestamp < 1e12 ? n.timestamp * 1000 : n.timestamp)
}) })
} }
historyAdapter.history = arr historyAdapter.history = arr

View file

@ -4,6 +4,7 @@ import QtQuick
import Qt.labs.folderlistmodel import Qt.labs.folderlistmodel
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Commons
Singleton { Singleton {
id: root id: root
@ -11,12 +12,313 @@ Singleton {
// Public values // Public values
property real cpuUsage: 0 property real cpuUsage: 0
property real cpuTemp: 0 property real cpuTemp: 0
property real memoryUsageGb: 0 property real memGb: 0
property real memoryUsagePer: 0 property real memPercent: 0
property real diskUsage: 0 property real diskPercent: 0
property real rxSpeed: 0 property real rxSpeed: 0
property real txSpeed: 0 property real txSpeed: 0
// Configuration
property int sleepDuration: 3000
// Internal state for CPU calculation
property var prevCpuStats: null
// Internal state for network speed calculation
// Previous Bytes need to be stored as 'real' as they represent the total of bytes transfered
// since the computer started, so their value will easily overlfow a 32bit int.
property real prevRxBytes: 0
property real prevTxBytes: 0
property real prevTime: 0
// Cpu temperature is the most complex
readonly property var supportedTempCpuSensorNames: ["coretemp", "k10temp", "zenpower"]
property string cpuTempSensorName: ""
property string cpuTempHwmonPath: ""
// For Intel coretemp averaging of all cores/sensors
property var intelTempValues: []
property int intelTempFilesChecked: 0
property int intelTempMaxFiles: 20 // Will test up to temp20_input
// --------------------------------------------
Component.onCompleted: {
Logger.log("SystemStat", "Service started with interval:", root.sleepDuration, "ms")
// Kickoff the cpu name detection for temperature
cpuTempNameReader.checkNext()
}
// --------------------------------------------
// Timer for periodic updates
Timer {
id: updateTimer
interval: root.sleepDuration
repeat: true
running: true
triggeredOnStart: true
onTriggered: {
// Trigger all direct system files reads
memInfoFile.reload()
cpuStatFile.reload()
netDevFile.reload()
// Run df (disk free) one time
dfProcess.running = true
updateCpuTemperature()
}
}
// --------------------------------------------
// FileView components for reading system files
FileView {
id: memInfoFile
path: "/proc/meminfo"
onLoaded: parseMemoryInfo(text())
}
FileView {
id: cpuStatFile
path: "/proc/stat"
onLoaded: calculateCpuUsage(text())
}
FileView {
id: netDevFile
path: "/proc/net/dev"
onLoaded: calculateNetworkSpeed(text())
}
// --------------------------------------------
// Process to fetch disk usage in percent
// Uses 'df' aka 'disk free'
Process {
id: dfProcess
command: ["df", "--output=pcent", "/"]
running: false
stdout: StdioCollector {
onStreamFinished: {
const lines = text.trim().split('\n')
if (lines.length >= 2) {
const percent = lines[1].replace(/[^0-9]/g, '')
root.diskPercent = parseInt(percent) || 0
}
}
}
}
// --------------------------------------------
// CPU Temperature
// It's more complex.
// ----
// #1 - Find a common cpu sensor name ie: "coretemp", "k10temp", "zenpower"
FileView {
id: cpuTempNameReader
property int currentIndex: 0
function checkNext() {
if (currentIndex >= 10) {
// Check up to hwmon10
Logger.warn("No supported temperature sensor found")
return
}
//Logger.log("SystemStat", "---- Probing: hwmon", currentIndex)
cpuTempNameReader.path = `/sys/class/hwmon/hwmon${currentIndex}/name`
cpuTempNameReader.reload()
}
onLoaded: {
const name = text().trim()
if (root.supportedTempCpuSensorNames.includes(name)) {
root.cpuTempSensorName = name
root.cpuTempHwmonPath = `/sys/class/hwmon/hwmon${currentIndex}`
Logger.log("SystemStat", `Found ${root.cpuTempSensorName} CPU thermal sensor at ${root.cpuTempHwmonPath}`)
} else {
currentIndex++
Qt.callLater(() => {
// Qt.callLater is mandatory
checkNext()
})
}
}
onLoadFailed: function (error) {
currentIndex++
Qt.callLater(() => {
// Qt.callLater is mandatory
checkNext()
})
}
}
// ----
// #2 - Read sensor value
FileView {
id: cpuTempReader
printErrors: false
onLoaded: {
const data = text().trim()
if (root.cpuTempSensorName === "coretemp") {
// For Intel, collect all temperature values
const temp = parseInt(data) / 1000.0
//console.log(temp, cpuTempReader.path)
root.intelTempValues.push(temp)
Qt.callLater(() => {
// Qt.callLater is mandatory
checkNextIntelTemp()
})
} else {
// For AMD sensors (k10temp and zenpower), directly set the temperature
root.cpuTemp = Math.round(parseInt(data) / 1000.0)
}
}
onLoadFailed: function (error) {
Qt.callLater(() => {
// Qt.callLater is mandatory
checkNextIntelTemp()
})
}
}
// -------------------------------------------------------
// Parse memory info from /proc/meminfo
function parseMemoryInfo(text) {
if (!text)
return
const lines = text.split('\n')
let memTotal = 0
let memAvailable = 0
for (const line of lines) {
if (line.startsWith('MemTotal:')) {
memTotal = parseInt(line.split(/\s+/)[1]) || 0
} else if (line.startsWith('MemAvailable:')) {
memAvailable = parseInt(line.split(/\s+/)[1]) || 0
}
}
if (memTotal > 0) {
const usageKb = memTotal - memAvailable
root.memGb = (usageKb / 1000000).toFixed(1)
root.memPercent = Math.round((usageKb / memTotal) * 100)
}
}
// -------------------------------------------------------
// Calculate CPU usage from /proc/stat
function calculateCpuUsage(text) {
if (!text)
return
const lines = text.split('\n')
const cpuLine = lines[0]
// First line is total CPU
if (!cpuLine.startsWith('cpu '))
return
const parts = cpuLine.split(/\s+/)
const stats = {
"user": parseInt(parts[1]) || 0,
"nice": parseInt(parts[2]) || 0,
"system": parseInt(parts[3]) || 0,
"idle": parseInt(parts[4]) || 0,
"iowait": parseInt(parts[5]) || 0,
"irq": parseInt(parts[6]) || 0,
"softirq": parseInt(parts[7]) || 0,
"steal": parseInt(parts[8]) || 0,
"guest": parseInt(parts[9]) || 0,
"guestNice": parseInt(parts[10]) || 0
}
const totalIdle = stats.idle + stats.iowait
const total = Object.values(stats).reduce((sum, val) => sum + val, 0)
if (root.prevCpuStats) {
const prevTotalIdle = root.prevCpuStats.idle + root.prevCpuStats.iowait
const prevTotal = Object.values(root.prevCpuStats).reduce((sum, val) => sum + val, 0)
const diffTotal = total - prevTotal
const diffIdle = totalIdle - prevTotalIdle
if (diffTotal > 0) {
root.cpuUsage = (((diffTotal - diffIdle) / diffTotal) * 100).toFixed(1)
}
}
root.prevCpuStats = stats
}
// -------------------------------------------------------
// Calculate RX and TX speed from /proc/net/dev
// Average speed of all interfaces excepted 'lo'
function calculateNetworkSpeed(text) {
if (!text) {
return
}
const currentTime = Date.now() / 1000
const lines = text.split('\n')
let totalRx = 0
let totalTx = 0
for (var i = 2; i < lines.length; i++) {
const line = lines[i].trim()
if (!line) {
continue
}
const colonIndex = line.indexOf(':')
if (colonIndex === -1) {
continue
}
const iface = line.substring(0, colonIndex).trim()
if (iface === 'lo') {
continue
}
const statsLine = line.substring(colonIndex + 1).trim()
const stats = statsLine.split(/\s+/)
const rxBytes = parseInt(stats[0], 10) || 0
const txBytes = parseInt(stats[8], 10) || 0
totalRx += rxBytes
totalTx += txBytes
}
// Compute only if we have a previous run to compare to.
if (root.prevTime > 0) {
const timeDiff = currentTime - root.prevTime
// Avoid division by zero if time hasn't passed.
if (timeDiff > 0) {
let rxDiff = totalRx - root.prevRxBytes
let txDiff = totalTx - root.prevTxBytes
// Handle counter resets (e.g., WiFi reconnect), which would cause a negative value.
if (rxDiff < 0) {
rxDiff = 0
}
if (txDiff < 0) {
txDiff = 0
}
root.rxSpeed = Math.round(rxDiff / timeDiff) // Speed in Bytes/s
root.txSpeed = Math.round(txDiff / timeDiff)
}
}
root.prevRxBytes = totalRx
root.prevTxBytes = totalTx
root.prevTime = currentTime
}
// -------------------------------------------------------
// Helper function to format network speeds // Helper function to format network speeds
function formatSpeed(bytesPerSecond) { function formatSpeed(bytesPerSecond) {
if (bytesPerSecond < 1024) { if (bytesPerSecond < 1024) {
@ -30,27 +332,44 @@ Singleton {
} }
} }
// Background process emitting one JSON line per sample // -------------------------------------------------------
Process { // Function to start fetching and computing the cpu temperature
id: reader function updateCpuTemperature() {
running: true // For AMD sensors (k10temp and zenpower), only use Tctl sensor
command: ["sh", "-c", Quickshell.shellDir + "/Bin/system-stats.sh"] // temp1_input corresponds to Tctl (Temperature Control) on these sensors
stdout: SplitParser { if (root.cpuTempSensorName === "k10temp" || root.cpuTempSensorName === "zenpower") {
onRead: function (line) { cpuTempReader.path = `${root.cpuTempHwmonPath}/temp1_input`
try { cpuTempReader.reload()
const data = JSON.parse(line) } // For Intel coretemp, start averaging all available sensors/cores
root.cpuUsage = data.cpu else if (root.cpuTempSensorName === "coretemp") {
root.cpuTemp = data.cputemp root.intelTempValues = []
root.memoryUsageGb = data.memgb root.intelTempFilesChecked = 0
root.memoryUsagePer = data.memper checkNextIntelTemp()
root.diskUsage = data.diskper
root.rxSpeed = parseFloat(data.rx_speed) || 0
root.txSpeed = parseFloat(data.tx_speed) || 0
} catch (e) {
// ignore malformed lines
}
}
} }
} }
// -------------------------------------------------------
// Function to check next Intel temperature sensor
function checkNextIntelTemp() {
if (root.intelTempFilesChecked >= root.intelTempMaxFiles) {
// Calculate average of all found temperatures
if (root.intelTempValues.length > 0) {
let sum = 0
for (var i = 0; i < root.intelTempValues.length; i++) {
sum += root.intelTempValues[i]
}
root.cpuTemp = Math.round(sum / root.intelTempValues.length)
//Logger.log("SystemStat", `Averaged ${root.intelTempValues.length} CPU thermal sensors: ${root.cpuTemp}°C`)
} else {
Logger.warn("SystemStat", "No temperature sensors found for coretemp")
root.cpuTemp = 0
}
return
}
// Check next temperature file
root.intelTempFilesChecked++
cpuTempReader.path = `${root.cpuTempHwmonPath}/temp${root.intelTempFilesChecked}_input`
cpuTempReader.reload()
}
} }

View file

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

View file

@ -216,7 +216,11 @@ Singleton {
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Get specific monitor wallpaper - now from cache // Get specific monitor wallpaper - now from cache
function getWallpaper(screenName) { function getWallpaper(screenName) {
return currentWallpapers[screenName] || "" var path = currentWallpapers[screenName] || ""
if (path === "") {
return Settings.data.wallpaper.defaultWallpaper || ""
}
return path
} }
// ------------------------------------------------------------------- // -------------------------------------------------------------------

View file

@ -77,10 +77,12 @@ Rectangle {
RowLayout { RowLayout {
id: contentRow id: contentRow
anchors.centerIn: parent anchors.centerIn: parent
spacing: Style.marginS * scaling spacing: Style.marginXS * scaling
// Icon (optional) // Icon (optional)
NIcon { NIcon {
Layout.alignment: Qt.AlignVCenter
layoutTopMargin: 1 * scaling
visible: root.icon !== "" visible: root.icon !== ""
text: root.icon text: root.icon
font.pointSize: root.iconSize font.pointSize: root.iconSize
@ -105,6 +107,7 @@ Rectangle {
// Text // Text
NText { NText {
Layout.alignment: Qt.AlignVCenter
visible: root.text !== "" visible: root.text !== ""
text: root.text text: root.text
font.pointSize: root.fontSize font.pointSize: root.fontSize

View file

@ -27,6 +27,11 @@ RowLayout {
visible: root.label !== "" || root.description !== "" visible: root.label !== "" || root.description !== ""
} }
// Spacer to push the checkbox to the far right
Item {
Layout.fillWidth: true
}
Rectangle { Rectangle {
id: box id: box
@ -39,13 +44,13 @@ RowLayout {
Behavior on color { Behavior on color {
ColorAnimation { ColorAnimation {
duration: Style.animationNormal duration: Style.animationFast
} }
} }
Behavior on border.color { Behavior on border.color {
ColorAnimation { ColorAnimation {
duration: Style.animationNormal duration: Style.animationFast
} }
} }

View file

@ -1,34 +0,0 @@
import QtQuick
import qs.Commons
import qs.Services
import qs.Widgets
Rectangle {
id: root
signal entered
signal exited
signal clicked
width: textItem.paintedWidth
height: textItem.paintedHeight
color: Color.transparent
NText {
id: textItem
text: Time.time
anchors.centerIn: parent
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightBold
}
MouseArea {
id: clockMouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: root.entered()
onExited: root.exited()
onClicked: root.clicked()
}
}

View file

@ -8,8 +8,7 @@ import qs.Widgets
RowLayout { RowLayout {
id: root id: root
readonly property real preferredHeight: Style.baseWidgetSize * 1.1 * scaling property real minimumWidth: 280 * scaling
property real preferredWidth: 320 * scaling
property real popupHeight: 180 * scaling property real popupHeight: 180 * scaling
property string label: "" property string label: ""
@ -20,9 +19,11 @@ RowLayout {
property string currentKey: "" property string currentKey: ""
property string placeholder: "" property string placeholder: ""
readonly property real preferredHeight: Style.baseWidgetSize * 1.1 * scaling
signal selected(string key) signal selected(string key)
spacing: Style.marginS * scaling spacing: Style.marginL * scaling
Layout.fillWidth: true Layout.fillWidth: true
function findIndexByKey(key) { function findIndexByKey(key) {
@ -39,11 +40,15 @@ RowLayout {
description: root.description description: root.description
} }
Item {
Layout.fillWidth: true
}
ComboBox { ComboBox {
id: combo id: combo
Layout.preferredWidth: root.preferredWidth Layout.minimumWidth: root.minimumWidth
Layout.preferredHeight: height Layout.preferredHeight: root.preferredHeight
model: model model: model
currentIndex: findIndexByKey(currentKey) currentIndex: findIndexByKey(currentKey)
onActivated: { onActivated: {

View file

@ -1,8 +1,11 @@
import QtQuick import QtQuick
import qs.Commons import qs.Commons
import qs.Widgets import qs.Widgets
import QtQuick.Layouts
Text { Text {
// Optional layout nudge for optical alignment when used inside Layouts
property real layoutTopMargin: 0
text: "question_mark" text: "question_mark"
font.family: "Material Symbols Rounded" font.family: "Material Symbols Rounded"
font.pointSize: Style.fontSizeL * scaling font.pointSize: Style.fontSizeL * scaling
@ -12,4 +15,5 @@ Text {
} }
color: Color.mOnSurface color: Color.mOnSurface
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
Layout.topMargin: layoutTopMargin
} }

View file

@ -20,6 +20,7 @@ ColumnLayout {
font.capitalization: Font.Capitalize font.capitalization: Font.Capitalize
color: labelColor color: labelColor
visible: label !== "" visible: label !== ""
Layout.fillWidth: true
} }
NText { NText {

View file

@ -14,6 +14,8 @@ Item {
property color iconCircleColor: Color.mPrimary property color iconCircleColor: Color.mPrimary
property color iconTextColor: Color.mSurface property color iconTextColor: Color.mSurface
property color collapsedIconColor: Color.mOnSurface property color collapsedIconColor: Color.mOnSurface
property real iconRotation: 0
property real sizeRatio: 0.8 property real sizeRatio: 0.8
property bool autoHide: false property bool autoHide: false
property bool forceOpen: false property bool forceOpen: false
@ -37,13 +39,13 @@ Item {
property bool shouldAnimateHide: false property bool shouldAnimateHide: false
// Exposed width logic // Exposed width logic
readonly property int pillHeight: Style.baseWidgetSize * sizeRatio * scaling readonly property int iconSize: Math.round(Style.baseWidgetSize * sizeRatio * scaling)
readonly property int iconSize: Style.baseWidgetSize * sizeRatio * scaling readonly property int pillHeight: iconSize
readonly property int pillPaddingHorizontal: Style.marginM * scaling readonly property int pillPaddingHorizontal: Style.marginS * scaling
readonly property int pillOverlap: iconSize * 0.5 readonly property int pillOverlap: iconSize * 0.5
readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap) readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap)
width: iconSize + (effectiveShown ? maxPillWidth - pillOverlap : 0) width: iconSize + Math.max(0, pill.width - pillOverlap)
height: pillHeight height: pillHeight
Rectangle { Rectangle {
@ -65,7 +67,13 @@ Item {
NText { NText {
id: textItem id: textItem
anchors.centerIn: parent anchors.verticalCenter: parent.verticalCenter
x: {
// Little tweak to have a better text horizontal centering
var centerX = (parent.width - width) / 2
var offset = rightOpen ? Style.marginXS * scaling : -Style.marginXS * scaling
return centerX + offset
}
text: root.text text: root.text
font.pointSize: Style.fontSizeXS * scaling font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightBold font.weight: Style.fontWeightBold
@ -97,9 +105,10 @@ Item {
// When forced shown, match pill background; otherwise use accent when hovered // When forced shown, match pill background; otherwise use accent when hovered
color: forceOpen ? pillColor : (showPill ? iconCircleColor : Color.mSurfaceVariant) color: forceOpen ? pillColor : (showPill ? iconCircleColor : Color.mSurfaceVariant)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
border.width: Math.max(1, Style.borderS * scaling)
border.color: forceOpen ? Qt.alpha(Color.mOutline, 0.5) : Color.transparent
anchors.left: rightOpen ? parent.left : undefined x: rightOpen ? 0 : (parent.width - width)
anchors.right: rightOpen ? undefined : parent.right
Behavior on color { Behavior on color {
ColorAnimation { ColorAnimation {
@ -110,6 +119,7 @@ Item {
NIcon { NIcon {
text: root.icon text: root.icon
rotation: root.iconRotation
font.pointSize: Style.fontSizeM * scaling font.pointSize: Style.fontSizeM * scaling
// When forced shown, use pill text color; otherwise accent color when hovered // When forced shown, use pill text color; otherwise accent color when hovered
color: forceOpen ? textColor : (showPill ? iconTextColor : Color.mOnSurface) color: forceOpen ? textColor : (showPill ? iconTextColor : Color.mOnSurface)

View file

@ -44,7 +44,7 @@ RowLayout {
radius: height * 0.5 // Fully rounded like toggle radius: height * 0.5 // Fully rounded like toggle
color: Color.mSurfaceVariant color: Color.mSurfaceVariant
border.color: root.hovering ? Color.mPrimary : Color.mOutline border.color: root.hovering ? Color.mPrimary : Color.mOutline
border.width: 1 border.width: Math.max(1, Style.borderS * scaling)
Behavior on border.color { Behavior on border.color {
ColorAnimation { ColorAnimation {

View file

@ -13,4 +13,5 @@ Text {
font.kerning: true font.kerning: true
color: Color.mOnSurface color: Color.mOnSurface
renderType: Text.QtRendering renderType: Text.QtRendering
verticalAlignment: Text.AlignVCenter
} }