Compare commits

...

741 Commits

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

Might sound weird at first but it fixes a lot of inconsistencies/issues
we have had for a long time when a panel was open:
- can't close panel when clicking in a dead zone of the bar.
- hovering an icon on the bar used to make it look like you could
interact with it, but the click would just close the panel and not
actuall y do anything with bar .

I recommend turning back on dimming, as it is now way cooler. Changed
the default to true.
2025-09-16 21:53:11 -04:00
LemmyCook
6f1ae43d62 Dimmer: new implementation of the screen diming in a separate component. 2025-09-16 21:35:27 -04:00
LemmyCook
eb26aa10f7 NPanel: Reworked all margins and X,Y computation to make things simpler. Fix #298
- Temporarily removed Dimming as it was a pain to manage on each panel,
this will be reimplemented in a better way soon.
2025-09-16 20:28:07 -04:00
Randi Budi
cdfb110007 fix(nixos): power profile and battery monitoring with module 2025-09-17 04:20:37 +07:00
Randi Budi
b7d8f92414 fix(nixos): add wlsunset dependency for night light 2025-09-17 00:59:13 +07:00
LemmyCook
b625df6484 Icons: slightly smaller noctalia logo to better match the others. 2025-09-16 12:22:51 -04:00
LemmyCook
2c3eb6efda Launcher: AppPlugin, close panel immediately to avoid focusing issues. 2025-09-16 12:09:12 -04:00
Ly-sec
8e034cd912 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-16 15:25:52 +02:00
Ly-sec
03bdfdb340 Notification: replace unread badge with small circle 2025-09-16 15:25:46 +02:00
LemmyCook
95d2dbe3fc Optional capsule bg 2025-09-16 09:23:37 -04:00
LemmyCook
071100459f Better compact mode 2025-09-16 09:06:40 -04:00
LemmyCook
0da59954cd Workspace: less chunky when no numbers 2025-09-16 08:55:36 -04:00
LemmyCook
2e63f93d41 Workspace: less chunky 2025-09-16 08:48:19 -04:00
LemmyCook
c6ee99375d Screen recorder: typo fix 2025-09-16 08:35:43 -04:00
LemmyCook
ed6562475d Monitors configuration: improved description. Fix #292 2025-09-16 08:16:29 -04:00
LemmyCook
a2caebb8e5 Bar Density: improved workspace widget + slight density adjustments 2025-09-16 08:10:10 -04:00
Ly-sec
03698e7bb9 ActiveWindow: use same font height as MediaMini 2025-09-16 13:08:30 +02:00
Ly-sec
d8db086127 NotificationHistoryPanel: remove hover of notifications 2025-09-16 09:05:17 +02:00
LemmyCook
339505abe3 Workspace: better font sizing for active workspace 2025-09-16 00:42:44 -04:00
LemmyCook
b52451fde5 Bar density: leftover files from previous commit 2025-09-16 00:40:02 -04:00
LemmyCook
ac1902c76a Bar: compact mode works pretty well but need some more testing. 2025-09-16 00:39:30 -04:00
LemmyCook
93c674f356 SysMonitor: converted dual layout for vertical/horiz bar to a single grid layout 2025-09-15 23:06:06 -04:00
LemmyCook
5f3add5d99 autoformatting 2025-09-15 22:56:22 -04:00
LemmyCook
937675ebb3 TaskBar: implemented vertical mode 2025-09-15 22:56:05 -04:00
LemmyCook
47ef62beb3 Widgets Sizing: reworked our sizing approach to prepare for different bar densities. 2025-09-15 22:33:09 -04:00
LemmyCook
593a0bfc2c NColorPicker: sizing improvements 2025-09-15 21:36:09 -04:00
LemmyCook
abe51f4928 NSpinBox: use fixed font for number 2025-09-15 21:21:42 -04:00
LemmyCook
33a75d042d IconImage: They have to be asynchronous or the may crash QS on startup. TaskBar was crashing very often during development. 2025-09-15 21:07:42 -04:00
Ly-sec
2a62a13b16 README: remove useless layer-rule 2025-09-15 19:40:00 +02:00
LemmyCook
4edeedd5ad v2.9.2-dev 2025-09-15 13:19:12 -04:00
Ly-sec
8a17c047c9 README: add missing icon info 2025-09-15 19:01:18 +02:00
Ly-sec
899595ec5c Release v2.9.2 2025-09-15 14:52:09 +02:00
Ly-sec
dbf1020636 CustomButton: add script execution/polling support with text display 2025-09-15 14:37:29 +02:00
LemmyCook
7def695c0e AboutTab: Fix hover on contributos 2025-09-15 08:36:18 -04:00
LemmyCook
b51a87a981 NSlider: slightly more discrete bg track 2025-09-15 08:36:08 -04:00
LemmyCook
3da2682111 Settings: fix about tab alignment of download button 2025-09-15 07:58:33 -04:00
Ly-sec
758f2f2e55 SystemMonitor: fix network stats, move text above storage icon (vertical bar) 2025-09-15 13:43:58 +02:00
Lysec
26a27c3393 Merge pull request #282 from Mtendekuyokwa19/everforest
Everforest
2025-09-15 09:39:42 +02:00
Mtende Kuyokwa
9d9bfb54e1 everforest light 2025-09-15 09:30:26 +02:00
Mtende Kuyokwa
ab5b1e4d82 everforest dark complete 2025-09-15 08:40:11 +02:00
Ly-sec
8cb9f04a22 ScreenCorners: add solid black option 2025-09-15 08:28:57 +02:00
Ly-sec
22bc5a3bff Move ScreenCorners to the actualy screen edges when bar is floating,
edit vesktop template
ScreenCorners: move to screen corners on floating bar
BarTab: mention the ScreenCorner changes
vesktop: make read channels have less visible text
2025-09-15 08:08:32 +02:00
Ly-sec
f5982f41a2 Remove noctalia.svg 2025-09-15 07:59:39 +02:00
Ly-sec
9a80d51b10 Vesktop: replace old theme with midnight from refact0r for better looks 2025-09-15 07:59:06 +02:00
Ly-sec
fa838ecdb1 Cleaned up ColorSchemeTab, added program checks, added firefox template
Matugen: added firefox (pywalfox) template
SidePanelToggle: use ProgramCheckerService for gpu-screen-recorder
ColorSchemeTab: use NCollapsible for matugen templates, use
ProgramCheckerService to detect available programs (for matugen
templates)
NCollapsible: create collapsible category
2025-09-15 07:44:31 +02:00
ItsLemmy
c0d6780c3d Volume/Brightness/Microphone: fixed tooltips to new mapping 2025-09-14 23:45:17 -04:00
ItsLemmy
8935f9a0f9 NPill: fix broken mouse-wheel control 2025-09-14 23:42:21 -04:00
LemmyCook
b8b97c46a0 DistroLogo: respect original colors, and avoid changing bg color when hovering to compensate. 2025-09-14 23:07:00 -04:00
Lemmy
f2bbf70f93 Merge pull request #279 from MrDowntempo/better-cats
Updated Catppuccin to use more of the official colors.
2025-09-14 22:54:44 -04:00
Corey Woodworth
ecf468f78f Updated Catppuccin to use more of the official colors. Also changed Green to Teal in Mocha and a few minor tweaks for readability. 2025-09-14 22:50:29 -04:00
Lemmy
0b35fc1d2d Merge pull request #278 from kevindiaz314/main
feat: update tokyo night light color scheme with refined color values
2025-09-14 22:47:03 -04:00
LemmyCook
94c5d73a61 NPill: fix icon bg hover color 2025-09-14 22:46:40 -04:00
LemmyCook
f399a6d9f5 TrayMenu: improve tray opening direction in vertical bar more 2025-09-14 22:44:27 -04:00
Kevin Diaz
44fd859aec feat: update tokyo night light color scheme with refined color values 2025-09-14 22:33:13 -04:00
LemmyCook
53d0c3943d TrayMenu: Fix submenu burger icon color when hovered. 2025-09-14 22:24:39 -04:00
Lemmy
d80f923802 Merge pull request #229 from matejc/main
Fix for fingerprint flow on lock screen
2025-09-14 22:18:14 -04:00
Lemmy
1b861d7b7b Merge pull request #277 from kevindiaz314/main
feat: update tokyo night color scheme with refined color values
2025-09-14 22:07:10 -04:00
LemmyCook
65933208ec NPillVertical: match NHorizontal on margins and color 2025-09-14 22:03:44 -04:00
LemmyCook
5df218a789 Bar Widgets: Removed 3 unecessary anchors 2025-09-14 21:56:55 -04:00
LemmyCook
2f7a834b55 Bar: fix centering (againg) 2025-09-14 21:55:21 -04:00
LemmyCook
eca301553e Bar: better vertical centering on horizontal bar. 2025-09-14 21:51:43 -04:00
LemmyCook
91efa38101 HorizontalPill: different color for forceOpen + better margins 2025-09-14 21:49:29 -04:00
Kevin Diaz
acfe94f736 feat: update tokyo night color scheme with refined color values 2025-09-14 21:24:23 -04:00
LemmyCook
97bfcbb9e8 Clock: height calculation similar to NPill to avoid discrepancies 2025-09-14 21:15:27 -04:00
LemmyCook
9e47d91be2 v2.9.1-dev / git 2025-09-14 21:03:21 -04:00
LemmyCook
8872002225 Release 2.9.1 2025-09-14 21:01:14 -04:00
LemmyCook
519a85b251 AudioTab: fixed spinbox 2025-09-14 20:53:42 -04:00
LemmyCook
5aa7ff7e91 NValueSlider: new component + pimped NSlider with a small gradient and removed rounded corners due to issues. 2025-09-14 20:52:32 -04:00
LemmyCook
5ce5659b38 NPills: keep hover even when force open, as there are actions available on clicks. 2025-09-14 18:21:24 -04:00
LemmyCook
00459606ce Brightness: hotfix 2025-09-14 18:19:02 -04:00
LemmyCook
da1081700a NPill: replaced "Normal" by "On Hover" 2025-09-14 18:15:07 -04:00
LemmyCook
7e965262f5 NPill: Restored the old horizontal NPill 2025-09-14 18:07:43 -04:00
LemmyCook
19312d94c3 Removing test mode on battery 2025-09-14 17:21:38 -04:00
Ly-sec
118323e6b5 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-14 23:14:59 +02:00
Ly-sec
aed81e82b0 Remove "%" from NVerticalPill add force close option to it too
NVerticalPill: add force close option
Any vertical bar widget: remove "%" display to have nice horizontal text
BarTab: add "always hide percentage" option so the pills will never
expand (opposite of always show percentage)
2025-09-14 23:13:11 +02:00
Lemmy
76c167c2c2 Merge pull request #273 from ThatOneCalculator/fix/power-menu-suspend-icon
fix(consistency): use unfilled pause icon for suspend in power menu
2025-09-14 17:08:40 -04:00
Ly-sec
852e2fa4d1 Fix N*Pill force show layout 2025-09-14 22:39:16 +02:00
Ly-sec
17dceffff6 Overview: add autoPaddingEnabled:false to MultiEffect blur 2025-09-14 22:33:44 +02:00
Kainoa Kanter
b589f37e0b use unfilled pause icon for suspend in power menu 2025-09-14 13:27:07 -07:00
Lysec
682c6af231 Merge pull request #267 from ThatOneCalculator/patch-1
docs: mention `gpu-screen-recorder` Flatpak
2025-09-14 22:25:13 +02:00
Ly-sec
d71226c6bd Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-14 22:09:40 +02:00
Ly-sec
1f3725faf8 set version to dev 2025-09-14 22:09:38 +02:00
LemmyCook
efcec1b2f9 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-14 15:58:58 -04:00
LemmyCook
651322aef0 Bar: Removed unecessary qt5compat import 2025-09-14 15:58:56 -04:00
Ly-sec
a0a3a58668 Release v2.9.0
- **Floating Mode**: Added floating option for more flexible bar positioning
- **Vertical Orientation**: New vertical bar layout support

- **Exclusive Mode**: Added exclusive setting to prevent windows from rendering behind the dock
- **Floating Distance Control**: Added control for adjusting floating distance
- **Layout Refinements**: Various layout fixes for better visual consistency

- **Enhanced Navigation**: More panels now support closing with the Escape key
- **Settings Overhaul**: Complete revamp of the settings window tab content

- **Layout Editor**: Added ability to edit keyboard layouts directly

- **GPU Temperature**: Removed GPU temperature monitoring (resolved NVIDIA compatibility issues)

- **Compact Mode**: New compact version for space-constrained layouts

- **Hyprland Stability**: Added numerous null checks for improved Hyprland compatibility
- **Niri Support**: Fixed active window detection for the Niri compositor
- **Workspace Visibility**: Added toggle to hide unoccupied workspaces

- **Monochrome Theme**: Added new monochrome color scheme option

- **Bluetooth Stability**: More stable connections and adapter state management
- **Toast Notifications**: Fixed odd toast notification behavior
- **Font Service**: Improved font service reliability and added fuzzy search for the font selection in General Tab
2025-09-14 21:51:58 +02:00
Ly-sec
b3abe44d65 Bar: remove Qt5Compat import 2025-09-14 21:37:56 +02:00
LemmyCook
5b603472bd Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-14 15:25:59 -04:00
LemmyCook
57b0fe8a21 Wi-Fi: connect and disconnect toast messages 2025-09-14 15:25:57 -04:00
Ly-sec
f5561da3cc Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-14 21:25:14 +02:00
Ly-sec
11f6475b9f Some layout fixes to toggle and slider
NSlider(withLabel): fix some small layout issues
NToggle: fix vertical centering of the thumb
2025-09-14 21:24:11 +02:00
LemmyCook
fb2c5e0470 SysMon: removed unecessary Item {} 2025-09-14 15:14:45 -04:00
LemmyCook
b1764fddc8 SysMon: larger margin 2025-09-14 15:12:08 -04:00
Ly-sec
bb7f957e44 Clock: change to mono font 2025-09-14 21:10:16 +02:00
LemmyCook
0682315c9d Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-14 15:07:03 -04:00
LemmyCook
dd100597ed SysMon: better lookin 2025-09-14 15:05:34 -04:00
Ly-sec
6bc6380ee1 FontService: even more mono font fixes 2025-09-14 21:04:46 +02:00
Ly-sec
966089e471 FontService: more mono font fixes 2025-09-14 20:47:36 +02:00
Ly-sec
3956461254 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-14 20:38:32 +02:00
Ly-sec
02d114a05e FontService: more reliable mono lookup 2025-09-14 20:36:52 +02:00
LemmyCook
3764edafa8 Widgets: improved centering 2025-09-14 14:35:15 -04:00
Ly-sec
1cd0376381 Style: reduce vertical bar to 39 2025-09-14 20:30:26 +02:00
LemmyCook
2b154e2cdb Settings: added missing end divider to tabs 2025-09-14 13:58:00 -04:00
LemmyCook
933dfc402b Wifi+BT: added right click 2025-09-14 13:48:12 -04:00
LemmyCook
34d037d7dc Toast: improved clickability around toast 2025-09-14 13:37:15 -04:00
LemmyCook
76b6626073 Trying to match all buttons left/right click.
- left click: mute/unmute, cycle between functionality
- right click: open settings
- middle click: open external settings
2025-09-14 13:35:24 -04:00
LemmyCook
d348cfc2b0 Toast: refactored service vs UI. 2025-09-14 13:29:20 -04:00
LemmyCook
f9d7de2e3c Volume: Fixed missing externalHideTimer 2025-09-14 12:32:05 -04:00
LemmyCook
2ea00fffa5 NSlider: simplification, no Halo + some rounding 2025-09-14 12:22:26 -04:00
LemmyCook
b163dab241 NCheckbox+NToggle: better look 2025-09-14 12:03:15 -04:00
LemmyCook
af0f4818d8 Autoformatting 2025-09-14 11:51:04 -04:00
LemmyCook
8b6c7632af Tray: fixed with vertical bar 2025-09-14 11:50:18 -04:00
LemmyCook
d6d51d24c9 NPill: fix NPill icon color to match or icons (mOnSurface, even tho the bg is mSurfaceVariant) 2025-09-14 11:33:48 -04:00
LemmyCook
0c6aea7154 Vertical Bar! 2025-09-14 11:26:36 -04:00
LemmyCook
a61b2edd07 Settings: fully cleanup and aligned 2025-09-14 11:23:20 -04:00
LemmyCook
c108e7707a Settings: cleanup, almost there! 2025-09-14 11:14:37 -04:00
LemmyCook
f3123ba5b1 Settings: more cleanup - wip 2025-09-14 10:57:24 -04:00
LemmyCook
c09a93af48 NHeader: use label instead of title (matches NLabel) 2025-09-14 10:24:09 -04:00
LemmyCook
2a262999ce Merge branch 'vertical-bar' of github.com:noctalia-dev/noctalia-shell into vertical-bar 2025-09-14 10:17:30 -04:00
LemmyCook
7d952dc226 Settings: new display tab 2025-09-14 10:17:28 -04:00
Ly-sec
3cb838b455 NCheckbox: edit sizing
NToggle: edit sizing, fix thumb vertical center
2025-09-14 16:01:00 +02:00
Ly-sec
7594651e05 SettingsTabs: use NHeader, move display settings around 2025-09-14 15:50:23 +02:00
Ly-sec
0d611fc891 Merge branch 'vertical-bar' of https://github.com/noctalia-dev/noctalia-shell into vertical-bar 2025-09-14 13:46:05 +02:00
Ly-sec
8982909fae Edit Style.qml so barHeight check for vertical bar
SystemMonitor.qml: edit layout a little bit
2025-09-14 13:45:17 +02:00
ItsLemmy
132b331c7c Dock: fix floating distance when bar is at the bottom 2025-09-14 07:42:10 -04:00
ItsLemmy
85cef214c8 Merge branch 'vertical-bar' of github.com:Ly-sec/Noctalia into vertical-bar 2025-09-14 07:27:16 -04:00
ItsLemmy
80b4dad199 NPill: using monospace font 2025-09-14 07:27:14 -04:00
Ly-sec
aadbc9596d NSearchableComboBox: small layout change 2025-09-14 13:25:02 +02:00
Ly-sec
0949d154c1 Merge branch 'vertical-bar' of https://github.com/noctalia-dev/noctalia-shell into vertical-bar 2025-09-14 13:22:49 +02:00
Ly-sec
a86a0d33c1 NSearchableComboBox: created, uses fuzzy find
GeneralTab: replace NComboBox with NSearchableComboBox
2025-09-14 13:22:17 +02:00
ItsLemmy
e6372a2473 VerticalBar: smaller spacing and margin 2025-09-14 07:21:49 -04:00
ItsLemmy
e3d9ab5679 NPill better naming so files stay closeby 2025-09-14 07:21:32 -04:00
Ly-sec
ccd7458ea3 KeyboardLayout: fix language detection/parsing
Bar: add a tiny bit more spacing between widgets
NHorizontalPill: fix layout
MediaMini: set size to 0 if no media is playing
2025-09-14 10:21:53 +02:00
Ly-sec
d41b59d563 KeyboardLayout: fix ukranian iso code 2025-09-14 09:31:07 +02:00
Ly-sec
290ba4ac03 Fix N*Pill expanded text layout 2025-09-14 09:26:05 +02:00
Ly-sec
1ee14df915 Make things more readable 2025-09-14 09:05:51 +02:00
LemmyCook
76376a9783 Dock: do not show dock if no app/toplevel available 2025-09-13 22:13:02 -04:00
LemmyCook
880ac93662 autoformatting 2025-09-13 22:12:44 -04:00
LemmyCook
1157c8e21d FloatingBar: Wip 2025-09-13 22:04:36 -04:00
LemmyCook
2082cfe7c7 Merge branch 'main' into vertical-bar 2025-09-13 15:27:55 -04:00
LemmyCook
9a9f2886e0 Floating Bar: Fix for #265 (overlapping panels, toasts and notifications) 2025-09-13 15:23:27 -04:00
Ly-sec
0035fbcc4e NPill: act as loder for NVerticalPill and NHorizontalPill
NHorizontalPill: should be used for anything that expands horizontal
NVerticalPill: should be used for anything that expands vertical
2025-09-13 20:52:20 +02:00
Kainoa Kanter
5ca2c2a095 docs: mention gpu-screen-recorder Flatpak 2025-09-13 11:42:41 -07:00
Lysec
46103062d0 Merge pull request #266 from povvke/fix-app2unit-steam-games
Fix steam games not launching with app2unit
2025-09-13 20:02:10 +02:00
povvke
78a41c236c use the exec string itself to launch non terminal apps 2025-09-13 20:48:34 +03:00
Ly-sec
9dfac69e9e More spacing fixes 2025-09-13 19:28:44 +02:00
LemmyCook
de72236fe5 Merge branch 'main' into vertical-bar 2025-09-13 13:06:21 -04:00
LemmyCook
101e3125a9 Vertical bar: simpler management 2025-09-13 13:06:17 -04:00
Ly-sec
b443c9f492 Add compact clock again 2025-09-13 17:46:38 +02:00
Ly-sec
2a1e7832d6 Revert 8c81514 2025-09-13 17:44:31 +02:00
Ly-sec
8c815146e6 More fixes 2025-09-13 17:34:13 +02:00
LemmyCook
acae2b8c21 Dock: border alpha follows bg opacity 2025-09-13 11:09:55 -04:00
Ly-sec
004836fc8f More layout fixes 2025-09-13 17:00:49 +02:00
Ly-sec
b51f2d16cb Change Notification location 2025-09-13 16:47:32 +02:00
Ly-sec
6fba9d9f22 NPanel positioning fixes 2025-09-13 16:45:22 +02:00
LemmyCook
335e38d461 Floating Bar: simplified settings 2025-09-13 10:16:54 -04:00
Ly-sec
ee50d84a53 Fix spacing for vertical bar 2025-09-13 15:51:21 +02:00
Ly-sec
e706dabef3 Add BarService, use signals to check state of bar and update widgets accordingly 2025-09-13 15:31:23 +02:00
Ly-sec
dcedae46e5 Horizontal bar: try to get better spacing 2025-09-13 15:21:36 +02:00
LemmyCook
f27f9d35b0 Merge branch 'hyprland-smarter-detect' 2025-09-13 09:10:50 -04:00
Ly-sec
4f5acb7114 First iteration of vertical bar 2025-09-13 14:26:20 +02:00
Lysec
25ba27cbdd Merge pull request #262 from Mtendekuyokwa19/nixpkg
invalid nixpkg change
2025-09-13 13:14:26 +02:00
Mtende Kuyokwa
74da975ed4 invalid nixpkg change 2025-09-13 13:04:54 +02:00
Ly-sec
6f6a5b364a Bar: proper top/bottom margin check 2025-09-13 10:55:14 +02:00
Ly-sec
f670f88804 NPanel: add margin if bar is floating (except for SettingsPanel) 2025-09-13 10:25:50 +02:00
Ly-sec
814cb774a6 Notification: added margin if bar is floating 2025-09-13 10:13:51 +02:00
Ly-sec
50d8b54adf Bar: add floating setting 2025-09-13 10:11:57 +02:00
LemmyCook
ae931b791f Icons: replaced most left over filled icons by the outlined counterparts.
- only kept a few filled for basic controls (carets, play, pause,
etc...)
2025-09-12 23:20:33 -04:00
LemmyCook
dd4641eedd CompositorService: improved Hyprland detection so there is no warning on Niri. 2025-09-12 22:55:13 -04:00
LemmyCook
b66bb46fc1 We don't use qmlformat, we do use qmlfmt 2025-09-12 21:18:24 -04:00
LemmyCook
5079fc78d3 Removed ArchUpdateService 2025-09-12 21:16:50 -04:00
LemmyCook
7d2eaa46e6 qmlfmt: increase line-length to 360 to avoid hard-wrap.
+ cleaned up power menu/panel
2025-09-12 21:07:11 -04:00
LemmyCook
1043eaa39f Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-12 20:45:08 -04:00
LemmyCook
f16798f6e3 LockScreen: shorter tooltips 2025-09-12 20:45:06 -04:00
Lemmy
96acb1a679 Update README.md 2025-09-12 20:42:37 -04:00
LemmyCook
0f93797ab3 LockScreen: tooltip uniformisation 2025-09-12 18:22:12 -04:00
LemmyCook
3186a84d6b Settings: using "cloud-sun" for weather tab 2025-09-12 18:18:12 -04:00
Lemmy
d3ee66d845 Merge pull request #261 from MrDowntempo/monochrome
Updated Monochrome to not be more grayscale and pretty
2025-09-12 18:05:10 -04:00
Corey Woodworth
a8837283ab Updated Monochrome to not be more grayscale and pretty 2025-09-12 16:59:08 -04:00
LemmyCook
6fe0784c00 Autoformatting 2025-09-12 16:45:35 -04:00
Lemmy
59ce164b40 Merge pull request #257 from mkuritsu/main
Add toggle to hide unoccupied workspaces
2025-09-12 16:42:36 -04:00
mkuritsu
70144eb06f Fixed redundant comparison 2025-09-12 21:33:12 +01:00
Ly-sec
5136af5d95 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-12 22:20:48 +02:00
Ly-sec
ff42244c6d Revert hardcoded font change 2025-09-12 22:20:46 +02:00
mkuritsu
3a2bb40117 Add toggle to hide unoccupied workspaces 2025-09-12 21:16:44 +01:00
LemmyCook
bcd3100849 Merge branch 'dock-better-peeking' 2025-09-12 16:10:45 -04:00
LemmyCook
c8886629ad Dock: Float improvements, can click below dock and on the side. Should fix #237 2025-09-12 16:10:03 -04:00
Ly-sec
be4a69f6e0 Replace hardcoded font with check for default fonts, fall back to
inter/roboto
Settings: use font detection function
GeneralTab: let user know that it uses default fonts and falls back to
inter/roboto
FontService: add proper checks for default fonts (sans & mono)
2025-09-12 22:00:05 +02:00
LemmyCook
62b12d5436 Bar ethernet icon: unfilled 2025-09-12 14:50:09 -04:00
LemmyCook
99e75d51b8 Bar settings icon: unfilled 2025-09-12 14:48:13 -04:00
Lemmy
079c8f0803 Merge pull request #259 from ThatOneCalculator/settings-icons
fix: consistent settings icons
2025-09-12 14:46:26 -04:00
Kainoa Kanter
16f87cbfa3 fix: consistent settings icons 2025-09-12 11:22:41 -07:00
LemmyCook
380f31fbd9 BaBar Widgets: pass a proper section name instead of a longer string. 2025-09-12 12:54:09 -04:00
LemmyCook
28677d6888 Panels: added kb focus to BTPanel, NotifHistory, SidePanel, so they close with ESC. 2025-09-12 11:29:46 -04:00
Matej Cotman
b9ae772987 feat(Modules/LockScreen): divert PAM messages to user (eg: to notify the user about fingerprint reader) 2025-09-12 18:26:15 +03:00
Matej Cotman
4265290a0f fix(fingerprint): better fingerprint integration by removing the check for empty password 2025-09-12 18:26:11 +03:00
Lemmy
07e94b0f0e Update feature_request.md 2025-09-12 10:58:27 -04:00
Lemmy
307318918d Update bug_report.md 2025-09-12 10:58:11 -04:00
Lemmy
3f6662182e Merge pull request #258 from matejc/feat/notifications-close-on-clear
feat(Modules/Notification): auto-close history panel on clear history
2025-09-12 10:52:11 -04:00
Matej Cotman
be532fa146 feat(Modules/Notification): auto-close history panel on clear history 2025-09-12 17:44:14 +03:00
mkuritsu
722a59da80 add qmlformat simple file, add .gitignore with .qmlls.ini 2025-09-12 11:48:56 +01:00
Lemmy
3c97acf00f Merge pull request #255 from MrDowntempo/monochrome
Add Monochrome (Black & White) color scheme
2025-09-12 00:03:47 -04:00
Corey Woodworth
2d4fa59c41 Add Monochrome (Black & White) color scheme 2025-09-11 23:31:52 -04:00
LemmyCook
c5ca758d3e Settings: New Dock tab. 2025-09-11 23:31:40 -04:00
LemmyCook
6f70a98b83 ColorSchemeTab: fixed currently selected scheme to match wallpaper. 2025-09-11 23:18:13 -04:00
LemmyCook
424594a11a Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-11 22:46:21 -04:00
LemmyCook
f5ac42c692 Dock: Slightly more compact 2025-09-11 22:46:19 -04:00
Lemmy
675f96d0e6 Merge pull request #254 from SeraphimRP/patch-1
Fix a missing semicolon in Nix instructions.
2025-09-11 21:48:23 -04:00
Rdr. Seraphim Pardee
9570688294 Fix a missing semicolon in Nix instructions. 2025-09-11 20:45:48 -04:00
LemmyCook
626b745ce3 Settings: Added a ScreenCorners section in the general tab. 2025-09-11 19:22:07 -04:00
Lemmy
4afb98cf4c Merge pull request #244 from juvevood/screen-corners-radius
Screen Corners use general radius ratio of settings
2025-09-11 19:17:42 -04:00
LemmyCook
df2a9a246d Dock: New "exclusive" settings to ensure no windows go below. 2025-09-11 19:14:19 -04:00
Lemmy
d80e9ba3d5 Merge pull request #252 from BinaryQuantumSoul/patch-1
Update nix readme instructions
2025-09-11 19:01:49 -04:00
Lemmy
130c68b3e2 Merge pull request #250 from matejc/fix/bluetooth-switch
fix(bluetooth): rename wifiSwitch to bluetoothSwitch
2025-09-11 18:59:33 -04:00
Lemmy
6eea4a17a4 Merge pull request #253 from SailorSnoW/fix/screen-recorder-aarch
import gpu-screen-recorder only on x86_64 in nix flake
2025-09-11 18:56:29 -04:00
SailorSnoW
40dc8633ec import gpu-screen-recorder only on x86_64 2025-09-12 00:38:38 +02:00
QuantumSoul
12ac91d125 Update README.md 2025-09-12 00:31:41 +02:00
LemmyCook
87d86911d7 BarSectionEdit: fix click in the background closing panel, fix ghost bg color when dragging 2025-09-11 18:06:12 -04:00
LemmyCook
2872a7b5c9 Using NScrollView and NListView where it matters.
Not using them in tiny ListViews (ex: NComboBox, and Media player
dropdown)
2025-09-11 17:58:28 -04:00
LemmyCook
4067896434 New components: NScrollView + NListView
Allow controlling the handle color and stuf...
2025-09-11 17:56:47 -04:00
LemmyCook
78443451e4 Bar Widgets: Hover color switched from mPrimary to mTertiary for consistency 2025-09-11 17:30:52 -04:00
LemmyCook
719f5a20e7 Bar widget editor: better colors + autoformatting 2025-09-11 16:45:33 -04:00
LemmyCook
d8b12e6d6b Rosepine: revamped light theme by following RosePine Dawn 2025-09-11 16:29:11 -04:00
LemmyCook
9a0746d737 PowerToggle: was not receiving scaling which led to a broken bar. 2025-09-11 15:56:10 -04:00
LemmyCook
77f8b3937c RosePine: improve dark theme 2025-09-11 15:13:05 -04:00
Matej Cotman
3f4313635a fix(bluetooth): rename wifiSwitch to bluetoothSwitch to fix the toggle switch 2025-09-11 21:23:22 +03:00
LemmyCook
004d92a85d SidePanelToggle: use Noctalia logo by default 2025-09-11 13:34:04 -04:00
LemmyCook
720c17258b Weather: use the regular "sun" icon (unfilled) for better uniformity 2025-09-11 11:52:33 -04:00
LemmyCook
a8b312f3a7 SidePanel: even more robust with sizing forced everywhere 2025-09-11 11:47:09 -04:00
LemmyCook
4d6361dfe5 Updated font 2025-09-11 11:46:50 -04:00
LemmyCook
1f75819795 Tabler icons: commented out all broken icons (due to Qt's font rendering) 2025-09-11 11:26:50 -04:00
LemmyCook
50ddd2916c autoformatting 2025-09-11 11:26:29 -04:00
Ly-sec
d30e14f611 CompositorService: add tons of null checks to perhaps prevent QS crashes
(and add some logging)
ActiveWindow: added debounce for icons
KeyboardLayoutService: remove console logs
2025-09-11 17:18:25 +02:00
LemmyCook
227b0dd962 removed extra logs 2025-09-11 09:46:45 -04:00
LemmyCook
ac61086c95 Autoformatting 2025-09-11 09:45:26 -04:00
LemmyCook
0980f65751 Cloud-sun icon 2025-09-11 09:45:21 -04:00
LemmyCook
7aa3da2ff4 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-11 09:45:05 -04:00
LemmyCook
83fbb8f95d Clock: factorized many settings in a single combobox 2025-09-11 09:43:52 -04:00
Ly-sec
a029463527 CompositorService: use idx for niri workspaces 2025-09-11 14:31:36 +02:00
Ly-sec
baafe54d13 Clock: small changes to compact mode 2025-09-11 13:53:45 +02:00
Ly-sec
a1cbd35202 Clock: add compact mode with nnumeric/verbose date options 2025-09-11 13:03:39 +02:00
LemmyCook
1337a35a1e Removed video 2025-09-10 22:37:28 -04:00
Lysec
61006fbed0 Update README.md 2025-09-11 04:21:55 +02:00
Ly-sec
eff4337d35 README: more updates 2025-09-11 04:18:33 +02:00
Juve
f0733f19dd add a separate configuration item for edge of screen 2025-09-11 10:11:01 +08:00
Ly-sec
818df48787 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-11 04:01:02 +02:00
Ly-sec
0eedfba071 README: update preview 2025-09-11 04:00:49 +02:00
LemmyCook
2dc9e2f212 Bluetooth: proper synchronisation of the adapter state with the cached setting 2025-09-10 21:29:11 -04:00
LemmyCook
62a3b343cf Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-10 21:14:04 -04:00
LemmyCook
76be93a84d NPanel: fix 3 minor warnings 2025-09-10 21:14:01 -04:00
Ly-sec
b59c56170e Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-11 03:02:44 +02:00
Ly-sec
c9285d8c5b SystemMonitor: remove GPU temp 2025-09-11 03:02:36 +02:00
LemmyCook
b157d855a8 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-10 20:45:56 -04:00
LemmyCook
82ac49ce85 NPanel: simplified screen/scaling management 2025-09-10 20:45:50 -04:00
Ly-sec
7247a26586 KeyboardLayout: add tons of layouts, add Commons/KeyboardLayout.qml for ease of adding new ones 2025-09-11 01:40:25 +02:00
Ly-sec
be0b568f1f KeyboardLayout: increase font size and make it all caps 2025-09-11 01:10:04 +02:00
Lysec
e4b54e518c Merge pull request #243 from juvevood/fix-powerpanel-shortcut
fix for PowerPanel Shortcut invalid
2025-09-11 00:53:50 +02:00
Lysec
6ea1e2b4c7 Update README.md 2025-09-11 00:15:52 +02:00
Ly-sec
5b4c57eae2 Set version to dev 2025-09-10 23:28:23 +02:00
Ly-sec
6e5efc3244 Release v2.8.0
We've been busy squashing bugs and adding some nice improvements based on your feedback.
What's New
New Icon Set - Swapped out Material Symbols for Tabler icons. They look great and load faster since they're built right in.
Works on Any Linux Distro - Dropped the Arch-specific update checker so this works properly on whatever distro you're running. You can build your own update notifications with Custom Buttons if you want.
Icon Picker - Added a proper icon picker for custom button widgets. No more guessing icon names.
Smarter Audio Visualizer - The Cava visualizer actually pays attention now - it only kicks in when you're playing music or videos instead of running all the time.
Better Notifications - Notifications now show actual app names like "Firefox" instead of cryptic IDs like "org.mozilla.firefox".
Less Noise - Turned a bunch of those persistent notification popups into toast notifications so they don't stick around cluttering your screen.
Fixes

Active Window widget finally shows the right app icon and title consistently
Fixed a nasty crash on Hyprland
Screen recorder button disables itself if the recording software isn't installed
Added a force-enable option for Night Light so you can turn it on manually whenever
2025-09-10 23:19:22 +02:00
Ly-sec
271a887bbf Release Notes
We've been busy squashing bugs and adding some nice improvements based on your feedback.
What's New
New Icon Set - Swapped out Material Symbols for Tabler icons. They look great and load faster since they're built right in.
Works on Any Linux Distro - Dropped the Arch-specific update checker so this works properly on whatever distro you're running. You can build your own update notifications with Custom Buttons if you want.
Icon Picker - Added a proper icon picker for custom button widgets. No more guessing icon names.
Smarter Audio Visualizer - The Cava visualizer actually pays attention now - it only kicks in when you're playing music or videos instead of running all the time.
Better Notifications - Notifications now show actual app names like "Firefox" instead of cryptic IDs like "org.mozilla.firefox".
Less Noise - Turned a bunch of those persistent notification popups into toast notifications so they don't stick around cluttering your screen.
Fixes

Active Window widget finally shows the right app icon and title consistently
Fixed a nasty crash on Hyprland
Screen recorder button disables itself if the recording software isn't installed
Added a force-enable option for Night Light so you can turn it on manually whenever
2025-09-10 23:14:58 +02:00
Ly-sec
0571ba7325 test commit 2025-09-10 23:14:39 +02:00
Ly-sec
c2f6c39016 Revert "Release v2.8.0"
This reverts commit 2de2908509.
2025-09-10 23:13:02 +02:00
Ly-sec
2de2908509 Release v2.8.0
We've been busy squashing bugs and adding some nice improvements based on your feedback.
What's New
New Icon Set - Swapped out Material Symbols for Tabler icons. They look great and load faster since they're built right in.
Updater Widget - Dropped the Arch-specific update checker so this works properly on whatever distro you're running. You can build your own update widget with Custom Buttons if you want.
Icon Picker - Added a proper icon picker for custom button widgets. No more guessing icon names.
Better Notifications - Notifications now show actual app names like "Firefox" instead of cryptic IDs like "org.mozilla.firefox".
Less Noise - Turned a bunch of those persistent notification popups into toast notifications so they don't stick around cluttering your screen.
Fixes

Active Window widget finally shows the right app icon and title consistently
Fixed a nasty crash on Hyprland
Screen recorder button disables itself if the recording software isn't installed
Added a force-enable option for Night Light so you can turn it on manually whenever

That's what claude had  to offer😄
2025-09-10 23:07:54 +02:00
LemmyCook
99d56687ef SysStat: house keeping (keep cpu stuff grouped) 2025-09-10 14:34:27 -04:00
LemmyCook
434b8273f0 SystemStats: better gpu logging 2025-09-10 14:31:05 -04:00
LemmyCook
663382c81c Icons: "trash" instead of "trash-filled" 2025-09-10 10:56:31 -04:00
LemmyCook
3f388bdb4b Widgets Drag&Drop: fix for panel closing when clicking rapidly in the background of a widget. 2025-09-10 09:33:08 -04:00
LemmyCook
0a4317f712 More drag and drop fixes 2025-09-10 09:11:13 -04:00
LemmyCook
b9dbbf7bdd Widgets Drag&Drop: drop indicator and improved behavior 2025-09-10 09:02:09 -04:00
LemmyCook
6ed9a8c5ae SysMon: smaller font 2025-09-10 08:30:38 -04:00
LemmyCook
73de564bb6 IconPicker: fixed at 6 columns with slightly bigger icons 2025-09-10 08:13:10 -04:00
LemmyCook
1f62cdedb5 Icons: cloud-fog 2025-09-10 08:04:18 -04:00
Ly-sec
7ed0e894ec Icons: updated TablerIcons, NightLight 2025-09-10 13:51:37 +02:00
Ly-sec
d39a9a85bf SystemMonitor: add GPU temperature option 2025-09-10 13:17:35 +02:00
Ly-sec
d16d1c1d26 NotificationHistory: even more fixes for appIcon 2025-09-10 12:55:56 +02:00
Ly-sec
291ffac102 NotificationHistory: possible visibility fix for app icons 2025-09-10 12:52:32 +02:00
Ly-sec
2b18ed3c41 NotificationHistory: add app icon display 2025-09-10 12:47:04 +02:00
Ly-sec
3b50efc7d0 ColorScheme: possible fix for selecting colorscheme & dark mode toggle 2025-09-10 12:39:15 +02:00
Ly-sec
d91a635781 NightLight: add force activation 2025-09-10 12:34:52 +02:00
Juve
4afe2d8448 Screen Corners use gerneral radius ratio of settings 2025-09-10 12:55:50 +08:00
LemmyCook
74fce51c2d Icons: new aliases image => photo 2025-09-09 23:59:21 -04:00
Juve
44cdbfe5d7 fix for PowerPanel Shortcut invalid 2025-09-10 11:45:49 +08:00
LemmyCook
4fbb8314eb Icons: settings-network: sitemap-filled 2025-09-09 23:13:06 -04:00
LemmyCook
851a5a6f58 Icon: settings-network using 'sitemap' same as lan 2025-09-09 23:08:08 -04:00
LemmyCook
833808152e Icons: added icons to settings main content title + slightly smaller NCircleStat badges 2025-09-09 22:17:48 -04:00
LemmyCook
84706cab4b Icons: sun-wind for weather partly-cloudy 2025-09-09 21:42:43 -04:00
LemmyCook
e571f26583 Icons: improved ethernet icon 2025-09-09 21:33:31 -04:00
LemmyCook
16cea533da Bluetooth: added a button to enable/disable straight from the panel + minor improvements. 2025-09-09 21:23:57 -04:00
LemmyCook
b1f501f3f9 Added tabler icons license 2025-09-09 21:08:13 -04:00
LemmyCook
afcba942c7 Better settings icons 2025-09-09 20:38:16 -04:00
LemmyCook
5e6f77f875 More icons improvements 2025-09-09 20:32:19 -04:00
LemmyCook
1f9247c429 More icons fixes 2025-09-09 19:34:31 -04:00
LemmyCook
d089966249 NetworkService: bugfix the interface could not longer be enabled or disabled. 2025-09-09 18:53:53 -04:00
LemmyCook
b2d629e6a1 More icons 2025-09-09 18:43:39 -04:00
LemmyCook
032087b611 Battery: disabled test mode 2025-09-09 18:31:59 -04:00
LemmyCook
22dd2bf75c All power icons 2025-09-09 18:30:44 -04:00
LemmyCook
7adc7f43cc Bluetooth devices icons 2025-09-09 18:18:44 -04:00
LemmyCook
a38f49cb35 More icons work 2025-09-09 18:10:25 -04:00
LemmyCook
ca7684c944 ArchUpdater: permanently removed 2025-09-09 18:10:11 -04:00
LemmyCook
955369ab13 More icons work 2025-09-09 17:34:14 -04:00
LemmyCook
48f6c0705b New icons: more icons and cleanup 2025-09-09 17:02:57 -04:00
LemmyCook
43eec0e387 Refactor icons font wip 2025-09-09 14:46:11 -04:00
LemmyCook
b1f9609cd3 Renamed Icons.qml to AppIcons.qml for clarity 2025-09-09 14:16:37 -04:00
Ly-sec
4f731b67d1 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-09 18:38:48 +02:00
Ly-sec
6549b0fc57 NotificationHistoryPanel: possible solution for #235 2025-09-09 18:38:43 +02:00
LemmyCook
ffb972f7c6 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-09 12:28:12 -04:00
LemmyCook
6ed2daa386 NIconButton/CustomButton: added an extra flag to allow click when the button is disabled.
Helps with custom button to get redirected to the settings
2025-09-09 12:28:09 -04:00
LemmyCook
8a4042913b Bootstrap: make the icons map readonly 2025-09-09 12:27:25 -04:00
Lysec
fb3c2f3bb2 Merge pull request #241 from lonerOrz/fix/power
Fix: Incorrect 0% battery warning at startup
2025-09-09 18:20:35 +02:00
Ly-sec
5dc4ba504c PowerProfileService: don't show toast on non valid power profile 2025-09-09 18:15:44 +02:00
loner
c31dc75c63 Fix: Incorrect 0% battery warning at startup 2025-09-10 00:13:00 +08:00
Ly-sec
1f0be929d7 Edit README and also the flake
README: remove breaking change notice (we use a fontloader for the
bootstrap font)
flake: remove material and bootstrap font dependency
2025-09-09 17:57:04 +02:00
LemmyCook
56ea1f92fa Merge branch 'bootstrap-icons' 2025-09-09 11:44:27 -04:00
Ly-sec
5042d4d747 BootstrapIcons: bundle font with noctalia, use fontloader 2025-09-09 17:41:16 +02:00
Ly-sec
144406ae0e PowerProfile: create PowerProfileService, use it for the BarWidget and
PowerProfilesCard
2025-09-09 17:12:56 +02:00
Ly-sec
3d51f758f8 README: small change 2025-09-09 17:03:28 +02:00
Ly-sec
107f6fdfce README: Update breaking changes text 2025-09-09 16:31:19 +02:00
Ly-sec
e4d499b550 Revert "README: Update breaking changes text"
This reverts commit 9c3726bdb1.
2025-09-09 16:30:36 +02:00
Ly-sec
9c3726bdb1 README: Update breaking changes text 2025-09-09 16:28:47 +02:00
Ly-sec
a4534b1611 Merge branch 'bootstrap-icons' 2025-09-09 16:14:18 +02:00
Ly-sec
e4cad6ed20 Update README and flake.nix
README: inform users about breaking changes (due to the font change)
flake: attempt to install the bootstrap-icons font
2025-09-09 15:44:11 +02:00
LemmyCook
5f1cfb9072 CustomButton: no border 2025-09-09 09:27:46 -04:00
LemmyCook
a6ccc8b0da NButton: fix issue when no icon defined 2025-09-09 09:22:05 -04:00
LemmyCook
fe139c208a CustomIcomButton: changed default icon to "heart" 2025-09-09 09:16:48 -04:00
LemmyCook
64c1e842f9 Merge branch 'bootstrap-icons' of github.com:noctalia-dev/noctalia-shell into bootstrap-icons 2025-09-09 09:13:29 -04:00
LemmyCook
cfd7dec04d WeatherCard: Vertical centering of icons 2025-09-09 09:13:27 -04:00
Ly-sec
a00676f5db Merge branch 'bootstrap-icons' of https://github.com/noctalia-dev/noctalia-shell into bootstrap-icons 2025-09-09 15:12:49 +02:00
Ly-sec
61cf7ab843 CustomButtonWidget: add icon picker to improve usability 2025-09-09 15:12:46 +02:00
LemmyCook
d76d1c628a NIconButton: animation on color (bg+fg) 2025-09-09 08:56:30 -04:00
LemmyCook
f6b3f6d2ec ProfileCard: more discrete System uptime 2025-09-09 08:49:08 -04:00
LemmyCook
24863c2527 Merge branch 'bootstrap-icons' of github.com:noctalia-dev/noctalia-shell into bootstrap-icons 2025-09-09 08:27:46 -04:00
LemmyCook
ecd6141739 Toast: better spacing/margin 2025-09-09 08:27:44 -04:00
Ly-sec
ee44920fa4 Merge branch 'bootstrap-icons' of https://github.com/noctalia-dev/noctalia-shell into bootstrap-icons 2025-09-09 14:26:29 +02:00
Ly-sec
864cbfcfab NSpinBox: remove unicode, use Bootstrap.qml 2025-09-09 14:26:27 +02:00
LemmyCook
73541eec49 ActiveWindow + MediaMini: width boosted to 6% 2025-09-09 08:25:25 -04:00
LemmyCook
87425efa88 Merge branch 'bootstrap-icons' of github.com:noctalia-dev/noctalia-shell into bootstrap-icons 2025-09-09 08:23:36 -04:00
LemmyCook
4455074493 ActiveWindow+MediaMini: auto min & max width 2025-09-09 08:23:34 -04:00
LemmyCook
5e23476089 NIconButton: font size auto determined by button size 2025-09-09 08:17:00 -04:00
Ly-sec
1232c0268c Merge branch 'bootstrap-icons' of https://github.com/noctalia-dev/noctalia-shell into bootstrap-icons 2025-09-09 14:12:29 +02:00
Ly-sec
ed9ee65885 ActiveWindow: add guarding for null title/icon (Hyprland)
CompositorService: turn title, appId and id into strings to perhaps
prevent crashing (Hyprland)
2025-09-09 14:11:18 +02:00
LemmyCook
bc7fe21d27 Widget Settings: always use MetaData as default + Removed non existing settting from space (debugMode leftovers) 2025-09-09 08:09:22 -04:00
LemmyCook
f7b0a28b1e Icon: different memory usage icon (cpu) 2025-09-09 08:06:14 -04:00
Ly-sec
b422a419cd BatteryWidget: add low battery threshold
NSpinBox: add bootstrap icons
2025-09-09 13:26:15 +02:00
Ly-sec
663f3abff5 Merge branch 'bootstrap-icons' of https://github.com/noctalia-dev/noctalia-shell into bootstrap-icons 2025-09-09 13:21:09 +02:00
Ly-sec
3c9ce6f8b5 ScreenRecorder: check for availability 2025-09-09 13:20:46 +02:00
LemmyCook
c9a128e439 Merge branch 'bootstrap-icons' of github.com:Ly-sec/Noctalia into bootstrap-icons 2025-09-09 07:18:46 -04:00
LemmyCook
e8f356f5ac ActiveWindow+MediaMini: Shifted one color each: mPrimary, mSecondary 2025-09-09 07:17:50 -04:00
Ly-sec
94d64a91b8 Add toasts & tooltips to a lot of things, add Disk Usage 2025-09-09 13:08:48 +02:00
LemmyCook
fdfe9ea2e1 WifiPanel: fix missing device icon 2025-09-09 01:45:32 -04:00
LemmyCook
d4d7b06b64 NPill + Clock color uniformisation 2025-09-09 01:44:14 -04:00
LemmyCook
56d87ecfcf Polishing
- Volume: better spread/usage of the 3 icons
- Rosepine colors: more contrast to compare to matugen
- NPill: different look when pile is always opened
2025-09-09 01:02:53 -04:00
LemmyCook
76ef2469e8 Shaders: path from root for easier maintenance + cleanup fallback icons 2025-09-09 00:35:12 -04:00
LemmyCook
16bd4b41dc Icons: ArchUpdater, LockScreen, PowerMenu, BT Device List 2025-09-08 23:32:35 -04:00
LemmyCook
0e4b79fd16 SystemStats / network: dont show bytes 2025-09-08 22:34:56 -04:00
LemmyCook
ad73f11b69 Removed: old system-stats script 2025-09-08 22:23:02 -04:00
LemmyCook
bacd65b274 Icons: 99% done 2025-09-08 22:21:18 -04:00
LemmyCook
1f8c55d581 Icons: huge cleanup 2025-09-08 22:05:57 -04:00
LemmyCook
ccdb4e0664 Icons: more icons 2025-09-08 21:37:01 -04:00
LemmyCook
c77784b5c1 Icons: most settings tabs 2025-09-08 21:23:57 -04:00
LemmyCook
74cf71755b Icons: battery + bt 2025-09-08 21:10:03 -04:00
LemmyCook
a4107c87c0 Icons: WIP using a proper mapping table 2025-09-08 21:05:48 -04:00
LemmyCook
8da2cdf430 Icons: better nightlight and notification history 2025-09-08 20:29:11 -04:00
LemmyCook
7e93e29f66 Icons: duo for nightlight 2025-09-08 20:20:40 -04:00
LemmyCook
b2e11137d4 Weather icon: fix thunderstorm 2025-09-08 20:11:26 -04:00
LemmyCook
d086d64d5f Icons: half of BT 2025-09-08 18:51:05 -04:00
LemmyCook
fa970986dc Icons: more icons 2025-09-08 18:45:09 -04:00
LemmyCook
4c9e89915e Icons: more icons 2025-09-08 17:53:55 -04:00
LemmyCook
97c7fd8073 Icons: more icons 2025-09-08 17:26:21 -04:00
LemmyCook
29167de546 Icons: picking from the right range 2025-09-08 17:11:24 -04:00
LemmyCook
d6f629d4bb Icon test 2025-09-08 16:50:13 -04:00
LemmyCook
b13c40e238 Icon: new speed icon 2025-09-08 16:46:42 -04:00
LemmyCook
170fbea7a4 Settings: better alignment with new icons + check icon on wallpaper selector 2025-09-08 16:17:34 -04:00
LemmyCook
08d2747f1e Icons: color picker + better tab alignment in settings
+ autoformatting
2025-09-08 16:08:53 -04:00
LemmyCook
b91112fc7a Icons: Plus and Minus
+ removed vertical hack in NIcon
2025-09-08 16:02:21 -04:00
LemmyCook
ea6b8e0c02 Icons: Brightness and battery 2025-09-08 15:53:50 -04:00
LemmyCook
404a1d3e8b New icons + some warning fixes 2025-09-08 15:22:43 -04:00
LemmyCook
6f1b88e76d more icons 2025-09-08 14:44:28 -04:00
LemmyCook
6169f88d90 Default skull icon 2025-09-08 14:33:20 -04:00
LemmyCook
6f4a4bb764 volume icons 2025-09-08 13:55:48 -04:00
LemmyCook
242ae17d0a panel icon 2025-09-08 13:29:17 -04:00
LemmyCook
736979c4dc more icons 2025-09-08 13:25:03 -04:00
LemmyCook
4b775fc29d Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-08 12:35:43 -04:00
LemmyCook
ed78b6b3f5 Basic bootstrap icons test 2025-09-08 12:35:29 -04:00
Lysec
a0494d1759 Merge pull request #236 from ThatOneCalculator/fix/animation-speed-logic
fix: divide instead of multiply animation speed
2025-09-08 18:27:41 +02:00
Kainoa Kanter
1c0c4e955a divide instead of multiply animation speed 2025-09-08 09:22:21 -07:00
LemmyCook
b639c3632d autoformatting 2025-09-08 11:51:32 -04:00
LemmyCook
6c93b1b768 Settings: Fix widget settings upgrade on startup, to never overwrite an existing setting with default value. 2025-09-08 10:32:26 -04:00
Ly-sec
d05255c15b Notification: show resolved app name instead of id (possibly fixes #230) 2025-09-08 15:38:29 +02:00
Lysec
59ef26af1c Merge pull request #233 from msdevpt/fix-clear-message
fix: message when no items on clipboard
2025-09-08 15:29:54 +02:00
LemmyCook
33c6ade8f8 Cava: runs only when MediaService is playing 2025-09-08 09:01:27 -04:00
LemmyCook
8c115b8bb0 Settings: fixed faulty widget upgrade 2025-09-08 08:59:30 -04:00
LemmyCook
3271fa1d23 Init: better widget upgrading process + less warnings when starting up without config or cache 2025-09-08 08:39:56 -04:00
LemmyCook
b43b065cf2 Wallpaper: minor optimizations/simplification 2025-09-08 07:51:01 -04: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
M.Silva
53ff6cc21a fix: message when no items on clipboard 2025-09-07 18:48:50 +01: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
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
LemmyCook
7860c41959 Network/Wi-Fi: Removed auto polling every 30sec. Factorized more code and cleaned logs 2025-09-06 14:14:47 -04:00
LemmyCook
fc1ee9fb2f Network/WiFi: improve UI with more immediate feedback on operations.
+ proper deletion of profiles when forgetting a network
2025-09-06 13:03:22 -04:00
LemmyCook
5bc8f410e7 Network/Wi-Fi: smarter logging to avoid flood 2025-09-06 09:32:02 -04:00
Ly-sec
3d9ef8c2ed switch to dev version 2025-09-06 14:20:31 +02:00
Ly-sec
0e53ce3ac0 Release v2.6.0
SettingsPanel: added keyboard navigation
BluetoothPanel: UI enhancements
WiFiPanel: UI enhancements
NotificationPanel: UI enhancements
ColorPicker: UI enhancements
Toast: handle switching between toasts much better
Notification: add DND option
Notification: add actions
LauncherTab: add app2unit toggle
Spacer: added spacer widget with configurable width
ActiveWindow: fix hyprland icon display
PowerPanel: add keybind controls
NetworkService: make it way more reliable

More QoL fixes & changes
2025-09-06 14:17:12 +02:00
Ly-sec
4131e6503b Implement keyboard controls for PowerPanel as requested in ##227
PowerPanel: add support for keyboard controls
2025-09-06 12:44:19 +02:00
Ly-sec
0aaf78fc51 ActiveWindow: fix hyprland icon display (fixes #201) 2025-09-06 12:40:29 +02:00
Ly-sec
977b2d9e7c Added a Spacer widget so people can add spacing between other widgets
(as requested in ##226).
Spacer: create variable width invisible rectangle
BarWidgetSettingsDialog: add Spacer support
BarWidgetRegistry: add Spacer
2025-09-06 12:27:06 +02:00
Ly-sec
e76b2c5497 Launcher: fix app2unit execution, implemented #202 2025-09-06 12:18:14 +02:00
Ly-sec
8658e11c1d NotificationHistoryPanel: fix layout alignment 2025-09-06 12:16:53 +02:00
LemmyCook
b3e4486699 Network: better refresh vs wifi scan 2025-09-06 01:14:40 -04:00
LemmyCook
2398961473 Wifi: more clean ups and improvements 2025-09-06 01:04:08 -04:00
LemmyCook
a57bfeba31 Background: Qt.callLater does not accept a delay as parameter. 2025-09-06 00:36:03 -04:00
LemmyCook
2f416a87f0 Wifi/Network: refactoring to something simpler to maintain 2025-09-06 00:02:32 -04:00
LemmyCook
9a6c98c134 WiFi: removed status indicator 2025-09-05 23:18:23 -04:00
LemmyCook
35ca346246 Tooltip 2025-09-05 23:17:18 -04:00
LemmyCook
0fd9ac15cd One more tooltip 2025-09-05 22:38:20 -04:00
LemmyCook
ae12d77e29 Tooltips: should end with a coma. 2025-09-05 22:37:54 -04:00
Lemmy
9065257961 Merge pull request #225 from lonerOrz/fix/keyboard-layout-alignment
fix: align KeyboardLayout widget with other bar components
2025-09-05 22:31:41 -04:00
LemmyCook
561b55cb9e Autoformatting 2025-09-05 22:18:08 -04:00
LemmyCook
4f871296ae ColorPicker: splitted in two NColorPicker + NColorPickerDialog
+ fixed layout and a few little bugs
2025-09-05 22:17:58 -04:00
loner
55b74ad38f fix: align KeyboardLayout widget with other bar components 2025-09-06 09:46:42 +08:00
LemmyCook
8426e36f46 Time: improved human readable time + fixed a few tooltips. 2025-09-05 21:08:30 -04:00
LemmyCook
85d94aca01 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-05 21:04:09 -04:00
LemmyCook
39c7089cbc Notification: fixed persistent DND toast. 2025-09-05 21:04:02 -04:00
Ly-sec
eb072ff88a Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-06 02:16:13 +02:00
Ly-sec
0c4046b993 ColorPicker: UI overhaul 2025-09-06 02:15:51 +02:00
LemmyCook
90cd5467fe Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-05 19:57:24 -04:00
LemmyCook
05bfb6fc37 Do Not Disturb: factorized logic and toast in its proper service. 2025-09-05 19:57:22 -04:00
Lemmy
966b2410d3 Update README.md 2025-09-05 19:49:01 -04:00
LemmyCook
8ec1ad7255 TaskBar converted to Layout 2025-09-05 19:12:32 -04:00
LemmyCook
1efa1f4aa3 ActiveWindow: Converted to Layout 2025-09-05 19:06:15 -04:00
LemmyCook
0a48e5f34f Clock: text was too big 2025-09-05 18:59:53 -04:00
LemmyCook
ad305b3754 Dock: converted to Layout 2025-09-05 18:53:24 -04:00
LemmyCook
78cb7d4c15 MediaMini: fix visualizer not showing when track length is unknown (twitch) 2025-09-05 18:49:59 -04:00
LemmyCook
7b5c97f38a Tray: converted to Layout 2025-09-05 18:49:34 -04:00
Ly-sec
59bf98e04c Vesktop Template: fix placeholder text 2025-09-06 00:44:05 +02:00
LemmyCook
7feab63e5b Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-05 18:33:53 -04:00
LemmyCook
5d7e168a57 NCircleStat + KeyboardLayout: converted to Layout 2025-09-05 18:33:51 -04:00
Ly-sec
8038b7f6a0 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-06 00:31:28 +02:00
Ly-sec
2533c52e27 Launcher: add app2unit options (hopefully) 2025-09-06 00:30:47 +02:00
LemmyCook
cf624f4d65 Notification: Converted to Layout
+ removed fontPointSize on NIconButton. use sizeRatio instead.
2025-09-05 18:29:06 -04:00
LemmyCook
a4c98f1382 NotificationHistory: fully converted to Layout 2025-09-05 18:19:27 -04:00
LemmyCook
4768485974 LockScreen: converted to Layout 2025-09-05 18:15:28 -04:00
LemmyCook
9a14a5cc10 SettingsPanel: converted to layout 2025-09-05 18:05:42 -04:00
LemmyCook
cbffc1a14c SidePanel: height fix 2025-09-05 18:05:23 -04:00
Lysec
25e1c6e759 Merge pull request #224 from ThatOneCalculator/refactor/notification-default-action-text
make default notification action text "Open"
2025-09-05 23:50:06 +02:00
Kainoa Kanter
e41c35cb5b make default notification action text "Open" 2025-09-05 14:47:32 -07:00
LemmyCook
078e111ecd Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-05 17:47:20 -04:00
LemmyCook
01aeceddf4 PowerPanel: converted to Layout 2025-09-05 17:47:19 -04:00
LemmyCook
93a3bc2090 SysMonCard: converted to layout 2025-09-05 17:45:17 -04:00
Ly-sec
28b0536916 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-05 23:44:52 +02:00
Ly-sec
86734f17c4 Notification: remove some logging, implement #223 2025-09-05 23:44:49 +02:00
LemmyCook
94293e4c63 Bar SysMon: converted to Layout 2025-09-05 17:44:04 -04:00
LemmyCook
f06d0f4e1e Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-05 17:32:54 -04:00
Ly-sec
a5fc9d9ca9 Notification: add actions
README: add fix for niri action buttons for notifications
2025-09-05 23:31:55 +02:00
LemmyCook
c85a309aeb MediaMini: converted to Layout 2025-09-05 17:23:02 -04:00
Ly-sec
4cac584409 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-05 23:07:10 +02:00
Ly-sec
b30d3df15c Notification: only display app icon/avatar if the notification requested it 2025-09-05 23:07:05 +02:00
LemmyCook
6f69654816 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-05 17:06:10 -04:00
LemmyCook
8fedd7612d NToast: Column => ColumnLayout 2025-09-05 17:06:09 -04:00
Ly-sec
c16e6e7423 Notification: adjust layout 2025-09-05 23:00:03 +02:00
Ly-sec
c8a056f332 Notification: add DND option to widget and notification panel as requested in #212 2025-09-05 22:42:40 +02:00
Ly-sec
60950fb461 dock: add opacity slider as requested in #222 2025-09-05 22:36:04 +02:00
Ly-sec
a3aba8d0db Toast: update visibility for newest toast 2025-09-05 22:29:20 +02:00
LemmyCook
f9a48becce SettingsPanel: finaly fixed the conflict between scrollview and textinput! 2025-09-05 15:47:07 -04:00
LemmyCook
3140039ccb NTextInput: simplified code in an attempt to fix text selection issues with mouse.
Not fixed yet, but I know where the conflict is!
2025-09-05 15:08:45 -04:00
LemmyCook
56fedcf495 HooksTab: removed ScrollView which already exists in parent (SettingsPanel.qml) 2025-09-05 15:07:31 -04:00
LemmyCook
783e9fb140 AboutTab: improved look of "Download latest release" 2025-09-05 14:46:08 -04:00
LemmyCook
b69d6f57d4 Bump dev version 2025-09-05 14:41:04 -04:00
LemmyCook
125d844e3b NInputAction simplification 2025-09-05 14:19:08 -04:00
LemmyCook
f04ac180f0 NInputAction: use proper label/description + autoformatting 2025-09-05 14:13:05 -04:00
LemmyCook
1cab452352 WiFi: small improvements to UI and service 2025-09-05 13:35:07 -04:00
LemmyCook
f3d1d15b61 NPill: added support for middle mouse button 2025-09-05 13:34:31 -04:00
Lemmy
0915071299 Merge pull request #220 from ThatOneCalculator/refactor/audio-bar-widgets-click-consistency
refactor: consistent click behavior for volume & mic bar widgets
2025-09-05 13:29:16 -04:00
Kainoa Kanter
b787080715 consistent tooltip 2025-09-05 10:28:57 -07:00
LemmyCook
a69a6eda4d FontService: tweaked logs 2025-09-05 13:27:54 -04:00
Kainoa Kanter
dd757c2114 refactor: consistent click behavior for volume & mic bar widgets
- left click: open audio settings panel (unchanged) - right click:
toggle mute - middle click: pwvucontrol
2025-09-05 10:26:46 -07:00
LemmyCook
eedea01679 NetworkService: dont report empty errors 2025-09-05 12:04:49 -04:00
LemmyCook
0567da94dd WiFi: auto formattings (removed es6 syntax for split to not break qmlfmt) 2025-09-05 11:58:30 -04:00
LemmyCook
de92c989f2 Launcher: hotfix clicking on an item would not activate it. 2025-09-05 10:33:57 -04:00
LemmyCook
507843be21 --amend 2025-09-05 08:54:13 -04:00
LemmyCook
b9c1a8a54f WiFi: improved UI and service 2025-09-05 08:36:36 -04:00
LemmyCook
35283a6923 WiFi: cleaner look, similar to BT. 2025-09-05 00:55:47 -04:00
LemmyCook
9ae78eda45 Bluetooth: more UI polish 2025-09-04 23:48:16 -04:00
LemmyCook
cc8a24f445 Bluetooth Panel: UI cleanup/factorization 2025-09-04 23:26:19 -04:00
LemmyCook
5910c65bcf SidePanel: fix #218 sidepanel should open next to the button (as other panels) 2025-09-04 20:38:24 -04:00
LemmyCook
e5aee79d47 Removed all layer.enabled as they do not play well with fractional scaling. 2025-09-04 20:36:32 -04:00
LemmyCook
a249e15c58 WiFi: Fix password input placeholder dots are cut off #203 2025-09-04 19:58:08 -04:00
LemmyCook
cdcfe328d2 NPanel: rounding x,y coordinates to avoid artifacts 2025-09-04 19:53:33 -04:00
LemmyCook
784300f690 Dock: removed unecessary bg hover rect 2025-09-04 19:51:10 -04:00
LemmyCook
8ad2bef2f5 NButton: added support for right click and middle click, removed rippled effect. 2025-09-04 18:54:41 -04:00
LemmyCook
2bd30947fc SettingsPanel: reordered code. 2025-09-04 17:30:52 -04:00
LemmyCook
84e8793a29 SettingsPanel: improved keyboard controls 2025-09-04 17:28:35 -04:00
LemmyCook
be1643c5b8 SettingsPanel: added keyboard navigation (Tab, Vim, Up/Down) to change active tab. 2025-09-04 17:14:54 -04:00
LemmyCook
b00f058eac Removed log 2025-09-04 17:06:46 -04:00
LemmyCook
8bab23cfec Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-04 17:03:13 -04:00
LemmyCook
7b26e38f33 Launcher: improved/fixed keyboard controls (Ctrl+J / Ctrl+K) 2025-09-04 17:03:11 -04:00
Lemmy
9e6bd3be76 Update README.md 2025-09-04 16:17:39 -04:00
LemmyCook
97b016b21b Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-04 16:17:33 -04:00
LemmyCook
84fdb7c647 Wallpaper: added IPC to set a wallpaper
qs -c noctalia-shell ipc call wallpaper set $path $monitor

$monitor can be a monitor name or "all" or "" to assign to all monitors.
2025-09-04 16:17:31 -04:00
Lemmy
6d70944fc8 Update bug_report.md 2025-09-04 15:48:12 -04:00
Lemmy
fcb4fa1b59 Update bug_report.md 2025-09-04 15:46:28 -04:00
Lemmy
e69086f1a6 Merge pull request #213 from ThatOneCalculator/patch-1
docs: power management
2025-09-04 15:42:28 -04:00
Kainoa Kanter
fa22607c2c docs: power management 2025-09-04 12:41:00 -07:00
LemmyCook
9168eba07b autoformatting 2025-09-04 15:37:12 -04:00
Lemmy
5d11e37687 Merge pull request #210 from ThatOneCalculator/feat/caffeine-widget
feat: keep awake bar widget
2025-09-04 15:33:06 -04:00
Lemmy
4ea903b333 Merge pull request #208 from ThatOneCalculator/feat/power-toggle-widget
feat: power toggle bar widget
2025-09-04 15:30:56 -04:00
Lemmy
8fd805815d Merge pull request #205 from ThatOneCalculator/sidebar-toggle-settings
toggle settings panel on right-clicking side panel toggle
2025-09-04 15:29:43 -04:00
LemmyCook
c055690a9b Cleaned up init sequence 2025-09-04 15:27:17 -04:00
Kainoa Kanter
dcf146a097 feat: keep awake bar widget 2025-09-04 12:15:19 -07:00
LemmyCook
e3f50c0ce2 Hotfix: wallpaper was not set on startup. 2025-09-04 15:11:45 -04:00
Kainoa Kanter
c394368dc5 fix sizing 2025-09-04 12:03:41 -07:00
Kainoa Kanter
1f9c54438a feat: power toggle bar widget 2025-09-04 12:00:17 -07:00
Kainoa Kanter
f303f305af Merge branch 'noctalia-dev:main' into sidebar-toggle-settings 2025-09-04 11:54:32 -07:00
LemmyCook
5f1f3dce4a CustomButton: fix size to match other bars button 2025-09-04 14:52:45 -04:00
LemmyCook
f84889ca13 UpdateService: inverted logic 2025-09-04 14:46:29 -04:00
LemmyCook
b778a80c79 Settings: better icons for Hooks tab 2025-09-04 14:46:19 -04:00
Kainoa Kanter
0bf632a4b1 toggle settings panel on right-clicking side panel toggle 2025-09-04 11:20:01 -07:00
Ly-sec
321c513682 UpdateService: set release to false 2025-09-04 19:49:57 +02:00
Ly-sec
9db6a0d438 Release v2.5.0
- Launcher: full rework
- Notification: display app icon
- Hooks: let people create their own commands after wallpaper change &
  light/dark toggle
- NInputAction: create new widget

A lot of quality of life changes & fixes
2025-09-04 19:41:44 +02:00
Ly-sec
a9affb5ae4 Hooks: expose to grab the screen name 2025-09-04 19:15:50 +02:00
Ly-sec
46bc8939b4 Hooks: make hook activate after settings are updated 2025-09-04 18:23:46 +02:00
Ly-sec
fe6ecf7daf Launcher: remove 50 item limit, fixes #200 2025-09-04 18:15:04 +02:00
Ly-sec
a72b896c5f README: small changes 2025-09-04 18:12:36 +02:00
Ly-sec
a91d790074 HooksTab: replace NText with NLabel 2025-09-04 18:01:06 +02:00
Ly-sec
ac21deefa4 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-04 17:57:43 +02:00
Ly-sec
37eefe3663 Created Hook system (let's users run commands after specific actions)
NInputAction: create NTextInput with NButton
HooksService: add dark/light mode hook, add wallpaper change hook
HooksTab: create 1 NInputAction for each hook
Wallpaper: add hook functionallity
2025-09-04 17:54:58 +02:00
LemmyCook
2e082ed8b1 proper border on notifications 2025-09-04 11:45:07 -04:00
LemmyCook
c1bec66151 Cleanup: removed Color.applyOpacity in favor of Qt.alpha 2025-09-04 11:29:45 -04:00
LemmyCook
d53a404bf1 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-04 11:06:56 -04:00
LemmyCook
7ed4c209fe Optims: replaced a bunch of Qt.rgba by Qt.alpha 2025-09-04 11:06:54 -04:00
Ly-sec
83205d57d9 IPCHandler: small change to getActiveScreen() 2025-09-04 16:45:07 +02:00
Ly-sec
43bb3bdd0c IPCHandler: use getActiveScreen() everywhere 2025-09-04 16:42:28 +02:00
LemmyCook
cde3f088d1 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-04 10:26:47 -04:00
LemmyCook
3e7ebf44f3 IPC: more robust screen detection 2025-09-04 10:26:45 -04:00
Ly-sec
ac7092943c Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-04 16:19:02 +02:00
Ly-sec
e7bbb7fc00 CustomButton: let people use quotes etc 2025-09-04 16:18:58 +02:00
LemmyCook
2fda29c185 autoformatting 2025-09-04 10:17:02 -04:00
LemmyCook
2793863689 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-04 10:16:39 -04:00
LemmyCook
7fafda4747 NPanel: clear openedPanel attempt 2025-09-04 10:16:37 -04:00
LemmyCook
bb0f1e84ce IPC: Fail safe when no activeWindow detected 2025-09-04 10:16:15 -04:00
Ly-sec
3ceba43802 Notification: prefer notification image over app image 2025-09-04 16:12:50 +02:00
LemmyCook
f8ed4f48cf logs 2025-09-04 09:52:15 -04:00
LemmyCook
d319ab9bfc IPC: IPC calls now properly identified the proper monitor so that dimming and other stuff works better. 2025-09-04 09:37:50 -04:00
LemmyCook
902cdc39e0 Merge branch 'custom-buttons' 2025-09-04 08:40:00 -04:00
LemmyCook
00d3f81aa1 Bar: check if new widget modelData is available to avoid warnings. 2025-09-04 08:35:57 -04:00
LemmyCook
d8c91a942f SettingsPanel: restoring keyboard focus 2025-09-04 08:28:58 -04:00
LemmyCook
30e1c2d2b3 BarSectionEditor: cleaned up logs 2025-09-04 08:28:41 -04:00
Ly-sec
4229721774 Notification: add app icon support 2025-09-04 13:38:39 +02:00
LemmyCook
4a45e73125 BarSettings: better D&D 2025-09-04 00:58:41 -04:00
LemmyCook
9e819084af BarSettings: reworking drag&drop 2025-09-04 00:04:02 -04:00
LemmyCook
f39dd2aa1c Custom Button: better bar editor 2025-09-03 22:59:59 -04:00
LemmyCook
4f3e0bdb1e SettingsPanel: remove keyboard focus, so it will close gracefully if clicking on something else (like others NPanels) 2025-09-03 22:30:42 -04:00
LemmyCook
21383b03c5 Custom Buttons: working left/right/middle click 2025-09-03 22:22:22 -04:00
LemmyCook
17944211d5 Custom buttons: WIP support for left/right/middle click 2025-09-03 21:59:33 -04:00
LemmyCook
1f919e4469 NIconButton: added support for middle click 2025-09-03 21:59:03 -04:00
LemmyCook
06a11f003b SettingsPanel: fixed audio tab name 2025-09-03 21:58:51 -04:00
LemmyCook
807867ef42 Custom buttons: refactored files structure 2025-09-03 21:27:42 -04:00
LemmyCook
598bc48957 Custom buttons: improved UI, still wip 2025-09-03 20:51:51 -04:00
LemmyCook
7f34ca4122 Custom buttons: WIP implementing custom properties 2025-09-03 19:09:36 -04:00
Lysec
291cd5130d Update README.md 2025-09-04 00:45:37 +02:00
Ly-sec
280952aae3 README: add manual install IPC explanation 2025-09-04 00:45:07 +02:00
LemmyCook
3ba6899e69 Wallpaper: minor improvements 2025-09-03 17:51:02 -04:00
LemmyCook
65f73bb1ba Launcher: Restored keyboard navigation with PageUp/PageDown/Home/End + Vim Keys
Oddly Ctrl+J does not work for me...
2025-09-03 17:02:05 -04:00
LemmyCook
392f0e14b2 Launcher: fixed IPC calls + fix locked up results in clipboard after short successive opening. 2025-09-03 13:49:45 -04:00
LemmyCook
1e81a89a1a Merge branch 'launcher-evolved' 2025-09-03 11:23:15 -04:00
LemmyCook
11a13ce589 Launcher: Fix missing argument to onStatusChanged 2025-09-03 11:11:37 -04:00
LemmyCook
24620210fe Launcher: improved clipboard images look 2025-09-03 10:43:00 -04:00
LemmyCook
7b2d490ba7 Launcher: clipboard, prevent unecessary refresh while browsing 2025-09-03 10:25:44 -04:00
LemmyCook
20b29f98a7 Launcher: deleted ClipboardService, renamed CliphistService to ClipboardService. 2025-09-03 09:35:33 -04:00
LemmyCook
132dbce3a3 Launcher: wip image preview 2025-09-03 09:22:27 -04:00
LemmyCook
ded133d164 Launcher: wip image preview 2025-09-03 08:44:10 -04:00
LemmyCook
7548ffc191 Laucher: Fix wayland warning about focus surface stealing 2025-09-03 08:05:06 -04:00
LemmyCook
1599ee5682 Launcher: Working clipboard plugin 2025-09-03 08:01:24 -04:00
Ly-sec
40b57c2df0 Weather: change how default city is set 2025-09-03 13:50:16 +02:00
Ly-sec
c6e56d4264 Add default fallback city (fixes #199), add beginning of UpdateService
Weather: always fallback to "Tokyo" if the city name is empty
UpdateService: simple versioning control
2025-09-03 13:37:24 +02:00
Ly-sec
7141a91994 DistroLogoService: add NixOS path as requested in #197 2025-09-03 13:05:51 +02:00
LemmyCook
742a600e38 Launcher: first refactoring pass 2025-09-02 22:20:01 -04:00
LemmyCook
80a2e69eaa SidePanel: increased height by 8 pixels. 2025-09-02 20:01:00 -04:00
LemmyCook
a7ce6737ec NComboBox: slightly taller by default 2025-09-02 19:55:23 -04:00
LemmyCook
dfd7edc540 Settings: better default folder for wallpapers and videos 2025-09-02 19:55:06 -04:00
Ly-sec
ac65d19809 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-02 20:09:44 +02:00
Ly-sec
520da3e915 Replace NTextInput with NComboBox for font settings
FontService: use Qt.fontFamilies to grab available fonts and split Mono
fonts
NComboBox: allow height changes
GeneralTab: replace NTextInput with NComboBox
2025-09-02 20:07:10 +02:00
LemmyCook
d79011355c Dock: Fixed dock autohide when bar is at the bottom. 2025-09-02 13:35:24 -04:00
LemmyCook
5859270ad0 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-02 13:06:12 -04:00
LemmyCook
26dc143b1d Dock: allow clicking outside of the dock on the left and right side 2025-09-02 13:06:10 -04:00
Lemmy
76a8e644e0 Update README.md 2025-09-02 12:10:35 -04:00
Ly-sec
8d05cb9f3b README: fix small typo 2025-09-02 17:20:30 +02:00
Ly-sec
7f5d70bcc8 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-02 17:03:36 +02:00
Ly-sec
eea1586772 Added distro logo (for SidePanel widget)
BarTab: add toggle for distro logo replacement
DistroLogoService: handle all logo detection logic
SidePanelToggle: add support for distro logo
WidgetLoader: fix small issue with with screen null warning
2025-09-02 17:01:38 +02:00
Lemmy
6740959866 Update README.md - Cleaned IPC calls area. 2025-09-02 10:50:57 -04:00
Lemmy
f9c1fa78aa Update README.md - added DarkMode IPC calls 2025-09-02 10:42:39 -04:00
LemmyCook
f385b24e8c IPC: added darkMode control
"call darkMode toggle"
"call darkMode setDark"
"call darkMode setLight"
2025-09-02 10:40:47 -04:00
LemmyCook
508c1407be DarkModeToggle: new bar widget 2025-09-02 10:34:05 -04:00
LemmyCook
b908dc0ed2 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-02 08:51:10 -04:00
LemmyCook
6c041fb27f Settings: minor UI improvements 2025-09-02 08:51:08 -04:00
LemmyCook
63a545736c WeatherCard: minor UI improvements (colors and size) 2025-09-02 08:48:24 -04:00
Ly-sec
91b355689c Wallpaper: add ipc call to set new random wallpaper 2025-09-02 14:42:04 +02:00
Ly-sec
3e598cf1cd UtilitiesCard: close SidePanel when we start recording 2025-09-02 14:37:54 +02:00
Ly-sec
9781005a21 Added Thumbnail to ClipboardHistory images, fix keyboard navigation
ClipboardHistory: Add image thumbnail, fix navigation viewport
Launcher: replace TextField with NTextInput
2025-09-02 14:32:02 +02:00
LemmyCook
468272d4c9 NColorPicker: added theme colors to the first row of the palette. 2025-09-01 22:38:01 -04:00
LemmyCook
d5e83aa9de Wallpaper: added fill color that may show up around wallpaper (depends on fillMode)
+ New Widget NColorPicker
+ New Widget NButton
2025-09-01 22:27:49 -04:00
LemmyCook
69a5f0c2c0 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-01 21:30:40 -04:00
LemmyCook
f9194dd741 Wallpaper: added fillMode to all shaders (no, crop, fit, stretch) 2025-09-01 21:30:38 -04:00
Ly-sec
fff7cbde22 Merge branch 'main' of https://github.com/noctalia-dev/noctalia-shell 2025-09-02 02:11:38 +02:00
Ly-sec
b796a00374 Add proper check if cliphist is available
CliphistService: Check if cliphist is available, if not do not spam logs
2025-09-02 02:10:20 +02:00
LemmyCook
cb7b1d92c6 Wallpaper: improved over conditional wallpaper management
- hide all wallpaper settings if feature is disabled
- hide wallpaper selector if feature is disabled
- hide quick access wallpaper if feature is disabled
2025-09-01 19:58:05 -04:00
LemmyCook
fac72b257c Recompiled stripes shader 2025-09-01 19:56:43 -04:00
Lemmy
e6a1bc6e27 Merge pull request #189 from lonerOrz/feat/wallpaper
Added a toggle for Noctalia-shell wallpaper management
2025-09-01 19:34:47 -04:00
Ly-sec
65794b52ec Fix double click on SidePanel to close
NPanel: remove WlrLayershell.keyboardFocus.OnDemand, only add it to
specific panels.
2025-09-01 23:57:51 +02:00
LemmyCook
9a4317739b SettingsPanel: better var naming 2025-09-01 15:16:28 -04:00
LemmyCook
de32b86f7c SettingsPanel: Improved auto-sizing so it should work well on large and small screens 2025-09-01 15:11:37 -04:00
LemmyCook
5a1faa0fd4 SettingsPanel: ensure we never clip screen height 2025-09-01 15:08:15 -04:00
LemmyCook
57d912efc8 Toast: proper scaling + brought back assignation to WlrLayer.Overlay so its above all. 2025-09-01 15:03:30 -04:00
LemmyCook
87067f7062 TrayMenu: fix dynamic scaling 2025-09-01 14:41:12 -04:00
LemmyCook
210bbac583 ScalingService: 1st pass of the refactoring via signals instead of nested bindings for better efficienty and compatibility with old versions of Qt 2025-09-01 13:52:12 -04:00
loner
81d3bad747 fix: shader error
The shader compilation error occurred in wp_stripes.frag, which handles
the "stripes" wallpaper transition. The bug was
  caused by the modulo operator (%), which is not supported in the older
GLSL version your system is using for
  compilation.

  I fixed it by replacing the incompatible % operator with the standard
mod() function, which works across all GLSL
  versions.
2025-09-02 00:13:54 +08:00
loner
2ddb14a95f Added a toggle for Noctalia-shell wallpaper management 2025-09-02 00:13:54 +08:00
LemmyCook
934c8c61b3 WallpaperSelector: current wallpaper border is a real border not a huge colored rectangle. looks better when switching wallpaper 2025-09-01 11:14:19 -04:00
Ly-sec
459bb59dd5 NightLight: moved from DisplayTab to BrightnessTab 2025-09-01 15:37:25 +02:00
LemmyCook
f1c9ed9caa Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-01 09:30:55 -04:00
LemmyCook
5d950b0a5e LightMode: better overview and transparency 2025-09-01 09:30:51 -04:00
Ly-sec
6f78079bc5 UtilitiesCard WallpaperSelector: add right click to choose random
wallpaper
2025-09-01 15:15:59 +02:00
LemmyCook
e3d62388f7 Merge branch 'main' of github.com:noctalia-dev/noctalia-shell 2025-09-01 09:07:25 -04:00
LemmyCook
5fef9cfe6b WallpaperService: refactored to a simpler signal based approach. 2025-09-01 09:07:23 -04:00
Ly-sec
4a4bec5aec Add support for user based templates (~/.config/matugen/config.toml) as
requested in #185
MatugenService: add logic to scan for the matugen config.toml
ColorSchemeTab: add NCheckbox to toggle user based templates
2025-09-01 14:54:01 +02:00
Ly-sec
4193d3c87c Remove test-microphone.sh 2025-09-01 14:24:59 +02:00
Ly-sec
0fd83498ea Create Microphone widget as requested in #180
Microphone: hook up microphone functionallity to bar widget
2025-09-01 14:22:45 +02:00
Ly-sec
00c94755c5 Replace Mask ScreenCorners with Canvas
ScreenCorners: replace Mask with Canvas, RAM usage seems fine
2025-09-01 14:06:16 +02:00
quadbyte
e8c2042290 Settings: better looking settings panel on 1080p 2025-09-01 00:15:49 -04:00
LemmyCook
6bcb85137b BarTab/NSectionEditor: minor UI improvements 2025-08-31 22:55:51 -04:00
204 changed files with 23436 additions and 10003 deletions

View File

@@ -1,7 +1,7 @@
---
name: Bug Report
about: Report a bug from noctalia-shell
title: "[Bug]: "
title: "[Bug] "
labels: bug
assignees: ''
---
@@ -21,9 +21,9 @@ Explain what you expected to happen.
Add screenshots if applicable.
### Environment
- Distro [e.g., CachyOS, NixOS, Arch, ...]
- Compositor [ e.g., Hyprland, Niri, ...]
- Version: [e.g., 1.0.0 or `main`]
- Distro: [e.g., CachyOS, NixOS, Arch, ...]
- Compositor: [ e.g., Hyprland, Niri, ...]
- Noctalia-shell Version: [e.g., 1.0.0, available in About tab]
### Additional Context
Add any other context about the problem here.
Add any other context about the problem here.

View File

@@ -1,7 +1,7 @@
---
name: Feature Request
about: Suggest a new feature or improvement
title: "[Feature]: "
title: "[Feature] "
labels: enhancement
assignees: ''
---

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.qmlls.ini

View File

@@ -4,7 +4,7 @@
"mOnPrimary": "#11111b",
"mSecondary": "#fab387",
"mOnSecondary": "#11111b",
"mTertiary": "#a6e3a1",
"mTertiary": "#94e2d5",
"mOnTertiary": "#11111b",
"mError": "#f38ba8",
"mOnError": "#11111b",
@@ -16,19 +16,19 @@
"mShadow": "#11111b"
},
"light": {
"mPrimary": "#9349ef",
"mPrimary": "#8839ef",
"mOnPrimary": "#eff1f5",
"mSecondary": "#f67525",
"mSecondary": "#fe640b",
"mOnSecondary": "#eff1f5",
"mTertiary": "#40b635",
"mTertiary": "#40a02b",
"mOnTertiary": "#eff1f5",
"mError": "#f38ba8",
"mOnError": "#11111b",
"mError": "#d20f39",
"mOnError": "#dce0e8",
"mSurface": "#eff1f5",
"mOnSurface": "#4c4f69",
"mSurfaceVariant": "#ccd0da",
"mOnSurfaceVariant": "#6c6f85",
"mOutline": "#aeb5c4",
"mOutline": "#a5adcb",
"mShadow": "#dce0e8"
}
}

View File

@@ -0,0 +1,34 @@
{
"dark": {
"mPrimary": "#D3C6AA",
"mOnPrimary": "#232A2E",
"mSecondary": "#D3C6AA",
"mOnSecondary": "#232A2E",
"mTertiary": "#9DA9A0",
"mOnTertiary": "#232A2E",
"mError": "#E67E80",
"mOnError": "#232A2E",
"mSurface": "#232A2E",
"mOnSurface": "#859289",
"mSurfaceVariant": "#2D353B",
"mOnSurfaceVariant": "#D3C6AA",
"mOutline": "#D3C6AA",
"mShadow": "#475258"
},
"light": {
"mPrimary": "#434F55",
"mOnPrimary": "#D3C6AA",
"mSecondary": "#232a2e",
"mOnSecondary": "#D3C6AA",
"mTertiary": "#333c43",
"mOnTertiary": "#9DA9A0",
"mError": "#E66868",
"mOnError": "#9DA9A0",
"mSurface": "#BEC5B2",
"mOnSurface": "#333C43",
"mSurfaceVariant": "#9DA9A0",
"mOnSurfaceVariant": "#232A2E",
"mOutline": "#232A2E",
"mShadow": "#ECF5ED"
}
}

View File

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

View File

@@ -0,0 +1,34 @@
{
"dark": {
"mPrimary": "#aaaaaa",
"mOnPrimary": "#111111",
"mSecondary": "#a7a7a7",
"mOnSecondary": "#111111",
"mTertiary": "#cccccc",
"mOnTertiary": "#111111",
"mError": "#dddddd",
"mOnError": "#111111",
"mSurface": "#111111",
"mOnSurface": "#828282",
"mSurfaceVariant": "#191919",
"mOnSurfaceVariant": "#5d5d5d",
"mOutline": "#3c3c3c",
"mShadow": "#000000"
},
"light": {
"mPrimary": "#555555",
"mOnPrimary": "#eeeeee",
"mSecondary": "#505058",
"mOnSecondary": "#eeeeee",
"mTertiary": "#333333",
"mOnTertiary": "#eeeeee",
"mError": "#222222",
"mOnError": "#efefef",
"mSurface": "#d4d4d4",
"mOnSurface": "#696969",
"mSurfaceVariant": "#e8e8e8",
"mOnSurfaceVariant": "#9e9e9e",
"mOutline": "#c3c3c3",
"mShadow": "#fafafa"
}
}

View File

@@ -1,34 +1,34 @@
{
"dark": {
"mPrimary": "#ebbcba",
"mOnPrimary": "#1f1d2e",
"mOnPrimary": "#191724",
"mSecondary": "#9ccfd8",
"mOnSecondary": "#1f1d2e",
"mTertiary": "#f6c177",
"mOnTertiary": "#1f1d2e",
"mOnSecondary": "#191724",
"mTertiary": "#524f67",
"mOnTertiary": "#e0def4",
"mError": "#eb6f92",
"mOnError": "#1f1d2e",
"mSurface": "#1f1d2e",
"mOnError": "#191724",
"mSurface": "#191724",
"mOnSurface": "#e0def4",
"mSurfaceVariant": "#26233a",
"mOnSurfaceVariant": "#908caa",
"mOutline": "#403d52",
"mShadow": "#1f1d2e"
"mShadow": "#191724"
},
"light": {
"mPrimary": "#d46e6b",
"mPrimary": "#d7827e",
"mOnPrimary": "#faf4ed",
"mSecondary": "#56949f",
"mOnSecondary": "#faf4ed",
"mTertiary": "#31748f",
"mOnTertiary": "#232136",
"mTertiary": "#cecacd",
"mOnTertiary": "#575279",
"mError": "#b4637a",
"mOnError": "#f2e9e1",
"mSurface": "#e0def4",
"mOnSurface": "#232136",
"mSurfaceVariant": "#bcb8e7",
"mOnError": "#faf4ed",
"mSurface": "#faf4ed",
"mOnSurface": "#575279",
"mSurfaceVariant": "#f2e9e1",
"mOnSurfaceVariant": "#797593",
"mOutline": "#9893a5",
"mShadow": "#575279"
"mOutline": "#dfdad9",
"mShadow": "#faf4ed"
}
}

View File

@@ -1,34 +1,34 @@
{
"dark": {
"mPrimary": "#ff9e64",
"mOnPrimary": "#1a1b26",
"mSecondary": "#e0af68",
"mOnSecondary": "#1a1b26",
"mTertiary": "#7aa2f7",
"mOnTertiary": "#1a1b26",
"mPrimary": "#7aa2f7",
"mOnPrimary": "#16161e",
"mSecondary": "#bb9af7",
"mOnSecondary": "#16161e",
"mTertiary": "#9ece6a",
"mOnTertiary": "#16161e",
"mError": "#f7768e",
"mOnError": "#1a1b26",
"mOnError": "#16161e",
"mSurface": "#1a1b26",
"mOnSurface": "#a9b1d6",
"mSurfaceVariant": "#292e42",
"mOnSurfaceVariant": "#787c99",
"mOutline": "#3d4462",
"mShadow": "#1a1b26"
"mOnSurface": "#c0caf5",
"mSurfaceVariant": "#24283b",
"mOnSurfaceVariant": "#9aa5ce",
"mOutline": "#565f89",
"mShadow": "#15161e"
},
"light": {
"mPrimary": "#fd5d00",
"mOnPrimary": "#e6e7ed",
"mSecondary": "#bb8027",
"mOnSecondary": "#e6e7ed",
"mTertiary": "#4a80f4",
"mOnTertiary": "#e6e7ed",
"mError": "#965027",
"mOnError": "#e6e7ed",
"mSurface": "#e6e7ed",
"mOnSurface": "#343b58",
"mSurfaceVariant": "#d5d6db",
"mOnSurfaceVariant": "#40434f",
"mOutline": "#babbc3",
"mShadow": "#c0caf5"
"mPrimary": "#2e7de9",
"mOnPrimary": "#e1e2e7",
"mSecondary": "#9854f1",
"mOnSecondary": "#e1e2e7",
"mTertiary": "#587539",
"mOnTertiary": "#e1e2e7",
"mError": "#f52a65",
"mOnError": "#e1e2e7",
"mSurface": "#e1e2e7",
"mOnSurface": "#3760bf",
"mSurfaceVariant": "#d0d5e3",
"mOnSurfaceVariant": "#6172b0",
"mOutline": "#b4b5b9",
"mShadow": "#a8aecb"
}
}

View File

@@ -0,0 +1,16 @@
Tabler Licenses - Detailed Usage Rights and Guidelines
This is a legal agreement between you, the Purchaser, and Tabler. Purchasing or downloading of any Tabler product (Tabler Admin Template, Tabler Icons, Tabler Emails, Tabler Illustrations), constitutes your acceptance of the terms of this license, Tabler terms of service and Tabler private policy.
Tabler Admin Template and Tabler Icons License*
Tabler Admin Template and Tabler Icons are available under MIT License.
Copyright (c) 2018-2025 Tabler
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
See more at Tabler Admin Template MIT License See more at Tabler Icons MIT License

Binary file not shown.

View File

@@ -7,6 +7,7 @@ import qs.Commons
// Central place to define which templates we generate and where they write.
// Users can extend it by dropping additional templates into:
// - Assets/Matugen/templates/
// - ~/.config/matugen/ (when enableUserTemplates is true)
Singleton {
id: root
@@ -50,28 +51,31 @@ Singleton {
lines.push("\n[templates.ghostty]")
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/ghostty.conf"')
lines.push('output_path = "~/.config/ghostty/themes/noctalia"')
lines.push(
"post_hook = \"grep -q '^theme *= *' ~/.config/ghostty/config; and sed -i 's/^theme *= *.*/theme = noctalia/' ~/.config/ghostty/config; or echo 'theme = noctalia' >> ~/.config/ghostty/config\"")
lines.push("post_hook = \"grep -q '^theme *= *' ~/.config/ghostty/config; and sed -i 's/^theme *= *.*/theme = noctalia/' ~/.config/ghostty/config; or echo 'theme = noctalia' >> ~/.config/ghostty/config\"")
}
if (Settings.data.matugen.foot) {
lines.push("\n[templates.foot]")
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/foot.conf"')
lines.push('output_path = "~/.config/foot/themes/noctalia"')
lines.push(
'post_hook = "sed -i /themes/d ~/.config/foot/foot.ini && echo include=~/.config/foot/themes/noctalia >> ~/.config/foot/foot.ini"')
lines.push('post_hook = "sed -i /themes/d ~/.config/foot/foot.ini && echo include=~/.config/foot/themes/noctalia >> ~/.config/foot/foot.ini"')
}
if (Settings.data.matugen.fuzzel) {
lines.push("\n[templates.fuzzel]")
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/fuzzel.conf"')
lines.push('output_path = "~/.config/fuzzel/themes/noctalia"')
lines.push(
'post_hook = "sed -i /themes/d ~/.config/fuzzel/fuzzel.ini && echo include=~/.config/fuzzel/themes/noctalia >> ~/.config/fuzzel/fuzzel.ini"')
lines.push('post_hook = "sed -i /themes/d ~/.config/fuzzel/fuzzel.ini && echo include=~/.config/fuzzel/themes/noctalia >> ~/.config/fuzzel/fuzzel.ini"')
}
if (Settings.data.matugen.vesktop) {
lines.push("\n[templates.vesktop]")
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/vesktop.css"')
lines.push('output_path = "~/.config/vesktop/themes/noctalia.theme.css"')
}
if (Settings.data.matugen.pywalfox) {
lines.push("\n[templates.pywalfox]")
lines.push('input_path = "' + Quickshell.shellDir + '/Assets/Matugen/templates/pywalfox.json"')
lines.push('output_path = "~/.cache/wal/colors.json"')
lines.push('post_hook = "pywalfox update"')
}
return lines.join("\n") + "\n"
}

View File

@@ -0,0 +1,22 @@
{
"wallpaper": "{{image}}",
"alpha": "100",
"colors": {
"color0": "{{colors.background.default.hex}}",
"color1": "",
"color2": "",
"color3": "",
"color4": "",
"color5": "",
"color6": "",
"color7": "",
"color8": "",
"color9": "",
"color10": "{{colors.primary.default.hex}}",
"color11": "",
"color12": "",
"color13": "{{colors.surface_bright.default.hex}}",
"color14": "",
"color15": "{{colors.on_surface.default.hex}}"
}
}

View File

@@ -1,572 +1,113 @@
/*
* Vesktop Theme
* Generated with Matugen
* Base was taken from https://github.com/catppuccin/discord <3
/**
* @name noctalia
* @description Original theme: midnight | A dark, rounded discord theme.
* @author refact0r
* @version 1.6.2
* @invite nz87hXyvcy
* @website https://github.com/refact0r/midnight-discord
* @source https://github.com/refact0r/midnight-discord/blob/master/midnight.theme.css
* @authorId 508863359777505290
* @authorLink https://www.refact0r.dev
*/
/* IMPORTANT: make sure to enable dark mode in discord settings for the theme to apply properly!!! */
/* Dark Theme */
.visual-refresh.theme-dark,
.visual-refresh .theme-dark {
/* Brand Colors */
--brand-experiment: {{colors.primary.default.hex}};
--bg-brand: {{colors.primary.default.hex}};
--brand-500: {{colors.primary.default.hex}} !important;
--text-link: {{colors.primary.default.hex}} !important;
--text-brand: {{colors.primary.default.hex}};
--control-brand-foreground: {{colors.primary.default.hex}};
--control-brand-foreground-new: {{colors.primary.default.hex}};
--mention-foreground: {{colors.primary.default.hex}};
--mention-background: {{colors.primary.default.hex}}20;
--focus-primary: {{colors.primary.default.hex}};
--logo-primary: {{colors.on_surface.default.hex}};
--badge-brand-bg: {{colors.primary.default.hex}};
--badge-brand-text: {{colors.on_primary.default.hex}};
@import url('https://refact0r.github.io/midnight-discord/build/midnight.css');
/* Text Colors */
--header-primary: {{colors.on_surface.default.hex}} !important;
--header-secondary: {{colors.on_surface_variant.default.hex}} !important;
--text-normal: {{colors.on_surface.default.hex}} !important;
--text-default: {{colors.on_surface.default.hex}};
--text-muted: {{colors.on_surface_variant.default.hex}} !important;
--text-primary: {{colors.on_surface.default.hex}};
--text-secondary: {{colors.on_surface_variant.default.hex}};
--text-tertiary: {{colors.on_surface_variant.default.hex}} !important;
--interactive-normal: {{colors.on_surface.default.hex}} !important;
--interactive-muted: {{colors.on_surface_variant.default.hex}};
--interactive-hover: {{colors.on_surface.default.hex}};
--interactive-active: {{colors.on_surface.default.hex}};
/* customize things here */
:root {
/* font, change to 'gg sans' for default discord font*/
--font: 'figtree';
/* Main Background Colors - Bar color (mSurface) colors.surface.default.hex*/
--background-primary: {{colors.surface_variant.default.hex}} !important;
--background-floating: {{colors.surface_variant.default.hex}} !important;
--background-surface-high: {{colors.surface_variant.default.hex}} !important;
--modal-background: {{colors.surface_variant.default.hex}} !important;
--app-background-frame: {{colors.surface_variant.default.hex}} !important;
--home-background: {{colors.surface_variant.default.hex}} !important;
--chat-background: {{colors.surface_variant.default.hex}} !important;
--chat-background-default: {{colors.surface_variant.default.hex}} !important;
--chat-input-container-background: {{colors.surface_container.default.hex}} !important;
/* Secondary Background Colors - Workspace color (mSurfaceVariant) */
--background-secondary: {{colors.surface.default.hex}} !important;
--background-secondary-alt: {{colors.surface.default.hex}} !important;
--background-surface-higher: {{colors.surface.default.hex}} !important;
--background-base-low: {{colors.surface.default.hex}} !important;
--background-base-lower: {{colors.surface.default.hex}} !important;
--channeltextarea-background: {{colors.surface_container.default.hex}} !important;
--modal-footer-background: {{colors.surface.default.hex}} !important;
/* New Messages Banner */
--background-mentioned: {{colors.primary.default.hex}}15 !important;
--background-mentioned-hover: {{colors.primary.default.hex}}20 !important;
--text-mentioned: {{colors.on_surface.default.hex}} !important;
--text-mentioned-hover: {{colors.on_surface.default.hex}} !important;
--text-mentioned-link: {{colors.primary.default.hex}} !important;
/* Additional Discord-specific variables for new messages banner */
--background-message-automod: {{colors.primary.default.hex}}15 !important;
--background-message-automod-hover: {{colors.primary.default.hex}}20 !important;
--background-message-highlight: {{colors.primary.default.hex}}15 !important;
--background-message-highlight-hover: {{colors.primary.default.hex}}20 !important;
/* Discord unread messages banner specific variables */
--background-mentioned: {{colors.primary.default.hex}}15 !important;
--background-mentioned-hover: {{colors.primary.default.hex}}20 !important;
--text-mentioned: {{colors.on_surface.default.hex}} !important;
--text-mentioned-hover: {{colors.on_surface.default.hex}} !important;
--text-mentioned-link: {{colors.primary.default.hex}} !important;
/* Additional Discord banner text variables */
--text-normal: {{colors.on_surface.default.hex}} !important;
--text-default: {{colors.on_surface.default.hex}} !important;
--text-primary: {{colors.on_surface.default.hex}} !important;
--text-secondary: {{colors.on_surface_variant.default.hex}} !important;
--text-tertiary: {{colors.on_surface_variant.default.hex}} !important;
--text-muted: {{colors.on_surface_variant.default.hex}} !important;
--interactive-normal: {{colors.on_surface.default.hex}} !important;
--interactive-muted: {{colors.on_surface_variant.default.hex}} !important;
/* Additional Discord banner variables */
--background-message-automod: {{colors.primary.default.hex}}15 !important;
--background-message-automod-hover: {{colors.primary.default.hex}}20 !important;
--background-message-highlight: {{colors.primary.default.hex}}15 !important;
--background-message-highlight-hover: {{colors.primary.default.hex}}20 !important;
--background-message-hover: {{colors.surface_variant.default.hex}}50 !important;
--background-modifier-hover: {{colors.surface_variant.default.hex}}80 !important;
--background-modifier-selected: {{colors.primary.default.hex}}20 !important;
--background-modifier-accent: {{colors.primary.default.hex}}30 !important;
--background-modifier-active: {{colors.primary.default.hex}}25 !important;
/* Chat Input Improvements */
--text-input-background: {{colors.surface_container.default.hex}} !important;
--text-input-border: {{colors.outline.default.hex}} !important;
--text-input-border-hover: {{colors.primary.default.hex}} !important;
/* Additional Discord-specific input variables */
--deprecated-text-input-bg: {{colors.surface_container.default.hex}} !important;
--deprecated-text-input-border: {{colors.outline.default.hex}} !important;
--deprecated-text-input-border-hover: {{colors.primary.default.hex}} !important;
--input-background: {{colors.surface_container.default.hex}} !important;
--input-border: {{colors.outline.default.hex}} !important;
--input-placeholder-text: {{colors.on_surface_variant.default.hex}} !important;
/* Elevated/Container Backgrounds */
--background-tertiary: {{colors.surface_container.default.hex}} !important;
--background-accent: {{colors.surface_container.default.hex}} !important;
--background-surface-highest: {{colors.surface_container_high.default.hex}} !important;
--background-base-lowest: {{colors.surface_container.default.hex}} !important;
/* top left corner text */
--corner-text: 'Midnight';
/* Border Colors */
--border-faint: {{colors.outline_variant.default.hex}};
--border-strong: {{colors.surface_container.default.hex}};
--border-normal: {{colors.surface_container_high.default.hex}};
--border-subtle: {{colors.surface.default.hex}} !important;
--chat-border: {{colors.surface_container_high.default.hex}};
/* color of status indicators and window controls */
--online-indicator: {{colors.inverse_primary.default.hex}}; /* change to #23a55a for default green */
--dnd-indicator: {{colors.error.default.hex}}; /* change to #f13f43 for default red */
--idle-indicator: {{colors.tertiary_container.default.hex}}; /* change to #f0b232 for default yellow */
--streaming-indicator: {{colors.on_primary.default.hex}}; /* change to #593695 for default purple */
/* Status Colors */
--status-positive: {{colors.tertiary.default.hex}};
--status-positive-background: {{colors.tertiary.default.hex}};
--status-positive-text: {{colors.on_tertiary.default.hex}};
--text-positive: {{colors.tertiary.default.hex}};
--text-feedback-positive: {{colors.tertiary.default.hex}};
--background-feedback-positive: {{colors.tertiary.default.hex}}20;
--info-positive-background: {{colors.tertiary.default.hex}}20;
--info-positive-foreground: {{colors.tertiary.default.hex}};
--info-positive-text: {{colors.on_surface.default.hex}};
/* accent colors */
--accent-1: {{colors.tertiary.default.hex}}; /* links */
--accent-2: {{colors.primary.default.hex}}; /* general unread/mention elements, some icons when active */
--accent-3: {{colors.primary.default.hex}}; /* accent buttons */
--accent-4: {{colors.surface_bright.default.hex}}; /* accent buttons when hovered */
--accent-5: {{colors.primary_fixed_dim.default.hex}}; /* accent buttons when clicked */
--mention: {{colors.surface.default.hex}}; /* mentions & mention messages */
--mention-hover: {{colors.surface_bright.default.hex}}; /* mentions & mention messages when hovered */
--status-warning: {{colors.secondary.default.hex}};
--status-warning-background: {{colors.secondary.default.hex}};
--status-warning-text: {{colors.on_secondary.default.hex}};
--text-warning: {{colors.secondary.default.hex}};
--text-feedback-warning: {{colors.secondary.default.hex}};
--background-feedback-warning: {{colors.secondary.default.hex}}20;
--info-warning-background: {{colors.secondary.default.hex}}20;
--info-warning-foreground: {{colors.secondary.default.hex}};
--info-warning-text: {{colors.on_surface.default.hex}};
/* text colors */
--text-0: {{colors.surface.default.hex}}; /* text on colored elements */
--text-1: {{colors.on_surface.default.hex}}; /* other normally white text */
--text-2: {{colors.on_surface.default.hex}}; /* headings and important text */
--text-3: {{colors.on_surface_variant.default.hex}}; /* normal text */
--text-4: {{colors.on_surface_variant.default.hex}}; /* icon buttons and channels */
--text-5: {{colors.outline.default.hex}}; /* muted channels/chats and timestamps */
--status-danger: {{colors.error.default.hex}};
--status-danger-background: {{colors.error.default.hex}};
--status-danger-text: {{colors.on_error.default.hex}};
--text-danger: {{colors.error.default.hex}};
--text-feedback-critical: {{colors.error.default.hex}};
--background-feedback-critical: {{colors.error.default.hex}}20;
--info-danger-background: {{colors.error.default.hex}}20;
--info-danger-foreground: {{colors.error.default.hex}};
--info-danger-text: {{colors.on_surface.default.hex}};
/* background and dark colors */
--bg-1: {{colors.primary.default.hex}}; /* dark buttons when clicked */
--bg-2: {{colors.surface_container_high.default.hex}}; /* dark buttons */
--bg-3: {{colors.surface_container_low.default.hex}}; /* spacing, secondary elements */
--bg-4: {{colors.surface.default.hex}}; /* main background color */
--hover: {{colors.surface_bright.default.hex}}; /* channels and buttons when hovered */
--active: {{colors.surface_bright.default.hex}}; /* channels and buttons when clicked or selected */
--message-hover: {{colors.surface_bright.default.hex}}; /* messages when hovered */
/* Button Colors */
--button-secondary-background: {{colors.surface_variant.default.hex}} !important;
--button-secondary-background-hover: {{colors.surface_container.default.hex}};
--button-secondary-background-active: {{colors.surface_container.default.hex}};
--button-secondary-background-disabled: {{colors.surface_variant.default.hex}};
--button-secondary-text: {{colors.on_surface.default.hex}} !important;
/* amount of spacing and padding */
--spacing: 12px;
--button-filled-brand-text: {{colors.on_primary.default.hex}};
--button-filled-brand-background: {{colors.primary.default.hex}};
--button-filled-brand-background-hover: {{colors.primary.default.hex}};
--button-filled-brand-background-active: {{colors.primary.default.hex}};
/* animations */
/* ALL ANIMATIONS CAN BE DISABLED WITH REDUCED MOTION IN DISCORD SETTINGS */
--list-item-transition: 0.2s ease; /* channels/members/settings hover transition */
--unread-bar-transition: 0.2s ease; /* unread bar moving into view transition */
--moon-spin-transition: 0.4s ease; /* moon icon spin */
--icon-spin-transition: 1s ease; /* round icon button spin (settings, emoji, etc.) */
/* Input Colors */
--input-background: {{colors.surface_container.default.hex}};
--input-border: {{colors.outline.default.hex}};
--input-placeholder-text: {{colors.on_surface_variant.default.hex}};
/* corner roundness (border-radius) */
--roundness-xl: 22px; /* roundness of big panel outer corners */
--roundness-l: 20px; /* popout panels */
--roundness-m: 16px; /* smaller panels, images, embeds */
--roundness-s: 12px; /* members, settings inputs */
--roundness-xs: 10px; /* channels, buttons */
--roundness-xxs: 8px; /* searchbar, small elements */
/* Scrollbar Colors */
--scrollbar-thin-thumb: {{colors.primary.default.hex}};
--scrollbar-thin-track: transparent;
--scrollbar-auto-thumb: {{colors.primary.default.hex}};
--scrollbar-auto-track: {{colors.surface_container_high.default.hex}};
--scrollbar-auto-scrollbar-color-thumb: {{colors.primary.default.hex}};
--scrollbar-auto-scrollbar-color-track: {{colors.surface_container_high.default.hex}};
/* direct messages moon icon */
/* change to block to show, none to hide */
--discord-icon: none; /* discord icon */
--moon-icon: block; /* moon icon */
--moon-icon-url: url('https://upload.wikimedia.org/wikipedia/commons/c/c4/Font_Awesome_5_solid_moon.svg'); /* custom icon url */
--moon-icon-size: auto;
/* Icon Colors */
--icon-muted: {{colors.on_surface_variant.default.hex}};
--icon-default: {{colors.on_surface.default.hex}};
--icon-primary: {{colors.on_surface.default.hex}};
--icon-secondary: {{colors.on_surface_variant.default.hex}};
--icon-tertiary: {{colors.on_surface_variant.default.hex}} !important;
/* Channel Colors */
--channels-default: {{colors.on_surface_variant.default.hex}} !important;
--channel-icon: {{colors.on_surface_variant.default.hex}} !important;
--channel-text-area-placeholder: {{colors.on_surface.default.hex}}80;
/* Selection and Hover States */
--background-modifier-hover: {{colors.surface_variant.default.hex}}80;
--background-modifier-selected: {{colors.primary.default.hex}}20 !important;
--background-modifier-accent: {{colors.primary.default.hex}}30;
--background-modifier-active: {{colors.primary.default.hex}}25 !important;
--background-message-hover: {{colors.surface_variant.default.hex}}50 !important;
--background-message-highlight: {{colors.primary.default.hex}}15;
--background-message-highlight-hover: {{colors.primary.default.hex}}20;
/* Code Block - Use workspace background */
--background-code: {{colors.surface_container.default.hex}};
--textbox-markdown-syntax: {{colors.on_surface_variant.default.hex}};
/* Spoiler */
--spoiler-revealed-background: {{colors.surface_container.default.hex}};
--spoiler-hidden-background: {{colors.surface_variant.default.hex}};
/* White/Black Overrides */
--white: {{colors.on_surface.default.hex}};
--white-400: {{colors.on_surface.default.hex}};
--white-500: {{colors.on_surface.default.hex}};
--white-600: {{colors.on_surface_variant.default.hex}};
--white-700: {{colors.on_surface_variant.default.hex}};
--black-500: {{colors.surface_container_high.default.hex}};
/* Force styling for Discord unread messages banner */
--unread-bar-background: {{colors.primary.default.hex}}15 !important;
--unread-bar-text: {{colors.on_surface.default.hex}} !important;
--unread-bar-hover: {{colors.primary.default.hex}}20 !important;
/* Additional Discord unread bar variables */
--background-mentioned: {{colors.primary.default.hex}}15 !important;
--background-mentioned-hover: {{colors.primary.default.hex}}20 !important;
--text-mentioned: {{colors.on_surface.default.hex}} !important;
--text-mentioned-hover: {{colors.on_surface.default.hex}} !important;
--text-mentioned-link: {{colors.primary.default.hex}} !important;
/* Discord banner specific variables */
--background-message-automod: {{colors.primary.default.hex}}15 !important;
--background-message-automod-hover: {{colors.primary.default.hex}}20 !important;
--background-message-highlight: {{colors.primary.default.hex}}15 !important;
--background-message-highlight-hover: {{colors.primary.default.hex}}20 !important;
/* Discord unread bar specific variables */
--background-mentioned: {{colors.primary.default.hex}}15 !important;
--background-mentioned-hover: {{colors.primary.default.hex}}20 !important;
--text-mentioned: {{colors.on_surface.default.hex}} !important;
--text-mentioned-hover: {{colors.on_surface.default.hex}} !important;
--text-mentioned-link: {{colors.primary.default.hex}} !important;
/* Additional Discord text variables that might affect the banner */
--text-normal: {{colors.on_surface.default.hex}} !important;
--text-default: {{colors.on_surface.default.hex}} !important;
--text-primary: {{colors.on_surface.default.hex}} !important;
--text-secondary: {{colors.on_surface_variant.default.hex}} !important;
--text-tertiary: {{colors.on_surface_variant.default.hex}} !important;
--text-muted: {{colors.on_surface_variant.default.hex}} !important;
--interactive-normal: {{colors.on_surface.default.hex}} !important;
--interactive-muted: {{colors.on_surface_variant.default.hex}} !important;
/* Force styling for Discord chat input */
--chat-input-background: {{colors.surface_container.default.hex}} !important;
--chat-input-placeholder: {{colors.on_surface_variant.default.hex}} !important;
/* Discord unread messages banner specific variables */
--new-messages-bar-background: {{colors.surface_container.default.hex}} !important;
--new-messages-bar-text: {{colors.on_surface.default.hex}} !important;
--new-messages-bar-hover: {{colors.surface_container_high.default.hex}} !important;
--bar-button-background: {{colors.surface_container.default.hex}} !important;
--bar-button-text: {{colors.on_surface.default.hex}} !important;
--bar-button-hover: {{colors.surface_container_high.default.hex}} !important;
/* filter uncolorable elements to fit theme */
/* (just set to none, they're too much work to configure) */
--login-bg-filter: saturate(0.3) hue-rotate(-15deg) brightness(0.4); /* login background artwork */
--green-to-accent-3-filter: hue-rotate(56deg) saturate(1.43); /* add friend page explore icon */
--blurple-to-accent-3-filter: hue-rotate(304deg) saturate(0.84) brightness(1.2); /* add friend page school icon */
}
.visual-refresh.theme-dark ::selection,
.visual-refresh .theme-dark ::selection {
background-color: {{colors.primary.default.hex}};
/* Selected chat/friend text */
.selected_f5eb4b,
.selected_f6f816 .link_d8bfb3 {
color: var(--text-0) !important;
background: var(--accent-3) !important;
}
/* Force Discord unread messages banner styling */
.visual-refresh.theme-dark .newMessagesBar__0f481,
.visual-refresh.theme-dark .barButtonMain__0f481,
.visual-refresh.theme-dark .barButtonBase__0f481,
.visual-refresh.theme-dark .span__0f481 {
background-color: {{colors.surface_container.default.hex}} !important;
color: {{colors.on_surface.default.hex}} !important;
.selected_f6f816 .link_d8bfb3 * {
color: var(--text-0) !important;
fill: var(--text-0) !important;
}
.visual-refresh.theme-dark .newMessagesBar__0f481:hover,
.visual-refresh.theme-dark .barButtonMain__0f481:hover,
.visual-refresh.theme-dark .barButtonBase__0f481:hover {
background-color: {{colors.surface_container_high.default.hex}} !important;
/* Make channel name text less visible (darker) */
.name__2ea32 {
color: var(--text-5) !important;
opacity: 0.7 !important;
}
/* Force Discord chat input styling */
.visual-refresh.theme-dark .channelTextArea-rNsIhG,
.visual-refresh.theme-dark .channelTextArea-rNsIhG *,
.visual-refresh.theme-dark .scrollableContainer-2NUZem,
.visual-refresh.theme-dark [data-slate-editor="true"] {
background-color: {{colors.surface_container.default.hex}} !important;
/* Make unread channel names brighter */
.link__2ea32[aria-label*="unread"] .name__2ea32 {
color: var(--text-2) !important;
opacity: 1 !important;
font-weight: 600 !important;
}
.visual-refresh.theme-dark [data-slate-editor="true"]::placeholder,
.visual-refresh.theme-dark .channelTextArea-rNsIhG [data-slate-editor="true"]::placeholder {
color: {{colors.on_surface_variant.default.hex}} !important;
}
/* Discord Emoji Picker Theming */
.visual-refresh.theme-dark .contentWrapper__08434,
.visual-refresh.theme-dark .emojiPicker_c0e32c,
.visual-refresh.theme-dark .wrapper_c0e32c {
background-color: {{colors.surface.default.hex}} !important;
}
.visual-refresh.theme-dark .nav__08434,
.visual-refresh.theme-dark .navList__08434 {
background-color: {{colors.surface.default.hex}} !important;
}
.visual-refresh.theme-dark .navButton__08434 {
background-color: {{colors.surface.default.hex}} !important;
color: {{colors.on_surface_variant.default.hex}} !important;
}
.visual-refresh.theme-dark .navButtonActive__08434 {
background-color: {{colors.surface.default.hex}} !important;
color: {{colors.on_surface.default.hex}} !important;
}
.visual-refresh.theme-dark .searchBar_c0e32c,
.visual-refresh.theme-dark .input_a45028 {
background-color: {{colors.surface.default.hex}} !important;
color: {{colors.on_surface.default.hex}} !important;
}
.visual-refresh.theme-dark .input_a45028::placeholder {
color: {{colors.on_surface_variant.default.hex}} !important;
}
.visual-refresh.theme-dark .header_c656ac,
.visual-refresh.theme-dark .header__14245,
.visual-refresh.theme-dark .wrapper__14245 {
background-color: {{colors.surface_variant.default.hex}} !important;
color: {{colors.on_surface.default.hex}} !important;
}
.visual-refresh.theme-dark .headerLabel__14245 {
color: {{colors.on_surface.default.hex}} !important;
}
.visual-refresh.theme-dark .interactive__14245 {
color: {{colors.on_surface.default.hex}} !important;
}
.visual-refresh.theme-dark .header__14245 {
color: {{colors.on_surface.default.hex}} !important;
}
.visual-refresh.theme-dark .header__14245 * {
color: {{colors.on_surface.default.hex}} !important;
}
.visual-refresh.theme-dark .headerIcon__14245 svg,
.visual-refresh.theme-dark .headerCollapseIcon__14245 svg {
color: {{colors.on_surface.default.hex}} !important;
fill: {{colors.on_surface.default.hex}} !important;
}
.visual-refresh.theme-dark .emojiItem_fc7141 {
background-color: transparent !important;
}
.visual-refresh.theme-dark .emojiItem_fc7141:hover {
background-color: {{colors.surface_container.default.hex}} !important;
}
.visual-refresh.theme-dark .emojiItemSelected_fc7141 {
background-color: {{colors.primary.default.hex}}20 !important;
}
.visual-refresh.theme-dark .inspector_aeaaeb {
background-color: {{colors.surface_container.default.hex}} !important;
color: {{colors.on_surface.default.hex}} !important;
}
.visual-refresh.theme-dark .categoryList_c0e32c {
background-color: {{colors.surface.default.hex}} !important;
}
.visual-refresh.theme-dark .categoryItem_b9ee0c {
background-color: transparent !important;
}
.visual-refresh.theme-dark .categoryItem_b9ee0c:hover {
background-color: {{colors.surface_variant.default.hex}} !important;
}
.visual-refresh.theme-dark .categoryItemDefaultCategorySelected_b9ee0c {
background-color: {{colors.surface_variant.default.hex}} !important;
}
/* Additional Discord emoji picker elements */
.visual-refresh.theme-dark .navItem__08434 {
background-color: {{colors.surface_variant.default.hex}} !important;
color: {{colors.on_surface_variant.default.hex}} !important;
}
.visual-refresh.theme-dark .navItem__08434:hover {
background-color: {{colors.surface_container.default.hex}} !important;
}
.visual-refresh.theme-dark .stickersNavItem__08434 {
color: {{colors.on_surface_variant.default.hex}} !important;
}
.visual-refresh.theme-dark .wrapper__14245 {
background-color: {{colors.surface_variant.default.hex}} !important;
}
.visual-refresh.theme-dark .headerLabel__14245 {
color: {{colors.on_surface.default.hex}} !important;
}
.visual-refresh.theme-dark .headerIcon__14245 svg,
.visual-refresh.theme-dark .headerCollapseIcon__14245 svg {
color: {{colors.on_surface_variant.default.hex}} !important;
}
.visual-refresh.theme-dark .interactive__14245:hover {
background-color: {{colors.surface_container.default.hex}} !important;
}
/* Chat input styling */
.visual-refresh.theme-dark .scrollableContainer__74017,
.visual-refresh.theme-dark .themedBackground__74017,
.visual-refresh.theme-dark .inner__74017,
.visual-refresh.theme-dark .textArea__74017,
.visual-refresh.theme-dark .slateContainer_ec4baf,
.visual-refresh.theme-dark .markup__75297,
.visual-refresh.theme-dark .editor__1b31f,
.visual-refresh.theme-dark .slateTextArea_ec4baf {
background-color: {{colors.surface_container.default.hex}} !important;
color: {{colors.on_surface.default.hex}} !important;
}
.visual-refresh.theme-dark .emptyText__1464f {
color: {{colors.on_surface_variant.default.hex}} !important;
}
.visual-refresh.theme-dark .placeholder__1b31f {
color: {{colors.on_surface_variant.default.hex}} !important;
}
/* Message content styling */
.visual-refresh.theme-dark .messageContent_c19a55 {
color: {{colors.on_surface.default.hex}} !important;
background-color: {{colors.surface.default.hex}} !important;
}
.visual-refresh.theme-dark .messageContent_c19a55 .markup__75297 {
color: {{colors.on_surface.default.hex}} !important;
background-color: {{colors.surface.default.hex}} !important;
}
/* Message background styling */
.visual-refresh.theme-dark .message__5126c,
.visual-refresh.theme-dark .cozyMessage__5126c,
.visual-refresh.theme-dark .wrapper_c19a55,
.visual-refresh.theme-dark .contents_c19a55 {
background-color: {{colors.surface.default.hex}} !important;
}
/* Message hover effects */
.visual-refresh.theme-dark .message__5126c:hover {
background-color: {{colors.surface_variant.default.hex}} !important;
}
.visual-refresh.theme-dark .message__5126c:hover * {
color: {{colors.on_surface.default.hex}} !important;
}
/* Remove Discord's native quote/reply bar */
.visual-refresh.theme-dark .message__5126c::before {
display: none !important;
}
.visual-refresh.theme-dark .message__5126c.hasReply_c19a55::before {
display: none !important;
}
/* Channel styling - darker text for read channels */
.visual-refresh.theme-dark .link__2ea32 .name__2ea32 {
color: {{colors.outline.default.hex}} !important;
}
/* Unread channels keep normal color */
.visual-refresh.theme-dark .link__2ea32[aria-label*="unread"] .name__2ea32 {
color: {{colors.on_surface.default.hex}} !important;
}
/* Search input styling */
.visual-refresh.theme-dark .inner_a45028 {
background-color: {{colors.surface_variant.default.hex}} !important;
}
.visual-refresh.theme-dark .input_a45028 {
background-color: transparent !important;
}
.visual-refresh.theme-dark .input_a45028::placeholder {
color: {{colors.on_surface_variant.default.hex}} !important;
}
/* Chat input placeholder styling */
.visual-refresh.theme-dark .emptyText__1464f {
color: {{colors.on_surface_variant.default.hex}} !important;
}
.visual-refresh.theme-dark .slateTextArea_ec4baf > div:first-child .emptyText__1464f::before {
content: "Message #general" !important;
color: {{colors.on_surface_variant.default.hex}} !important;
}
/* Hide placeholder when input is focused */
.visual-refresh.theme-dark .slateTextArea_ec4baf:focus .emptyText__1464f::before,
.visual-refresh.theme-dark .markup__75297:focus .emptyText__1464f::before {
display: none !important;
}
.visual-refresh.theme-dark .message__5126c:hover .messageContent_c19a55,
.visual-refresh.theme-dark .message__5126c:hover .markup__75297,
.visual-refresh.theme-dark .message__5126c:hover .header_c19a55,
.visual-refresh.theme-dark .message__5126c:hover .headerText_c19a55,
.visual-refresh.theme-dark .message__5126c:hover .username_c19a55,
.visual-refresh.theme-dark .message__5126c:hover .timestamp_c19a55 {
background-color: {{colors.surface_variant.default.hex}} !important;
}
.visual-refresh.theme-dark .categoryIcon_b9ee0c svg {
color: {{colors.on_surface_variant.default.hex}} !important;
}
.visual-refresh.theme-dark .unicodeShortcut_b9ee0c {
background-color: {{colors.surface_container.default.hex}} !important;
color: {{colors.on_surface.default.hex}} !important;
}
.visual-refresh.theme-dark .unicodeShortcut_b9ee0c:hover {
background-color: {{colors.surface_container_high.default.hex}} !important;
}
.visual-refresh.theme-dark .unicodeShortcut_b9ee0c svg {
color: {{colors.on_surface.default.hex}} !important;
}
/* Number badge styling */
.visual-refresh.theme-dark .numberBadge__2b1f5 {
color: {{colors.surface.default.hex}} !important;
background-color: {{colors.primary.default.hex}} !important;
}
/* New badge styling */
.visual-refresh.theme-dark .newBadge__4ed1a {
color: {{colors.surface.default.hex}} !important;
background-color: {{colors.primary.default.hex}} !important;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View File

@@ -4,4 +4,4 @@
# Can be installed from AUR "qmlfmt-git"
# Requires qt6-5compat
find . -name "*.qml" -print -exec qmlfmt -e -b 120 -t 2 -i 2 -w {} \;
find . -name "*.qml" -print -exec qmlfmt -e -b 360 -t 2 -i 2 -w {} \;

View File

@@ -1,270 +0,0 @@
#!/usr/bin/env -S bash
# A Bash script to monitor system stats and output them in JSON format.
# --- Configuration ---
# Default sleep duration in seconds. Can be overridden by the first argument.
SLEEP_DURATION=3
# --- Argument Parsing ---
# Check if a command-line argument is provided for the sleep duration.
if [[ -n "$1" ]]; then
# Basic validation to ensure the argument is a number (integer or float).
if [[ "$1" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
SLEEP_DURATION=$1
else
# Output to stderr if the format is invalid.
echo "Warning: Invalid duration format '$1'. Using default of ${SLEEP_DURATION}s." >&2
fi
fi
# --- Global Cache Variables ---
# These variables will store the discovered CPU temperature sensor path and type
# to avoid searching for it on every loop iteration.
TEMP_SENSOR_PATH=""
TEMP_SENSOR_TYPE=""
# Network speed monitoring variables
PREV_RX_BYTES=0
PREV_TX_BYTES=0
PREV_TIME=0
# --- Data Collection Functions ---
#
# Gets memory usage in GB, MB, and as a percentage.
#
get_memory_info() {
awk '
/MemTotal/ {total=$2}
/MemAvailable/ {available=$2}
END {
if (total > 0) {
usage_kb = total - available
usage_gb = usage_kb / 1000000
usage_percent = (usage_kb / total) * 100
printf "%.1f %.0f\n", usage_gb, usage_percent
} else {
# Fallback if /proc/meminfo is unreadable or empty.
print "0.0 0 0"
}
}
' /proc/meminfo
}
#
# Gets the usage percentage of the root filesystem ("/").
#
get_disk_usage() {
# df gets disk usage. --output=pcent shows only the percentage for the root path.
# tail -1 gets the data line, and tr removes the '%' sign and whitespace.
df --output=pcent / | tail -1 | tr -d ' %'
}
#
# Calculates current CPU usage over a short interval.
#
get_cpu_usage() {
# Read all 10 CPU time fields to prevent errors on newer kernels.
read -r cpu prev_user prev_nice prev_system prev_idle prev_iowait prev_irq prev_softirq prev_steal prev_guest prev_guest_nice < /proc/stat
# Calculate previous total and idle times.
local prev_total_idle=$((prev_idle + prev_iowait))
local prev_total=$((prev_user + prev_nice + prev_system + prev_idle + prev_iowait + prev_irq + prev_softirq + prev_steal + prev_guest + prev_guest_nice))
# Wait for a short period.
sleep 0.05
# Read all 10 CPU time fields again for the second measurement.
read -r cpu user nice system idle iowait irq softirq steal guest guest_nice < /proc/stat
# Calculate new total and idle times.
local total_idle=$((idle + iowait))
local total=$((user + nice + system + idle + iowait + irq + softirq + steal + guest + guest_nice))
# Add a check to prevent division by zero if total hasn't changed.
if (( total <= prev_total )); then
echo "0.0"
return
fi
# Calculate the difference over the interval.
local diff_total=$((total - prev_total))
local diff_idle=$((total_idle - prev_total_idle))
# Use awk for floating-point calculation and print the percentage.
awk -v total="$diff_total" -v idle="$diff_idle" '
BEGIN {
if (total > 0) {
# Formula: 100 * (Total - Idle) / Total
usage = 100 * (total - idle) / total
printf "%.1f\n", usage
} else {
print "0.0"
}
}'
}
#
# Finds and returns the CPU temperature in degrees Celsius.
# Caches the sensor path for efficiency.
#
get_cpu_temp() {
# If the sensor path hasn't been found yet, search for it.
if [[ -z "$TEMP_SENSOR_PATH" ]]; then
for dir in /sys/class/hwmon/hwmon*; do
# Check if the 'name' file exists and read it.
if [[ -f "$dir/name" ]]; then
local name
name=$(<"$dir/name")
# Check for supported sensor types.
if [[ "$name" == "coretemp" || "$name" == "k10temp" || "$name" == "zenpower" ]]; then
TEMP_SENSOR_PATH=$dir
TEMP_SENSOR_TYPE=$name
break # Found it, no need to keep searching.
fi
fi
done
fi
# If after searching no sensor was found, return 0.
if [[ -z "$TEMP_SENSOR_PATH" ]]; then
echo 0
return
fi
# --- Get temp based on sensor type ---
if [[ "$TEMP_SENSOR_TYPE" == "coretemp" ]]; then
# For Intel 'coretemp', average all available temperature sensors.
local total_temp=0
local sensor_count=0
# Use a for loop with a glob to iterate over all temp input files.
# This is more efficient than 'find' for this simple case.
for temp_file in "$TEMP_SENSOR_PATH"/temp*_input; do
# The glob returns the pattern itself if no files match,
# so we must check if the file actually exists.
if [[ -f "$temp_file" ]]; then
total_temp=$((total_temp + $(<"$temp_file")))
sensor_count=$((sensor_count + 1))
fi
done
if (( sensor_count > 0 )); then
# Use awk for the final division to handle potential floating point numbers
# and convert from millidegrees to integer degrees Celsius.
awk -v total="$total_temp" -v count="$sensor_count" 'BEGIN { print int(total / count / 1000) }'
else
# If no sensor files were found, return 0.
echo 0
fi
elif [[ "$TEMP_SENSOR_TYPE" == "k10temp" ]]; then
# For AMD 'k10temp', find the 'Tctl' sensor, which is the control temperature.
local tctl_input=""
for label_file in "$TEMP_SENSOR_PATH"/temp*_label; do
if [[ -f "$label_file" ]] && [[ $(<"$label_file") == "Tctl" ]]; then
# The input file has the same name but with '_input' instead of '_label'.
tctl_input="${label_file%_label}_input"
break
fi
done
if [[ -f "$tctl_input" ]]; then
# Read the temperature and convert from millidegrees to degrees.
echo "$(( $(<"$tctl_input") / 1000 ))"
else
echo 0 # Fallback
fi
elif [[ "$TEMP_SENSOR_TYPE" == "zenpower" ]]; then
# For zenpower, read the first available temp sensor
for temp_file in "$TEMP_SENSOR_PATH"/temp*_input; do
if [[ -f "$temp_file" ]]; then
local temp_value
temp_value=$(cat "$temp_file" | tr -d '\n\r') # Remove any newlines
echo "$((temp_value / 1000))"
return
fi
done
echo 0
if [[ -f "$tctl_input" ]]; then
# Read the temperature and convert from millidegrees to degrees.
echo "$(($(<"$tctl_input") / 1000))"
else
echo 0 # Fallback
fi
else
echo 0 # Should not happen if cache logic is correct.
fi
}
# --- Main Loop ---
# This loop runs indefinitely, gathering and printing stats.
while true; do
# Call the functions to gather all the data.
# get_memory_info
read -r mem_gb mem_per <<< "$(get_memory_info)"
# Command substitution captures the single output from the other functions.
disk_per=$(get_disk_usage)
cpu_usage=$(get_cpu_usage)
cpu_temp=$(get_cpu_temp)
# Get network speeds
current_time=$(date +%s.%N)
total_rx=0
total_tx=0
# Read total bytes from /proc/net/dev for all interfaces
while IFS=: read -r interface stats; do
# Skip only loopback interface, allow other interfaces
if [[ "$interface" =~ ^lo[[:space:]]*$ ]]; then
continue
fi
# Extract rx and tx bytes (fields 1 and 9 in the stats part)
rx_bytes=$(echo "$stats" | awk '{print $1}')
tx_bytes=$(echo "$stats" | awk '{print $9}')
# Add to totals if they are valid numbers
if [[ "$rx_bytes" =~ ^[0-9]+$ ]] && [[ "$tx_bytes" =~ ^[0-9]+$ ]]; then
total_rx=$((total_rx + rx_bytes))
total_tx=$((total_tx + tx_bytes))
fi
done < <(tail -n +3 /proc/net/dev)
# Calculate speeds if we have previous data
rx_speed=0
tx_speed=0
if [[ "$PREV_TIME" != "0" ]]; then
time_diff=$(awk -v current="$current_time" -v prev="$PREV_TIME" 'BEGIN { printf "%.3f", current - prev }')
rx_diff=$((total_rx - PREV_RX_BYTES))
tx_diff=$((total_tx - PREV_TX_BYTES))
# Calculate speeds in bytes per second using awk
rx_speed=$(awk -v rx="$rx_diff" -v time="$time_diff" 'BEGIN { printf "%.0f", rx / time }')
tx_speed=$(awk -v tx="$tx_diff" -v time="$time_diff" 'BEGIN { printf "%.0f", tx / time }')
fi
# Update previous values for next iteration
PREV_RX_BYTES=$total_rx
PREV_TX_BYTES=$total_tx
PREV_TIME=$current_time
# Use printf to format the final JSON output string, adding the mem_mb key.
printf '{"cpu": "%s", "cputemp": "%s", "memgb":"%s", "memper": "%s", "diskper": "%s", "rx_speed": "%s", "tx_speed": "%s"}\n' \
"$cpu_usage" \
"$cpu_temp" \
"$mem_gb" \
"$mem_per" \
"$disk_per" \
"$rx_speed" \
"$tx_speed"
# Wait for the specified duration before the next update.
sleep "$SLEEP_DURATION"
done

View File

@@ -9,3 +9,24 @@ for i in {1..8}; do
done
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

53
Commons/AppIcons.qml Normal file
View File

@@ -0,0 +1,53 @@
pragma Singleton
import QtQuick
import Quickshell
import qs.Services
Singleton {
id: root
function iconFromName(iconName, fallbackName) {
const fallback = fallbackName || "application-x-executable"
try {
if (iconName && typeof Quickshell !== 'undefined' && Quickshell.iconPath) {
const p = Quickshell.iconPath(iconName, fallback)
if (p && p !== "")
return p
}
} catch (e) {
// ignore and fall back
}
try {
return Quickshell.iconPath ? (Quickshell.iconPath(fallback, true) || "") : ""
} catch (e2) {
return ""
}
}
// Resolve icon path for a DesktopEntries appId - safe on missing entries
function iconForAppId(appId, fallbackName) {
const fallback = fallbackName || "application-x-executable"
if (!appId)
return iconFromName(fallback, fallback)
try {
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
return iconFromName(fallback, fallback)
const entry = (DesktopEntries.heuristicLookup) ? DesktopEntries.heuristicLookup(appId) : DesktopEntries.byId(appId)
const name = entry && entry.icon ? entry.icon : ""
return iconFromName(name || fallback, fallback)
} catch (e) {
return iconFromName(fallback, fallback)
}
}
// Distro logo helper (absolute path or empty string)
function distroLogoPath() {
try {
return (typeof OSInfo !== 'undefined' && OSInfo.distroIconPath) ? OSInfo.distroIconPath : ""
} catch (e) {
return ""
}
}
}

View File

@@ -44,14 +44,6 @@ Singleton {
property color transparent: "transparent"
// -----------
function applyOpacity(color, opacity) {
// Convert color to string and apply opacity
if (!color)
return "transparent"
return color.toString().replace("#", "#" + opacity)
}
// --------------------------------
// Default colors: RosePine
QtObject {
@@ -110,7 +102,8 @@ Singleton {
// FileView to load custom colors data from colors.json
FileView {
id: customColorsFile
path: Settings.configDir + "colors.json"
path: Settings.directoriesCreated ? (Settings.configDir + "colors.json") : undefined
printErrors: false
watchChanges: true
onFileChanged: {
Logger.log("Color", "Reloading colors from disk")
@@ -120,6 +113,13 @@ Singleton {
Logger.log("Color", "Writing colors to disk")
writeAdapter()
}
// Trigger initial load when path changes from empty to actual path
onPathChanged: {
if (path !== undefined) {
reload()
}
}
onLoadFailed: function (error) {
if (error.toString().includes("No such file") || error === 2) {
// File doesn't exist, create it with default values

View File

@@ -1,54 +1,49 @@
pragma Singleton
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Services
import qs.Commons
import qs.Commons.IconsSets
Singleton {
id: icons
id: root
function iconFromName(iconName, fallbackName) {
const fallback = fallbackName || "application-x-executable"
try {
if (iconName && typeof Quickshell !== 'undefined' && Quickshell.iconPath) {
const p = Quickshell.iconPath(iconName, fallback)
if (p && p !== "")
return p
// Expose the font family name for easy access
readonly property string fontFamily: fontLoader.name
readonly property string defaultIcon: TablerIcons.defaultIcon
readonly property var icons: TablerIcons.icons
readonly property var aliases: TablerIcons.aliases
readonly property string fontPath: "/Assets/Fonts/tabler/tabler-icons.ttf"
Component.onCompleted: {
Logger.log("Icons", "Service started")
}
function get(iconName) {
// Check in aliases first
if (aliases[iconName] !== undefined) {
iconName = aliases[iconName]
}
// Find the appropriate codepoint
return icons[iconName]
}
FontLoader {
id: fontLoader
source: Quickshell.shellDir + fontPath
}
// Monitor font loading status
Connections {
target: fontLoader
function onStatusChanged() {
if (fontLoader.status === FontLoader.Ready) {
Logger.log("Icons", "Font loaded successfully:", fontFamily)
} else if (fontLoader.status === FontLoader.Error) {
Logger.error("Icons", "Font failed to load")
}
} catch (e) {
// ignore and fall back
}
try {
return Quickshell.iconPath ? (Quickshell.iconPath(fallback, true) || "") : ""
} catch (e2) {
return ""
}
}
// Resolve icon path for a DesktopEntries appId - safe on missing entries
function iconForAppId(appId, fallbackName) {
const fallback = fallbackName || "application-x-executable"
if (!appId)
return iconFromName(fallback, fallback)
try {
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
return iconFromName(fallback, fallback)
const entry = (DesktopEntries.heuristicLookup) ? DesktopEntries.heuristicLookup(
appId) : DesktopEntries.byId(appId)
const name = entry && entry.icon ? entry.icon : ""
return iconFromName(name || fallback, fallback)
} catch (e) {
return iconFromName(fallback, fallback)
}
}
// Distro logo helper (absolute path or empty string)
function distroLogoPath() {
try {
return (typeof OSInfo !== 'undefined' && OSInfo.distroIconPath) ? OSInfo.distroIconPath : ""
} catch (e) {
return ""
}
}
}

File diff suppressed because it is too large Load Diff

205
Commons/KeyboardLayout.qml Normal file
View File

@@ -0,0 +1,205 @@
pragma Singleton
import QtQuick
QtObject {
id: root
// Comprehensive language name to ISO code mapping
property var languageMap: {
"english"// English variants
: "us",
"american": "us",
"united states": "us",
"us english": "us",
"british": "gb",
"uk": "ua",
"united kingdom"// FIXED: Ukrainian language code should map to Ukraine
: "gb",
"english (uk)": "gb",
"canadian": "ca",
"canada": "ca",
"canadian english": "ca",
"australian": "au",
"australia": "au",
"swedish"// Nordic countries
: "se",
"svenska": "se",
"sweden": "se",
"norwegian": "no",
"norsk": "no",
"norway": "no",
"danish": "dk",
"dansk": "dk",
"denmark": "dk",
"finnish": "fi",
"suomi": "fi",
"finland": "fi",
"icelandic": "is",
"íslenska": "is",
"iceland": "is",
"german"// Western/Central European Germanic
: "de",
"deutsch": "de",
"germany": "de",
"austrian": "at",
"austria": "at",
"österreich": "at",
"swiss": "ch",
"switzerland": "ch",
"schweiz": "ch",
"suisse": "ch",
"dutch": "nl",
"nederlands": "nl",
"netherlands": "nl",
"holland": "nl",
"belgian": "be",
"belgium": "be",
"belgië": "be",
"belgique": "be",
"french"// Romance languages (Western/Southern Europe)
: "fr",
"français": "fr",
"france": "fr",
"canadian french": "ca",
"spanish": "es",
"español": "es",
"spain": "es",
"castilian": "es",
"italian": "it",
"italiano": "it",
"italy": "it",
"portuguese": "pt",
"português": "pt",
"portugal": "pt",
"catalan": "ad",
"català": "ad",
"andorra": "ad",
"romanian"// Eastern European Romance
: "ro",
"română": "ro",
"romania": "ro",
"russian"// Slavic languages (Eastern Europe)
: "ru",
"русский": "ru",
"russia": "ru",
"polish": "pl",
"polski": "pl",
"poland": "pl",
"czech": "cz",
"čeština": "cz",
"czech republic": "cz",
"slovak": "sk",
"slovenčina": "sk",
"slovakia": "sk",
"uk": "ua",
"ukrainian"// Ukrainian language code
: "ua",
"українська": "ua",
"ukraine": "ua",
"bulgarian": "bg",
"български": "bg",
"bulgaria": "bg",
"serbian": "rs",
"srpski": "rs",
"serbia": "rs",
"croatian": "hr",
"hrvatski": "hr",
"croatia": "hr",
"slovenian": "si",
"slovenščina": "si",
"slovenia": "si",
"bosnian": "ba",
"bosanski": "ba",
"bosnia": "ba",
"macedonian": "mk",
"македонски": "mk",
"macedonia": "mk",
"irish"// Celtic languages (Western Europe)
: "ie",
"gaeilge": "ie",
"ireland": "ie",
"welsh": "gb",
"cymraeg": "gb",
"wales": "gb",
"scottish": "gb",
"gàidhlig": "gb",
"scotland": "gb",
"estonian"// Baltic languages (Northern Europe)
: "ee",
"eesti": "ee",
"estonia": "ee",
"latvian": "lv",
"latviešu": "lv",
"latvia": "lv",
"lithuanian": "lt",
"lietuvių": "lt",
"lithuania": "lt",
"hungarian"// Other European languages
: "hu",
"magyar": "hu",
"hungary": "hu",
"greek": "gr",
"ελληνικά": "gr",
"greece": "gr",
"albanian": "al",
"shqip": "al",
"albania": "al",
"maltese": "mt",
"malti": "mt",
"malta": "mt",
"turkish"// West/Southwest Asian languages
: "tr",
"türkçe": "tr",
"turkey": "tr",
"arabic": "ar",
"العربية": "ar",
"arab": "ar",
"hebrew": "il",
"עברית": "il",
"israel": "il",
"brazilian"// South American languages
: "br",
"brazilian portuguese": "br",
"brasil": "br",
"brazil": "br",
"japanese"// East Asian languages
: "jp",
"日本語": "jp",
"japan": "jp",
"korean": "kr",
"한국어": "kr",
"korea": "kr",
"south korea": "kr",
"chinese": "cn",
"中文": "cn",
"china": "cn",
"simplified chinese": "cn",
"traditional chinese": "tw",
"taiwan": "tw",
"繁體中文": "tw",
"thai"// Southeast Asian languages
: "th",
"ไทย": "th",
"thailand": "th",
"vietnamese": "vn",
"tiếng việt": "vn",
"vietnam": "vn",
"hindi"// South Asian languages
: "in",
"हिन्दी": "in",
"india": "in",
"afrikaans"// African languages
: "za",
"south africa": "za",
"south african": "za",
"qwerty"// Layout variants
: "us",
"dvorak": "us",
"colemak": "us",
"workman": "us",
"azerty": "fr",
"norman": "fr",
"qwertz": "de"
}
}

View File

@@ -13,23 +13,28 @@ Singleton {
// Default config directory: ~/.config/noctalia
// Default cache directory: ~/.cache/noctalia
property string shellName: "noctalia"
property string configDir: Quickshell.env("NOCTALIA_CONFIG_DIR") || (Quickshell.env("XDG_CONFIG_HOME")
|| Quickshell.env(
"HOME") + "/.config") + "/" + shellName + "/"
property string cacheDir: Quickshell.env("NOCTALIA_CACHE_DIR") || (Quickshell.env("XDG_CACHE_HOME") || Quickshell.env(
"HOME") + "/.cache") + "/" + shellName + "/"
property string configDir: Quickshell.env("NOCTALIA_CONFIG_DIR") || (Quickshell.env("XDG_CONFIG_HOME") || Quickshell.env("HOME") + "/.config") + "/" + shellName + "/"
property string cacheDir: Quickshell.env("NOCTALIA_CACHE_DIR") || (Quickshell.env("XDG_CACHE_HOME") || Quickshell.env("HOME") + "/.cache") + "/" + shellName + "/"
property string cacheDirImages: cacheDir + "images/"
property string settingsFile: Quickshell.env("NOCTALIA_SETTINGS_FILE") || (configDir + "settings.json")
property string defaultWallpaper: Qt.resolvedUrl("../Assets/Tests/wallpaper.png")
property string defaultAvatar: Quickshell.env("HOME") + "/.face"
property string defaultVideosDirectory: Quickshell.env("HOME") + "/Videos"
property string defaultLocation: "Tokyo"
property string defaultWallpapersDirectory: Quickshell.env("HOME") + "/Pictures/Wallpapers"
property string defaultWallpaper: Quickshell.shellDir + "/Assets/Wallpaper/noctalia.png"
// Used to access via Settings.data.xxx.yyy
readonly property alias data: adapter
property bool isLoaded: false
property bool directoriesCreated: false
// Signal emitted when settings are loaded after startupcale changes
signal settingsLoaded
// -----------------------------------------------------
// Function to validate monitor configurations
function validateMonitorConfigurations() {
var availableScreenNames = []
@@ -50,24 +55,148 @@ Singleton {
}
}
if (!hasValidBarMonitor) {
Logger.log("Settings",
"No configured bar monitors found on system, clearing bar monitor list to show on all screens")
Logger.warn("Settings", "No configured bar monitors found on system, clearing bar monitor list to show on all screens")
adapter.bar.monitors = []
} else {
Logger.log("Settings", "Found valid bar monitors, keeping configuration")
//Logger.log("Settings", "Found valid bar monitors, keeping configuration")
}
} else {
Logger.log("Settings", "Bar monitor list is empty, will show on all available screens")
//Logger.log("Settings", "Bar monitor list is empty, will show on all available screens")
}
}
Item {
Component.onCompleted: {
// ensure settings dir exists
Quickshell.execDetached(["mkdir", "-p", configDir])
Quickshell.execDetached(["mkdir", "-p", cacheDir])
Quickshell.execDetached(["mkdir", "-p", cacheDirImages])
// -----------------------------------------------------
// If the settings structure has changed, ensure
// backward compatibility by upgrading the settings
function upgradeSettingsData() {
const sections = ["left", "center", "right"]
// -----------------
// 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
}
}
}
}
// -----------------
// 2nd. remove any non existing widget type
for (var s = 0; s < sections.length; s++) {
const sectionName = sections[s]
const widgets = adapter.bar.widgets[sectionName]
// Iterate backward through the widgets array, so it does not break when removing a widget
for (var i = widgets.length - 1; i >= 0; i--) {
var widget = widgets[i]
if (!BarWidgetRegistry.hasWidget(widget.id)) {
widgets.splice(i, 1)
Logger.warn(`Settings`, `Deleted invalid widget ${widget.id}`)
}
}
}
// -----------------
// 3nd. migrate global settings to user settings
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]
// 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
}
if (upgradeWidget(widget)) {
Logger.log("Settings", `Upgraded ${widget.id} widget:`, JSON.stringify(widget))
}
}
}
// Upgrade the density of the bar so the look stay the same for people who upgrade.
if (adapter.settingsVersion == 2) {
adapter.bar.density = "comfortable"
adapter.settingsVersion++
}
}
// -----------------------------------------------------
function upgradeWidget(widget) {
// Backup the widget definition before altering
const widgetBefore = JSON.stringify(widget)
switch (widget.id) {
// Get back to global settings for these two clock settings
case "Clock":
if (widget.use12HourClock !== undefined) {
adapter.location.use12hourFormat = widget.use12HourClock
delete widget.use12HourClock
}
if (widget.reverseDayMonth !== undefined) {
adapter.location.monthBeforeDay = widget.reverseDayMonth
delete widget.reverseDayMonth
}
break
}
// Inject missing default setting (metaData) from BarWidgetRegistry
const keys = Object.keys(BarWidgetRegistry.widgetMetadata[widget.id])
for (var i = 0; i < keys.length; i++) {
const k = keys[i]
if (k === "id" || k === "allowUserSettings") {
continue
}
if (widget[k] === undefined) {
widget[k] = BarWidgetRegistry.widgetMetadata[widget.id][k]
}
}
// Compare settings, to detect if something has been upgraded
const widgetAfter = JSON.stringify(widget)
return (widgetAfter !== widgetBefore)
}
// -----------------------------------------------------
// Kickoff essential services
function kickOffServices() {
// Ensure our location singleton is created as soon as possible so we start fetching weather asap
LocationService.init()
NightLightService.apply()
ColorSchemeService.init()
MatugenService.init()
// Ensure wallpapers are restored after settings have been loaded
WallpaperService.init()
FontService.init()
HooksService.init()
BluetoothService.init()
}
// -----------------------------------------------------
// Ensure directories exist before FileView tries to read files
Component.onCompleted: {
// ensure settings dir exists
Quickshell.execDetached(["mkdir", "-p", configDir])
Quickshell.execDetached(["mkdir", "-p", cacheDir])
Quickshell.execDetached(["mkdir", "-p", cacheDirImages])
// Mark directories as created and trigger file loading
directoriesCreated = true
}
// Don't write settings to disk immediately
@@ -81,23 +210,33 @@ Singleton {
FileView {
id: settingsFileView
path: settingsFile
path: directoriesCreated ? settingsFile : undefined
printErrors: false
watchChanges: true
onFileChanged: reload()
onAdapterUpdated: saveTimer.start()
Component.onCompleted: function () {
reload()
// Trigger initial load when path changes from empty to actual path
onPathChanged: {
if (path !== undefined) {
reload()
}
}
onLoaded: function () {
if (!isLoaded) {
Logger.log("Settings", "----------------------------")
Logger.log("Settings", "Settings loaded successfully")
upgradeSettingsData()
validateMonitorConfigurations()
kickOffServices()
isLoaded = true
Qt.callLater(function () {
// Some stuff like settings validation should just be executed once on startup and not on every reload
validateMonitorConfigurations()
})
// Emit the signal
root.settingsLoaded()
}
}
onLoadFailed: function (error) {
@@ -109,49 +248,83 @@ Singleton {
JsonAdapter {
id: adapter
property int settingsVersion: 3
// bar
property JsonObject bar: JsonObject {
property string position: "top" // Possible values: "top", "bottom"
property bool showActiveWindowIcon: true
property bool alwaysShowBatteryPercentage: false
property bool showNetworkStats: false
property string position: "top" // "top", "bottom", "left", or "right"
property real backgroundOpacity: 1.0
property string showWorkspaceLabel: "none"
property list<string> monitors: []
property string density: "default" // "compact", "default", "comfortable"
property bool showCapsule: true
// Floating bar settings
property bool floating: false
property real marginVertical: 0.25
property real marginHorizontal: 0.25
// Widget configuration for modular bar system
property JsonObject widgets
widgets: JsonObject {
property list<string> left: ["SystemMonitor", "ActiveWindow", "MediaMini"]
property list<string> center: ["Workspace"]
property list<string> right: ["ScreenRecorderIndicator", "Tray", "NotificationHistory", "WiFi", "Bluetooth", "Battery", "Volume", "Brightness", "NightLight", "Clock", "SidePanelToggle"]
property list<var> left: [{
"id": "SystemMonitor"
}, {
"id": "ActiveWindow"
}, {
"id": "MediaMini"
}]
property list<var> center: [{
"id": "Workspace"
}]
property list<var> right: [{
"id": "ScreenRecorderIndicator"
}, {
"id": "Tray"
}, {
"id": "NotificationHistory"
}, {
"id": "WiFi"
}, {
"id": "Bluetooth"
}, {
"id": "Battery"
}, {
"id": "Volume"
}, {
"id": "Brightness"
}, {
"id": "NightLight"
}, {
"id": "Clock"
}, {
"id": "SidePanelToggle"
}]
}
}
// general
property JsonObject general: JsonObject {
property string avatarImage: defaultAvatar
property bool dimDesktop: false
property bool dimDesktop: true
property bool showScreenCorners: false
property bool forceBlackScreenCorners: false
property real radiusRatio: 1.0
// Animation speed multiplier (0.1x - 2.0x)
property real screenRadiusRatio: 1.0
property real animationSpeed: 1.0
// Replace sidepanel toggle with distro logo (shown in bar and/or side panel)
property bool useDistroLogoForSidepanel: false
}
// location
property JsonObject location: JsonObject {
property string name: "Tokyo"
property string name: defaultLocation
property bool useFahrenheit: false
property bool reverseDayMonth: false
property bool use12HourClock: false
property bool showDateWithClock: false
property bool use12hourFormat: false
property bool monthBeforeDay: false
property bool showWeekNumberInCalendar: false
}
// screen recorder
property JsonObject screenRecorder: JsonObject {
property string directory: "~/Videos"
property string directory: defaultVideosDirectory
property int frameRate: 60
property string audioCodec: "opus"
property string videoCodec: "h264"
@@ -164,9 +337,12 @@ Singleton {
// wallpaper
property JsonObject wallpaper: JsonObject {
property string directory: "/usr/share/wallpapers"
property bool enabled: true
property string directory: defaultWallpapersDirectory
property bool enableMultiMonitorDirectories: false
property bool setWallpaperOnAllMonitors: true
property string fillMode: "crop"
property color fillColor: "#000000"
property bool randomEnabled: false
property int randomIntervalSec: 300 // 5 min
property int transitionDuration: 1500 // 1500 ms
@@ -177,18 +353,21 @@ Singleton {
// applauncher
property JsonObject appLauncher: JsonObject {
// When disabled, Launcher hides clipboard command and ignores cliphist
property bool enableClipboardHistory: true
property bool enableClipboardHistory: false
// Position: center, top_left, top_right, bottom_left, bottom_right, bottom_center, top_center
property string position: "center"
property real backgroundOpacity: 1.0
property list<string> pinnedExecs: []
property bool useApp2Unit: false
property bool sortByMostUsed: true
}
// dock
property JsonObject dock: JsonObject {
property bool autoHide: false
property bool exclusive: false
property real backgroundOpacity: 1.0
property real floatingRatio: 1.0
property list<string> monitors: []
}
@@ -200,26 +379,30 @@ Singleton {
// notifications
property JsonObject notifications: JsonObject {
property bool doNotDisturb: false
property list<string> monitors: []
// Last time the user opened the notification history (ms since e899999999999998poch)
property real lastSeenTs: 0
// Duration settings for different urgency levels (in seconds)
property int lowUrgencyDuration: 3
property int normalUrgencyDuration: 8
property int criticalUrgencyDuration: 15
}
// audio
property JsonObject audio: JsonObject {
property bool showMiniplayerAlbumArt: false
property bool showMiniplayerCava: false
property string visualizerType: "linear"
property int volumeStep: 5
property int cavaFrameRate: 60
// MPRIS controls
property string visualizerType: "linear"
property list<string> mprisBlacklist: []
property string preferredPlayer: ""
}
// ui
property JsonObject ui: JsonObject {
property string fontDefault: "Roboto" // Default font for all text
property string fontFixed: "DejaVu Sans Mono" // Fixed width font for terminal
property string fontBillboard: "Inter" // Large bold font for clocks and prominent displays
property string fontDefault: "Roboto"
property string fontFixed: "DejaVu Sans Mono"
property string fontBillboard: "Inter"
property list<var> monitorsScaling: []
property bool idleInhibitorEnabled: false
}
@@ -247,17 +430,27 @@ Singleton {
property bool foot: false
property bool fuzzel: false
property bool vesktop: false
property bool pywalfox: false
property bool enableUserTemplates: false
}
// night light
property JsonObject nightLight: JsonObject {
property bool enabled: false
property bool forced: false
property bool autoSchedule: true
property string nightTemp: "4000"
property string dayTemp: "6500"
property string manualSunrise: "06:30"
property string manualSunset: "18:30"
}
// hooks
property JsonObject hooks: JsonObject {
property bool enabled: false
property string wallpaperChange: ""
property string darkModeChange: ""
}
}
}
}

View File

@@ -29,11 +29,15 @@ Singleton {
property int fontWeightBold: 700
// Radii
property int radiusXXS: 4 * Settings.data.general.radiusRatio
property int radiusXS: 8 * Settings.data.general.radiusRatio
property int radiusS: 12 * Settings.data.general.radiusRatio
property int radiusM: 16 * Settings.data.general.radiusRatio
property int radiusL: 20 * Settings.data.general.radiusRatio
//screen Radii
property int screenRadius: 20 * Settings.data.general.screenRadiusRatio
// Border
property int borderS: 1
property int borderM: 2
@@ -56,18 +60,41 @@ Singleton {
property real opacityFull: 1.0
// Animation duration (ms)
property int animationFast: Math.round(150 * Settings.data.general.animationSpeed)
property int animationNormal: Math.round(300 * Settings.data.general.animationSpeed)
property int animationSlow: Math.round(450 * Settings.data.general.animationSpeed)
// Dimensions
property int barHeight: 36
property int capsuleHeight: (barHeight * 0.73)
property int baseWidgetSize: 32
property int sliderWidth: 200
property int animationFast: Math.round(150 / 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 animationSlowest: Math.round(750 / Settings.data.general.animationSpeed)
// Delays
property int tooltipDelay: 300
property int tooltipDelayLong: 1200
property int pillDelay: 500
// Settings widgets base size
property real baseWidgetSize: 33
property real sliderWidth: 200
// Bar Dimensions
property real barHeight: {
if (Settings.data.bar.density === "compact") {
return (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 27 : 25
}
if (Settings.data.bar.density === "default") {
return (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 33 : 31
}
if (Settings.data.bar.density === "comfortable") {
return (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? 39 : 37
}
}
property real capsuleHeight: {
if (Settings.data.bar.density === "compact") {
return barHeight * 0.85
}
if (Settings.data.bar.density === "default") {
return barHeight * 0.82
}
if (Settings.data.bar.density === "comfortable") {
return barHeight * 0.73
}
}
}

View File

@@ -9,52 +9,38 @@ Singleton {
id: root
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) {
let dayName = date.toLocaleDateString(Qt.locale(), "ddd")
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
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
// Returns a Unix Timestamp (in seconds)
readonly property int timestamp: {
return Math.floor(date / 1000)
}
readonly property string dateString: {
function formatDate(monthBeforeDay = true) {
let now = date
let dayName = now.toLocaleDateString(Qt.locale(), "ddd")
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
let day = now.getDate()
let suffix
if (day > 3 && day < 21)
suffix = 'th'
suffix = 'th'
else
switch (day % 10) {
switch (day % 10) {
case 1:
suffix = "st"
break
suffix = "st"
break
case 2:
suffix = "nd"
break
suffix = "nd"
break
case 3:
suffix = "rd"
break
suffix = "rd"
break
default:
suffix = "th"
}
suffix = "th"
}
let month = now.toLocaleDateString(Qt.locale(), "MMMM")
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)
readonly property int timestamp: {
return Math.floor(date / 1000)
return `${dayName}, ` + (monthBeforeDay ? `${month} ${day}${suffix} ${year}` : `${day}${suffix} ${month} ${year}`)
}
@@ -78,23 +64,34 @@ Singleton {
}
// Format an easy to read approximate duration ex: 4h32m
// Used to display the time remaining on the Battery widget
// Used to display the time remaining on the Battery widget, computer uptime, etc..
function formatVagueHumanReadableDuration(totalSeconds) {
const hours = Math.floor(totalSeconds / 3600)
const minutes = Math.floor((totalSeconds - (hours * 3600)) / 60)
const seconds = totalSeconds - (hours * 3600) - (minutes * 60)
if (typeof totalSeconds !== 'number' || totalSeconds < 0) {
return '0s'
}
var str = ""
if (hours) {
str += hours.toString() + "h"
}
if (minutes) {
str += minutes.toString() + "m"
}
// Floor the input to handle decimal seconds
totalSeconds = Math.floor(totalSeconds)
const days = Math.floor(totalSeconds / 86400)
const hours = Math.floor((totalSeconds % 86400) / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
const parts = []
if (days)
parts.push(`${days}d`)
if (hours)
parts.push(`${hours}h`)
if (minutes)
parts.push(`${minutes}m`)
// Only show seconds if no hours and no minutes
if (!hours && !minutes) {
str += seconds.toString() + "s"
parts.push(`${seconds}s`)
}
return str
return parts.join('')
}
Timer {

View File

@@ -1,527 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Widgets
NPanel {
id: root
panelWidth: 380 * scaling
panelHeight: 500 * scaling
panelAnchorRight: true
panelContent: Rectangle {
color: Color.mSurface
radius: Style.radiusL * scaling
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginM * scaling
// Header
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NIcon {
text: "system_update_alt"
font.pointSize: Style.fontSizeXXL * scaling
color: Color.mPrimary
}
NText {
text: "System Updates"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
}
// Reset button (only show if update failed)
NIconButton {
visible: ArchUpdaterService.updateFailed
icon: "refresh"
tooltipText: "Reset update state"
sizeRatio: 0.8
colorBg: Color.mError
colorFg: Color.mOnError
onClicked: {
ArchUpdaterService.resetUpdateState()
}
}
NIconButton {
icon: "close"
tooltipText: "Close"
sizeRatio: 0.8
onClicked: root.close()
}
}
NDivider {
Layout.fillWidth: true
}
// Update summary (only show when packages are available and terminal is configured)
NText {
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& !ArchUpdaterService.checkFailed && !ArchUpdaterService.aurBusy
&& ArchUpdaterService.totalUpdates > 0 && ArchUpdaterService.terminalAvailable
&& ArchUpdaterService.aurHelperAvailable
text: ArchUpdaterService.totalUpdates + " package" + (ArchUpdaterService.totalUpdates !== 1 ? "s" : "") + " can be updated"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightMedium
color: Color.mOnSurface
Layout.fillWidth: true
}
// Package selection info (only show when not updating and have packages and terminal is configured)
NText {
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& !ArchUpdaterService.checkFailed && !ArchUpdaterService.aurBusy
&& ArchUpdaterService.totalUpdates > 0 && ArchUpdaterService.terminalAvailable
&& ArchUpdaterService.aurHelperAvailable
text: ArchUpdaterService.selectedPackagesCount + " of " + ArchUpdaterService.totalUpdates + " packages selected"
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
}
// Update in progress state
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
visible: ArchUpdaterService.updateInProgress
spacing: Style.marginM * scaling
Item {
Layout.fillHeight: true
} // Spacer
NIcon {
text: "hourglass_empty"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Update in progress"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Please check your terminal window for update progress and prompts."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
Item {
Layout.fillHeight: true
} // Spacer
}
// Terminal not available state
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: !ArchUpdaterService.terminalAvailable && !ArchUpdaterService.updateInProgress
&& !ArchUpdaterService.updateFailed
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
text: "terminal"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mError
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Terminal not configured"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "The TERMINAL environment variable is not set. Please set it to your preferred terminal (e.g., kitty, alacritty, foot) in your shell configuration."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
}
}
// AUR helper not available state
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: ArchUpdaterService.terminalAvailable && !ArchUpdaterService.aurHelperAvailable
&& !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& !ArchUpdaterService.checkFailed && !ArchUpdaterService.aurBusy
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
text: "package"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mError
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "AUR helper not found"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "No AUR helper (yay or paru) is installed. Please install either yay or paru to manage AUR packages. yay is recommended."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
}
}
// Check failed state (AUR down, network issues, etc.)
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: ArchUpdaterService.checkFailed && !ArchUpdaterService.updateInProgress
&& !ArchUpdaterService.updateFailed && ArchUpdaterService.terminalAvailable
&& ArchUpdaterService.aurHelperAvailable
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
text: "error"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mError
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Cannot check for updates"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: ArchUpdaterService.lastCheckError
|| "AUR helper is unavailable or network connection failed. This could be due to AUR being down, network issues, or missing AUR helper (yay/paru)."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
// Prominent refresh button
NIconButton {
icon: "refresh"
tooltipText: "Try checking again"
sizeRatio: 1.2
colorBg: Color.mPrimary
colorFg: Color.mOnPrimary
onClicked: {
ArchUpdaterService.forceRefresh()
}
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Style.marginL * scaling
}
}
}
// Update failed state
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: ArchUpdaterService.updateFailed
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
text: "error_outline"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mError
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Update failed"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Check your terminal for error details and try again."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
// Prominent refresh button
NIconButton {
icon: "refresh"
tooltipText: "Refresh and try again"
sizeRatio: 1.2
colorBg: Color.mPrimary
colorFg: Color.mOnPrimary
onClicked: {
ArchUpdaterService.resetUpdateState()
}
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Style.marginL * scaling
}
}
}
// No updates available state
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& !ArchUpdaterService.checkFailed && !ArchUpdaterService.aurBusy
&& ArchUpdaterService.totalUpdates === 0 && ArchUpdaterService.terminalAvailable
&& ArchUpdaterService.aurHelperAvailable
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
text: "check_circle"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "System is up to date"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "All packages are current. Check back later for updates."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
}
}
// Checking for updates state
Item {
Layout.fillWidth: true
Layout.fillHeight: true
visible: ArchUpdaterService.aurBusy && !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& ArchUpdaterService.terminalAvailable && ArchUpdaterService.aurHelperAvailable
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NBusyIndicator {
Layout.alignment: Qt.AlignHCenter
size: Style.fontSizeXXXL * scaling
color: Color.mPrimary
}
NText {
text: "Checking for updates"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Scanning package databases for available updates..."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.maximumWidth: 280 * scaling
}
}
}
// Package list (only show when not in any special state)
NBox {
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& !ArchUpdaterService.checkFailed && !ArchUpdaterService.aurBusy
&& ArchUpdaterService.totalUpdates > 0 && ArchUpdaterService.terminalAvailable
&& ArchUpdaterService.aurHelperAvailable
Layout.fillWidth: true
Layout.fillHeight: true
// Combine repo and AUR lists in order: repos first, then AUR
property var items: (ArchUpdaterService.repoPackages || []).concat(ArchUpdaterService.aurPackages || [])
ListView {
id: unifiedList
anchors.fill: parent
anchors.margins: Style.marginM * scaling
cacheBuffer: Math.round(300 * scaling)
clip: true
model: parent.items
delegate: Rectangle {
width: unifiedList.width
height: 44 * scaling
color: Color.transparent
radius: Style.radiusS * scaling
RowLayout {
anchors.fill: parent
spacing: Style.marginS * scaling
// Checkbox for selection
NCheckbox {
id: checkbox
label: ""
description: ""
checked: ArchUpdaterService.isPackageSelected(modelData.name)
baseSize: Math.max(Style.baseWidgetSize * 0.7, 14)
onToggled: function (checked) {
ArchUpdaterService.togglePackageSelection(modelData.name)
// Force refresh of the checked property
checkbox.checked = ArchUpdaterService.isPackageSelected(modelData.name)
}
}
// Package info
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXXS * scaling
NText {
text: modelData.name
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
}
NText {
text: modelData.oldVersion + " → " + modelData.newVersion
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mOnSurfaceVariant
Layout.fillWidth: true
}
}
// Source tag (AUR vs PAC)
Rectangle {
visible: !!modelData.source
radius: width * 0.5
color: modelData.source === "aur" ? Color.mTertiary : Color.mSecondary
Layout.alignment: Qt.AlignVCenter
implicitHeight: Style.fontSizeS * 1.8 * scaling
// Width based on label content + horizontal padding
implicitWidth: badgeText.implicitWidth + Math.max(12 * scaling, Style.marginS * scaling)
NText {
id: badgeText
anchors.centerIn: parent
text: modelData.source === "aur" ? "AUR" : "PAC"
font.pointSize: Style.fontSizeXXS * scaling
font.weight: Style.fontWeightBold
color: modelData.source === "aur" ? Color.mOnTertiary : Color.mOnSecondary
}
}
}
}
}
}
// Action buttons (only show when not updating)
RowLayout {
visible: !ArchUpdaterService.updateInProgress && !ArchUpdaterService.updateFailed
&& !ArchUpdaterService.checkFailed && ArchUpdaterService.terminalAvailable
&& ArchUpdaterService.aurHelperAvailable
Layout.fillWidth: true
spacing: Style.marginL * scaling
NIconButton {
icon: "refresh"
tooltipText: ArchUpdaterService.aurBusy ? "Checking for updates..." : (!ArchUpdaterService.canPoll ? "Refresh available soon" : "Refresh package lists")
onClicked: {
ArchUpdaterService.forceRefresh()
}
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
Layout.fillWidth: true
enabled: !ArchUpdaterService.aurBusy
}
NIconButton {
icon: "system_update_alt"
tooltipText: "Update all packages"
enabled: ArchUpdaterService.totalUpdates > 0
onClicked: {
ArchUpdaterService.runUpdate()
root.close()
}
colorBg: ArchUpdaterService.totalUpdates > 0 ? Color.mPrimary : Color.mSurfaceVariant
colorFg: ArchUpdaterService.totalUpdates > 0 ? Color.mOnPrimary : Color.mOnSurfaceVariant
Layout.fillWidth: true
}
NIconButton {
icon: "check_box"
tooltipText: "Update selected packages"
enabled: ArchUpdaterService.selectedPackagesCount > 0
onClicked: {
if (ArchUpdaterService.selectedPackagesCount > 0) {
ArchUpdaterService.runSelectiveUpdate()
root.close()
}
}
colorBg: ArchUpdaterService.selectedPackagesCount > 0 ? Color.mPrimary : Color.mSurfaceVariant
colorFg: ArchUpdaterService.selectedPackagesCount > 0 ? Color.mOnPrimary : Color.mOnSurfaceVariant
Layout.fillWidth: true
}
}
}
}
}

View File

@@ -3,6 +3,8 @@ import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services
import qs.Modules.SettingsPanel
import qs.Widgets
Variants {
id: backgroundVariants
@@ -12,13 +14,12 @@ Variants {
required property ShellScreen modelData
active: Settings.isLoaded && modelData
active: Settings.isLoaded && modelData && Settings.data.wallpaper.enabled
sourceComponent: PanelWindow {
id: root
// Internal state management
property bool firstWallpaper: true
property string transitionType: "fade"
property real transitionProgress: 0
@@ -37,17 +38,39 @@ Variants {
property real stripesCount: 16
property real stripesAngle: 0
// External state management
property string servicedWallpaper: modelData ? WallpaperService.getWallpaper(modelData.name) : ""
// Used to debounce wallpaper changes
property string futureWallpaper: ""
onServicedWallpaperChanged: {
// Set wallpaper immediately on startup
if (firstWallpaper) {
firstWallpaper = false
setWallpaperImmediate(servicedWallpaper)
} else {
futureWallpaper = servicedWallpaper
debounceTimer.restart()
// Fillmode default is "crop"
property real fillMode: 1.0
property vector4d fillColor: Qt.vector4d(Settings.data.wallpaper.fillColor.r, Settings.data.wallpaper.fillColor.g, Settings.data.wallpaper.fillColor.b, 1.0)
// On startup assign wallpaper immediately
Component.onCompleted: {
fillMode = WallpaperService.getFillModeUniform()
var path = modelData ? WallpaperService.getWallpaper(modelData.name) : ""
setWallpaperImmediate(path)
}
Connections {
target: Settings.data.wallpaper
function onFillModeChanged() {
fillMode = WallpaperService.getFillModeUniform()
}
}
// External state management
Connections {
target: WallpaperService
function onWallpaperChanged(screenName, path) {
if (screenName === modelData.name) {
// Update wallpaper display
// Set wallpaper immediately on startup
futureWallpaper = path
debounceTimer.restart()
}
}
}
@@ -76,21 +99,16 @@ Variants {
Image {
id: currentWallpaper
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
source: ""
smooth: true
mipmap: false
visible: false
cache: false
// currentWallpaper should not be asynchronous to avoid flickering when swapping next to current.
asynchronous: false
asynchronous: true
}
Image {
id: nextWallpaper
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
source: ""
smooth: true
mipmap: false
@@ -108,7 +126,18 @@ Variants {
property variant source1: currentWallpaper
property variant source2: nextWallpaper
property real progress: root.transitionProgress
fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/wp_fade.frag.qsb")
// Fill mode properties
property real fillMode: root.fillMode
property vector4d fillColor: root.fillColor
property real imageWidth1: source1.sourceSize.width
property real imageHeight1: source1.sourceSize.height
property real imageWidth2: source2.sourceSize.width
property real imageHeight2: source2.sourceSize.height
property real screenWidth: width
property real screenHeight: height
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_fade.frag.qsb")
}
// Wipe transition shader
@@ -123,7 +152,17 @@ Variants {
property real smoothness: root.edgeSmoothness
property real direction: root.wipeDirection
fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/wp_wipe.frag.qsb")
// Fill mode properties
property real fillMode: root.fillMode
property vector4d fillColor: root.fillColor
property real imageWidth1: source1.sourceSize.width
property real imageHeight1: source1.sourceSize.height
property real imageWidth2: source2.sourceSize.width
property real imageHeight2: source2.sourceSize.height
property real screenWidth: width
property real screenHeight: height
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_wipe.frag.qsb")
}
// Disc reveal transition shader
@@ -140,7 +179,17 @@ Variants {
property real centerX: root.discCenterX
property real centerY: root.discCenterY
fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/wp_disc.frag.qsb")
// Fill mode properties
property real fillMode: root.fillMode
property vector4d fillColor: root.fillColor
property real imageWidth1: source1.sourceSize.width
property real imageHeight1: source1.sourceSize.height
property real imageWidth2: source2.sourceSize.width
property real imageHeight2: source2.sourceSize.height
property real screenWidth: width
property real screenHeight: height
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_disc.frag.qsb")
}
// Diagonal stripes transition shader
@@ -157,7 +206,17 @@ Variants {
property real stripeCount: root.stripesCount
property real angle: root.stripesAngle
fragmentShader: Qt.resolvedUrl("../../Shaders/qsb/wp_stripes.frag.qsb")
// Fill mode properties
property real fillMode: root.fillMode
property vector4d fillColor: root.fillColor
property real imageWidth1: source1.sourceSize.width
property real imageHeight1: source1.sourceSize.height
property real imageWidth2: source2.sourceSize.width
property real imageHeight2: source2.sourceSize.height
property real screenWidth: width
property real screenHeight: height
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/wp_stripes.frag.qsb")
}
// Animation for the transition progress
@@ -168,20 +227,16 @@ Variants {
from: 0.0
to: 1.0
// The stripes shader feels faster visually, we make it a bit slower here.
duration: transitionType == "stripes" ? Settings.data.wallpaper.transitionDuration
* 1.6 : Settings.data.wallpaper.transitionDuration
duration: transitionType == "stripes" ? Settings.data.wallpaper.transitionDuration * 1.6 : Settings.data.wallpaper.transitionDuration
easing.type: Easing.InOutCubic
onFinished: {
// Swap images after transition completes
currentWallpaper.source = nextWallpaper.source
nextWallpaper.source = ""
transitionProgress = 0.0
}
}
function startTransition() {
if (!transitioning && nextWallpaper.source != currentWallpaper.source) {
transitionAnimation.start()
Qt.callLater(() => {
currentWallpaper.asynchronous = true
})
}
}
@@ -193,19 +248,21 @@ Variants {
}
function setWallpaperWithTransition(source) {
if (source != currentWallpaper.source) {
if (transitioning) {
// We are interrupting a transition
transitionAnimation.stop()
transitionProgress = 0
currentWallpaper.source = nextWallpaper.source
nextWallpaper.source = ""
}
nextWallpaper.source = source
startTransition()
if (source === currentWallpaper.source) {
return
}
if (transitioning) {
// We are interrupting a transition
transitionAnimation.stop()
transitionProgress = 0
currentWallpaper.source = nextWallpaper.source
nextWallpaper.source = ""
}
nextWallpaper.source = source
currentWallpaper.asynchronous = false
transitionAnimation.start()
}
// Main method that actually trigger the wallpaper change

View File

@@ -12,13 +12,26 @@ Variants {
delegate: Loader {
required property ShellScreen modelData
active: Settings.isLoaded && CompositorService.isNiri && modelData
active: Settings.isLoaded && CompositorService.isNiri && modelData && Settings.data.wallpaper.enabled
property string wallpaper: ""
sourceComponent: PanelWindow {
Component.onCompleted: {
if (modelData) {
Logger.log("Overview", "Loading Overview component for Niri on", modelData.name)
}
wallpaper = modelData ? WallpaperService.getWallpaper(modelData.name) : ""
}
// External state management
Connections {
target: WallpaperService
function onWallpaperChanged(screenName, path) {
if (screenName === modelData.name) {
wallpaper = path
}
}
}
color: Color.transparent
@@ -38,7 +51,7 @@ Variants {
id: bgImage
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
source: modelData ? WallpaperService.getWallpaper(modelData.name) : ""
source: wallpaper
smooth: true
mipmap: false
cache: false
@@ -47,14 +60,16 @@ Variants {
MultiEffect {
anchors.fill: parent
source: bgImage
autoPaddingEnabled: false
blurEnabled: true
blur: 0.48
blurMax: 128
}
// Make the overview darker
Rectangle {
anchors.fill: parent
color: Qt.rgba(Color.mSurface.r, Color.mSurface.g, Color.mSurface.b, 0.5)
color: Settings.data.colorSchemes.darkMode ? Qt.alpha(Color.mSurface, Style.opacityMedium) : Qt.alpha(Color.mOnSurface, Style.opacityMedium)
}
}
}

View File

@@ -16,23 +16,26 @@ Loader {
id: root
required property ShellScreen modelData
readonly property real scaling: ScalingService.scale(screen)
property real scaling: ScalingService.getScreenScale(screen)
screen: modelData
// Visible color
property color ringColor: Qt.rgba(Color.mSurface.r, Color.mSurface.g, Color.mSurface.b,
Settings.data.bar.backgroundOpacity)
// The amount subtracted from full size for the inner cutout
// Inner size = full size - borderWidth (per axis)
property int borderWidth: Style.borderM
// Rounded radius for the inner cutout
property int innerRadius: 20
property color cornerColor: Settings.data.general.forceBlackScreenCorners ? Qt.rgba(0, 0, 0, 1) : Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity)
property real cornerRadius: Style.screenRadius * scaling
property real cornerSize: Style.screenRadius * scaling
Connections {
target: ScalingService
function onScaleChanged(screenName, scale) {
if (screenName === screen.name) {
scaling = scale
}
}
}
color: Color.transparent
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "quickshell-corner"
// Do not take keyboard focus and make the surface click-through
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
anchors {
@@ -43,111 +46,205 @@ Loader {
}
margins {
top: ((modelData && Settings.data.bar.monitors.includes(modelData.name))
|| (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "top"
&& Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
bottom: ((modelData && Settings.data.bar.monitors.includes(modelData.name))
|| (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "bottom"
&& Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
}
// Source we want to show only as a ring
Rectangle {
id: overlaySource
anchors.fill: parent
color: root.ringColor
}
// Texture for overlaySource
ShaderEffectSource {
id: overlayTexture
anchors.fill: parent
sourceItem: overlaySource
hideSource: true
live: true
visible: false
}
// Mask via Canvas: paint opaque white, then punch rounded inner hole
Canvas {
id: maskSource
anchors.fill: parent
antialiasing: true
renderTarget: Canvas.FramebufferObject
onPaint: {
const ctx = getContext("2d")
ctx.reset()
ctx.clearRect(0, 0, width, height)
// Solid white base (alpha=1)
ctx.globalCompositeOperation = "source-over"
ctx.fillStyle = "#ffffffff"
ctx.fillRect(0, 0, width, height)
// Punch hole using destination-out with rounded rect path
const x = Math.round(root.borderWidth / 2)
const y = Math.round(root.borderWidth / 2)
const w = Math.max(0, width - root.borderWidth)
const h = Math.max(0, height - root.borderWidth)
const r = Math.max(0, Math.min(root.innerRadius, Math.min(w, h) / 2))
ctx.globalCompositeOperation = "destination-out"
ctx.fillStyle = "#ffffffff"
ctx.beginPath()
// rounded rectangle path using arcTo
ctx.moveTo(x + r, y)
ctx.lineTo(x + w - r, y)
ctx.arcTo(x + w, y, x + w, y + r, r)
ctx.lineTo(x + w, y + h - r)
ctx.arcTo(x + w, y + h, x + w - r, y + h, r)
ctx.lineTo(x + r, y + h)
ctx.arcTo(x, y + h, x, y + h - r, r)
ctx.lineTo(x, y + r)
ctx.arcTo(x, y, x + r, y, r)
ctx.closePath()
ctx.fill()
}
onWidthChanged: requestPaint()
onHeightChanged: requestPaint()
}
// Repaint mask when properties change
Connections {
function onBorderWidthChanged() {
maskSource.requestPaint()
}
function onRingColorChanged() {}
function onInnerRadiusChanged() {
maskSource.requestPaint()
}
target: root
}
// Texture for maskSource; hides the original
ShaderEffectSource {
id: maskTexture
anchors.fill: parent
sourceItem: maskSource
hideSource: true
live: true
visible: false
}
// Apply mask to show only the ring area
MultiEffect {
anchors.fill: parent
source: overlayTexture
maskEnabled: true
maskSource: maskTexture
maskInverted: false
maskSpreadAtMax: 0.75
// When bar is floating, corners should be at screen edges (no margins)
// When bar is not floating, respect bar margins as before
top: !Settings.data.bar.floating && ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "top" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
bottom: !Settings.data.bar.floating && ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "bottom" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
left: !Settings.data.bar.floating && ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "left" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
right: !Settings.data.bar.floating && ((modelData && Settings.data.bar.monitors.includes(modelData.name)) || (Settings.data.bar.monitors.length === 0)) && Settings.data.bar.position === "right" && Settings.data.bar.backgroundOpacity > 0 ? Math.round(Style.barHeight * scaling) : 0
}
mask: Region {}
// Top-left concave corner
Canvas {
id: topLeftCorner
anchors.top: parent.top
anchors.left: parent.left
width: cornerSize
height: cornerSize
antialiasing: true
renderTarget: Canvas.FramebufferObject
smooth: false
onPaint: {
const ctx = getContext("2d")
if (!ctx)
return
ctx.reset()
ctx.clearRect(0, 0, width, height)
// Fill the entire area with the corner color
ctx.fillStyle = root.cornerColor
ctx.fillRect(0, 0, width, height)
// Cut out the rounded corner using destination-out
ctx.globalCompositeOperation = "destination-out"
ctx.fillStyle = "#ffffff"
ctx.beginPath()
ctx.arc(width, height, root.cornerRadius, 0, 2 * Math.PI)
ctx.fill()
}
onWidthChanged: if (available)
requestPaint()
onHeightChanged: if (available)
requestPaint()
Connections {
target: root
function onCornerColorChanged() {
if (topLeftCorner.available)
topLeftCorner.requestPaint()
}
function onCornerRadiusChanged() {
if (topLeftCorner.available)
topLeftCorner.requestPaint()
}
}
}
// Top-right concave corner
Canvas {
id: topRightCorner
anchors.top: parent.top
anchors.right: parent.right
width: cornerSize
height: cornerSize
antialiasing: true
renderTarget: Canvas.FramebufferObject
smooth: true
onPaint: {
const ctx = getContext("2d")
if (!ctx)
return
ctx.reset()
ctx.clearRect(0, 0, width, height)
ctx.fillStyle = root.cornerColor
ctx.fillRect(0, 0, width, height)
ctx.globalCompositeOperation = "destination-out"
ctx.fillStyle = "#ffffff"
ctx.beginPath()
ctx.arc(0, height, root.cornerRadius, 0, 2 * Math.PI)
ctx.fill()
}
onWidthChanged: if (available)
requestPaint()
onHeightChanged: if (available)
requestPaint()
Connections {
target: root
function onCornerColorChanged() {
if (topRightCorner.available)
topRightCorner.requestPaint()
}
function onCornerRadiusChanged() {
if (topRightCorner.available)
topRightCorner.requestPaint()
}
}
}
// Bottom-left concave corner
Canvas {
id: bottomLeftCorner
anchors.bottom: parent.bottom
anchors.left: parent.left
width: cornerSize
height: cornerSize
antialiasing: true
renderTarget: Canvas.FramebufferObject
smooth: true
onPaint: {
const ctx = getContext("2d")
if (!ctx)
return
ctx.reset()
ctx.clearRect(0, 0, width, height)
ctx.fillStyle = root.cornerColor
ctx.fillRect(0, 0, width, height)
ctx.globalCompositeOperation = "destination-out"
ctx.fillStyle = "#ffffff"
ctx.beginPath()
ctx.arc(width, 0, root.cornerRadius, 0, 2 * Math.PI)
ctx.fill()
}
onWidthChanged: if (available)
requestPaint()
onHeightChanged: if (available)
requestPaint()
Connections {
target: root
function onCornerColorChanged() {
if (bottomLeftCorner.available)
bottomLeftCorner.requestPaint()
}
function onCornerRadiusChanged() {
if (bottomLeftCorner.available)
bottomLeftCorner.requestPaint()
}
}
}
// Bottom-right concave corner
Canvas {
id: bottomRightCorner
anchors.bottom: parent.bottom
anchors.right: parent.right
width: cornerSize
height: cornerSize
antialiasing: true
renderTarget: Canvas.FramebufferObject
smooth: true
onPaint: {
const ctx = getContext("2d")
if (!ctx)
return
ctx.reset()
ctx.clearRect(0, 0, width, height)
ctx.fillStyle = root.cornerColor
ctx.fillRect(0, 0, width, height)
ctx.globalCompositeOperation = "destination-out"
ctx.fillStyle = "#ffffff"
ctx.beginPath()
ctx.arc(0, 0, root.cornerRadius, 0, 2 * Math.PI)
ctx.fill()
}
onWidthChanged: if (available)
requestPaint()
onHeightChanged: if (available)
requestPaint()
Connections {
target: root
function onCornerColorChanged() {
if (bottomRightCorner.available)
bottomRightCorner.requestPaint()
}
function onCornerRadiusChanged() {
if (bottomRightCorner.available)
bottomRightCorner.requestPaint()
}
}
}
}
}
}

View File

@@ -8,6 +8,7 @@ import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.Notification
import qs.Modules.Bar.Extras
Variants {
model: Quickshell.screens
@@ -16,115 +17,223 @@ Variants {
id: root
required property ShellScreen modelData
readonly property real scaling: modelData ? ScalingService.scale(modelData) : 1.0
property real scaling: ScalingService.getScreenScale(modelData)
active: Settings.isLoaded && modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name)
|| (Settings.data.bar.monitors.length === 0)) : false
Connections {
target: ScalingService
function onScaleChanged(screenName, scale) {
if ((modelData !== null) && (screenName === modelData.name)) {
scaling = scale
}
}
}
active: Settings.isLoaded && modelData && modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false
sourceComponent: PanelWindow {
screen: modelData || null
WlrLayershell.namespace: "noctalia-bar"
implicitHeight: Math.round(Style.barHeight * scaling)
implicitHeight: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? screen.height : Math.round(Style.barHeight * scaling)
implicitWidth: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? Math.round(Style.barHeight * scaling) : screen.width
color: Color.transparent
anchors {
top: Settings.data.bar.position === "top"
bottom: Settings.data.bar.position === "bottom"
left: true
right: true
top: Settings.data.bar.position === "top" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
bottom: Settings.data.bar.position === "bottom" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
left: Settings.data.bar.position === "left" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
right: Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
}
// Floating bar margins - only apply when floating is enabled
margins {
top: Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
bottom: Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0
left: Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
right: Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0
}
Item {
anchors.fill: parent
clip: true
// Background fill
// Background fill with shadow
Rectangle {
id: bar
anchors.fill: parent
color: Qt.rgba(Color.mSurface.r, Color.mSurface.g, Color.mSurface.b, Settings.data.bar.backgroundOpacity)
layer.enabled: true
color: Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity)
// Floating bar rounded corners
radius: Settings.data.bar.floating ? Style.radiusL : 0
}
// ------------------------------
// Left Section - Dynamic Widgets
Row {
id: leftSection
objectName: "leftSection"
Loader {
anchors.fill: parent
sourceComponent: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? verticalBarComponent : horizontalBarComponent
}
height: parent.height
anchors.left: parent.left
anchors.leftMargin: Style.marginS * scaling
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
// For vertical bars
Component {
id: verticalBarComponent
Item {
anchors.fill: parent
Repeater {
model: Settings.data.bar.widgets.left
delegate: NWidgetLoader {
widgetName: modelData
widgetProps: {
"screen": root.modelData || null,
"barSection": parent.objectName,
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.left.length
// Top section (left widgets)
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Style.marginM * root.scaling
spacing: Style.marginS * root.scaling
Repeater {
model: Settings.data.bar.widgets.left
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": "left",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.left.length
}
Layout.alignment: Qt.AlignHCenter
}
}
}
// Center section (center widgets)
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * root.scaling
Repeater {
model: Settings.data.bar.widgets.center
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": "center",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.center.length
}
Layout.alignment: Qt.AlignHCenter
}
}
}
// Bottom section (right widgets)
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Style.marginM * root.scaling
spacing: Style.marginS * root.scaling
Repeater {
model: Settings.data.bar.widgets.right
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": "right",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.right.length
}
Layout.alignment: Qt.AlignHCenter
}
}
}
}
}
// ------------------------------
// Center Section - Dynamic Widgets
Row {
id: centerSection
objectName: "centerSection"
// For horizontal bars
Component {
id: horizontalBarComponent
Item {
anchors.fill: parent
height: parent.height
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
Repeater {
model: Settings.data.bar.widgets.center
delegate: NWidgetLoader {
widgetName: modelData
widgetProps: {
"screen": root.modelData || null,
"barSection": parent.objectName,
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.center.length
}
// Left Section
RowLayout {
id: leftSection
objectName: "leftSection"
anchors.left: parent.left
anchors.leftMargin: Style.marginS * root.scaling
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * root.scaling
Repeater {
model: Settings.data.bar.widgets.left
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": "left",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.left.length
}
Layout.alignment: Qt.AlignVCenter
}
}
}
}
}
// ------------------------------
// Right Section - Dynamic Widgets
Row {
id: rightSection
objectName: "rightSection"
height: parent.height
anchors.right: bar.right
anchors.rightMargin: Style.marginS * scaling
anchors.verticalCenter: bar.verticalCenter
spacing: Style.marginS * scaling
Repeater {
model: Settings.data.bar.widgets.right
delegate: NWidgetLoader {
widgetName: modelData
widgetProps: {
"screen": root.modelData || null,
"barSection": parent.objectName,
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.right.length
}
// Center Section
RowLayout {
id: centerSection
objectName: "centerSection"
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * root.scaling
Repeater {
model: Settings.data.bar.widgets.center
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": "center",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.center.length
}
Layout.alignment: Qt.AlignVCenter
}
}
}
// Right Section
RowLayout {
id: rightSection
objectName: "rightSection"
anchors.right: parent.right
anchors.rightMargin: Style.marginS * root.scaling
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * root.scaling
Repeater {
model: Settings.data.bar.widgets.right
delegate: BarWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": "right",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.right.length
}
Layout.alignment: Qt.AlignVCenter
}
}
}
}
}

View File

@@ -0,0 +1,111 @@
import QtQuick
import QtQuick.Controls
import qs.Commons
import qs.Services
import qs.Widgets
Item {
id: root
property string icon: ""
property string text: ""
property string suffix: ""
property string tooltipText: ""
property bool autoHide: false
property bool forceOpen: false
property bool forceClose: false
property bool disableOpen: false
property bool rightOpen: false
property bool hovered: false
property bool compact: false
readonly property string barPosition: Settings.data.bar.position
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
signal shown
signal hidden
signal entered
signal exited
signal clicked
signal rightClicked
signal middleClicked
signal wheel(int delta)
// Dynamic sizing based on loaded component
width: pillLoader.item ? pillLoader.item.width : 0
height: pillLoader.item ? pillLoader.item.height : 0
// Loader to switch between vertical and horizontal pill implementations
Loader {
id: pillLoader
sourceComponent: isVerticalBar ? verticalPillComponent : horizontalPillComponent
Component {
id: verticalPillComponent
BarPillVertical {
icon: root.icon
text: root.text
suffix: root.suffix
tooltipText: root.tooltipText
autoHide: root.autoHide
forceOpen: root.forceOpen
forceClose: root.forceClose
disableOpen: root.disableOpen
rightOpen: root.rightOpen
hovered: root.hovered
compact: root.compact
onShown: root.shown()
onHidden: root.hidden()
onEntered: root.entered()
onExited: root.exited()
onClicked: root.clicked()
onRightClicked: root.rightClicked()
onMiddleClicked: root.middleClicked()
onWheel: delta => root.wheel(delta)
}
}
Component {
id: horizontalPillComponent
BarPillHorizontal {
icon: root.icon
text: root.text
suffix: root.suffix
tooltipText: root.tooltipText
autoHide: root.autoHide
forceOpen: root.forceOpen
forceClose: root.forceClose
disableOpen: root.disableOpen
rightOpen: root.rightOpen
hovered: root.hovered
compact: root.compact
onShown: root.shown()
onHidden: root.hidden()
onEntered: root.entered()
onExited: root.exited()
onClicked: root.clicked()
onRightClicked: root.rightClicked()
onMiddleClicked: root.middleClicked()
onWheel: delta => root.wheel(delta)
}
}
}
function show() {
if (pillLoader.item && pillLoader.item.show) {
pillLoader.item.show()
}
}
function hide() {
if (pillLoader.item && pillLoader.item.hide) {
pillLoader.item.hide()
}
}
function showDelayed() {
if (pillLoader.item && pillLoader.item.showDelayed) {
pillLoader.item.showDelayed()
}
}
}

View File

@@ -2,26 +2,25 @@ import QtQuick
import QtQuick.Controls
import qs.Commons
import qs.Services
import qs.Widgets
Item {
id: root
property string icon: ""
property string text: ""
property string suffix: ""
property string tooltipText: ""
property color pillColor: Color.mSurfaceVariant
property color textColor: Color.mOnSurface
property color iconCircleColor: Color.mPrimary
property color iconTextColor: Color.mSurface
property color collapsedIconColor: Color.mOnSurface
property real sizeRatio: 0.8
property bool autoHide: false
property bool forceOpen: false
property bool forceClose: false
property bool disableOpen: false
property bool rightOpen: false
property bool hovered: false
property bool compact: false
// Effective shown state (true if hovered/animated open or forced)
readonly property bool effectiveShown: forceOpen || showPill
readonly property bool revealed: forceOpen || showPill
signal shown
signal hidden
@@ -29,32 +28,34 @@ Item {
signal exited
signal clicked
signal rightClicked
signal middleClicked
signal wheel(int delta)
// Internal state
property bool showPill: false
property bool shouldAnimateHide: false
// Exposed width logic
readonly property int pillHeight: Style.baseWidgetSize * sizeRatio * scaling
readonly property int iconSize: Style.baseWidgetSize * sizeRatio * scaling
readonly property int pillPaddingHorizontal: Style.marginM * scaling
readonly property int pillOverlap: iconSize * 0.5
readonly property int maxPillWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap)
readonly property int pillHeight: Math.round(Style.capsuleHeight * scaling)
readonly property int pillPaddingHorizontal: Math.round(Style.capsuleHeight * 0.2 * scaling)
readonly property int pillOverlap: Math.round(Style.capsuleHeight * 0.5 * scaling)
readonly property int pillMaxWidth: Math.max(1, textItem.implicitWidth + pillPaddingHorizontal * 2 + pillOverlap)
width: iconSize + (effectiveShown ? maxPillWidth - pillOverlap : 0)
readonly property real iconSize: Math.max(1, compact ? pillHeight * 0.65 : pillHeight * 0.48)
readonly property real textSize: Math.max(1, compact ? pillHeight * 0.45 : pillHeight * 0.33)
width: pillHeight + Math.max(0, pill.width - pillOverlap)
height: pillHeight
Rectangle {
id: pill
width: effectiveShown ? maxPillWidth : 1
width: revealed ? pillMaxWidth : 1
height: pillHeight
x: rightOpen ? (iconCircle.x + iconCircle.width / 2) : // Opens right
(iconCircle.x + iconCircle.width / 2) - width // Opens left
opacity: effectiveShown ? Style.opacityFull : Style.opacityNone
color: pillColor
opacity: revealed ? Style.opacityFull : Style.opacityNone
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
topLeftRadius: rightOpen ? 0 : pillHeight * 0.5
bottomLeftRadius: rightOpen ? 0 : pillHeight * 0.5
@@ -64,12 +65,23 @@ Item {
NText {
id: textItem
anchors.centerIn: parent
text: root.text
font.pointSize: Style.fontSizeXS * scaling
anchors.verticalCenter: parent.verticalCenter
x: {
// Better text horizontal centering
var centerX = (parent.width - width) / 2
var offset = rightOpen ? Style.marginXS * scaling : -Style.marginXS * scaling
if (forceOpen) {
// If its force open, the icon disc background is the same color as the bg pill move text slightly
offset += rightOpen ? -Style.marginXXS * scaling : Style.marginXXS * scaling
}
return centerX + offset
}
text: root.text + root.suffix
font.family: Settings.data.ui.fontFixed
font.pointSize: textSize
font.weight: Style.fontWeightBold
color: textColor
visible: effectiveShown
color: forceOpen ? Color.mOnSurface : Color.mPrimary
visible: revealed
}
Behavior on width {
@@ -90,15 +102,13 @@ Item {
Rectangle {
id: iconCircle
width: iconSize
height: iconSize
width: pillHeight
height: pillHeight
radius: width * 0.5
// When forced shown, match pill background; otherwise use accent when hovered
color: forceOpen ? pillColor : (showPill ? iconCircleColor : Color.mSurfaceVariant)
color: hovered ? Color.mTertiary : Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
anchors.verticalCenter: parent.verticalCenter
anchors.left: rightOpen ? parent.left : undefined
anchors.right: rightOpen ? undefined : parent.right
x: rightOpen ? 0 : (parent.width - width)
Behavior on color {
ColorAnimation {
@@ -108,10 +118,9 @@ Item {
}
NIcon {
text: root.icon
font.pointSize: Style.fontSizeM * scaling
// When forced shown, use pill text color; otherwise accent color when hovered
color: forceOpen ? textColor : (showPill ? iconTextColor : Color.mOnSurface)
icon: root.icon
font.pointSize: iconSize
color: hovered ? Color.mOnTertiary : Color.mOnSurface
// Center horizontally
x: (iconCircle.width - width) / 2
// Center vertically accounting for font metrics
@@ -126,7 +135,7 @@ Item {
target: pill
property: "width"
from: 1
to: maxPillWidth
to: pillMaxWidth
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
@@ -166,7 +175,7 @@ Item {
NumberAnimation {
target: pill
property: "width"
from: maxPillWidth
from: pillMaxWidth
to: 1
duration: Style.animationNormal
easing.type: Easing.InCubic
@@ -207,8 +216,9 @@ Item {
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onEntered: {
hovered = true
root.entered()
tooltip.show()
if (disableOpen) {
@@ -219,6 +229,7 @@ Item {
}
}
onExited: {
hovered = false
root.exited()
if (!forceOpen) {
hide()
@@ -230,11 +241,11 @@ Item {
root.clicked()
} else if (mouse.button === Qt.RightButton) {
root.rightClicked()
} else if (mouse.button === Qt.MiddleButton) {
root.middleClicked()
}
}
onWheel: wheel => {
root.wheel(wheel.angleDelta.y)
}
onWheel: wheel => root.wheel(wheel.angleDelta.y)
}
function show() {

View File

@@ -0,0 +1,335 @@
import QtQuick
import QtQuick.Controls
import qs.Commons
import qs.Services
import qs.Widgets
Item {
id: root
property string icon: ""
property string text: ""
property string suffix: ""
property string tooltipText: ""
property bool autoHide: false
property bool forceOpen: false
property bool forceClose: false
property bool disableOpen: false
property bool rightOpen: false
property bool hovered: false
property bool compact: false
// Bar position detection for pill direction
readonly property string barPosition: Settings.data.bar.position
readonly property bool isVerticalBar: barPosition === "left" || barPosition === "right"
// Determine pill direction based on section position
readonly property bool openDownward: rightOpen
readonly property bool openUpward: !rightOpen
// Effective shown state (true if animated open or forced, but not if force closed)
readonly property bool revealed: !forceClose && (forceOpen || showPill)
signal shown
signal hidden
signal entered
signal exited
signal clicked
signal rightClicked
signal middleClicked
signal wheel(int delta)
// Internal state
property bool showPill: false
property bool shouldAnimateHide: false
// Sizing logic for vertical bars
readonly property int buttonSize: Math.round(Style.capsuleHeight * scaling)
readonly property int pillHeight: buttonSize
readonly property int pillPaddingVertical: 3 * 2 * scaling // Very precise adjustment don't replace by Style.margin
readonly property int pillOverlap: buttonSize * 0.5
readonly property int maxPillWidth: buttonSize
readonly property int maxPillHeight: Math.max(1, textItem.implicitHeight + pillPaddingVertical * 4)
readonly property real iconSize: Math.max(1, compact ? pillHeight * 0.65 : pillHeight * 0.48)
readonly property real textSize: Math.max(1, compact ? pillHeight * 0.38 : pillHeight * 0.33)
// For vertical bars: width is just icon size, height includes pill space
width: buttonSize
height: revealed ? (buttonSize + maxPillHeight - pillOverlap) : buttonSize
Rectangle {
id: pill
width: revealed ? maxPillWidth : 1
height: revealed ? maxPillHeight : 1
// Position based on direction - center the pill relative to the icon
x: 0
y: openUpward ? (iconCircle.y + iconCircle.height / 2 - height) : (iconCircle.y + iconCircle.height / 2)
opacity: revealed ? Style.opacityFull : Style.opacityNone
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
// Radius logic for vertical expansion - rounded on the side that connects to icon
topLeftRadius: openUpward ? buttonSize * 0.5 : 0
bottomLeftRadius: openDownward ? buttonSize * 0.5 : 0
topRightRadius: openUpward ? buttonSize * 0.5 : 0
bottomRightRadius: openDownward ? buttonSize * 0.5 : 0
anchors.horizontalCenter: parent.horizontalCenter
NText {
id: textItem
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: {
var offset = openDownward ? pillPaddingVertical * 0.75 : -pillPaddingVertical * 0.75
if (forceOpen) {
// If its force open, the icon disc background is the same color as the bg pill move text slightly
offset += rightOpen ? -Style.marginXXS * scaling : Style.marginXXS * scaling
}
return offset
}
text: root.text + root.suffix
font.family: Settings.data.ui.fontFixed
font.pointSize: textSize
font.weight: Style.fontWeightMedium
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: forceOpen ? Color.mOnSurface : Color.mPrimary
visible: revealed
}
Behavior on width {
enabled: showAnim.running || hideAnim.running
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
}
Behavior on height {
enabled: showAnim.running || hideAnim.running
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
}
Behavior on opacity {
enabled: showAnim.running || hideAnim.running
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
}
}
Rectangle {
id: iconCircle
width: buttonSize
height: buttonSize
radius: width * 0.5
color: hovered ? Color.mTertiary : Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
// Icon positioning based on direction
x: 0
y: openUpward ? (parent.height - height) : 0
anchors.horizontalCenter: parent.horizontalCenter
Behavior on color {
ColorAnimation {
duration: Style.animationNormal
easing.type: Easing.InOutQuad
}
}
NIcon {
icon: root.icon
font.pointSize: iconSize
color: hovered ? Color.mOnTertiary : Color.mOnSurface
// Center horizontally
x: (iconCircle.width - width) / 2
// Center vertically accounting for font metrics
y: (iconCircle.height - height) / 2 + (height - contentHeight) / 2
}
}
ParallelAnimation {
id: showAnim
running: false
NumberAnimation {
target: pill
property: "width"
from: 1
to: maxPillWidth
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
NumberAnimation {
target: pill
property: "height"
from: 1
to: maxPillHeight
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
NumberAnimation {
target: pill
property: "opacity"
from: 0
to: 1
duration: Style.animationNormal
easing.type: Easing.OutCubic
}
onStarted: {
showPill = true
}
onStopped: {
delayedHideAnim.start()
root.shown()
}
}
SequentialAnimation {
id: delayedHideAnim
running: false
PauseAnimation {
duration: 2500
}
ScriptAction {
script: if (shouldAnimateHide) {
hideAnim.start()
}
}
}
ParallelAnimation {
id: hideAnim
running: false
NumberAnimation {
target: pill
property: "width"
from: maxPillWidth
to: 1
duration: Style.animationNormal
easing.type: Easing.InCubic
}
NumberAnimation {
target: pill
property: "height"
from: maxPillHeight
to: 1
duration: Style.animationNormal
easing.type: Easing.InCubic
}
NumberAnimation {
target: pill
property: "opacity"
from: 1
to: 0
duration: Style.animationNormal
easing.type: Easing.InCubic
}
onStopped: {
showPill = false
shouldAnimateHide = false
root.hidden()
}
}
NTooltip {
id: tooltip
target: pill
text: root.tooltipText
positionLeft: barPosition === "right"
positionRight: barPosition === "left"
positionAbove: Settings.data.bar.position === "bottom"
delay: Style.tooltipDelayLong
}
Timer {
id: showTimer
interval: Style.pillDelay
onTriggered: {
if (!showPill) {
showAnim.start()
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onEntered: {
hovered = true
root.entered()
tooltip.show()
if (disableOpen || forceClose) {
return
}
if (!forceOpen) {
showDelayed()
}
}
onExited: {
hovered = false
root.exited()
if (!forceOpen && !forceClose) {
hide()
}
tooltip.hide()
}
onClicked: function (mouse) {
if (mouse.button === Qt.LeftButton) {
root.clicked()
} else if (mouse.button === Qt.RightButton) {
root.rightClicked()
} else if (mouse.button === Qt.MiddleButton) {
root.middleClicked()
}
}
onWheel: wheel => root.wheel(wheel.angleDelta.y)
}
function show() {
if (!showPill) {
shouldAnimateHide = autoHide
showAnim.start()
} else {
hideAnim.stop()
delayedHideAnim.restart()
}
}
function hide() {
if (forceOpen) {
return
}
if (showPill) {
hideAnim.start()
}
showTimer.stop()
}
function showDelayed() {
if (!showPill) {
shouldAnimateHide = autoHide
showTimer.start()
} else {
hideAnim.stop()
delayedHideAnim.restart()
}
}
onForceOpenChanged: {
if (forceOpen) {
// Immediately lock open without animations
showAnim.stop()
hideAnim.stop()
delayedHideAnim.stop()
showPill = true
} else {
hide()
}
}
}

View File

@@ -0,0 +1,76 @@
import QtQuick
import Quickshell
import qs.Services
import qs.Commons
Item {
id: root
property string widgetId: ""
property var widgetProps: ({})
property string screenName: widgetProps.screen ? widgetProps.screen.name : ""
property string section: widgetProps.section || ""
property int sectionIndex: widgetProps.sectionWidgetIndex || 0
Connections {
target: ScalingService
function onScaleChanged(aScreenName, scale) {
if (loader.item && loader.item.screen && aScreenName === screenName) {
loader.item['scaling'] = scale
}
}
}
// Don't reserve space unless the loaded widget is really visible
implicitWidth: loader.item ? loader.item.visible ? loader.item.implicitWidth : 0 : 0
implicitHeight: loader.item ? loader.item.visible ? loader.item.implicitHeight : 0 : 0
Loader {
id: loader
anchors.fill: parent
active: Settings.isLoaded && widgetId !== ""
sourceComponent: {
if (!active) {
return null
}
return BarWidgetRegistry.getWidget(widgetId)
}
onLoaded: {
if (item && widgetProps) {
// Apply properties to loaded widget
for (var prop in widgetProps) {
if (item.hasOwnProperty(prop)) {
item[prop] = widgetProps[prop]
}
}
}
// Register this widget instance with BarService
if (screenName && section) {
BarService.registerWidget(screenName, section, widgetId, sectionIndex, item)
}
if (item.hasOwnProperty("onLoaded")) {
item.onLoaded()
}
//Logger.log("BarWidgetLoader", "Loaded", widgetId, "on screen", item.screen.name)
}
Component.onDestruction: {
// Unregister when destroyed
if (screenName && section) {
BarService.unregisterWidget(screenName, section, widgetId, sectionIndex)
}
}
}
// Error handling
onWidgetIdChanged: {
if (widgetId && !BarWidgetRegistry.hasWidget(widgetId)) {
Logger.warn("BarWidgetLoader", "Widget not found in bar registry:", widgetId)
}
}
}

View File

@@ -15,15 +15,23 @@ PopupWindow {
property bool isSubMenu: false
property bool isHovered: rootMouseArea.containsMouse
property ShellScreen screen
property real scaling: screen ? ScalingService.scale(screen) : 1.0
property real scaling: ScalingService.getScreenScale(screen)
Connections {
target: ScalingService
function onScaleChanged(screenName, scale) {
if ((screen != null) && (screenName === screen.name)) {
scaling = scale
}
}
}
readonly property int menuWidth: 180
implicitWidth: menuWidth * scaling
// Use the content height of the Flickable for implicit height
implicitHeight: Math.min(screen ? screen.height * 0.9 : Screen.height * 0.9,
flickable.contentHeight + (Style.marginS * 2 * scaling))
implicitHeight: Math.min(screen ? screen.height * 0.9 : Screen.height * 0.9, flickable.contentHeight + (Style.marginS * 2 * scaling))
visible: false
color: Color.transparent
anchor.item: anchorItem
@@ -150,8 +158,7 @@ PopupWindow {
NText {
id: text
Layout.fillWidth: true
color: (modelData?.enabled
?? true) ? (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface) : Color.mOnSurfaceVariant
color: (modelData?.enabled ?? true) ? (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface) : Color.mOnSurfaceVariant
text: modelData?.text !== "" ? modelData?.text.replace(/[\n\r]+/g, ' ') : "..."
font.pointSize: Style.fontSizeS * scaling
verticalAlignment: Text.AlignVCenter
@@ -167,11 +174,11 @@ PopupWindow {
}
NIcon {
text: modelData?.hasChildren ? "menu" : ""
icon: modelData?.hasChildren ? "menu" : ""
font.pointSize: Style.fontSizeS * scaling
verticalAlignment: Text.AlignVCenter
visible: modelData?.hasChildren ?? false
color: Color.mOnSurface
color: (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface)
}
}
@@ -213,9 +220,32 @@ PopupWindow {
const submenuWidth = menuWidth * scaling // Assuming a similar width as the parent
const overlap = 4 * scaling // A small overlap to bridge the mouse path
// Check if there's enough space on the right
// Determine submenu opening direction based on bar position and available space
let openLeft = false
// Check bar position first
const barPosition = Settings.data.bar.position
const globalPos = entry.mapToGlobal(0, 0)
const openLeft = (globalPos.x + entry.width + submenuWidth > (screen ? screen.width : Screen.width))
if (barPosition === "right") {
// Bar is on the right, prefer opening submenus to the left
openLeft = true
} else if (barPosition === "left") {
// Bar is on the left, prefer opening submenus to the right
openLeft = false
} else {
// Bar is horizontal (top/bottom) or undefined, use space-based logic
openLeft = (globalPos.x + entry.width + submenuWidth > screen.width)
// Secondary check: ensure we don't open off-screen
if (openLeft && globalPos.x - submenuWidth < 0) {
// Would open off the left edge, force right opening
openLeft = false
} else if (!openLeft && globalPos.x + entry.width + submenuWidth > screen.width) {
// Would open off the right edge, force left opening
openLeft = true
}
}
// Position with overlap
const anchorX = openLeft ? -submenuWidth + overlap : entry.width - overlap

View File

@@ -2,38 +2,132 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Commons
import qs.Services
import qs.Widgets
Row {
Item {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
readonly property real minWidth: 160
readonly property real maxWidth: 400
property real scaling: 1.0
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
visible: getTitle() !== ""
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
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
// 6% of total width
readonly property real minWidth: Math.max(1, screen.width * 0.06)
readonly property real maxWidth: minWidth * 2
readonly property string barPosition: Settings.data.bar.position
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
readonly property bool compact: (Settings.data.bar.density === "compact")
implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)
implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * 0.8 * scaling) : (horizontalLayout.implicitWidth + Style.marginM * 2 * scaling)
readonly property real textSize: {
var base = isVertical ? width : height
return Math.max(1, compact ? base * 0.43 : base * 0.33)
}
readonly property real iconSize: textSize * 1.25
function getTitle() {
// Use the service's focusedWindowTitle property which is updated immediately
// when WindowOpenedOrChanged events are received
return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : ""
try {
return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : ""
} catch (e) {
Logger.warn("ActiveWindow", "Error getting title:", e)
return ""
}
}
visible: getTitle() !== ""
function calculatedVerticalHeight() {
// Use standard widget height like other widgets
return Math.round(Style.capsuleHeight * scaling)
}
function calculatedHorizontalWidth() {
let total = Style.marginM * 2 * scaling // internal padding
if (showIcon) {
total += Style.capsuleHeight * 0.5 * scaling + 2 * scaling // icon + spacing
}
// Calculate actual text width more accurately
const title = getTitle()
if (title !== "") {
// Estimate text width: average character width * number of characters
const avgCharWidth = Style.fontSizeS * scaling * 0.6 // rough estimate
const titleWidth = Math.min(title.length * avgCharWidth, 80 * scaling)
total += titleWidth
}
// Row layout handles spacing between widgets
return Math.max(total, Style.capsuleHeight * scaling) // Minimum width
}
function getAppIcon() {
const focusedWindow = CompositorService.getFocusedWindow()
if (!focusedWindow || !focusedWindow.appId)
return ""
try {
// Try CompositorService first
const focusedWindow = CompositorService.getFocusedWindow()
if (focusedWindow && focusedWindow.appId) {
try {
const idValue = focusedWindow.appId
const normalizedId = (typeof idValue === 'string') ? idValue : String(idValue)
const iconResult = AppIcons.iconForAppId(normalizedId.toLowerCase())
if (iconResult && iconResult !== "") {
return iconResult
}
} catch (iconError) {
Logger.warn("ActiveWindow", "Error getting icon from CompositorService:", iconError)
}
}
return Icons.iconForAppId(focusedWindow.appId)
// Fallback to ToplevelManager
if (ToplevelManager && ToplevelManager.activeToplevel) {
try {
const activeToplevel = ToplevelManager.activeToplevel
if (activeToplevel.appId) {
const idValue2 = activeToplevel.appId
const normalizedId2 = (typeof idValue2 === 'string') ? idValue2 : String(idValue2)
const iconResult2 = AppIcons.iconForAppId(normalizedId2.toLowerCase())
if (iconResult2 && iconResult2 !== "") {
return iconResult2
}
}
} catch (fallbackError) {
Logger.warn("ActiveWindow", "Error getting icon from ToplevelManager:", fallbackError)
}
}
return ""
} catch (e) {
Logger.warn("ActiveWindow", "Error in getAppIcon:", e)
return ""
}
}
// A hidden text element to safely measure the full title width
// A hidden text element to safely measure the full title width
NText {
id: fullTitleMetrics
visible: false
@@ -43,33 +137,37 @@ Row {
}
Rectangle {
// Let the Rectangle size itself based on its content (the Row)
id: windowTitleRect
visible: root.visible
width: row.width + Style.marginM * 2 * scaling
height: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
anchors.left: (barPosition === "top" || barPosition === "bottom") ? parent.left : undefined
anchors.top: (barPosition === "left" || barPosition === "right") ? parent.top : undefined
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : (horizontalLayout.implicitWidth + Style.marginM * 2 * scaling)
height: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : Math.round(Style.capsuleHeight * scaling)
radius: width / 2
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
Item {
id: mainContainer
anchors.fill: parent
anchors.leftMargin: Style.marginS * scaling
anchors.rightMargin: Style.marginS * scaling
anchors.leftMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
anchors.rightMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
clip: true
Row {
id: row
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
// Horizontal layout for top/bottom bars
RowLayout {
id: horizontalLayout
anchors.centerIn: parent
spacing: 2 * scaling
visible: barPosition === "top" || barPosition === "bottom"
// Window icon
Item {
width: Style.fontSizeL * scaling * 1.2
height: Style.fontSizeL * scaling * 1.2
anchors.verticalCenter: parent.verticalCenter
visible: getTitle() !== "" && Settings.data.bar.showActiveWindowIcon
Layout.preferredWidth: Style.capsuleHeight * 0.75 * scaling
Layout.preferredHeight: Style.capsuleHeight * 0.75 * scaling
Layout.alignment: Qt.AlignVCenter
visible: getTitle() !== "" && showIcon
IconImage {
id: windowIcon
@@ -78,31 +176,41 @@ Row {
asynchronous: true
smooth: true
visible: source !== ""
// Handle loading errors gracefully
onStatusChanged: {
if (status === Image.Error) {
Logger.warn("ActiveWindow", "Failed to load icon:", source)
}
}
}
}
NText {
id: titleText
// For short titles, show full. For long titles, truncate and expand on hover
width: {
if (mouseArea.containsMouse) {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
} else {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling))
Layout.preferredWidth: {
try {
if (mouseArea.containsMouse) {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
} else {
return Math.round(Math.min(fullTitleMetrics.contentWidth, 80 * scaling)) // Limited width for horizontal bars
}
} catch (e) {
Logger.warn("ActiveWindow", "Error calculating width:", e)
return 80 * scaling
}
}
Layout.alignment: Qt.AlignVCenter
horizontalAlignment: Text.AlignLeft
text: getTitle()
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
elide: mouseArea.containsMouse ? Text.ElideNone : Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mSecondary
color: Color.mPrimary
clip: true
Behavior on width {
Behavior on Layout.preferredWidth {
NumberAnimation {
duration: Style.animationSlow
easing.type: Easing.InOutCubic
@@ -111,12 +219,84 @@ Row {
}
}
// Vertical layout for left/right bars - icon only
Item {
id: verticalLayout
anchors.centerIn: parent
width: parent.width - Style.marginXS * scaling * 2
height: parent.height - Style.marginXS * scaling * 2
visible: barPosition === "left" || barPosition === "right"
// Window icon
Item {
width: Style.capsuleHeight * 0.75 * scaling
height: Style.capsuleHeight * 0.75 * scaling
anchors.centerIn: parent
IconImage {
id: windowIconVertical
anchors.fill: parent
source: getAppIcon()
asynchronous: true
smooth: true
visible: source !== ""
// Handle loading errors gracefully
onStatusChanged: {
if (status === Image.Error) {
Logger.warn("ActiveWindow", "Failed to load icon:", source)
}
}
}
}
}
// Mouse area for hover detection
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
if (barPosition === "left" || barPosition === "right") {
tooltip.show()
}
}
onExited: {
if (barPosition === "left" || barPosition === "right") {
tooltip.hide()
}
}
}
// Hover tooltip with full title (only for vertical bars)
NTooltip {
id: tooltip
target: verticalLayout
text: getTitle()
positionLeft: barPosition === "right"
positionRight: barPosition === "left"
delay: 500
}
}
}
Connections {
target: CompositorService
function onActiveWindowChanged() {
try {
windowIcon.source = Qt.binding(getAppIcon)
windowIconVertical.source = Qt.binding(getAppIcon)
} catch (e) {
Logger.warn("ActiveWindow", "Error in onActiveWindowChanged:", e)
}
}
function onWindowListChanged() {
try {
windowIcon.source = Qt.binding(getAppIcon)
windowIconVertical.source = Qt.binding(getAppIcon)
} catch (e) {
Logger.warn("ActiveWindow", "Error in onWindowListChanged:", e)
}
}
}

View File

@@ -1,80 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
NIconButton {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
sizeRatio: 0.8
colorBg: Color.mSurfaceVariant
colorBorder: Color.transparent
colorBorderHover: Color.transparent
colorFg: {
if (!ArchUpdaterService.terminalAvailable || !ArchUpdaterService.aurHelperAvailable) {
return Color.mError
}
return (ArchUpdaterService.totalUpdates === 0) ? Color.mOnSurface : Color.mPrimary
}
// Icon states
icon: {
if (!ArchUpdaterService.terminalAvailable) {
return "terminal"
}
if (!ArchUpdaterService.aurHelperAvailable) {
return "package"
}
if (ArchUpdaterService.aurBusy) {
return "sync"
}
if (ArchUpdaterService.totalUpdates > 0) {
return "system_update_alt"
}
return "task_alt"
}
// Tooltip with repo vs AUR breakdown and sample lists
tooltipText: {
if (!ArchUpdaterService.terminalAvailable) {
return "Terminal not configured\nSet TERMINAL environment variable"
}
if (!ArchUpdaterService.aurHelperAvailable) {
return "AUR helper not found\nInstall yay or paru"
}
if (ArchUpdaterService.aurBusy) {
return "Checking for updates…"
}
const total = ArchUpdaterService.totalUpdates
if (total === 0) {
return "System is up to date ✓"
}
let header = (total === 1) ? "1 package can be updated" : (total + " packages can be updated")
const pacCount = ArchUpdaterService.updates
const aurCount = ArchUpdaterService.aurUpdates
const pacmanTooltip = (pacCount > 0) ? ((pacCount === 1) ? "1 system package" : pacCount + " system packages") : ""
const aurTooltip = (aurCount > 0) ? ((aurCount === 1) ? "1 AUR package" : aurCount + " AUR packages") : ""
let tooltip = header
if (pacmanTooltip !== "") {
tooltip += "\n" + pacmanTooltip
}
if (aurTooltip !== "") {
tooltip += "\n" + aurTooltip
}
return tooltip
}
onClicked: {
// Always allow panel to open, never block
PanelService.getPanel("archUpdaterPanel").toggle(screen, this)
}
}

View File

@@ -5,17 +5,45 @@ import QtQuick.Layouts
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.Bar.Extras
Item {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property string barSection: ""
property int sectionWidgetIndex: 0
property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
// Track if we've already notified to avoid spam
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
readonly property string displayMode: widgetSettings.displayMode !== undefined ? widgetSettings.displayMode : widgetMetadata.displayMode
readonly property real warningThreshold: widgetSettings.warningThreshold !== undefined ? widgetSettings.warningThreshold : widgetMetadata.warningThreshold
// Test mode
readonly property bool testMode: false
readonly property int testPercent: 100
readonly property bool testCharging: false
// 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
implicitWidth: pill.width
@@ -23,15 +51,12 @@ Item {
// Helper to evaluate and possibly notify
function maybeNotify(percent, charging) {
const p = Math.round(percent)
// Only notify exactly at 15%, not at 0% or any other percentage
if (!charging && p === 15 && !root.hasNotifiedLowBattery) {
Quickshell.execDetached(
["notify-send", "-u", "critical", "-i", "battery-caution", "Low Battery", `Battery is at ${p}%. Please connect charger.`])
// Only notify once we are a below threshold
if (!charging && !root.hasNotifiedLowBattery && percent <= warningThreshold) {
root.hasNotifiedLowBattery = true
}
// Reset when charging starts or when battery recovers above 20%
if (charging || p > 20) {
ToastService.showWarning("Low Battery", `Battery is at ${Math.round(percent)}%. Please connect the charger.`)
} else if (root.hasNotifiedLowBattery && (charging || percent > warningThreshold + 5)) {
// Reset when charging starts or when battery recovers 5% above threshold
root.hasNotifiedLowBattery = false
}
}
@@ -40,99 +65,61 @@ Item {
Connections {
target: UPower.displayDevice
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)
var currentPercent = UPower.displayDevice.percentage * 100
var isCharging = UPower.displayDevice.state === UPowerDeviceState.Charging
root.maybeNotify(currentPercent, isCharging)
}
function onStateChanged() {
let battery = UPower.displayDevice
let isReady = battery && battery.ready && battery.isLaptopBattery && battery.isPresent
let charging = isReady ? battery.state === UPowerDeviceState.Charging : false
var isCharging = UPower.displayDevice.state === UPowerDeviceState.Charging
// Reset notification flag when charging starts
if (charging) {
if (isCharging) {
root.hasNotifiedLowBattery = false
}
// Also re-evaluate maybeNotify, as state might have changed
var currentPercent = UPower.displayDevice.percentage * 100
root.maybeNotify(currentPercent, isCharging)
}
}
NPill {
BarPill {
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)
icon: batteryIcon()
text: ((isReady && battery.isLaptopBattery) || testMode) ? Math.round(percent) + "%" : "-"
textColor: charging ? Color.mPrimary : Color.mOnSurface
iconCircleColor: Color.mPrimary
collapsedIconColor: Color.mOnSurface
compact: (Settings.data.bar.density === "compact")
rightOpen: BarService.getPillDirection(root)
icon: testMode ? BatteryService.getIcon(testPercent, testCharging, true) : BatteryService.getIcon(percent, charging, isReady)
text: (isReady || testMode) ? Math.round(percent) : "-"
suffix: "%"
autoHide: false
forceOpen: isReady && (testMode || battery.isLaptopBattery) && Settings.data.bar.alwaysShowBatteryPercentage
forceOpen: isReady && (testMode || battery.isLaptopBattery) && displayMode === "alwaysShow"
forceClose: displayMode === "alwaysHide"
disableOpen: (!isReady || (!testMode && !battery.isLaptopBattery))
tooltipText: {
let lines = []
if (testMode) {
lines.push("Time left: " + Time.formatVagueHumanReadableDuration(12345))
lines.push(`Time left: ${Time.formatVagueHumanReadableDuration(12345)}.`)
return lines.join("\n")
}
if (!isReady || !battery.isLaptopBattery) {
return "No battery detected"
return "No battery detected."
}
if (battery.timeToEmpty > 0) {
lines.push("Time left: " + Time.formatVagueHumanReadableDuration(battery.timeToEmpty))
lines.push(`Time left: ${Time.formatVagueHumanReadableDuration(battery.timeToEmpty)}.`)
}
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) {
const rate = battery.changeRate
if (rate > 0) {
lines.push(charging ? "Charging rate: " + rate.toFixed(2) + " W" : "Discharging rate: " + rate.toFixed(
2) + " W")
lines.push(charging ? "Charging rate: " + rate.toFixed(2) + " W." : "Discharging rate: " + rate.toFixed(2) + " W.")
} 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 {
lines.push("Estimating...")
}
} else {
lines.push(charging ? "Charging" : "Discharging")
lines.push(charging ? "Charging." : "Discharging.")
}
if (battery.healthPercentage !== undefined && battery.healthPercentage > 0) {
lines.push("Health: " + Math.round(battery.healthPercentage) + "%")

View File

@@ -11,16 +11,17 @@ NIconButton {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property real scaling: 1.0
visible: Settings.data.network.bluetoothEnabled
sizeRatio: 0.8
colorBg: Color.mSurfaceVariant
baseSize: Style.capsuleHeight
compact: (Settings.data.bar.density === "compact")
colorBg: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
colorFg: Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
icon: "bluetooth"
tooltipText: "Bluetooth devices"
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(screen, this)
icon: Settings.data.network.bluetoothEnabled ? "bluetooth" : "bluetooth-off"
tooltipText: "Bluetooth devices."
onClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
onRightClicked: PanelService.getPanel("bluetoothPanel")?.toggle(this)
}

View File

@@ -4,16 +4,34 @@ import qs.Commons
import qs.Modules.SettingsPanel
import qs.Services
import qs.Widgets
import qs.Modules.Bar.Extras
Item {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property string barSection: ""
property int sectionWidgetIndex: 0
property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode
// Used to avoid opening the pill on Quickshell startup
property bool firstBrightnessReceived: false
@@ -28,8 +46,7 @@ Item {
function getIcon() {
var monitor = getMonitor()
var brightness = monitor ? monitor.brightness : 0
return brightness <= 0 ? "brightness_1" : brightness < 0.33 ? "brightness_low" : brightness
< 0.66 ? "brightness_medium" : "brightness_high"
return brightness <= 0.5 ? "brightness-low" : "brightness-high"
}
// Connection used to open the pill when brightness changes
@@ -37,46 +54,45 @@ Item {
target: getMonitor()
ignoreUnknownSignals: true
function onBrightnessUpdated() {
Logger.log("Bar-Brightness", "OnBrightnessUpdated")
var monitor = getMonitor()
if (!monitor)
return
var currentBrightness = monitor.brightness
// Ignore if this is the first time or if brightness hasn't actually changed
// Ignore if this is the first time we receive an update.
// Most likely service just kicked off.
if (!firstBrightnessReceived) {
firstBrightnessReceived = true
monitor.lastBrightness = currentBrightness
return
}
// Only show pill if brightness actually changed (not just loaded from settings)
if (Math.abs(currentBrightness - monitor.lastBrightness) > 0.1) {
pill.show()
}
monitor.lastBrightness = currentBrightness
pill.show()
hideTimerAfterChange.restart()
}
}
NPill {
Timer {
id: hideTimerAfterChange
interval: 2500
running: false
repeat: false
onTriggered: pill.hide()
}
BarPill {
id: pill
rightOpen: BarWidgetRegistry.getNPillDirection(root)
compact: (Settings.data.bar.density === "compact")
rightOpen: BarService.getPillDirection(root)
icon: getIcon()
iconCircleColor: Color.mPrimary
collapsedIconColor: Color.mOnSurface
autoHide: false // Important to be false so we can hover as long as we want
text: {
var monitor = getMonitor()
return monitor ? (Math.round(monitor.brightness * 100) + "%") : ""
return monitor ? Math.round(monitor.brightness * 100) : ""
}
suffix: text.length > 0 ? "%" : "-"
forceOpen: displayMode === "alwaysShow"
forceClose: displayMode === "alwaysHide"
tooltipText: {
var monitor = getMonitor()
if (!monitor)
return ""
return "Brightness: " + Math.round(monitor.brightness * 100) + "%\nMethod: " + monitor.method
+ "\nLeft click for advanced settings.\nScroll up/down to change brightness."
return "Brightness: " + Math.round(monitor.brightness * 100) + "%\nRight click for settings.\nScroll to modify brightness."
}
onWheel: function (angle) {
@@ -92,8 +108,14 @@ Item {
onClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel")
settingsPanel.requestedTab = SettingsPanel.Tab.Brightness
settingsPanel.open(screen)
settingsPanel.requestedTab = SettingsPanel.Tab.Display
settingsPanel.open()
}
onRightClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel")
settingsPanel.requestedTab = SettingsPanel.Tab.Display
settingsPanel.open()
}
}
}

View File

@@ -1,4 +1,5 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
@@ -8,26 +9,204 @@ Rectangle {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property real scaling: 1.0
implicitWidth: clock.width + Style.marginM * 2 * scaling
implicitHeight: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
// Clock Icon with attached calendar
NClock {
id: clock
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
NTooltip {
id: tooltip
text: Time.dateString
target: clock
positionAbove: Settings.data.bar.position === "bottom"
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property string barPosition: Settings.data.bar.position
readonly property bool compact: (Settings.data.bar.density === "compact")
readonly property bool use12h: Settings.data.location.use12hourFormat
readonly property bool monthBeforeDay: Settings.data.location.monthBeforeDay
// Resolve settings: try user settings or defaults from BarWidgetRegistry
readonly property string displayFormat: widgetSettings.displayFormat !== undefined ? widgetSettings.displayFormat : widgetMetadata.displayFormat
// Use compact mode for vertical bars
readonly property bool verticalMode: barPosition === "left" || barPosition === "right"
implicitWidth: verticalMode ? Math.round(Style.capsuleHeight * scaling) : Math.round(layout.implicitWidth + Style.marginM * 2 * scaling)
implicitHeight: verticalMode ? Math.round(Style.capsuleHeight * 2.5 * scaling) : Math.round(Style.capsuleHeight * scaling) // Match BarPill
radius: Math.round(Style.radiusS * scaling)
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
Item {
id: clockContainer
anchors.fill: parent
anchors.margins: compact ? 0 : Style.marginXS * scaling
ColumnLayout {
id: layout
anchors.centerIn: parent
spacing: verticalMode ? -2 * scaling : -3 * scaling
// Compact mode for vertical bars - Time section (HH, MM)
Repeater {
model: verticalMode ? 2 : 1
NText {
readonly property bool showSeconds: (displayFormat === "time-seconds")
readonly property bool inlineDate: (displayFormat === "time-date")
readonly property var now: Time.date
text: {
if (verticalMode) {
// Compact mode: time section (first 2 lines)
switch (index) {
case 0:
// Hours
if (use12h) {
const hours = now.getHours()
const displayHours = hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours)
return displayHours.toString().padStart(2, '0')
} else {
return now.getHours().toString().padStart(2, '0')
}
case 1:
// Minutes
return now.getMinutes().toString().padStart(2, '0')
default:
return ""
}
} else {
// Normal mode: single line with time
let timeStr = ""
if (use12h) {
// 12-hour format with proper padding and consistent spacing
const hours = now.getHours()
const displayHours = hours === 0 ? 12 : (hours > 12 ? hours - 12 : hours)
const paddedHours = displayHours.toString().padStart(2, '0')
const minutes = now.getMinutes().toString().padStart(2, '0')
const ampm = hours < 12 ? 'AM' : 'PM'
if (showSeconds) {
const seconds = now.getSeconds().toString().padStart(2, '0')
timeStr = `${paddedHours}:${minutes}:${seconds} ${ampm}`
} else {
timeStr = `${paddedHours}:${minutes} ${ampm}`
}
} else {
// 24-hour format with padding
const hours = now.getHours().toString().padStart(2, '0')
const minutes = now.getMinutes().toString().padStart(2, '0')
if (showSeconds) {
const seconds = now.getSeconds().toString().padStart(2, '0')
timeStr = `${hours}:${minutes}:${seconds}`
} else {
timeStr = `${hours}:${minutes}`
}
}
// Add inline date if needed
if (inlineDate) {
let dayName = now.toLocaleDateString(Qt.locale(), "ddd")
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
const day = now.getDate().toString().padStart(2, '0')
let month = now.toLocaleDateString(Qt.locale(), "MMM")
timeStr += " - " + (monthBeforeDay ? `${dayName}, ${month} ${day}` : `${dayName}, ${day} ${month}`)
}
return timeStr
}
}
font.family: Settings.data.ui.fontFixed
font.pointSize: verticalMode ? Style.fontSizeXXS * scaling : Style.fontSizeXS * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
}
// Separator line for compact mode (between time and date)
Rectangle {
visible: verticalMode
Layout.preferredWidth: 20 * scaling
Layout.preferredHeight: 2 * scaling
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: 3 * scaling
Layout.bottomMargin: 3 * scaling
color: Color.mPrimary
opacity: 0.3
radius: 1 * scaling
}
// Compact mode for vertical bars - Date section (DD, MM)
Repeater {
model: verticalMode ? 2 : 0
NText {
readonly property var now: Time.date
text: {
if (verticalMode) {
// Compact mode: date section (last 2 lines)
switch (index) {
case 0:
return monthBeforeDay ? (now.getMonth() + 1).toString().padStart(2, '0') : now.getDate().toString().padStart(2, '0')
case 1:
return monthBeforeDay ? now.getDate().toString().padStart(2, '0') : (now.getMonth() + 1).toString().padStart(2, '0')
default:
return ""
}
}
return ""
}
font.pointSize: Style.fontSizeXXS * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
}
// Second line for normal mode (date)
NText {
visible: !verticalMode && (displayFormat === "time-date-short")
text: {
const now = Time.date
const day = now.getDate().toString().padStart(2, '0')
const month = (now.getMonth() + 1).toString().padStart(2, '0')
return monthBeforeDay ? `${month}/${day}` : `${day}/${month}`
}
// Enable fixed-width font for consistent spacing
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXXS * scaling
font.weight: Style.fontWeightRegular
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
}
}
NTooltip {
id: tooltip
text: `${Time.formatDate(monthBeforeDay)}.`
target: clockContainer
positionAbove: Settings.data.bar.position === "bottom"
}
MouseArea {
id: clockMouseArea
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
onEntered: {
if (!PanelService.getPanel("calendarPanel")?.active) {
tooltip.show()
@@ -38,7 +217,7 @@ Rectangle {
}
onClicked: {
tooltip.hide()
PanelService.getPanel("calendarPanel")?.toggle(screen, this)
PanelService.getPanel("calendarPanel")?.toggle(this)
}
}
}

View File

@@ -0,0 +1,139 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.SettingsPanel
import qs.Modules.Bar.Extras
Item {
id: root
// Widget properties passed from Bar.qml
property var screen
property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
// Use settings or defaults from BarWidgetRegistry
readonly property string customIcon: widgetSettings.icon || widgetMetadata.icon
readonly property string leftClickExec: widgetSettings.leftClickExec || widgetMetadata.leftClickExec
readonly property string rightClickExec: widgetSettings.rightClickExec || widgetMetadata.rightClickExec
readonly property string middleClickExec: widgetSettings.middleClickExec || widgetMetadata.middleClickExec
readonly property string textCommand: widgetSettings.textCommand !== undefined ? widgetSettings.textCommand : (widgetMetadata.textCommand || "")
readonly property int textIntervalMs: widgetSettings.textIntervalMs !== undefined ? widgetSettings.textIntervalMs : (widgetMetadata.textIntervalMs || 3000)
readonly property bool hasExec: (leftClickExec || rightClickExec || middleClickExec)
implicitWidth: pill.width
implicitHeight: pill.height
BarPill {
id: pill
rightOpen: BarService.getPillDirection(root)
icon: customIcon
text: _dynamicText
compact: (Settings.data.bar.density === "compact")
autoHide: false
forceOpen: _dynamicText !== ""
forceClose: false
disableOpen: true
tooltipText: {
if (!hasExec) {
return "Custom Button - Configure in settings"
} else {
var lines = []
if (leftClickExec !== "") {
lines.push(`Left click: ${leftClickExec}.`)
}
if (rightClickExec !== "") {
lines.push(`Right click: ${rightClickExec}.`)
}
if (middleClickExec !== "") {
lines.push(`Middle click: ${middleClickExec}.`)
}
return lines.join("\n")
}
}
onClicked: root.onClicked()
onRightClicked: root.onRightClicked()
onMiddleClicked: root.onMiddleClicked()
}
// Internal state for dynamic text
property string _dynamicText: ""
// Periodically run the text command (if set)
Timer {
id: refreshTimer
interval: Math.max(250, textIntervalMs)
repeat: true
running: (textCommand && textCommand.length > 0)
triggeredOnStart: true
onTriggered: {
if (!textCommand || textCommand.length === 0)
return
if (textProc.running)
return
textProc.command = ["sh", "-lc", textCommand]
textProc.running = true
}
}
Process {
id: textProc
stdout: StdioCollector {}
stderr: StdioCollector {}
onExited: (exitCode, exitStatus) => {
var out = String(stdout.text || "").trim()
if (out.indexOf("\n") !== -1) {
out = out.split("\n")[0]
}
_dynamicText = out
}
}
function onClicked() {
if (leftClickExec) {
Quickshell.execDetached(["sh", "-c", leftClickExec])
Logger.log("CustomButton", `Executing command: ${leftClickExec}`)
} else if (!hasExec) {
// No script was defined, open settings
var settingsPanel = PanelService.getPanel("settingsPanel")
settingsPanel.requestedTab = SettingsPanel.Tab.Bar
settingsPanel.open()
}
}
function onRightClicked() {
if (rightClickExec) {
Quickshell.execDetached(["sh", "-c", rightClickExec])
Logger.log("CustomButton", `Executing command: ${rightClickExec}`)
}
}
function onMiddleClicked() {
if (middleClickExec) {
Quickshell.execDetached(["sh", "-c", middleClickExec])
Logger.log("CustomButton", `Executing command: ${middleClickExec}`)
}
}
}

View File

@@ -0,0 +1,21 @@
import Quickshell
import qs.Commons
import qs.Widgets
import qs.Services
NIconButton {
id: root
property ShellScreen screen
property real scaling: 1.0
icon: "dark-mode"
tooltipText: "Toggle light/dark mode."
compact: (Settings.data.bar.density === "compact")
baseSize: Style.capsuleHeight
colorBg: Settings.data.colorSchemes.darkMode ? (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent) : Color.mPrimary
colorFg: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mOnPrimary
colorBorder: Color.transparent
colorBorderHover: Color.transparent
onClicked: Settings.data.colorSchemes.darkMode = !Settings.data.colorSchemes.darkMode
}

View File

@@ -0,0 +1,22 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
NIconButton {
id: root
property ShellScreen screen
property real scaling: 1.0
baseSize: Style.capsuleHeight
compact: (Settings.data.bar.density === "compact")
icon: IdleInhibitorService.isInhibited ? "keep-awake-on" : "keep-awake-off"
tooltipText: IdleInhibitorService.isInhibited ? "Disable keep awake" : "Enable keep awake"
colorBg: IdleInhibitorService.isInhibited ? Color.mPrimary : (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
colorFg: IdleInhibitorService.isInhibited ? Color.mOnPrimary : Color.mOnSurface
colorBorder: Color.transparent
onClicked: IdleInhibitorService.manualToggle()
}

View File

@@ -1,37 +1,56 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Io
import qs.Commons
import qs.Services
import qs.Widgets
import qs.Modules.Bar.Extras
Row {
Item {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property string barSection: ""
property int sectionWidgetIndex: 0
property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode
// Use the shared service for keyboard layout
property string currentLayout: KeyboardLayoutService.currentLayout
width: pill.width
height: pill.height
implicitWidth: pill.width
implicitHeight: pill.height
NPill {
BarPill {
id: pill
rightOpen: BarWidgetRegistry.getNPillDirection(root)
icon: "keyboard_alt"
iconCircleColor: Color.mPrimary
collapsedIconColor: Color.mOnSurface
anchors.verticalCenter: parent.verticalCenter
compact: (Settings.data.bar.density === "compact")
rightOpen: BarService.getPillDirection(root)
icon: "keyboard"
autoHide: false // Important to be false so we can hover as long as we want
text: currentLayout
tooltipText: "Keyboard layout: " + currentLayout
text: currentLayout.toUpperCase()
tooltipText: "Keyboard layout: " + currentLayout.toUpperCase()
forceOpen: root.displayMode === "forceOpen"
forceClose: root.displayMode === "alwaysHide"
onClicked: {
// You could open keyboard settings here if needed

View File

@@ -7,23 +7,65 @@ import qs.Commons
import qs.Services
import qs.Widgets
Row {
Item {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
readonly property real minWidth: 160
readonly property real maxWidth: 400
property real scaling: 1.0
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
visible: MediaService.currentPlayer !== null && MediaService.canPlay
width: MediaService.currentPlayer !== null && MediaService.canPlay ? implicitWidth : 0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property string barPosition: Settings.data.bar.position
readonly property bool compact: (Settings.data.bar.density === "compact")
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
// 6% of total width
readonly property real minWidth: Math.max(1, screen.width * 0.06)
readonly property real maxWidth: minWidth * 2
function getTitle() {
return MediaService.trackTitle + (MediaService.trackArtist !== "" ? ` - ${MediaService.trackArtist}` : "")
}
function calculatedVerticalHeight() {
return Math.round(Style.baseWidgetSize * 0.8 * scaling)
}
function calculatedHorizontalWidth() {
let total = Style.marginM * 2 * scaling // internal padding
if (showAlbumArt) {
total += 18 * scaling + 2 * scaling // album art + spacing
} else {
total += Style.fontSizeL * scaling + 2 * scaling // icon + spacing
}
total += Math.min(fullTitleMetrics.contentWidth, maxWidth * scaling) // title text
// Row layout handles spacing between widgets
return total
}
implicitHeight: visible ? ((barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)) : 0
implicitWidth: visible ? ((barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (rowLayout.implicitWidth + Style.marginM * 2 * scaling)) : 0
visible: MediaService.currentPlayer !== null && MediaService.canPlay
// A hidden text element to safely measure the full title width
NText {
id: fullTitleMetrics
@@ -34,15 +76,13 @@ Row {
Rectangle {
id: mediaMini
// Let the Rectangle size itself based on its content (the Row)
width: row.width + Style.marginM * 2 * scaling
height: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
visible: root.visible
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : (rowLayout.implicitWidth + Style.marginM * 2 * scaling)
height: (barPosition === "left" || barPosition === "right") ? Math.round(Style.baseWidgetSize * 0.8 * scaling) : Math.round(Style.capsuleHeight * scaling)
radius: (barPosition === "left" || barPosition === "right") ? width / 2 : Math.round(Style.radiusM * scaling)
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
// Used to anchor the tooltip, so the tooltip does not move when the content expands
Item {
@@ -54,14 +94,13 @@ Row {
Item {
id: mainContainer
anchors.fill: parent
anchors.leftMargin: Style.marginS * scaling
anchors.rightMargin: Style.marginS * scaling
anchors.leftMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
anchors.rightMargin: (barPosition === "left" || barPosition === "right") ? 0 : Style.marginS * scaling
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "linear"
&& MediaService.isPlaying && MediaService.trackLength > 0
active: showVisualizer && visualizerType == "linear" && MediaService.isPlaying
z: 0
sourceComponent: LinearSpectrum {
@@ -71,68 +110,70 @@ Row {
fillColor: Color.mOnSurfaceVariant
opacity: 0.4
}
}
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "mirrored"
&& MediaService.isPlaying && MediaService.trackLength > 0
z: 0
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: showVisualizer && visualizerType == "mirrored" && MediaService.isPlaying
z: 0
sourceComponent: MirroredSpectrum {
width: mainContainer.width - Style.marginS * scaling
height: mainContainer.height - Style.marginS * scaling
values: CavaService.values
fillColor: Color.mOnSurfaceVariant
opacity: 0.4
}
}
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: Settings.data.audio.showMiniplayerCava && Settings.data.audio.visualizerType == "wave"
&& MediaService.isPlaying && MediaService.trackLength > 0
z: 0
sourceComponent: WaveSpectrum {
width: mainContainer.width - Style.marginS * scaling
height: mainContainer.height - Style.marginS * scaling
values: CavaService.values
fillColor: Color.mOnSurfaceVariant
opacity: 0.4
}
sourceComponent: MirroredSpectrum {
width: mainContainer.width - Style.marginS * scaling
height: mainContainer.height - Style.marginS * scaling
values: CavaService.values
fillColor: Color.mOnSurfaceVariant
opacity: 0.4
}
}
Row {
id: row
Loader {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
active: showVisualizer && visualizerType == "wave" && MediaService.isPlaying
z: 0
sourceComponent: WaveSpectrum {
width: mainContainer.width - Style.marginS * scaling
height: mainContainer.height - Style.marginS * scaling
values: CavaService.values
fillColor: Color.mOnSurfaceVariant
opacity: 0.4
}
}
// Horizontal layout for top/bottom bars
RowLayout {
id: rowLayout
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
visible: barPosition === "top" || barPosition === "bottom"
z: 1 // Above the visualizer
NIcon {
id: windowIcon
text: MediaService.isPlaying ? "pause" : "play_arrow"
icon: MediaService.isPlaying ? "media-pause" : "media-play"
font.pointSize: Style.fontSizeL * scaling
verticalAlignment: Text.AlignVCenter
anchors.verticalCenter: parent.verticalCenter
visible: !Settings.data.audio.showMiniplayerAlbumArt && getTitle() !== "" && !trackArt.visible
Layout.alignment: Qt.AlignVCenter
visible: !showAlbumArt && getTitle() !== "" && !trackArt.visible
}
Column {
anchors.verticalCenter: parent.verticalCenter
visible: Settings.data.audio.showMiniplayerAlbumArt
ColumnLayout {
Layout.alignment: Qt.AlignVCenter
visible: showAlbumArt
spacing: 0
Item {
width: Math.round(18 * scaling)
height: Math.round(18 * scaling)
Layout.preferredWidth: Math.round(18 * scaling)
Layout.preferredHeight: Math.round(18 * scaling)
NImageCircled {
id: trackArt
anchors.fill: parent
imagePath: MediaService.trackArtUrl
fallbackIcon: MediaService.isPlaying ? "pause" : "play_arrow"
fallbackIcon: MediaService.isPlaying ? "media-pause" : "media-play"
fallbackIconSize: 10 * scaling
borderWidth: 0
border.color: Color.transparent
}
@@ -142,23 +183,23 @@ Row {
NText {
id: titleText
// For short titles, show full. For long titles, truncate and expand on hover
width: {
Layout.preferredWidth: {
if (mouseArea.containsMouse) {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.maxWidth * scaling))
} else {
return Math.round(Math.min(fullTitleMetrics.contentWidth, root.minWidth * scaling))
}
}
Layout.alignment: Qt.AlignVCenter
text: getTitle()
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
elide: Text.ElideRight
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mTertiary
color: Color.mSecondary
Behavior on width {
Behavior on Layout.preferredWidth {
NumberAnimation {
duration: Style.animationSlow
easing.type: Easing.InOutCubic
@@ -167,6 +208,33 @@ Row {
}
}
// Vertical layout for left/right bars - icon only
Item {
id: verticalLayout
anchors.centerIn: parent
width: parent.width - Style.marginM * scaling * 2
height: parent.height - Style.marginM * scaling * 2
visible: barPosition === "left" || barPosition === "right"
z: 1 // Above the visualizer
// Media icon
Item {
width: Style.baseWidgetSize * 0.5 * scaling
height: Style.baseWidgetSize * 0.5 * scaling
anchors.centerIn: parent
visible: getTitle() !== ""
NIcon {
id: mediaIconVertical
anchors.fill: parent
icon: MediaService.isPlaying ? "media-pause" : "media-play"
font.pointSize: Style.fontSizeL * scaling
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
}
}
// Mouse area for hover detection
MouseArea {
id: mouseArea
@@ -189,12 +257,18 @@ Row {
}
onEntered: {
if (tooltip.text !== "") {
if (barPosition === "left" || barPosition === "right") {
tooltip.show()
} else if (tooltip.text !== "") {
tooltip.show()
}
}
onExited: {
tooltip.hide()
if (barPosition === "left" || barPosition === "right") {
tooltip.hide()
} else {
tooltip.hide()
}
}
}
}
@@ -203,16 +277,23 @@ Row {
NTooltip {
id: tooltip
text: {
var str = ""
if (MediaService.canGoNext) {
str += "Right click for next\n"
if (barPosition === "left" || barPosition === "right") {
return getTitle()
} else {
var str = ""
if (MediaService.canGoNext) {
str += "Right click for next.\n"
}
if (MediaService.canGoPrevious) {
str += "Middle click for previous."
}
return str
}
if (MediaService.canGoPrevious) {
str += "Middle click for previous\n"
}
return str
}
target: anchor
target: (barPosition === "left" || barPosition === "right") ? verticalLayout : anchor
positionLeft: barPosition === "right"
positionRight: barPosition === "left"
positionAbove: Settings.data.bar.position === "bottom"
delay: 500
}
}

View File

@@ -0,0 +1,124 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import qs.Commons
import qs.Modules.SettingsPanel
import qs.Services
import qs.Widgets
import qs.Modules.Bar.Extras
Item {
id: root
property ShellScreen screen
property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode
// Used to avoid opening the pill on Quickshell startup
property bool firstInputVolumeReceived: false
property int wheelAccumulator: 0
implicitWidth: pill.width
implicitHeight: pill.height
function getIcon() {
if (AudioService.inputMuted) {
return "microphone-mute"
}
return (AudioService.inputVolume <= Number.EPSILON) ? "microphone-mute" : "microphone"
}
// Connection used to open the pill when input volume changes
Connections {
target: AudioService.source?.audio ? AudioService.source?.audio : null
function onVolumeChanged() {
// Logger.log("Bar:Microphone", "onInputVolumeChanged")
if (!firstInputVolumeReceived) {
// Ignore the first volume change
firstInputVolumeReceived = true
} else {
pill.show()
externalHideTimer.restart()
}
}
}
// Connection used to open the pill when input mute state changes
Connections {
target: AudioService.source?.audio ? AudioService.source?.audio : null
function onMutedChanged() {
// Logger.log("Bar:Microphone", "onInputMutedChanged")
if (!firstInputVolumeReceived) {
// Ignore the first mute change
firstInputVolumeReceived = true
} else {
pill.show()
externalHideTimer.restart()
}
}
}
Timer {
id: externalHideTimer
running: false
interval: 1500
onTriggered: {
pill.hide()
}
}
BarPill {
id: pill
rightOpen: BarService.getPillDirection(root)
icon: getIcon()
compact: (Settings.data.bar.density === "compact")
autoHide: false // Important to be false so we can hover as long as we want
text: Math.floor(AudioService.inputVolume * 100)
suffix: "%"
forceOpen: displayMode === "alwaysShow"
forceClose: displayMode === "alwaysHide"
tooltipText: "Microphone: " + Math.round(AudioService.inputVolume * 100) + "%\nLeft click to toggle mute.\nRight click for settings.\nScroll to modify volume."
onWheel: function (delta) {
wheelAccumulator += delta
if (wheelAccumulator >= 120) {
wheelAccumulator = 0
AudioService.setInputVolume(AudioService.inputVolume + AudioService.stepVolume)
} else if (wheelAccumulator <= -120) {
wheelAccumulator = 0
AudioService.setInputVolume(AudioService.inputVolume - AudioService.stepVolume)
}
}
onClicked: {
AudioService.setInputMuted(!AudioService.inputMuted)
}
onRightClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel")
settingsPanel.requestedTab = SettingsPanel.Tab.Audio
settingsPanel.open()
}
onMiddleClicked: {
Quickshell.execDetached(["pwvucontrol"])
}
}
}

View File

@@ -4,6 +4,7 @@ import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Modules.SettingsPanel
import qs.Services
import qs.Widgets
@@ -11,22 +12,32 @@ NIconButton {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property real scaling: 1.0
sizeRatio: 0.8
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
compact: (Settings.data.bar.density === "compact")
baseSize: Style.capsuleHeight
colorBg: Settings.data.nightLight.forced ? Color.mPrimary : (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
colorFg: Settings.data.nightLight.forced ? Color.mOnPrimary : Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
icon: Settings.data.nightLight.enabled ? "bedtime" : "bedtime_off"
tooltipText: `Night light: ${Settings.data.nightLight.enabled ? "enabled" : "disabled"}\nLeft click to toggle.\nRight click to access settings.`
onClicked: Settings.data.nightLight.enabled = !Settings.data.nightLight.enabled
icon: Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? "nightlight-forced" : "nightlight-on") : "nightlight-off"
tooltipText: `Night light: ${Settings.data.nightLight.enabled ? (Settings.data.nightLight.forced ? "forced." : "enabled.") : "disabled."}\nLeft click to cycle (disabled normal forced).\nRight click to access settings.`
onClicked: {
if (!Settings.data.nightLight.enabled) {
Settings.data.nightLight.enabled = true
Settings.data.nightLight.forced = false
} else if (Settings.data.nightLight.enabled && !Settings.data.nightLight.forced) {
Settings.data.nightLight.forced = true
} else {
Settings.data.nightLight.enabled = false
Settings.data.nightLight.forced = false
}
}
onRightClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel")
settingsPanel.requestedTab = SettingsPanel.Tab.Display
settingsPanel.open(screen)
settingsPanel.open()
}
}

View File

@@ -11,14 +11,78 @@ NIconButton {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property real scaling: 1.0
sizeRatio: 0.8
icon: "notifications"
tooltipText: "Notification history"
colorBg: Color.mSurfaceVariant
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
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
}
baseSize: Style.capsuleHeight
compact: (Settings.data.bar.density === "compact")
icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell"
tooltipText: Settings.data.notifications.doNotDisturb ? "Notification history.\nRight-click to disable 'Do Not Disturb'." : "Notification history.\nRight-click to enable 'Do Not Disturb'."
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
colorFg: Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
onClicked: PanelService.getPanel("notificationHistoryPanel")?.toggle(screen, this)
onClicked: {
var panel = PanelService.getPanel("notificationHistoryPanel")
panel?.toggle(this)
Settings.data.notifications.lastSeenTs = Time.timestamp * 1000
}
onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
Loader {
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 2 * scaling
anchors.topMargin: 1 * scaling
z: 2
active: showUnreadBadge && (!hideWhenZero || computeUnreadCount() > 0)
sourceComponent: Rectangle {
id: badge
readonly property int count: computeUnreadCount()
height: 8 * scaling
width: height
radius: height / 2
color: Color.mError
border.color: Color.mSurface
border.width: 1
visible: count > 0 || !hideWhenZero
}
}
}

View File

@@ -10,51 +10,17 @@ NIconButton {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property var powerProfiles: PowerProfiles
readonly property bool hasPP: powerProfiles.hasPerformanceProfile
property real scaling: 1.0
sizeRatio: 0.8
visible: hasPP
baseSize: Style.capsuleHeight
visible: PowerProfileService.available
function profileIcon() {
if (!hasPP)
return "balance"
if (powerProfiles.profile === PowerProfile.Performance)
return "speed"
if (powerProfiles.profile === PowerProfile.Balanced)
return "balance"
if (powerProfiles.profile === PowerProfile.PowerSaver)
return "eco"
}
function profileName() {
if (!hasPP)
return "Unknown"
if (powerProfiles.profile === PowerProfile.Performance)
return "Performance"
if (powerProfiles.profile === PowerProfile.Balanced)
return "Balanced"
if (powerProfiles.profile === PowerProfile.PowerSaver)
return "Power Saver"
}
function changeProfile() {
if (!hasPP)
return
if (powerProfiles.profile === PowerProfile.Performance)
powerProfiles.profile = PowerProfile.PowerSaver
else if (powerProfiles.profile === PowerProfile.Balanced)
powerProfiles.profile = PowerProfile.Performance
else if (powerProfiles.profile === PowerProfile.PowerSaver)
powerProfiles.profile = PowerProfile.Balanced
}
icon: root.profileIcon()
tooltipText: root.profileName()
colorBg: Color.mSurfaceVariant
colorFg: Color.mOnSurface
icon: PowerProfileService.getIcon()
tooltipText: `Current power profile is "${PowerProfileService.getName()}".`
compact: (Settings.data.bar.density === "compact")
colorBg: (PowerProfileService.profile === PowerProfile.Balanced) ? (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent) : Color.mPrimary
colorFg: (PowerProfileService.profile === PowerProfile.Balanced) ? Color.mOnSurface : Color.mOnPrimary
colorBorder: Color.transparent
colorBorderHover: Color.transparent
onClicked: root.changeProfile()
onClicked: PowerProfileService.cycleProfile()
}

View File

@@ -0,0 +1,23 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
NIconButton {
id: root
property ShellScreen screen
property real scaling: 1.0
compact: (Settings.data.bar.density === "compact")
baseSize: Style.capsuleHeight
icon: "power"
tooltipText: "Power Settings"
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
colorFg: Color.mError
colorBorder: Color.transparent
colorBorderHover: Color.transparent
onClicked: PanelService.getPanel("powerPanel")?.toggle()
}

View File

@@ -8,14 +8,14 @@ NIconButton {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property real scaling: 1.0
visible: ScreenRecorderService.isRecording
icon: "videocam"
tooltipText: "Screen recording is active\nClick to stop recording"
sizeRatio: 0.8
icon: "camera-video"
tooltipText: "Screen recording is active.\nClick to stop recording."
compact: (Settings.data.bar.density === "compact")
baseSize: Style.capsuleHeight
colorBg: Color.mPrimary
colorFg: Color.mOnPrimary
anchors.verticalCenter: parent.verticalCenter
onClicked: ScreenRecorderService.toggleRecording()
}

View File

@@ -1,4 +1,7 @@
import QtQuick
import Quickshell
import Quickshell.Widgets
import QtQuick.Effects
import qs.Commons
import qs.Widgets
import qs.Services
@@ -7,17 +10,47 @@ NIconButton {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property real scaling: 1.0
icon: "widgets"
tooltipText: "Open side panel"
sizeRatio: 0.8
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
colorBg: Color.mSurfaceVariant
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
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 ? "" : "noctalia"
tooltipText: "Open side panel."
baseSize: Style.capsuleHeight
compact: (Settings.data.bar.density === "compact")
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
colorFg: Color.mOnSurface
colorBgHover: useDistroLogo ? Color.mSurfaceVariant : Color.mTertiary
colorBorder: Color.transparent
colorBorderHover: Color.transparent
colorBorderHover: useDistroLogo ? Color.mTertiary : Color.transparent
onClicked: PanelService.getPanel("sidePanel")?.toggle(this)
onRightClicked: PanelService.getPanel("settingsPanel")?.toggle()
anchors.verticalCenter: parent.verticalCenter
onClicked: PanelService.getPanel("sidePanel")?.toggle(screen)
IconImage {
id: logo
anchors.centerIn: parent
width: root.width * 0.8
height: width
source: useDistroLogo ? DistroLogoService.osLogo : ""
visible: useDistroLogo && source !== ""
smooth: true
asynchronous: true
}
}

View File

@@ -0,0 +1,40 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
Item {
id: root
// Widget properties passed from Bar.qml
property var screen
property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
// Use settings or defaults from BarWidgetRegistry
readonly property int spacerWidth: widgetSettings.width !== undefined ? widgetSettings.width : widgetMetadata.width
// Set the width based on user settings
implicitWidth: spacerWidth * scaling
implicitHeight: Style.barHeight * scaling
width: implicitWidth
height: implicitHeight
}

View File

@@ -1,145 +1,305 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Commons
import qs.Services
import qs.Widgets
Row {
Rectangle {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property real scaling: 1.0
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
Rectangle {
// Let the Rectangle size itself based on its content (the Row)
width: row.width + Style.marginM * scaling * 2
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
height: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
readonly property string barPosition: Settings.data.bar.position
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
readonly property bool compact: (Settings.data.bar.density === "compact")
anchors.verticalCenter: parent.verticalCenter
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
readonly property bool showDiskUsage: (widgetSettings.showDiskUsage !== undefined) ? widgetSettings.showDiskUsage : widgetMetadata.showDiskUsage
readonly property real textSize: {
var base = isVertical ? width * 0.82 : height
return Math.max(1, compact ? base * 0.43 : base * 0.33)
}
readonly property real iconSize: textSize * 1.25
anchors.centerIn: parent
implicitWidth: isVertical ? Math.round(Style.capsuleHeight * scaling) : Math.round(mainGrid.implicitWidth + Style.marginM * 2 * scaling)
implicitHeight: isVertical ? Math.round(mainGrid.implicitHeight + Style.marginM * 2 * scaling) : Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
GridLayout {
id: mainGrid
anchors.centerIn: parent
// Dynamic layout based on bar orientation
flow: isVertical ? GridLayout.TopToBottom : GridLayout.LeftToRight
rows: isVertical ? -1 : 1
columns: isVertical ? 1 : -1
rowSpacing: isVertical ? (Style.marginS * scaling) : (Style.marginXS * scaling)
columnSpacing: isVertical ? (Style.marginXS * scaling) : (Style.marginXS * scaling)
// CPU Usage Component
Item {
id: mainContainer
anchors.fill: parent
anchors.leftMargin: Style.marginS * scaling
anchors.rightMargin: Style.marginS * scaling
Layout.preferredWidth: cpuUsageContent.implicitWidth
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
visible: showCpuUsage
Row {
id: row
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * scaling
Row {
id: cpuUsageLayout
spacing: Style.marginXS * scaling
GridLayout {
id: cpuUsageContent
anchors.centerIn: parent
flow: isVertical ? GridLayout.TopToBottom : GridLayout.LeftToRight
rows: isVertical ? 2 : 1
columns: isVertical ? 1 : 2
rowSpacing: Style.marginXXS * scaling
columnSpacing: Style.marginXXS * scaling
NIcon {
id: cpuUsageIcon
text: "speed"
anchors.verticalCenter: parent.verticalCenter
}
NText {
id: cpuUsageText
text: `${SystemStatService.cpuUsage}%`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
NText {
text: isVertical ? `${Math.round(SystemStatService.cpuUsage)}%` : `${SystemStatService.cpuUsage}%`
font.family: Settings.data.ui.fontFixed
font.pointSize: textSize
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
Layout.row: isVertical ? 0 : 0
Layout.column: isVertical ? 0 : 1
}
// CPU Temperature Component
Row {
id: cpuTempLayout
// spacing is thin here to compensate for the vertical thermometer icon
spacing: Style.marginXXS * scaling
NIcon {
icon: "cpu-usage"
font.pointSize: iconSize
Layout.alignment: Qt.AlignCenter
Layout.row: isVertical ? 1 : 0
Layout.column: 0
}
}
}
NIcon {
text: "thermometer"
anchors.verticalCenter: parent.verticalCenter
}
// CPU Temperature Component
Item {
Layout.preferredWidth: cpuTempContent.implicitWidth
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
visible: showCpuTemp
NText {
text: `${SystemStatService.cpuTemp}°C`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
GridLayout {
id: cpuTempContent
anchors.centerIn: parent
flow: isVertical ? GridLayout.TopToBottom : GridLayout.LeftToRight
rows: isVertical ? 2 : 1
columns: isVertical ? 1 : 2
rowSpacing: Style.marginXXS * scaling
columnSpacing: Style.marginXXS * scaling
NText {
text: isVertical ? `${SystemStatService.cpuTemp}°` : `${SystemStatService.cpuTemp}°C`
font.family: Settings.data.ui.fontFixed
font.pointSize: textSize
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
Layout.row: isVertical ? 0 : 0
Layout.column: isVertical ? 0 : 1
}
// Memory Usage Component
Row {
id: memoryUsageLayout
spacing: Style.marginXS * scaling
NIcon {
icon: "cpu-temperature"
font.pointSize: iconSize
Layout.alignment: Qt.AlignCenter
Layout.row: isVertical ? 1 : 0
Layout.column: 0
}
}
}
NIcon {
text: "memory"
anchors.verticalCenter: parent.verticalCenter
}
// Memory Usage Component
Item {
Layout.preferredWidth: memoryContent.implicitWidth
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
visible: showMemoryUsage
NText {
text: `${SystemStatService.memoryUsageGb}G`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
GridLayout {
id: memoryContent
anchors.centerIn: parent
flow: isVertical ? GridLayout.TopToBottom : GridLayout.LeftToRight
rows: isVertical ? 2 : 1
columns: isVertical ? 1 : 2
rowSpacing: Style.marginXXS * scaling
columnSpacing: Style.marginXXS * scaling
NText {
text: {
if (showMemoryAsPercent) {
return `${SystemStatService.memPercent}%`
} else {
return isVertical ? `${Math.round(SystemStatService.memGb)}G` : `${SystemStatService.memGb}G`
}
}
font.family: Settings.data.ui.fontFixed
font.pointSize: textSize
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
Layout.row: isVertical ? 0 : 0
Layout.column: isVertical ? 0 : 1
}
// Network Download Speed Component
Row {
id: networkDownloadLayout
spacing: Style.marginXS * scaling
visible: Settings.data.bar.showNetworkStats
NIcon {
icon: "memory"
font.pointSize: iconSize
Layout.alignment: Qt.AlignCenter
Layout.row: isVertical ? 1 : 0
Layout.column: 0
}
}
}
NIcon {
text: "download"
anchors.verticalCenter: parent.verticalCenter
}
// Network Download Speed Component
Item {
Layout.preferredWidth: downloadContent.implicitWidth
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
visible: showNetworkStats
NText {
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
GridLayout {
id: downloadContent
anchors.centerIn: parent
flow: isVertical ? GridLayout.TopToBottom : GridLayout.LeftToRight
rows: isVertical ? 2 : 1
columns: isVertical ? 1 : 2
rowSpacing: Style.marginXXS * scaling
columnSpacing: isVertical ? (Style.marginXXS * scaling) : (Style.marginXS * scaling)
NText {
text: isVertical ? SystemStatService.formatCompactSpeed(SystemStatService.rxSpeed) : SystemStatService.formatSpeed(SystemStatService.rxSpeed)
font.family: Settings.data.ui.fontFixed
font.pointSize: textSize
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
Layout.row: isVertical ? 0 : 0
Layout.column: isVertical ? 0 : 1
}
// Network Upload Speed Component
Row {
id: networkUploadLayout
spacing: Style.marginXS * scaling
visible: Settings.data.bar.showNetworkStats
NIcon {
icon: "download-speed"
font.pointSize: iconSize
Layout.alignment: Qt.AlignCenter
Layout.row: isVertical ? 1 : 0
Layout.column: 0
}
}
}
NIcon {
text: "upload"
anchors.verticalCenter: parent.verticalCenter
}
// Network Upload Speed Component
Item {
Layout.preferredWidth: uploadContent.implicitWidth
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
visible: showNetworkStats
NText {
text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeS * scaling
font.weight: Style.fontWeightMedium
anchors.verticalCenter: parent.verticalCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
}
GridLayout {
id: uploadContent
anchors.centerIn: parent
flow: isVertical ? GridLayout.TopToBottom : GridLayout.LeftToRight
rows: isVertical ? 2 : 1
columns: isVertical ? 1 : 2
rowSpacing: Style.marginXXS * scaling
columnSpacing: isVertical ? (Style.marginXXS * scaling) : (Style.marginXS * scaling)
NText {
text: isVertical ? SystemStatService.formatCompactSpeed(SystemStatService.txSpeed) : SystemStatService.formatSpeed(SystemStatService.txSpeed)
font.family: Settings.data.ui.fontFixed
font.pointSize: textSize
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
Layout.row: isVertical ? 0 : 0
Layout.column: isVertical ? 0 : 1
}
NIcon {
icon: "upload-speed"
font.pointSize: iconSize
Layout.alignment: Qt.AlignCenter
Layout.row: isVertical ? 1 : 0
Layout.column: 0
}
}
}
// Disk Usage Component (primary drive)
Item {
Layout.preferredWidth: diskContent.implicitWidth
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.alignment: isVertical ? Qt.AlignHCenter : Qt.AlignVCenter
visible: showDiskUsage
GridLayout {
id: diskContent
anchors.centerIn: parent
flow: isVertical ? GridLayout.TopToBottom : GridLayout.LeftToRight
rows: isVertical ? 2 : 1
columns: isVertical ? 1 : 2
rowSpacing: Style.marginXXS * scaling
columnSpacing: isVertical ? (Style.marginXXS * scaling) : (Style.marginXS * scaling)
NText {
text: `${SystemStatService.diskPercent}%`
font.family: Settings.data.ui.fontFixed
font.pointSize: textSize
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
color: Color.mPrimary
Layout.row: isVertical ? 0 : 0
Layout.column: isVertical ? 0 : 1
}
NIcon {
icon: "storage"
font.pointSize: iconSize
Layout.alignment: Qt.AlignCenter
Layout.row: isVertical ? 1 : 0
Layout.column: 0
}
}
}

View File

@@ -1,7 +1,6 @@
pragma ComponentBehavior
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
@@ -12,21 +11,34 @@ import qs.Widgets
Rectangle {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property real scaling: 1.0
readonly property real itemSize: Style.baseWidgetSize * 0.8 * scaling
readonly property bool isVerticalBar: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
readonly property bool compact: (Settings.data.bar.density === "compact")
readonly property real itemSize: compact ? Style.capsuleHeight * 0.9 * scaling : Style.capsuleHeight * 0.8 * scaling
// Always visible when there are toplevels
implicitWidth: taskbarRow.width + Style.marginM * scaling * 2
implicitHeight: Math.round(Style.capsuleHeight * scaling)
implicitWidth: isVerticalBar ? Math.round(Style.capsuleHeight * scaling) : taskbarLayout.implicitWidth + Style.marginM * scaling * 2
implicitHeight: isVerticalBar ? taskbarLayout.implicitHeight + Style.marginM * scaling * 2 : Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
Row {
id: taskbarRow
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
spacing: Style.marginXXS * root.scaling
GridLayout {
id: taskbarLayout
anchors.fill: parent
anchors {
leftMargin: isVerticalBar ? undefined : Style.marginM * scaling
rightMargin: isVerticalBar ? undefined : Style.marginM * scaling
topMargin: compact ? 0 : isVerticalBar ? Style.marginM * scaling : undefined
bottomMargin: compact ? 0 : isVerticalBar ? Style.marginM * scaling : undefined
}
// Configure GridLayout to behave like RowLayout or ColumnLayout
rows: isVerticalBar ? -1 : 1 // -1 means unlimited
columns: isVerticalBar ? 1 : -1 // -1 means unlimited
rowSpacing: isVerticalBar ? Style.marginXXS * root.scaling : 0
columnSpacing: isVerticalBar ? 0 : Style.marginXXS * root.scaling
Repeater {
model: ToplevelManager && ToplevelManager.toplevels ? ToplevelManager.toplevels : []
@@ -35,14 +47,16 @@ Rectangle {
required property Toplevel modelData
property Toplevel toplevel: modelData
property bool isActive: ToplevelManager.activeToplevel === modelData
width: root.itemSize
height: root.itemSize
Layout.preferredWidth: root.itemSize
Layout.preferredHeight: root.itemSize
Layout.alignment: Qt.AlignCenter
Rectangle {
id: iconBackground
anchors.centerIn: parent
width: root.itemSize * 0.75
height: root.itemSize * 0.75
width: parent.width
height: parent.height
color: taskbarItem.isActive ? Color.mPrimary : root.color
border.width: 0
radius: Math.round(Style.radiusXS * root.scaling)
@@ -52,10 +66,11 @@ Rectangle {
IconImage {
id: appIcon
anchors.centerIn: parent
width: Style.marginL * root.scaling
height: Style.marginL * root.scaling
source: Icons.iconForAppId(taskbarItem.modelData.appId)
width: parent.width
height: parent.height
source: AppIcons.iconForAppId(taskbarItem.modelData.appId)
smooth: true
asynchronous: true
}
}
@@ -89,7 +104,7 @@ Rectangle {
NTooltip {
id: taskbarTooltip
text: taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown App"
text: taskbarItem.modelData.title || taskbarItem.modelData.appId || "Unknown App."
target: taskbarItem
positionAbove: Settings.data.bar.position === "bottom"
}

View File

@@ -14,27 +14,38 @@ Rectangle {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
readonly property real itemSize: 24 * scaling
property real scaling: 1.0
readonly property string barPosition: Settings.data.bar.position
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
readonly property bool compact: (Settings.data.bar.density === "compact")
readonly property real itemSize: isVertical ? width * 0.75 : height * 0.85
function onLoaded() {
// When the widget is fully initialized with its props set the screen for the trayMenu
if (trayMenu.item) {
trayMenu.item.screen = screen
}
}
visible: SystemTray.items.values.length > 0
implicitWidth: tray.width + Style.marginM * scaling * 2
implicitHeight: Math.round(Style.capsuleHeight * scaling)
implicitWidth: isVertical ? Math.round(Style.capsuleHeight * scaling) : (trayFlow.implicitWidth + Style.marginS * scaling * 2)
implicitHeight: isVertical ? (trayFlow.implicitHeight + Style.marginS * scaling * 2) : Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
Layout.alignment: Qt.AlignVCenter
Row {
id: tray
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
Flow {
id: trayFlow
anchors.centerIn: parent
spacing: Style.marginS * scaling
flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight
Repeater {
id: repeater
model: SystemTray.items
delegate: Item {
width: itemSize
height: itemSize
@@ -102,9 +113,21 @@ Rectangle {
if (modelData.hasMenu && modelData.menu && trayMenu.item) {
trayPanel.open()
// Anchor the menu to the tray icon item (parent) and position it below the icon
const menuX = (width / 2) - (trayMenu.item.width / 2)
const menuY = Math.round(Style.barHeight * scaling)
// Position menu based on bar position
let menuX, menuY
if (barPosition === "left") {
// For left bar: position menu to the right of the bar
menuX = width + Style.marginM * scaling
menuY = 0
} else if (barPosition === "right") {
// For right bar: position menu to the left of the bar
menuX = -trayMenu.item.width - Style.marginM * scaling
menuY = 0
} else {
// For horizontal bars: center horizontally and position below
menuX = (width / 2) - (trayMenu.item.width / 2)
menuY = Math.round(Style.barHeight * scaling)
}
trayMenu.item.menu = modelData.menu
trayMenu.item.showAt(parent, menuX, menuY)
} else {
@@ -138,13 +161,14 @@ Rectangle {
function open() {
visible = true
PanelService.willOpenPanel(trayPanel)
}
function close() {
visible = false
trayMenu.item.hideMenu()
if (trayMenu.item) {
trayMenu.item.hideMenu()
}
}
// Clicking outside of the rectangle to close
@@ -156,12 +180,6 @@ Rectangle {
Loader {
id: trayMenu
source: "../Extras/TrayMenu.qml"
onLoaded: {
if (item) {
item.screen = screen
}
}
}
}
}

View File

@@ -6,16 +6,34 @@ import qs.Commons
import qs.Modules.SettingsPanel
import qs.Services
import qs.Widgets
import qs.Modules.Bar.Extras
Item {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property string barSection: ""
property int sectionWidgetIndex: 0
property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property bool isBarVertical: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
readonly property string displayMode: (widgetSettings.displayMode !== undefined) ? widgetSettings.displayMode : widgetMetadata.displayMode
// Used to avoid opening the pill on Quickshell startup
property bool firstVolumeReceived: false
property int wheelAccumulator: 0
@@ -25,9 +43,9 @@ Item {
function getIcon() {
if (AudioService.muted) {
return "volume_off"
return "volume-mute"
}
return AudioService.volume <= Number.EPSILON ? "volume_off" : (AudioService.volume < 0.33 ? "volume_down" : "volume_up")
return (AudioService.volume <= Number.EPSILON) ? "volume-zero" : (AudioService.volume <= 0.5) ? "volume-low" : "volume-high"
}
// Connection used to open the pill when volume changes
@@ -54,17 +72,18 @@ Item {
}
}
NPill {
BarPill {
id: pill
rightOpen: BarWidgetRegistry.getNPillDirection(root)
compact: (Settings.data.bar.density === "compact")
rightOpen: BarService.getPillDirection(root)
icon: getIcon()
iconCircleColor: Color.mPrimary
collapsedIconColor: Color.mOnSurface
autoHide: false // Important to be false so we can hover as long as we want
text: Math.floor(AudioService.volume * 100) + "%"
tooltipText: "Volume: " + Math.round(
AudioService.volume * 100) + "%\nLeft click for advanced settings.\nScroll up/down to change volume."
text: Math.floor(AudioService.volume * 100)
suffix: "%"
forceOpen: displayMode === "alwaysShow"
forceClose: displayMode === "alwaysHide"
tooltipText: "Volume: " + Math.round(AudioService.volume * 100) + "%\nLeft click to toggle mute.\nRight click for settings.\nScroll to modify volume."
onWheel: function (delta) {
wheelAccumulator += delta
@@ -77,18 +96,15 @@ Item {
}
}
onClicked: {
var settingsPanel = PanelService.getPanel("settingsPanel")
settingsPanel.requestedTab = SettingsPanel.Tab.AudioService
settingsPanel.open(screen)
AudioService.setOutputMuted(!AudioService.muted)
}
onRightClicked: {
pwvucontrolProcess.running = true
var settingsPanel = PanelService.getPanel("settingsPanel")
settingsPanel.requestedTab = SettingsPanel.Tab.Audio
settingsPanel.open()
}
onMiddleClicked: {
Quickshell.execDetached(["pwvucontrol"])
}
}
Process {
id: pwvucontrolProcess
command: ["pwvucontrol"]
running: false
}
}

View File

@@ -11,29 +11,19 @@ NIconButton {
id: root
property ShellScreen screen
property real scaling: ScalingService.scale(screen)
property real scaling: 1.0
visible: Settings.data.network.wifiEnabled
sizeRatio: 0.8
Component.onCompleted: {
Logger.log("WiFi", "Widget component completed")
Logger.log("WiFi", "NetworkService available:", !!NetworkService)
if (NetworkService) {
Logger.log("WiFi", "NetworkService.networks available:", !!NetworkService.networks)
}
}
colorBg: Color.mSurfaceVariant
compact: (Settings.data.bar.density === "compact")
baseSize: Style.capsuleHeight
colorBg: (Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent)
colorFg: Color.mOnSurface
colorBorder: Color.transparent
colorBorderHover: Color.transparent
icon: {
try {
if (NetworkService.ethernet) {
return "lan"
if (NetworkService.ethernetConnected) {
return "ethernet"
}
let connected = false
let signalStrength = 0
@@ -44,12 +34,13 @@ NIconButton {
break
}
}
return connected ? NetworkService.signalIcon(signalStrength) : "wifi_find"
return connected ? NetworkService.signalIcon(signalStrength) : "wifi-off"
} catch (error) {
Logger.error("WiFi", "Error getting icon:", error)
Logger.error("Wi-Fi", "Error getting icon:", error)
return "signal_wifi_bad"
}
}
tooltipText: "Network / Wi-Fi"
onClicked: PanelService.getPanel("wifiPanel")?.toggle(screen, this)
tooltipText: "Manage Wi-Fi."
onClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
onRightClicked: PanelService.getPanel("wifiPanel")?.toggle(this)
}

View File

@@ -11,8 +11,39 @@ import qs.Services
Item {
id: root
property ShellScreen screen: null
property real scaling: ScalingService.scale(screen)
property ShellScreen screen
property real scaling: 1.0
// Widget properties passed from Bar.qml for per-instance settings
property string widgetId: ""
property string section: ""
property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: {
if (section && sectionWidgetIndex >= 0) {
var widgets = Settings.data.bar.widgets[section]
if (widgets && sectionWidgetIndex < widgets.length) {
return widgets[sectionWidgetIndex]
}
}
return {}
}
readonly property string barPosition: Settings.data.bar.position
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
readonly property bool compact: (Settings.data.bar.density === "compact")
readonly property real baseDimensionRatio: {
const b = compact ? 0.85 : 0.65
if (widgetSettings.labelMode === "none") {
return b * 0.75
}
return b
}
readonly property string labelMode: (widgetSettings.labelMode !== undefined) ? widgetSettings.labelMode : widgetMetadata.labelMode
readonly property bool hideUnoccupied: (widgetSettings.hideUnoccupied !== undefined) ? widgetSettings.hideUnoccupied : widgetMetadata.hideUnoccupied
property bool isDestroying: false
property bool hovered: false
@@ -22,30 +53,50 @@ Item {
property bool effectsActive: false
property color effectColor: Color.mPrimary
property int horizontalPadding: Math.round(16 * scaling)
property int spacingBetweenPills: Math.round(8 * scaling)
property int horizontalPadding: Math.round(Style.marginS * scaling)
property int spacingBetweenPills: Math.round(Style.marginXS * scaling)
signal workspaceChanged(int workspaceId, color accentColor)
implicitHeight: Math.round(Style.barHeight * scaling)
implicitWidth: {
implicitWidth: isVertical ? Math.round(Style.barHeight * scaling) : computeWidth()
implicitHeight: isVertical ? computeHeight() : Math.round(Style.barHeight * scaling)
function getWorkspaceWidth(ws) {
const d = Style.capsuleHeight * root.baseDimensionRatio
if (ws.isFocused)
return d * 2.5
else
return d
}
function getWorkspaceHeight(ws) {
const d = Style.capsuleHeight * root.baseDimensionRatio
if (ws.isFocused)
return d * 3
else
return d
}
function computeWidth() {
let total = 0
for (var i = 0; i < localWorkspaces.count; i++) {
const ws = localWorkspaces.get(i)
total += calculatedWsWidth(ws)
total += getWorkspaceWidth(ws)
}
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills
total += horizontalPadding * 2
return total
return Math.round(total)
}
function calculatedWsWidth(ws) {
if (ws.isFocused)
return Math.round(44 * scaling)
else if (ws.isActive)
return Math.round(28 * scaling)
else
return Math.round(20 * scaling)
function computeHeight() {
let total = 0
for (var i = 0; i < localWorkspaces.count; i++) {
const ws = localWorkspaces.get(i)
total += getWorkspaceHeight(ws)
}
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills
total += horizontalPadding * 2
return Math.round(total)
}
Component.onCompleted: {
@@ -57,6 +108,7 @@ Item {
}
onScreenChanged: refreshWorkspaces()
onHideUnoccupiedChanged: refreshWorkspaces()
Connections {
target: WorkspaceService
@@ -71,11 +123,15 @@ Item {
for (var i = 0; i < WorkspaceService.workspaces.count; i++) {
const ws = WorkspaceService.workspaces.get(i)
if (ws.output.toLowerCase() === screen.name.toLowerCase()) {
if (hideUnoccupied && !ws.isOccupied && !ws.isFocused) {
continue
}
localWorkspaces.append(ws)
}
}
}
workspaceRepeater.model = localWorkspaces
workspaceRepeaterHorizontal.model = localWorkspaces
workspaceRepeaterVertical.model = localWorkspaces
updateWorkspaceFocus()
}
@@ -124,57 +180,49 @@ Item {
Rectangle {
id: workspaceBackground
width: parent.width - Style.marginS * scaling * 2
height: Math.round(Style.capsuleHeight * scaling)
width: isVertical ? Math.round(Style.capsuleHeight * scaling) : parent.width
height: isVertical ? parent.height : Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant
color: Settings.data.bar.showCapsule ? Color.mSurfaceVariant : Color.transparent
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
layer.enabled: true
layer.effect: MultiEffect {
shadowColor: Color.mShadow
shadowVerticalOffset: 0
shadowHorizontalOffset: 0
shadowOpacity: 0.10
}
}
// Horizontal layout for top/bottom bars
Row {
id: pillRow
spacing: spacingBetweenPills
anchors.verticalCenter: workspaceBackground.verticalCenter
width: root.width - horizontalPadding * 2
x: horizontalPadding
visible: !isVertical
Repeater {
id: workspaceRepeater
id: workspaceRepeaterHorizontal
model: localWorkspaces
Item {
id: workspacePillContainer
height: (Settings.data.bar.showWorkspaceLabel !== "none") ? Math.round(18 * scaling) : Math.round(14 * scaling)
width: root.calculatedWsWidth(model)
width: root.getWorkspaceWidth(model)
height: Style.capsuleHeight * root.baseDimensionRatio
Rectangle {
id: pill
anchors.fill: parent
Loader {
active: (Settings.data.bar.showWorkspaceLabel !== "none")
active: (labelMode !== "none")
sourceComponent: Component {
Text {
// Center horizontally
x: (pill.width - width) / 2
// Center vertically accounting for font metrics
y: (pill.height - height) / 2 + (height - contentHeight) / 2
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)
} else {
return model.idx.toString()
}
}
font.pointSize: model.isFocused ? Style.fontSizeXS * scaling : Style.fontSizeXXS * scaling
font.pointSize: model.isFocused ? workspacePillContainer.height * 0.45 : workspacePillContainer.height * 0.42
font.capitalization: Font.AllUppercase
font.family: Settings.data.ui.fontFixed
font.weight: Style.fontWeightBold
@@ -186,8 +234,6 @@ Item {
return Color.mOnError
if (model.isActive || model.isOccupied)
return Color.mOnSecondary
if (model.isUrgent)
return Color.mOnError
return Color.mOnSurface
}
@@ -203,8 +249,6 @@ Item {
return Color.mError
if (model.isActive || model.isOccupied)
return Color.mSecondary
if (model.isUrgent)
return Color.mError
return Color.mOutline
}
@@ -288,4 +332,148 @@ Item {
}
}
}
// Vertical layout for left/right bars
Column {
id: pillColumn
spacing: spacingBetweenPills
anchors.horizontalCenter: workspaceBackground.horizontalCenter
y: horizontalPadding
visible: isVertical
Repeater {
id: workspaceRepeaterVertical
model: localWorkspaces
Item {
id: workspacePillContainerVertical
width: Style.capsuleHeight * root.baseDimensionRatio
height: root.getWorkspaceHeight(model)
Rectangle {
id: pillVertical
anchors.fill: parent
Loader {
active: (labelMode !== "none")
sourceComponent: Component {
Text {
x: (pillVertical.width - width) / 2
y: (pillVertical.height - height) / 2 + (height - contentHeight) / 2
text: {
if (labelMode === "name" && model.name && model.name.length > 0) {
return model.name.substring(0, 2)
} else {
return model.idx.toString()
}
}
font.pointSize: model.isFocused ? workspacePillContainerVertical.width * 0.45 : workspacePillContainerVertical.width * 0.42
font.capitalization: Font.AllUppercase
font.family: Settings.data.ui.fontFixed
font.weight: Style.fontWeightBold
wrapMode: Text.Wrap
color: {
if (model.isFocused)
return Color.mOnPrimary
if (model.isUrgent)
return Color.mOnError
if (model.isActive || model.isOccupied)
return Color.mOnSecondary
return Color.mOnSurface
}
}
}
}
radius: width * 0.5
color: {
if (model.isFocused)
return Color.mPrimary
if (model.isUrgent)
return Color.mError
if (model.isActive || model.isOccupied)
return Color.mSecondary
return Color.mOutline
}
scale: model.isFocused ? 1.0 : 0.9
z: 0
MouseArea {
id: pillMouseAreaVertical
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
WorkspaceService.switchToWorkspace(model.idx)
}
hoverEnabled: true
}
// Material 3-inspired smooth animation for width, height, scale, color, opacity, and radius
Behavior on width {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
Behavior on height {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
Behavior on scale {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutCubic
}
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.InOutCubic
}
}
Behavior on radius {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
}
Behavior on width {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
Behavior on height {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
// Burst effect overlay for focused pill (smaller outline)
Rectangle {
id: pillBurstVertical
anchors.centerIn: workspacePillContainerVertical
width: workspacePillContainerVertical.width + 18 * root.masterProgress * scale
height: workspacePillContainerVertical.height + 18 * root.masterProgress * scale
radius: width / 2
color: Color.transparent
border.color: root.effectColor
border.width: Math.max(1, Math.round((2 + 6 * (1.0 - root.masterProgress)) * scaling))
opacity: root.effectsActive && model.isFocused ? (1.0 - root.masterProgress) * 0.7 : 0
visible: root.effectsActive && model.isFocused
z: 1
}
}
}
}
}

View File

@@ -36,65 +36,39 @@ ColumnLayout {
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
Rectangle {
id: bluetoothDeviceRectangle
property bool canConnect: BluetoothService.canConnect(modelData)
property bool canDisconnect: BluetoothService.canDisconnect(modelData)
property bool isBusy: BluetoothService.isDeviceBusy(modelData)
id: device
Layout.fillWidth: true
Layout.preferredHeight: 64 * scaling + (10 * scaling * modelData.batteryAvailable)
radius: Style.radiusM * scaling
color: {
if (availableDeviceArea.containsMouse) {
if (canDisconnect && !isBusy)
return Color.mError
if (!isBusy)
return Color.mTertiary
return Color.mPrimary
}
readonly property bool canConnect: BluetoothService.canConnect(modelData)
readonly property bool canDisconnect: BluetoothService.canDisconnect(modelData)
readonly property bool isBusy: BluetoothService.isDeviceBusy(modelData)
function getContentColor(defaultColor = Color.mOnSurface) {
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mPrimary
if (modelData.blocked)
return Color.mError
return Color.mSurfaceVariant
return defaultColor
}
border.color: Color.mOutline
Layout.fillWidth: true
Layout.preferredHeight: deviceLayout.implicitHeight + (Style.marginM * scaling * 2)
radius: Style.radiusM * scaling
color: Color.mSurface
border.width: Math.max(1, Style.borderS * scaling)
NTooltip {
id: tooltip
target: bluetoothDeviceRectangle
positionAbove: Settings.data.bar.position === "bottom"
text: root.tooltipText
}
border.color: getContentColor(Color.mOutline)
RowLayout {
id: deviceLayout
anchors.fill: parent
anchors.margins: Style.marginM * scaling
spacing: Style.marginS * scaling
spacing: Style.marginM * scaling
Layout.alignment: Qt.AlignVCenter
// One device BT icon
NIcon {
text: BluetoothService.getDeviceIcon(modelData)
icon: BluetoothService.getDeviceIcon(modelData)
font.pointSize: Style.fontSizeXXL * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
color: getContentColor(Color.mOnSurface)
Layout.alignment: Qt.AlignVCenter
}
@@ -106,25 +80,23 @@ ColumnLayout {
NText {
text: modelData.name || modelData.deviceName
font.pointSize: Style.fontSizeM * scaling
elide: Text.ElideRight
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
font.weight: Style.fontWeightMedium
elide: Text.ElideRight
color: getContentColor(Color.mOnSurface)
Layout.fillWidth: true
}
// Status
NText {
text: BluetoothService.getStatusString(modelData)
visible: text !== ""
font.pointSize: Style.fontSizeXS * scaling
color: getContentColor(Color.mOnSurfaceVariant)
}
// Signal Strength
RowLayout {
visible: modelData.signalStrength !== undefined
Layout.fillWidth: true
spacing: Style.marginXS * scaling
@@ -132,76 +104,30 @@ ColumnLayout {
NText {
text: BluetoothService.getSignalStrength(modelData)
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurfaceVariant
}
color: getContentColor(Color.mOnSurfaceVariant)
}
NIcon {
visible: modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
text: BluetoothService.getSignalIcon(modelData)
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && !modelData.pairing
&& !modelData.blocked
color: getContentColor(Color.mOnSurface)
}
NText {
text: (modelData.signalStrength !== undefined
&& modelData.signalStrength > 0) ? modelData.signalStrength + "%" : ""
visible: modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
text: (modelData.signalStrength !== undefined && modelData.signalStrength > 0) ? modelData.signalStrength + "%" : ""
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurface
}
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && !modelData.pairing
&& !modelData.blocked
color: getContentColor(Color.mOnSurface)
}
}
// Battery
NText {
visible: modelData.batteryAvailable
text: BluetoothService.getBattery(modelData)
font.pointSize: Style.fontSizeXS * scaling
color: {
if (availableDeviceArea.containsMouse)
return Color.mOnTertiary
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mOnPrimary
if (modelData.blocked)
return Color.mOnError
return Color.mOnSurfaceVariant
}
color: getContentColor(Color.mOnSurfaceVariant)
}
}
@@ -211,96 +137,85 @@ ColumnLayout {
}
// Call to action
Rectangle {
Layout.preferredWidth: 80 * scaling
Layout.preferredHeight: 28 * scaling
radius: Style.radiusM * scaling
NButton {
id: button
visible: (modelData.state !== BluetoothDeviceState.Connecting)
color: Color.transparent
border.color: {
if (availableDeviceArea.containsMouse) {
return Color.mOnTertiary
}
if (bluetoothDeviceRectangle.canDisconnect && !isBusy) {
enabled: (canConnect || canDisconnect) && !isBusy
outlined: !button.hovered
fontSize: Style.fontSizeXS * scaling
fontWeight: Style.fontWeightMedium
backgroundColor: {
if (device.canDisconnect && !isBusy) {
return Color.mError
}
return Color.mPrimary
}
border.width: Math.max(1, Style.borderS * scaling)
opacity: canConnect || isBusy || canDisconnect ? 1 : 0.5
NText {
anchors.centerIn: parent
text: {
if (modelData.pairing) {
return "Pairing..."
}
if (modelData.blocked) {
return "Blocked"
}
if (modelData.connected) {
return "Disconnect"
}
return "Connect"
tooltipText: root.tooltipText
text: {
if (modelData.pairing) {
return "Pairing..."
}
font.pointSize: Style.fontSizeXS * scaling
font.weight: Style.fontWeightMedium
color: {
if (availableDeviceArea.containsMouse) {
return Color.mOnTertiary
}
if (bluetoothDeviceRectangle.canDisconnect && !isBusy) {
return Color.mError
} else {
return Color.mPrimary
}
if (modelData.blocked) {
return "Blocked"
}
if (modelData.connected) {
return "Disconnect"
}
return "Connect"
}
}
}
MouseArea {
id: availableDeviceArea
acceptedButtons: Qt.LeftButton | Qt.RightButton
anchors.fill: parent
hoverEnabled: true
cursorShape: (canConnect || canDisconnect)
&& !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
onEntered: {
if (root.tooltipText && !isBusy) {
tooltip.show()
}
}
onExited: {
if (root.tooltipText && !isBusy) {
tooltip.hide()
}
}
onClicked: function (mouse) {
if (!modelData || modelData.pairing) {
return
}
if (root.tooltipText && !isBusy) {
tooltip.hide()
}
if (mouse.button === Qt.LeftButton) {
icon: (isBusy ? "busy" : null)
onClicked: {
if (modelData.connected) {
BluetoothService.disconnectDevice(modelData)
} else {
BluetoothService.connectDeviceWithTrust(modelData)
}
} else if (mouse.button === Qt.RightButton) {
}
onRightClicked: {
BluetoothService.forgetDevice(modelData)
}
}
}
// MouseArea {
// id: availableDeviceArea
// acceptedButtons: Qt.LeftButton | Qt.RightButton
// anchors.fill: parent
// hoverEnabled: true
// cursorShape: (canConnect || canDisconnect)
// && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
// onEntered: {
// if (root.tooltipText && !isBusy) {
// tooltip.show()
// }
// }
// onExited: {
// if (root.tooltipText && !isBusy) {
// tooltip.hide()
// }
// }
// onClicked: function (mouse) {
// if (!modelData || modelData.pairing) {
// return
// }
// if (root.tooltipText && !isBusy) {
// tooltip.hide()
// }
// if (mouse.button === Qt.LeftButton) {
// if (modelData.connected) {
// BluetoothService.disconnectDevice(modelData)
// } else {
// BluetoothService.connectDeviceWithTrust(modelData)
// }
// } else if (mouse.button === Qt.RightButton) {
// BluetoothService.forgetDevice(modelData)
// }
// }
// }
}
}
}

View File

@@ -11,9 +11,9 @@ import qs.Widgets
NPanel {
id: root
panelWidth: 380 * scaling
panelHeight: 500 * scaling
panelAnchorRight: true
preferredWidth: 380
preferredHeight: 500
panelKeyboardFocus: true
panelContent: Rectangle {
color: Color.transparent
@@ -29,7 +29,7 @@ NPanel {
spacing: Style.marginM * scaling
NIcon {
text: "bluetooth"
icon: "bluetooth"
font.pointSize: Style.fontSizeXXL * scaling
color: Color.mPrimary
}
@@ -42,10 +42,18 @@ NPanel {
Layout.fillWidth: true
}
NToggle {
id: bluetoothSwitch
checked: Settings.data.network.bluetoothEnabled
onToggled: checked => BluetoothService.setBluetoothEnabled(checked)
baseSize: Style.baseWidgetSize * 0.65 * scaling
}
NIconButton {
icon: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop_circle" : "refresh"
enabled: Settings.data.network.bluetoothEnabled
icon: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "refresh"
tooltipText: "Refresh Devices"
sizeRatio: 0.8
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
if (BluetoothService.adapter) {
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering
@@ -55,8 +63,8 @@ NPanel {
NIconButton {
icon: "close"
tooltipText: "Close"
sizeRatio: 0.8
tooltipText: "Close."
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
root.close()
}
@@ -67,16 +75,50 @@ NPanel {
Layout.fillWidth: true
}
ScrollView {
Rectangle {
visible: !(BluetoothService.adapter && BluetoothService.adapter.enabled)
Layout.fillWidth: true
Layout.fillHeight: true
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ScrollBar.vertical.policy: ScrollBar.AsNeeded
color: Color.transparent
// Center the content within this rectangle
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
NIcon {
icon: "bluetooth-off"
font.pointSize: 64 * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Bluetooth is disabled"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Enable Bluetooth to see available devices."
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
}
}
NScrollView {
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
Layout.fillWidth: true
Layout.fillHeight: true
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
clip: true
contentWidth: availableWidth
ColumnLayout {
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
width: parent.width
spacing: Style.marginM * scaling
@@ -97,12 +139,11 @@ NPanel {
// Known devices
BluetoothDevicesList {
label: "Known devices"
tooltipText: "Left click to connect, right click to forget"
tooltipText: "Left click to connect.\nRight click to forget."
property var items: {
if (!BluetoothService.adapter || !Bluetooth.devices)
return []
var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && !dev.connected
&& (dev.paired || dev.trusted))
var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && !dev.connected && (dev.paired || dev.trusted))
return BluetoothService.sortDevices(filtered)
}
model: items
@@ -124,9 +165,9 @@ NPanel {
Layout.fillWidth: true
}
// Fallback
// Fallback - No devices, scanning
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
spacing: Style.marginM * scaling
visible: {
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices) {
@@ -134,21 +175,18 @@ NPanel {
}
var availableCount = Bluetooth.devices.values.filter(dev => {
return dev && !dev.paired && !dev.pairing
&& !dev.blocked
&& (dev.signalStrength === undefined
|| dev.signalStrength > 0)
return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0)
}).length
return (availableCount === 0)
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: Style.marginM * scaling
spacing: Style.marginXS * scaling
NIcon {
text: "sync"
font.pointSize: Style.fontSizeXLL * 1.5 * scaling
icon: "refresh"
font.pointSize: Style.fontSizeXXL * 1.5 * scaling
color: Color.mPrimary
RotationAnimation on rotation {
@@ -164,12 +202,11 @@ NPanel {
text: "Scanning for devices..."
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
font.weight: Style.fontWeightMedium
}
}
NText {
text: "Make sure your device is in pairing mode"
text: "Make sure your device is in pairing mode."
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter

View File

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

View File

@@ -12,284 +12,384 @@ import qs.Widgets
Variants {
model: Quickshell.screens
delegate: Loader {
delegate: Item {
required property ShellScreen modelData
readonly property real scaling: ScalingService.scale(modelData)
property real scaling: ScalingService.getScreenScale(modelData)
active: Settings.isLoaded && modelData ? Settings.data.dock.monitors.includes(modelData.name) : false
Connections {
target: ScalingService
function onScaleChanged(screenName, scale) {
if (screenName === modelData.name) {
scaling = scale
}
}
}
sourceComponent: PanelWindow {
id: dockWindow
// Shared properties between peek and dock windows
readonly property bool autoHide: Settings.data.dock.autoHide
readonly property int hideDelay: 500
readonly property int showDelay: 100
readonly property int hideAnimationDuration: Style.animationFast
readonly property int showAnimationDuration: Style.animationFast
readonly property int peekHeight: 1 // no scaling for peek
readonly property int iconSize: 36 * scaling
readonly property int floatingMargin: Settings.data.dock.floatingRatio * Style.marginL * scaling
screen: modelData
// Bar detection and positioning properties
readonly property bool hasBar: modelData.name ? (Settings.data.bar.monitors.includes(modelData.name) || (Settings.data.bar.monitors.length === 0)) : false
readonly property bool barAtBottom: hasBar && Settings.data.bar.position === "bottom"
readonly property int barHeight: Style.barHeight * scaling
// Auto-hide properties - make reactive to settings changes
property bool autoHide: Settings.data.dock.autoHide || (Settings.data.bar.position === "bottom")
property bool hidden: autoHide
property int hideDelay: 500
property int showDelay: 100
property int hideAnimationDuration: Style.animationFast
property int showAnimationDuration: Style.animationFast
property int peekHeight: 2
property int fullHeight: dockContainer.height
property int iconSize: 36
// Shared state between windows
property bool dockHovered: false
property bool anyAppHovered: false
property bool hidden: autoHide
property bool peekHovered: false
// Track hover state
property bool dockHovered: false
property bool anyAppHovered: false
// Separate property to control Loader - stays true during animations
property bool dockLoaded: !autoHide // Start loaded if autoHide is off
// Dock is only shown if explicitely toggled
exclusionMode: ExclusionMode.Ignore
// Timer to unload dock after hide animation completes
Timer {
id: unloadTimer
interval: hideAnimationDuration + 50 // Add small buffer
onTriggered: {
if (hidden && autoHide) {
dockLoaded = false
}
}
}
anchors.bottom: true
anchors.left: true
anchors.right: true
focusable: false
color: Color.transparent
implicitHeight: iconSize * 1.4 * scaling
// Watch for autoHide setting changes
onAutoHideChanged: {
if (!autoHide) {
// If auto-hide is disabled, show the dock
hidden = false
hideTimer.stop()
showTimer.stop()
} else {
// If auto-hide is enabled, start hidden
// Timer for auto-hide delay
Timer {
id: hideTimer
interval: hideDelay
onTriggered: {
if (autoHide && !dockHovered && !anyAppHovered && !peekHovered) {
hidden = true
unloadTimer.restart() // Start unload timer when hiding
}
}
}
// Timer for auto-hide delay
Timer {
id: hideTimer
interval: hideDelay
onTriggered: {
if (autoHide && !dockHovered && !anyAppHovered) {
hidden = true
}
// Timer for show delay
Timer {
id: showTimer
interval: showDelay
onTriggered: {
if (autoHide) {
dockLoaded = true // Load dock immediately
hidden = false // Then trigger show animation
unloadTimer.stop() // Cancel any pending unload
}
}
}
// Timer for show delay
Timer {
id: showTimer
interval: showDelay
onTriggered: hidden = false
// Watch for autoHide setting changes
onAutoHideChanged: {
if (!autoHide) {
hidden = false
dockLoaded = true
hideTimer.stop()
showTimer.stop()
unloadTimer.stop()
} else {
hidden = true
unloadTimer.restart() // Schedule unload after animation
}
}
// Behavior for smooth hide/show animations
Behavior on margins.bottom {
NumberAnimation {
duration: hidden ? hideAnimationDuration : showAnimationDuration
easing.type: Easing.InOutQuad
// PEEK WINDOW - Always visible when auto-hide is enabled
Loader {
active: Settings.isLoaded && modelData && Settings.data.dock.monitors.includes(modelData.name) && autoHide
sourceComponent: PanelWindow {
id: peekWindow
screen: modelData
anchors.bottom: true
anchors.left: true
anchors.right: true
focusable: false
color: Color.transparent
WlrLayershell.namespace: "noctalia-dock-peek"
WlrLayershell.exclusionMode: ExclusionMode.Auto // Always exclusive
implicitHeight: peekHeight
Rectangle {
anchors.fill: parent
color: barAtBottom ? Qt.alpha(Color.mSurface, Settings.data.bar.backgroundOpacity) : Color.transparent
}
}
MouseArea {
id: screenEdgeMouseArea
x: 0
y: modelData && modelData.geometry ? modelData.geometry.height - (fullHeight + 10 * scaling) : 0
width: screen.width
height: fullHeight + 10 * scaling
hoverEnabled: true
propagateComposedEvents: true
onEntered: {
if (autoHide && hidden) {
showTimer.start()
}
}
onExited: {
if (autoHide && !hidden && !dockHovered && !anyAppHovered) {
hideTimer.start()
}
}
}
margins.bottom: hidden ? -(fullHeight - peekHeight) : 0
Rectangle {
id: dockContainer
width: dock.width + 48 * scaling
height: iconSize * 1.4 * scaling
color: Color.mSurface
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
topLeftRadius: Style.radiusL * scaling
topRightRadius: Style.radiusL * scaling
MouseArea {
id: dockMouseArea
id: peekArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
onEntered: {
dockHovered = true
if (autoHide) {
showTimer.stop()
hideTimer.stop()
hidden = false
peekHovered = true
if (hidden) {
showTimer.start()
}
}
onExited: {
dockHovered = false
// Only start hide timer if we're not hovering over any app
if (autoHide && !anyAppHovered) {
hideTimer.start()
peekHovered = false
if (!hidden && !dockHovered && !anyAppHovered) {
hideTimer.restart()
}
}
}
}
}
// DOCK WINDOW
Loader {
active: Settings.isLoaded && modelData && Settings.data.dock.monitors.includes(modelData.name) && dockLoaded && ToplevelManager && (ToplevelManager.toplevels.values.length > 0)
sourceComponent: PanelWindow {
id: dockWindow
screen: modelData
focusable: false
color: Color.transparent
WlrLayershell.namespace: "noctalia-dock-main"
WlrLayershell.exclusionMode: Settings.data.dock.exclusive ? ExclusionMode.Auto : ExclusionMode.Ignore
// Size to fit the dock container exactly
implicitWidth: dockContainerWrapper.width
implicitHeight: dockContainerWrapper.height
// Position above the bar if it's at bottom
anchors.bottom: true
margins.bottom: {
switch (Settings.data.bar.position) {
case "bottom":
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling + floatingMargin : floatingMargin)
default:
return floatingMargin
}
}
// Rectangle {
// anchors.fill: parent
// color: "#000FF0"
// z: -1
// }
// Wrapper item for scale/opacity animations
Item {
id: dock
width: runningAppsRow.width
height: parent.height - (20 * scaling)
anchors.centerIn: parent
id: dockContainerWrapper
width: dockContainer.width
height: dockContainer.height
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
NTooltip {
id: appTooltip
visible: false
positionAbove: true
// Apply animations to this wrapper
opacity: hidden ? 0 : 1
scale: hidden ? 0.85 : 1
Behavior on opacity {
NumberAnimation {
duration: hidden ? hideAnimationDuration : showAnimationDuration
easing.type: Easing.InOutQuad
}
}
function getAppIcon(toplevel: Toplevel): string {
if (!toplevel)
return ""
return Icons.iconForAppId(toplevel.appId?.toLowerCase())
Behavior on scale {
NumberAnimation {
duration: hidden ? hideAnimationDuration : showAnimationDuration
easing.type: hidden ? Easing.InQuad : Easing.OutBack
easing.overshoot: hidden ? 0 : 1.05
}
}
Row {
id: runningAppsRow
spacing: Style.marginL * scaling
height: parent.height
Rectangle {
id: dockContainer
width: dockLayout.implicitWidth + Style.marginM * scaling * 2
height: Math.round(iconSize * 1.5)
color: Qt.alpha(Color.mSurface, Settings.data.dock.backgroundOpacity)
anchors.centerIn: parent
radius: Style.radiusL * scaling
border.width: Math.max(1, Style.borderS * scaling)
border.color: Qt.alpha(Color.mOutline, Settings.data.dock.backgroundOpacity)
Repeater {
model: ToplevelManager ? ToplevelManager.toplevels : null
MouseArea {
id: dockMouseArea
anchors.fill: parent
hoverEnabled: true
delegate: Rectangle {
id: appButton
width: iconSize * scaling
height: iconSize * scaling
color: Color.transparent
radius: Style.radiusM * scaling
property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData
property bool hovered: appMouseArea.containsMouse
property string appId: modelData ? modelData.appId : ""
property string appTitle: modelData ? modelData.title : ""
// Hover background
Rectangle {
id: hoverBackground
anchors.fill: parent
color: appButton.hovered ? Color.mSurfaceVariant : Color.transparent
radius: parent.radius
opacity: appButton.hovered ? 0.8 : 0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutQuad
}
}
onEntered: {
dockHovered = true
if (autoHide) {
showTimer.stop()
hideTimer.stop()
unloadTimer.stop() // Cancel unload if hovering
}
}
// The icon
Image {
id: appIcon
width: iconSize * scaling
height: iconSize * scaling
anchors.centerIn: parent
source: dock.getAppIcon(modelData)
visible: source.toString() !== ""
smooth: true
mipmap: false
antialiasing: false
fillMode: Image.PreserveAspectFit
scale: appButton.hovered ? 1.1 : 1.0
Behavior on scale {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutBack
}
}
onExited: {
dockHovered = false
if (autoHide && !anyAppHovered && !peekHovered) {
hideTimer.restart()
}
}
}
// Fall back if no icon
NText {
anchors.centerIn: parent
visible: !appIcon.visible
text: "question_mark"
font.family: "Material Symbols Rounded"
font.pointSize: iconSize * 0.7 * scaling
color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant
Item {
id: dock
width: dockLayout.implicitWidth
height: parent.height - (Style.marginM * 2 * scaling)
anchors.centerIn: parent
scale: appButton.hovered ? 1.1 : 1.0
function getAppIcon(toplevel: Toplevel): string {
if (!toplevel)
return ""
return AppIcons.iconForAppId(toplevel.appId?.toLowerCase())
}
Behavior on scale {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutBack
RowLayout {
id: dockLayout
spacing: Style.marginM * scaling
Layout.preferredHeight: parent.height
anchors.centerIn: parent
Repeater {
model: ToplevelManager ? ToplevelManager.toplevels : null
delegate: Item {
id: appButton
Layout.preferredWidth: iconSize
Layout.preferredHeight: iconSize
Layout.alignment: Qt.AlignCenter
property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData
property bool hovered: appMouseArea.containsMouse
property string appId: modelData ? modelData.appId : ""
property string appTitle: modelData ? modelData.title : ""
// Individual tooltip for this app
NTooltip {
id: appTooltip
target: appButton
positionAbove: true
visible: false
}
Image {
id: appIcon
width: iconSize
height: iconSize
anchors.centerIn: parent
source: dock.getAppIcon(modelData)
visible: source.toString() !== ""
sourceSize.width: iconSize * 2
sourceSize.height: iconSize * 2
smooth: true
mipmap: true
antialiasing: true
fillMode: Image.PreserveAspectFit
cache: true
scale: appButton.hovered ? 1.15 : 1.0
Behavior on scale {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
easing.overshoot: 1.2
}
}
}
// Fall back if no icon
NIcon {
anchors.centerIn: parent
visible: !appIcon.visible
icon: "question-mark"
font.pointSize: iconSize * 0.7
color: appButton.isActive ? Color.mPrimary : Color.mOnSurfaceVariant
scale: appButton.hovered ? 1.15 : 1.0
Behavior on scale {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutBack
easing.overshoot: 1.2
}
}
}
MouseArea {
id: appMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
onEntered: {
anyAppHovered = true
const appName = appButton.appTitle || appButton.appId || "Unknown"
appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName
appTooltip.isVisible = true
if (autoHide) {
showTimer.stop()
hideTimer.stop()
unloadTimer.stop() // Cancel unload if hovering app
}
}
onExited: {
anyAppHovered = false
appTooltip.hide()
if (autoHide && !dockHovered && !peekHovered) {
hideTimer.restart()
}
}
onClicked: function (mouse) {
if (mouse.button === Qt.MiddleButton && modelData?.close) {
modelData.close()
}
if (mouse.button === Qt.LeftButton && modelData?.activate) {
modelData.activate()
}
}
}
// Active indicator
Rectangle {
visible: isActive
width: iconSize * 0.2
height: iconSize * 0.1
color: Color.mPrimary
radius: Style.radiusXS * scaling
anchors.top: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
// 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
}
}
}
}
}
MouseArea {
id: appMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
onEntered: {
anyAppHovered = true
const appName = appButton.appTitle || appButton.appId || "Unknown"
appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName
appTooltip.target = appButton
appTooltip.isVisible = true
if (autoHide) {
showTimer.stop()
hideTimer.stop()
hidden = false
}
}
onExited: {
anyAppHovered = false
appTooltip.hide()
// Only start hide timer if we're not hovering over the dock
if (autoHide && !dockHovered) {
hideTimer.start()
}
}
onClicked: function (mouse) {
if (mouse.button === Qt.MiddleButton && modelData?.close) {
modelData.close()
}
if (mouse.button === Qt.LeftButton && modelData?.activate) {
modelData.activate()
}
}
}
Rectangle {
visible: isActive
width: iconSize * 0.75
height: 4 * scaling
color: Color.mPrimary
radius: Style.radiusXS
anchors.top: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: Style.marginXXS * scaling
}
}
}
}

View File

@@ -1,6 +1,8 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import qs.Commons
import qs.Services
Item {
@@ -9,23 +11,27 @@ Item {
IpcHandler {
target: "screenRecorder"
function toggle() {
ScreenRecorderService.toggleRecording()
if (ScreenRecorderService.isAvailable) {
ScreenRecorderService.toggleRecording()
}
}
}
IpcHandler {
target: "settings"
function toggle() {
settingsPanel.toggle(Quickshell.screens[0])
settingsPanel.toggle()
}
}
IpcHandler {
target: "notifications"
function toggleHistory() {
notificationHistoryPanel.toggle(Quickshell.screens[0])
// Will attempt to open the panel next to the bar button if any.
notificationHistoryPanel.toggle(BarService.lookupWidget("NotificationHistory"))
}
function toggleDoNotDisturb() {// TODO
function toggleDND() {
Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
}
}
@@ -36,45 +42,18 @@ Item {
}
}
IpcHandler {
target: "appLauncher"
function toggle() {
launcherPanel.toggle(Quickshell.screens[0])
}
function clipboard() {
launcherPanel.toggle(Quickshell.screens[0])
// Use the setSearchText function to set clipboard mode
Qt.callLater(() => {
launcherPanel.setSearchText(">clip ")
})
}
function calculator() {
launcherPanel.toggle(Quickshell.screens[0])
// Use the setSearchText function to set calculator mode
Qt.callLater(() => {
launcherPanel.setSearchText(">calc ")
})
}
}
IpcHandler {
target: "launcher"
function toggle() {
launcherPanel.toggle(Quickshell.screens[0])
launcherPanel.toggle()
}
function clipboard() {
launcherPanel.toggle(Quickshell.screens[0])
// Use the setSearchText function to set clipboard mode
Qt.callLater(() => {
launcherPanel.setSearchText(">clip ")
})
launcherPanel.setSearchText(">clip ")
launcherPanel.toggle()
}
function calculator() {
launcherPanel.toggle(Quickshell.screens[0])
// Use the setSearchText function to set calculator mode
Qt.callLater(() => {
launcherPanel.setSearchText(">calc ")
})
launcherPanel.setSearchText(">calc ")
launcherPanel.toggle()
}
}
@@ -99,6 +78,19 @@ Item {
}
}
IpcHandler {
target: "darkMode"
function toggle() {
Settings.data.colorSchemes.darkMode = !Settings.data.colorSchemes.darkMode
}
function setDark() {
Settings.data.colorSchemes.darkMode = true
}
function setLight() {
Settings.data.colorSchemes.darkMode = false
}
}
IpcHandler {
target: "volume"
function increase() {
@@ -108,7 +100,7 @@ Item {
AudioService.decreaseVolume()
}
function muteOutput() {
AudioService.setMuted(!AudioService.muted)
AudioService.setOutputMuted(!AudioService.muted)
}
function muteInput() {
if (AudioService.source?.ready && AudioService.source?.audio) {
@@ -120,14 +112,38 @@ Item {
IpcHandler {
target: "powerPanel"
function toggle() {
powerPanel.toggle(Quickshell.screens[0])
powerPanel.toggle()
}
}
IpcHandler {
target: "sidePanel"
function toggle() {
sidePanel.toggle(Quickshell.screens[0])
// Will attempt to open the panel next to the bar button if any.
sidePanel.toggle(BarService.lookupWidget("SidePanelToggle"))
}
}
// Wallpaper IPC: trigger a new random wallpaper
IpcHandler {
target: "wallpaper"
function toggle() {
if (Settings.data.wallpaper.enabled) {
wallpaperSelector.toggle()
}
}
function random() {
if (Settings.data.wallpaper.enabled) {
WallpaperService.setRandomWallpaper()
}
}
function set(path: string, screen: string) {
if (screen === "all" || screen === "") {
screen = undefined
}
WallpaperService.changeWallpaper(path, screen)
}
}
}

View File

@@ -1,151 +0,0 @@
import QtQuick
import Quickshell
import qs.Commons
import "../../Helpers/AdvancedMath.js" as AdvancedMath
QtObject {
id: calculator
// Function to evaluate mathematical expressions
function evaluate(expression) {
if (!expression || expression.trim() === "") {
return {
"isValid": false,
"result": "",
"displayResult": "",
"error": "Empty expression"
}
}
try {
// Try advanced math first
if (typeof AdvancedMath !== 'undefined') {
const result = AdvancedMath.evaluate(expression.trim())
const displayResult = AdvancedMath.formatResult(result)
return {
"isValid": true,
"result": result,
"displayResult": displayResult,
"expression": expression,
"error": ""
}
} else {
// Fallback to basic evaluation
Logger.warn("Calculator", "AdvancedMath not available, using basic eval")
// Basic preprocessing for common functions
var processed = expression.trim(
).replace(/\bpi\b/gi,
Math.PI).replace(/\be\b/gi,
Math.E).replace(/\bsqrt\s*\(/g,
'Math.sqrt(').replace(/\bsin\s*\(/g,
'Math.sin(').replace(/\bcos\s*\(/g,
'Math.cos(').replace(/\btan\s*\(/g, 'Math.tan(').replace(/\blog\s*\(/g, 'Math.log10(').replace(/\bln\s*\(/g, 'Math.log(').replace(/\bexp\s*\(/g, 'Math.exp(').replace(/\bpow\s*\(/g, 'Math.pow(').replace(/\babs\s*\(/g, 'Math.abs(')
// Sanitize and evaluate
if (!/^[0-9+\-*/().\s\w,]+$/.test(processed)) {
throw new Error("Invalid characters in expression")
}
const result = eval(processed)
if (!isFinite(result) || isNaN(result)) {
throw new Error("Invalid result")
}
const displayResult = Number.isInteger(result) ? result.toString() : result.toFixed(6).replace(/\.?0+$/, '')
return {
"isValid": true,
"result": result,
"displayResult": displayResult,
"expression": expression,
"error": ""
}
}
} catch (error) {
return {
"isValid": false,
"result": "",
"displayResult": "",
"error": error.message || error.toString()
}
}
}
// Generate calculator entry for display
function createEntry(expression, searchContext = "") {
const evaluation = evaluate(expression)
if (!evaluation.isValid) {
return {
"isCalculator": true,
"name": "Invalid expression",
"content": evaluation.error,
"icon": "error",
"execute": function () {// Do nothing for invalid expressions
}
}
}
const displayName = searchContext
=== "calc" ? `${expression} = ${evaluation.displayResult}` : `${expression} = ${evaluation.displayResult}`
return {
"isCalculator": true,
"name": displayName,
"result": evaluation.result,
"expr": expression,
"displayResult": evaluation.displayResult,
"icon": "calculate",
"execute": function () {
Quickshell.clipboardText = evaluation.displayResult
// Also copy using shell command for better compatibility
Quickshell.execDetached(
["sh", "-lc", `printf %s ${evaluation.displayResult} | wl-copy -t text/plain;charset=utf-8`])
Quickshell.execDetached(
["notify-send", "Calculator", `${expression} = ${evaluation.displayResult} (copied to clipboard)`])
}
}
}
// Create placeholder entry for empty calculator mode
function createPlaceholderEntry() {
return {
"isCalculator": true,
"name": "Calculator",
"content": "Try: sqrt(16), sin(1), cos(0), pi*2, exp(1), pow(2,8), abs(-5)",
"icon": "calculate",
"execute": function () {// Do nothing for placeholder
}
}
}
// Process calculator queries
function processQuery(query, searchContext = "") {
const results = []
if (searchContext === "calc") {
// Handle ">calc" mode
const expr = query.slice(5).trim()
if (expr && expr !== "") {
results.push(createEntry(expr, "calc"))
} else {
results.push(createPlaceholderEntry())
}
} else if (query.startsWith(">") && query.length > 1 && !query.startsWith(">clip") && !query.startsWith(">calc")) {
// Handle direct math expressions after ">"
const mathExpr = query.slice(1).trim()
const evaluation = evaluate(mathExpr)
if (evaluation.isValid) {
results.push(createEntry(mathExpr, "direct"))
}
// If invalid, don't add anything - let it fall through to regular search
}
return results
}
}

View File

@@ -1,113 +0,0 @@
import QtQuick
import Quickshell
import qs.Commons
import qs.Services
QtObject {
id: clipboardHistory
function parseImageMeta(preview) {
const re = /\[\[\s*binary data\s+([\d\.]+\s*(?:KiB|MiB|GiB|B))\s+(\w+)\s+(\d+)x(\d+)\s*\]\]/i
const m = (preview || "").match(re)
if (!m)
return null
return {
"size": m[1],
"fmt": (m[2] || "").toUpperCase(),
"w": Number(m[3]),
"h": Number(m[4])
}
}
function formatTextPreview(preview) {
const normalized = (preview || "").replace(/\s+/g, ' ').trim()
const lines = normalized.split(/\n+/)
const title = (lines[0] || "Text").slice(0, 60)
let subtitle = ""
if (lines.length > 1) {
subtitle = lines[1].slice(0, 80)
} else {
subtitle = `${normalized.length} chars`
}
return {
"title": title,
"subtitle": subtitle
}
}
function createClipboardEntry(item) {
if (item.isImage) {
const meta = parseImageMeta(item.preview)
const title = meta ? `Image ${meta.w}×${meta.h}` : "Image"
const subtitle = meta ? `${meta.size} · ${meta.fmt}` : (item.preview || "")
return {
"isClipboard": true,
"name": title,
"content": subtitle,
"icon": "image",
"type": 'image',
"id": item.id,
"mime": item.mime
}
} else {
const parts = formatTextPreview(item.preview)
return {
"isClipboard": true,
"name": parts.title,
"content": parts.subtitle,
"icon": "content_paste",
"type": 'text',
"id": item.id
}
}
}
function createEmptyEntry() {
return {
"isClipboard": true,
"name": "No clipboard history",
"content": "No matching clipboard entries found",
"icon": "content_paste_off",
"execute": function () {}
}
}
function processQuery(query, items) {
const results = []
if (!query.startsWith(">clip")) {
return results
}
const searchTerm = query.slice(5).trim().toLowerCase()
// Dependency hook without side effects
const _rev = CliphistService.revision
const source = items || CliphistService.items
source.forEach(function (item) {
const hay = (item.preview || "").toLowerCase()
if (!searchTerm || hay.indexOf(searchTerm) !== -1) {
const entry = createClipboardEntry(item)
// Attach execute at this level to avoid duplicating functions
entry.execute = function () {
CliphistService.copyToClipboard(item.id)
}
results.push(entry)
}
})
if (results.length === 0) {
results.push(createEmptyEntry())
}
return results
}
function refresh() {
CliphistService.list(100)
}
function clearAll() {
CliphistService.wipeAll()
}
}

View File

@@ -1,414 +1,334 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Effects
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import qs.Commons
import qs.Services
import qs.Widgets
import "../../Helpers/FuzzySort.js" as Fuzzysort
NPanel {
id: root
panelWidth: Math.min(700 * scaling, screen?.width * 0.75)
panelHeight: Math.min(550 * scaling, screen?.height * 0.8)
// Positioning derives from Settings.data.bar.position for vertical (top/bottom)
// and from Settings.data.appLauncher.position for horizontal vs center.
// Options: center, top_left, top_right, bottom_left, bottom_right, bottom_center, top_center
readonly property string launcherPosition: Settings.data.appLauncher.position
panelAnchorHorizontalCenter: launcherPosition === "center" || (launcherPosition.endsWith("_center"))
// Panel configuration
preferredWidth: 500
preferredWidthRatio: 0.3
preferredHeight: 600
preferredHeightRatio: 0.5
panelKeyboardFocus: true
panelBackgroundColor: Qt.alpha(Color.mSurface, Settings.data.appLauncher.backgroundOpacity)
// Positioning
readonly property string launcherPosition: Settings.data.appLauncher.position
panelAnchorHorizontalCenter: launcherPosition === "center" || launcherPosition.endsWith("_center")
panelAnchorVerticalCenter: launcherPosition === "center"
panelAnchorLeft: launcherPosition !== "center" && (launcherPosition.endsWith("_left"))
panelAnchorRight: launcherPosition !== "center" && (launcherPosition.endsWith("_right"))
panelAnchorLeft: launcherPosition !== "center" && launcherPosition.endsWith("_left")
panelAnchorRight: launcherPosition !== "center" && launcherPosition.endsWith("_right")
panelAnchorBottom: launcherPosition.startsWith("bottom_")
panelAnchorTop: launcherPosition.startsWith("top_")
// Background opacity following bar's approach
panelBackgroundColor: Qt.rgba(Color.mSurface.r, Color.mSurface.g, Color.mSurface.b,
Settings.data.appLauncher.backgroundOpacity)
// Properties
// Core state
property string searchText: ""
property bool shouldResetCursor: false
property int selectedIndex: 0
property var results: []
property var plugins: []
property var activePlugin: null
// Add function to set search text programmatically
readonly property int badgeSize: Math.round(Style.baseWidgetSize * 1.6 * scaling)
readonly property int entryHeight: Math.round(badgeSize + Style.marginM * 2 * scaling)
// Public API for plugins
function setSearchText(text) {
searchText = text
// The searchInput will automatically update via the text binding
// Focus and cursor position will be handled by the TextField's Component.onCompleted
}
onOpened: {
// Reset state when panel opens to avoid sticky modes
if (searchText === "") {
searchText = ""
selectedIndex = 0
// Plugin registration
function registerPlugin(plugin) {
plugins.push(plugin)
plugin.launcher = root
if (plugin.init)
plugin.init()
}
// Search handling
function updateResults() {
results = []
activePlugin = null
// Check for command mode
if (searchText.startsWith(">")) {
// Find plugin that handles this command
for (let plugin of plugins) {
if (plugin.handleCommand && plugin.handleCommand(searchText)) {
activePlugin = plugin
results = plugin.getResults(searchText)
break
}
}
// Show available commands if just ">"
if (searchText === ">" && !activePlugin) {
for (let plugin of plugins) {
if (plugin.commands) {
results = results.concat(plugin.commands())
}
}
}
} else {
// Regular search - let plugins contribute results
for (let plugin of plugins) {
if (plugin.handleSearch) {
const pluginResults = plugin.getResults(searchText)
results = results.concat(pluginResults)
}
}
}
selectedIndex = 0
}
onSearchTextChanged: updateResults()
// Lifecycle
onOpened: {
// Notify plugins
for (let plugin of plugins) {
if (plugin.onOpened)
plugin.onOpened()
}
updateResults()
}
onClosed: {
// Reset search bar when launcher is closed
// Reset search text
searchText = ""
selectedIndex = 0
shouldResetCursor = true
}
// Import modular components
Calculator {
id: calculator
}
ClipboardHistory {
id: clipboardHistory
}
// Poll cliphist while in clipboard mode to keep entries fresh
Timer {
id: clipRefreshTimer
interval: 2000
repeat: true
running: Settings.data.appLauncher.enableClipboardHistory && searchText.startsWith(">clip")
onTriggered: clipboardHistory.refresh()
}
// Properties
property var desktopEntries: DesktopEntries.applications.values
property int selectedIndex: 0
// Refresh clipboard when user starts typing clipboard commands
onSearchTextChanged: {
if (Settings.data.appLauncher.enableClipboardHistory && searchText.startsWith(">clip")) {
clipboardHistory.refresh()
}
}
// Main filtering logic
property var filteredEntries: {
// Explicit dependency so changes to items/decoded images retrigger this binding
const _clipItems = Settings.data.appLauncher.enableClipboardHistory ? CliphistService.items : []
const _clipRev = Settings.data.appLauncher.enableClipboardHistory ? CliphistService.revision : 0
var query = searchText ? searchText.toLowerCase() : ""
if (Settings.data.appLauncher.enableClipboardHistory && query.startsWith(">clip")) {
return clipboardHistory.processQuery(query, _clipItems)
}
if (!desktopEntries || desktopEntries.length === 0) {
return []
}
// Filter out entries that shouldn't be displayed
var visibleEntries = desktopEntries.filter(entry => {
if (!entry || entry.noDisplay) {
return false
}
return true
})
var results = []
// Handle special commands
if (query === ">") {
results.push({
"isCommand": true,
"name": ">calc",
"content": "Calculator - evaluate mathematical expressions",
"icon": "calculate",
"execute": executeCalcCommand
})
if (Settings.data.appLauncher.enableClipboardHistory) {
results.push({
"isCommand": true,
"name": ">clip",
"content": "Clipboard history - browse and restore clipboard items",
"icon": "content_paste",
"execute": executeClipCommand
})
}
return results
}
// Handle calculator
if (query.startsWith(">calc")) {
return calculator.processQuery(query, "calc")
}
// Handle direct math expressions after ">"
if (query.startsWith(">") && query.length > 1 && (!Settings.data.appLauncher.enableClipboardHistory
|| !query.startsWith(">clip")) && !query.startsWith(">calc")) {
const mathResults = calculator.processQuery(query, "direct")
if (mathResults.length > 0) {
return mathResults
}
// If math evaluation fails, fall through to regular search
}
// Regular app search
if (!query) {
results = results.concat(visibleEntries.sort(function (a, b) {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase())
}))
} else {
var fuzzyResults = Fuzzysort.go(query, visibleEntries, {
"keys": ["name", "comment", "genericName"]
})
results = results.concat(fuzzyResults.map(function (r) {
return r.obj
}))
}
return results
}
// Command execution functions
function executeCalcCommand() {
setSearchText(">calc ")
}
function executeClipCommand() {
setSearchText(">clip ")
}
// Navigation functions
function selectNext() {
if (filteredEntries.length > 0) {
selectedIndex = Math.min(selectedIndex + 1, filteredEntries.length - 1)
}
}
function selectPrev() {
if (filteredEntries.length > 0) {
selectedIndex = Math.max(selectedIndex - 1, 0)
}
}
function selectNextPage() {
if (filteredEntries.length > 0) {
const delegateHeight = 65 * scaling + (Style.marginXXS * scaling)
const page = Math.max(1, Math.floor(appsList.height / delegateHeight))
selectedIndex = Math.min(selectedIndex + page, filteredEntries.length - 1)
}
}
function selectPrevPage() {
if (filteredEntries.length > 0) {
const delegateHeight = 65 * scaling + (Style.marginXXS * scaling)
const page = Math.max(1, Math.floor(appsList.height / delegateHeight))
selectedIndex = Math.max(selectedIndex - page, 0)
}
}
function activateSelected() {
if (filteredEntries.length === 0)
return
var modelData = filteredEntries[selectedIndex]
if (modelData && modelData.execute) {
if (modelData.isCommand) {
modelData.execute()
return
} else {
modelData.execute()
}
root.close()
// Notify plugins
for (let plugin of plugins) {
if (plugin.onClosed)
plugin.onClosed()
}
}
// Load plugins
Component.onCompleted: {
Logger.log("Launcher", "Component completed")
Logger.log("Launcher", "DesktopEntries available:", typeof DesktopEntries !== 'undefined')
if (typeof DesktopEntries !== 'undefined') {
Logger.log("Launcher", "DesktopEntries.entries:",
DesktopEntries.entries ? DesktopEntries.entries.length : 'undefined')
// Load applications plugin
const appsPlugin = Qt.createComponent("Plugins/ApplicationsPlugin.qml").createObject(this)
if (appsPlugin) {
registerPlugin(appsPlugin)
Logger.log("Launcher", "Registered: ApplicationsPlugin")
} else {
Logger.error("Launcher", "Failed to load ApplicationsPlugin")
}
// Start clipboard refresh immediately on open if enabled
if (Settings.data.appLauncher.enableClipboardHistory) {
clipboardHistory.refresh()
// Load calculator plugin
const calcPlugin = Qt.createComponent("Plugins/CalculatorPlugin.qml").createObject(this)
if (calcPlugin) {
registerPlugin(calcPlugin)
Logger.log("Launcher", "Registered: CalculatorPlugin")
} else {
Logger.error("Launcher", "Failed to load CalculatorPlugin")
}
// Load clipboard history plugin
const clipboardPlugin = Qt.createComponent("Plugins/ClipboardPlugin.qml").createObject(this)
if (clipboardPlugin) {
registerPlugin(clipboardPlugin)
Logger.log("Launcher", "Registered: ClipboardPlugin")
} else {
Logger.error("Launcher", "Failed to load ClipboardPlugin")
}
}
// Main content container
// UI
panelContent: Rectangle {
id: ui
color: Color.transparent
// ---------------------
// Navigation
function selectNext() {
if (results.length > 0) {
// Clamp the index to not exceed the last item
selectedIndex = Math.min(selectedIndex + 1, results.length - 1)
}
}
function selectPrevious() {
if (results.length > 0) {
// Clamp the index to not go below the first item (0)
selectedIndex = Math.max(selectedIndex - 1, 0)
}
}
function selectFirst() {
selectedIndex = 0
}
function selectLast() {
if (results.length > 0) {
selectedIndex = results.length - 1
} else {
selectedIndex = 0
}
}
function selectNextPage() {
if (results.length > 0) {
const page = Math.max(1, Math.floor(resultsList.height / entryHeight))
selectedIndex = Math.min(selectedIndex + page, results.length - 1)
}
}
function selectPreviousPage() {
if (results.length > 0) {
const page = Math.max(1, Math.floor(resultsList.height / entryHeight))
selectedIndex = Math.max(selectedIndex - page, 0)
}
}
function activate() {
if (results.length > 0 && results[selectedIndex]) {
const item = results[selectedIndex]
if (item.onActivate) {
item.onActivate()
}
}
}
Shortcut {
sequence: "Ctrl+K"
onActivated: ui.selectPrevious()
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
}
Shortcut {
sequence: "Ctrl+J"
onActivated: ui.selectNext()
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
}
Shortcut {
sequence: "PgDown" // or "PageDown"
onActivated: ui.selectNextPage()
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
}
Shortcut {
sequence: "PgUp" // or "PageUp"
onActivated: ui.selectPreviousPage()
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
}
Shortcut {
sequence: "Home"
onActivated: ui.selectFirst()
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
}
Shortcut {
sequence: "End"
onActivated: ui.selectLast()
enabled: root.opened && searchInput.inputItem && searchInput.inputItem.activeFocus
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginM * scaling
// Search bar
Rectangle {
NTextInput {
id: searchInput
Layout.fillWidth: true
Layout.preferredHeight: Math.round(Style.barHeight * scaling)
Layout.bottomMargin: Style.marginM * scaling
radius: Style.radiusM * scaling
color: Color.mSurface
border.color: searchInput.activeFocus ? Color.mSecondary : Color.mOutline
border.width: Math.max(1, searchInput.activeFocus ? Style.borderM * scaling : Style.borderS * scaling)
Item {
anchors.fill: parent
anchors.margins: Style.marginM * scaling
fontSize: Style.fontSizeL * scaling
fontWeight: Style.fontWeightSemiBold
NIcon {
id: searchIcon
text: "search"
font.pointSize: Style.fontSizeXL * scaling
color: searchInput.activeFocus ? Color.mPrimary : Color.mOnSurface
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
text: searchText
placeholderText: "Search entries... or use > for commands"
TextField {
id: searchInput
placeholderText: searchText === "" ? "Search applications... (use > to view commands)" : "Search applications..."
color: Color.mOnSurface
placeholderTextColor: Color.mOnSurfaceVariant
background: null
font.pointSize: Style.fontSizeL * scaling
anchors.left: searchIcon.right
anchors.leftMargin: Style.marginS * scaling
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: searchText
onTextChanged: {
// Update the parent searchText property
if (searchText !== text) {
searchText = text
onTextChanged: searchText = text
Component.onCompleted: {
if (searchInput.inputItem && searchInput.inputItem.visible) {
searchInput.inputItem.forceActiveFocus()
// Override the TextField's default Home/End behavior
searchInput.inputItem.Keys.priority = Keys.BeforeItem
searchInput.inputItem.Keys.onPressed.connect(function (event) {
// Intercept Home and End BEFORE the TextField handles them
if (event.key === Qt.Key_Home) {
ui.selectFirst()
event.accepted = true
return
} else if (event.key === Qt.Key_End) {
ui.selectLast()
event.accepted = true
return
}
// Defer selectedIndex reset to avoid binding loops
Qt.callLater(() => selectedIndex = 0)
// Reset cursor position if needed
if (shouldResetCursor && text === "") {
cursorPosition = 0
shouldResetCursor = false
}
}
selectedTextColor: Color.mOnSurface
selectionColor: Color.mPrimary
padding: 0
verticalAlignment: TextInput.AlignVCenter
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
Component.onCompleted: {
// Focus the search bar by default and set cursor position
Qt.callLater(() => {
selectedIndex = 0
searchInput.forceActiveFocus()
// Set cursor to end if there's already text
if (searchText && searchText.length > 0) {
searchInput.cursorPosition = searchText.length
}
})
}
Keys.onDownPressed: selectNext()
Keys.onUpPressed: selectPrev()
Keys.onEnterPressed: activateSelected()
Keys.onReturnPressed: activateSelected()
Keys.onEscapePressed: root.close()
Keys.onPressed: event => {
if (event.key === Qt.Key_PageDown) {
appsList.cancelFlick()
root.selectNextPage()
event.accepted = true
} else if (event.key === Qt.Key_PageUp) {
appsList.cancelFlick()
root.selectPrevPage()
event.accepted = true
} else if (event.key === Qt.Key_Home) {
appsList.cancelFlick()
selectedIndex = 0
event.accepted = true
} else if (event.key === Qt.Key_End) {
appsList.cancelFlick()
if (filteredEntries.length > 0) {
selectedIndex = filteredEntries.length - 1
}
event.accepted = true
}
if (event.modifiers & Qt.ControlModifier) {
switch (event.key) {
case Qt.Key_J:
appsList.cancelFlick()
root.selectNext()
event.accepted = true
break
case Qt.Key_K:
appsList.cancelFlick()
root.selectPrev()
event.accepted = true
break
}
}
}
}
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
}
}
Behavior on border.width {
NumberAnimation {
duration: Style.animationFast
})
searchInput.inputItem.Keys.onDownPressed.connect(function (event) {
ui.selectNext()
})
searchInput.inputItem.Keys.onUpPressed.connect(function (event) {
ui.selectPrevious()
})
searchInput.inputItem.Keys.onReturnPressed.connect(function (event) {
ui.activate()
})
}
}
}
// Applications list
ListView {
id: appsList
// Results list
NListView {
id: resultsList
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
spacing: Style.marginXXS * scaling
model: filteredEntries
model: results
currentIndex: selectedIndex
boundsBehavior: Flickable.StopAtBounds
maximumFlickVelocity: 2500
flickDeceleration: 2000
clip: true
cacheBuffer: resultsList.height * 2
onCurrentIndexChanged: {
cancelFlick()
if (currentIndex >= 0) {
positionViewAtIndex(currentIndex, ListView.Contain)
}
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
delegate: Rectangle {
width: appsList.width - Style.marginS * scaling
height: 65 * scaling
id: entry
property bool isSelected: mouseArea.containsMouse || (index === selectedIndex)
// Property to reliably track the current item's ID.
// This changes whenever the delegate is recycled for a new item.
property var currentClipboardId: modelData.isImage ? modelData.clipboardId : ""
// When this delegate is assigned a new image item, trigger the decode.
onCurrentClipboardIdChanged: {
// Check if it's a valid ID and if the data isn't already cached.
if (currentClipboardId && !ClipboardService.getImageData(currentClipboardId)) {
ClipboardService.decodeToDataUrl(currentClipboardId, modelData.mime, null)
}
}
width: resultsList.width - Style.marginS * scaling
height: entryHeight
radius: Style.radiusM * scaling
property bool isSelected: index === selectedIndex
color: (appCardArea.containsMouse || isSelected) ? Color.mSecondary : Color.mSurface
color: entry.isSelected ? Color.mTertiary : Color.mSurface
Behavior on color {
ColorAnimation {
duration: Style.animationFast
}
}
Behavior on border.color {
ColorAnimation {
duration: Style.animationFast
}
}
Behavior on border.width {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.OutCirc
}
}
@@ -417,84 +337,128 @@ NPanel {
anchors.margins: Style.marginM * scaling
spacing: Style.marginM * scaling
// App/clipboard icon with background
// Icon badge or Image preview
Rectangle {
Layout.preferredWidth: Style.baseWidgetSize * 1.25 * scaling
Layout.preferredHeight: Style.baseWidgetSize * 1.25 * scaling
radius: Style.radiusS * scaling
color: appCardArea.containsMouse ? Qt.darker(Color.mPrimary, 1.1) : Color.mSurfaceVariant
property bool iconLoaded: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand)
|| (iconImg.status === Image.Ready && iconImg.source !== ""
&& iconImg.status !== Image.Error && iconImg.source !== "")
visible: !searchText.startsWith(">calc")
Layout.preferredWidth: badgeSize
Layout.preferredHeight: badgeSize
radius: Style.radiusM * scaling
color: Color.mSurfaceVariant
clip: true
// Clipboard image display (pull from cache)
Image {
id: clipboardImage
// Image preview for clipboard images
NImageRounded {
id: imagePreview
anchors.fill: parent
anchors.margins: Style.marginXS * scaling
visible: modelData.type === 'image'
source: modelData.type === 'image' ? (CliphistService.imageDataById[modelData.id] || "") : ""
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
visible: modelData.isImage
imageRadius: Style.radiusM * scaling
// This property creates a dependency on the service's revision counter
readonly property int _rev: ClipboardService.revision
// Fetches from the service's cache.
// The dependency on `_rev` ensures this binding is re-evaluated when the cache is updated.
imagePath: {
_rev
return ClipboardService.getImageData(modelData.clipboardId) || ""
}
// Loading indicator
Rectangle {
anchors.fill: parent
visible: parent.status === Image.Loading
color: Color.mSurfaceVariant
BusyIndicator {
anchors.centerIn: parent
running: true
width: Style.baseWidgetSize * 0.5 * scaling
height: width
}
}
// Error fallback
onStatusChanged: status => {
if (status === Image.Error) {
iconLoader.visible = true
imagePreview.visible = false
}
}
}
IconImage {
id: iconImg
// Icon fallback
Loader {
id: iconLoader
anchors.fill: parent
anchors.margins: Style.marginXS * scaling
asynchronous: true
source: modelData.isCalculator ? "" : modelData.isClipboard ? "" : modelData.isCommand ? modelData.icon : Icons.iconFromName(
modelData.icon,
"application-x-executable")
visible: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand || parent.iconLoaded)
&& modelData.type !== 'image'
}
Rectangle {
anchors.fill: parent
anchors.margins: Style.marginXS * scaling
radius: Style.radiusXS * scaling
color: Color.mPrimary
opacity: Style.opacityMedium
visible: !parent.iconLoaded
visible: !modelData.isImage || imagePreview.status === Image.Error
active: visible
sourceComponent: Component {
IconImage {
anchors.fill: parent
source: modelData.icon ? AppIcons.iconFromName(modelData.icon, "application-x-executable") : ""
visible: modelData.icon && source !== ""
asynchronous: true
}
}
}
// Fallback text if no icon and no image
NText {
anchors.centerIn: parent
visible: !parent.iconLoaded && !(modelData.isCalculator || modelData.isClipboard || modelData.isCommand)
visible: !imagePreview.visible && !iconLoader.visible
text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?"
font.pointSize: Style.fontSizeXXL * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
color: Color.mOnPrimary
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
// Image type indicator overlay
Rectangle {
visible: modelData.isImage && imagePreview.visible
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 2 * scaling
width: formatLabel.width + 6 * scaling
height: formatLabel.height + 2 * scaling
radius: Style.radiusM * scaling
color: Color.mSurfaceVariant
NText {
id: formatLabel
anchors.centerIn: parent
text: {
if (!modelData.isImage)
return ""
const desc = modelData.description || ""
const parts = desc.split(" • ")
return parts[0] || "IMG"
}
font.pointSize: Style.fontSizeXXS * scaling
color: Color.mPrimary
}
}
}
// App info
// Text content
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXXS * scaling
spacing: 0 * scaling
NText {
text: modelData.name || "Unknown"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: (appCardArea.containsMouse || isSelected) ? Color.mOnPrimary : Color.mOnSurface
color: entry.isSelected ? Color.mOnTertiary : Color.mOnSurface
elide: Text.ElideRight
Layout.fillWidth: true
}
NText {
text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : modelData.isClipboard ? modelData.content : modelData.isCommand ? modelData.content : (modelData.genericName || modelData.comment || "")
font.pointSize: Style.fontSizeM * scaling
color: (appCardArea.containsMouse || isSelected) ? Color.mOnPrimary : Color.mOnSurface
text: modelData.description || ""
font.pointSize: Style.fontSizeS * scaling
color: entry.isSelected ? Color.mOnTertiary : Color.mOnSurfaceVariant
elide: Text.ElideRight
Layout.fillWidth: true
visible: text !== ""
@@ -503,41 +467,34 @@ NPanel {
}
MouseArea {
id: appCardArea
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedIndex = index
activateSelected()
ui.activate()
}
}
}
}
// No results message
NText {
text: searchText.trim() !== "" ? "No applications found" : "No applications available"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
horizontalAlignment: Text.AlignHCenter
NDivider {
Layout.fillWidth: true
visible: filteredEntries.length === 0
}
// Results count
// Status
NText {
text: searchText.startsWith(
">clip") ? (Settings.data.appLauncher.enableClipboardHistory ? `${filteredEntries.length} clipboard item${filteredEntries.length !== 1 ? 's' : ''}` : `Clipboard history is disabled`) : searchText.startsWith(
">calc") ? `${filteredEntries.length} result${filteredEntries.length
!== 1 ? 's' : ''}` : `${filteredEntries.length} application${filteredEntries.length
!== 1 ? 's' : ''}`
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurface
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
visible: searchText.trim() !== ""
text: {
if (results.length === 0)
return searchText ? "No results" : ""
const prefix = activePlugin?.name ? `${activePlugin.name}: ` : ""
return prefix + `${results.length} result${results.length !== 1 ? 's' : ''}`
}
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurfaceVariant
horizontalAlignment: Text.AlignCenter
}
}
}

View File

@@ -0,0 +1,177 @@
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Services
import "../../../Helpers/FuzzySort.js" as Fuzzysort
Item {
property var launcher: null
property string name: "Applications"
property bool handleSearch: true
property var entries: []
// Persistent usage tracking stored in cacheDir
property string usageFilePath: Settings.cacheDir + "launcher_app_usage.json"
// Debounced saver to avoid excessive IO
Timer {
id: saveTimer
interval: 750
repeat: false
onTriggered: usageFile.writeAdapter()
}
FileView {
id: usageFile
path: usageFilePath
printErrors: false
watchChanges: false
onLoadFailed: function (error) {
if (error.toString().includes("No such file") || error === 2) {
writeAdapter()
}
}
onAdapterUpdated: saveTimer.start()
JsonAdapter {
id: usageAdapter
// key: app id/command, value: integer count
property var counts: ({})
}
}
function init() {
loadApplications()
}
function onOpened() {
// Refresh apps when launcher opens
loadApplications()
}
function loadApplications() {
if (typeof DesktopEntries === 'undefined') {
Logger.warn("ApplicationsPlugin", "DesktopEntries service not available")
return
}
const allApps = DesktopEntries.applications.values || []
entries = allApps.filter(app => app && !app.noDisplay)
Logger.log("ApplicationsPlugin", `Loaded ${entries.length} applications`)
}
function getResults(query) {
if (!entries || entries.length === 0)
return []
if (!query || query.trim() === "") {
// Return all apps, optionally sorted by usage
let sorted
if (Settings.data.appLauncher.sortByMostUsed) {
sorted = entries.slice().sort((a, b) => {
const ua = getUsageCount(a)
const ub = getUsageCount(b)
if (ub !== ua)
return ub - ua
return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase())
})
} else {
sorted = entries.slice().sort((a, b) => (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase()))
}
return sorted.map(app => createResultEntry(app))
}
// Use fuzzy search if available, fallback to simple search
if (typeof Fuzzysort !== 'undefined') {
const fuzzyResults = Fuzzysort.go(query, entries, {
"keys": ["name", "comment", "genericName"],
"threshold": -1000,
"limit": 20
})
return fuzzyResults.map(result => createResultEntry(result.obj))
} else {
// Fallback to simple search
const searchTerm = query.toLowerCase()
return entries.filter(app => {
const name = (app.name || "").toLowerCase()
const comment = (app.comment || "").toLowerCase()
const generic = (app.genericName || "").toLowerCase()
return name.includes(searchTerm) || comment.includes(searchTerm) || generic.includes(searchTerm)
}).sort((a, b) => {
// Prioritize name matches
const aName = a.name.toLowerCase()
const bName = b.name.toLowerCase()
const aStarts = aName.startsWith(searchTerm)
const bStarts = bName.startsWith(searchTerm)
if (aStarts && !bStarts)
return -1
if (!aStarts && bStarts)
return 1
return aName.localeCompare(bName)
}).slice(0, 20).map(app => createResultEntry(app))
}
}
function createResultEntry(app) {
return {
"name": app.name || "Unknown",
"description": app.genericName || app.comment || "",
"icon": app.icon || "application-x-executable",
"isImage": false,
"onActivate": function () {
// Close the launcher/NPanel immediately without any animations.
// Ensures we are not preventing the future focusing of the app
launcher.closeCompleted()
Logger.log("ApplicationsPlugin", `Launching: ${app.name}`)
// Record usage and persist asynchronously
if (Settings.data.appLauncher.sortByMostUsed)
recordUsage(app)
if (Settings.data.appLauncher.useApp2Unit && app.id) {
Logger.log("ApplicationsPlugin", `Using app2unit for: ${app.id}`)
if (app.runInTerminal)
Quickshell.execDetached(["app2unit", "--", app.id + ".desktop"])
else
Quickshell.execDetached(["app2unit", "--"].concat(app.command))
} else if (app.execute) {
app.execute()
} else {
Logger.log("ApplicationsPlugin", `Could not launch: ${app.name}`)
}
}
}
}
// -------------------------
// Usage tracking helpers
function getAppKey(app) {
if (app && app.id)
return String(app.id)
if (app && app.command && app.command.join)
return app.command.join(" ")
return String(app && app.name ? app.name : "unknown")
}
function getUsageCount(app) {
const key = getAppKey(app)
const m = usageAdapter && usageAdapter.counts ? usageAdapter.counts : null
if (!m)
return 0
const v = m[key]
return typeof v === 'number' && isFinite(v) ? v : 0
}
function recordUsage(app) {
const key = getAppKey(app)
if (!usageAdapter.counts)
usageAdapter.counts = ({})
const current = getUsageCount(app)
usageAdapter.counts[key] = current + 1
// Trigger save via debounced timer
saveTimer.restart()
}
}

View File

@@ -0,0 +1,105 @@
import QtQuick
import qs.Services
import "../../../Helpers/AdvancedMath.js" as AdvancedMath
Item {
property var launcher: null
property string name: "Calculator"
function handleCommand(query) {
// Handle >calc command or direct math expressions after >
return query.startsWith(">calc") || (query.startsWith(">") && query.length > 1 && isMathExpression(query.substring(1)))
}
function commands() {
return [{
"name": ">calc",
"description": "Calculator - evaluate mathematical expressions",
"icon": "accessories-calculator",
"isImage": false,
"onActivate": function () {
launcher.setSearchText(">calc ")
}
}]
}
function getResults(query) {
let expression = ""
if (query.startsWith(">calc")) {
expression = query.substring(5).trim()
} else if (query.startsWith(">")) {
expression = query.substring(1).trim()
} else {
return []
}
if (!expression) {
return [{
"name": "Calculator",
"description": "Enter a mathematical expression",
"icon": "accessories-calculator",
"isImage": false,
"onActivate": function () {}
}]
}
try {
let result = AdvancedMath.evaluate(expression.trim())
return [{
"name": AdvancedMath.formatResult(result),
"description": `${expression} = ${result}`,
"icon": "accessories-calculator",
"isImage": false,
"onActivate": function () {
// TODO: copy entry to clipboard via ClipHist
launcher.close()
}
}]
} catch (error) {
return [{
"name": "Error",
"description": error.message || "Invalid expression",
"icon": "dialog-error",
"isImage": false,
"onActivate": function () {}
}]
}
}
function evaluateExpression(expr) {
// Sanitize input - only allow safe characters
const sanitized = expr.replace(/[^0-9\+\-\*\/\(\)\.\s\%]/g, '')
if (sanitized !== expr) {
throw new Error("Invalid characters in expression")
}
// Don't allow empty expressions
if (!sanitized.trim()) {
throw new Error("Empty expression")
}
try {
// Use Function constructor for safe evaluation
// This is safer than eval() but still evaluate math
const result = Function('"use strict"; return (' + sanitized + ')')()
// Check for valid result
if (!isFinite(result)) {
throw new Error("Result is not a finite number")
}
// Round to reasonable precision to avoid floating point issues
return Math.round(result * 1000000000) / 1000000000
} catch (e) {
throw new Error("Invalid mathematical expression")
}
}
function isMathExpression(expr) {
// Check if string looks like a math expression
// Allow digits, operators, parentheses, decimal points, and whitespace
return /^[\d\s\+\-\*\/\(\)\.\%]+$/.test(expr)
}
}

View File

@@ -0,0 +1,268 @@
import QtQuick
import Quickshell
import qs.Commons
import qs.Services
Item {
id: root
// Plugin metadata
property string name: "Clipboard History"
property var launcher: null
// Plugin capabilities
property bool handleSearch: false // Don't handle regular search
// Internal state
property bool isWaitingForData: false
property bool gotResults: false
property string lastSearchText: ""
// Listen for clipboard data updates
Connections {
target: ClipboardService
function onListCompleted() {
if (gotResults && (lastSearchText === searchText)) {
// Do not update results after the first fetch.
// This will avoid the list resetting every 2seconds when the service updates.
return
}
// Refresh results if we're waiting for data or if clipboard plugin is active
if (isWaitingForData || (launcher && launcher.searchText.startsWith(">clip"))) {
isWaitingForData = false
gotResults = true
if (launcher) {
launcher.updateResults()
}
}
}
}
// Initialize plugin
function init() {
Logger.log("ClipboardPlugin", "Initialized")
// Pre-load clipboard data if service is active
if (ClipboardService.active) {
ClipboardService.list(100)
}
}
// Called when launcher opens
function onOpened() {
isWaitingForData = true
gotResults = false
lastSearchText = ""
// Refresh clipboard history when launcher opens
if (ClipboardService.active) {
ClipboardService.list(100)
}
}
// Check if this plugin handles the command
function handleCommand(searchText) {
return searchText.startsWith(">clip")
}
// Return available commands when user types ">"
function commands() {
return [{
"name": ">clip",
"description": "Search clipboard history",
"icon": "text-x-generic",
"isImage": false,
"onActivate": function () {
launcher.setSearchText(">clip ")
}
}, {
"name": ">clip clear",
"description": "Clear all clipboard history",
"icon": "text-x-generic",
"isImage": false,
"onActivate": function () {
ClipboardService.wipeAll()
launcher.close()
}
}]
}
// Get search results
function getResults(searchText) {
if (!searchText.startsWith(">clip")) {
return []
}
lastSearchText = searchText
const results = []
const query = searchText.slice(5).trim()
// Check if clipboard service is not active
if (!ClipboardService.active) {
return [{
"name": "Clipboard History Disabled",
"description": "Enable clipboard history in settings or install cliphist",
"icon": "view-refresh",
"isImage": false,
"onActivate": function () {}
}]
}
// Special command: clear
if (query === "clear") {
return [{
"name": "Clear Clipboard History",
"description": "Remove all items from clipboard history",
"icon": "delete_sweep",
"isImage": false,
"onActivate": function () {
ClipboardService.wipeAll()
launcher.close()
}
}]
}
// Show loading state if data is being loaded
if (ClipboardService.loading || isWaitingForData) {
return [{
"name": "Loading clipboard history...",
"description": "Please wait",
"icon": "view-refresh",
"isImage": false,
"onActivate": function () {}
}]
}
// Get clipboard items
const items = ClipboardService.items || []
// If no items and we haven't tried loading yet, trigger a load
if (items.count === 0 && !ClipboardService.loading) {
isWaitingForData = true
ClipboardService.list(100)
return [{
"name": "Loading clipboard history...",
"description": "Please wait",
"icon": "view-refresh",
"isImage": false,
"onActivate": function () {}
}]
}
// Search clipboard items
const searchTerm = query.toLowerCase()
// Filter and format results
items.forEach(function (item) {
const preview = (item.preview || "").toLowerCase()
// Skip if search term doesn't match
if (searchTerm && preview.indexOf(searchTerm) === -1) {
return
}
// Format the result based on type
let entry
if (item.isImage) {
entry = formatImageEntry(item)
} else {
entry = formatTextEntry(item)
}
// Add activation handler
entry.onActivate = function () {
ClipboardService.copyToClipboard(item.id)
launcher.close()
}
results.push(entry)
})
// Show empty state if no results
if (results.length === 0) {
results.push({
"name": searchTerm ? "No matching clipboard items" : "Clipboard is empty",
"description": searchTerm ? `No items containing "${query}"` : "Copy something to see it here",
"icon": "text-x-generic",
"isImage": false,
"onActivate": function () {// Do nothing
}
})
}
//Logger.log("ClipboardPlugin", `Returning ${results.length} results for query: "${query}"`)
return results
}
// Helper: Format image clipboard entry
function formatImageEntry(item) {
const meta = parseImageMeta(item.preview)
// The launcher's delegate will now be responsible for fetching the image data.
// This function's role is to provide the necessary metadata for that request.
return {
"name": meta ? `Image ${meta.w}×${meta.h}` : "Image",
"description": meta ? `${meta.fmt} ${meta.size}` : item.mime || "Image data",
"icon": "image",
"isImage": true,
"imageWidth": meta ? meta.w : 0,
"imageHeight": meta ? meta.h : 0,
"clipboardId": item.id,
"mime": item.mime
}
}
// Helper: Format text clipboard entry with preview
function formatTextEntry(item) {
const preview = (item.preview || "").trim()
const lines = preview.split('\n').filter(l => l.trim())
// Use first line as title, limit length
let title = lines[0] || "Empty text"
if (title.length > 60) {
title = title.substring(0, 57) + "..."
}
// Use second line or character count as description
let description = ""
if (lines.length > 1) {
description = lines[1]
if (description.length > 80) {
description = description.substring(0, 77) + "..."
}
} else {
const chars = preview.length
const words = preview.split(/\s+/).length
description = `${chars} characters, ${words} word${words !== 1 ? 's' : ''}`
}
return {
"name": title,
"description": description,
"icon": "text-x-generic",
"isImage": false
}
}
// Helper: Parse image metadata from preview string
function parseImageMeta(preview) {
const re = /\[\[\s*binary data\s+([\d\.]+\s*(?:KiB|MiB|GiB|B))\s+(\w+)\s+(\d+)x(\d+)\s*\]\]/i
const match = (preview || "").match(re)
if (!match) {
return null
}
return {
"size": match[1],
"fmt": (match[2] || "").toUpperCase(),
"w": Number(match[3]),
"h": Number(match[4])
}
}
// Public method to get image data for a clipboard item
// This can be called by the launcher when rendering
function getImageForItem(clipboardId) {
return ClipboardService.getImageData ? ClipboardService.getImageData(clipboardId) : null
}
}

View File

@@ -12,6 +12,7 @@ Scope {
property bool unlockInProgress: false
property bool showFailure: false
property string errorMessage: ""
property string infoMessage: ""
property bool pamAvailable: typeof PamContext !== "undefined"
onCurrentTextChanged: {
@@ -28,12 +29,6 @@ Scope {
return
}
if (currentText === "") {
errorMessage = "Password required"
showFailure = true
return
}
root.unlockInProgress = true
errorMessage = ""
showFailure = false
@@ -48,11 +43,12 @@ Scope {
user: Quickshell.env("USER")
onPamMessage: {
Logger.log("LockContext", "PAM message:", message, "isError:", messageIsError, "responseRequired:",
responseRequired)
Logger.log("LockContext", "PAM message:", message, "isError:", messageIsError, "responseRequired:", responseRequired)
if (messageIsError) {
errorMessage = message
} else {
infoMessage = message
}
if (responseRequired) {

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,7 @@ Variants {
id: root
required property ShellScreen modelData
readonly property real scaling: ScalingService.scale(modelData)
readonly property real scaling: ScalingService.getScreenScale(modelData)
// Access the notification model from the service
property ListModel notificationModel: NotificationService.notificationModel
@@ -25,10 +25,7 @@ Variants {
property var removingNotifications: ({})
// If no notification display activated in settings, then show them all
active: Settings.isLoaded && modelData
&& (NotificationService.notificationModel.count > 0) ? (Settings.data.notifications.monitors.includes(
modelData.name)
|| (Settings.data.notifications.monitors.length === 0)) : false
active: Settings.isLoaded && modelData && (NotificationService.notificationModel.count > 0) ? (Settings.data.notifications.monitors.includes(modelData.name) || (Settings.data.notifications.monitors.length === 0)) : false
visible: (NotificationService.notificationModel.count > 0)
@@ -36,13 +33,50 @@ Variants {
screen: modelData
color: Color.transparent
// Position based on bar location
anchors.top: Settings.data.bar.position === "top"
anchors.bottom: Settings.data.bar.position === "bottom"
anchors.right: true
margins.top: Settings.data.bar.position === "top" ? (Style.barHeight + Style.marginM) * scaling : 0
margins.bottom: Settings.data.bar.position === "bottom" ? (Style.barHeight + Style.marginM) * scaling : 0
margins.right: Style.marginM * scaling
// Position based on bar location - always at top
anchors.top: true
anchors.right: Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
anchors.left: Settings.data.bar.position === "left"
margins.top: {
switch (Settings.data.bar.position) {
case "top":
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0)
default:
return Style.marginM * scaling
}
}
margins.bottom: {
switch (Settings.data.bar.position) {
case "bottom":
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginVertical * Style.marginXL * scaling : 0)
default:
return 0
}
}
margins.left: {
switch (Settings.data.bar.position) {
case "left":
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0)
default:
return 0
}
}
margins.right: {
switch (Settings.data.bar.position) {
case "right":
return (Style.barHeight + Style.marginM) * scaling + (Settings.data.bar.floating ? Settings.data.bar.marginHorizontal * Style.marginXL * scaling : 0)
case "top":
case "bottom":
return Style.marginM * scaling
default:
return 0
}
}
implicitWidth: 360 * scaling
implicitHeight: Math.min(notificationStack.implicitHeight, (NotificationService.maxVisible * 120) * scaling)
//WlrLayershell.layer: WlrLayer.Overlay
@@ -78,12 +112,12 @@ Variants {
}
// Main notification container
Column {
ColumnLayout {
id: notificationStack
// Position based on bar location
anchors.top: Settings.data.bar.position === "top" ? parent.top : undefined
anchors.bottom: Settings.data.bar.position === "bottom" ? parent.bottom : undefined
anchors.right: parent.right
// Position based on bar location - always at top
anchors.top: parent.top
anchors.right: (Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom") ? parent.right : undefined
anchors.left: Settings.data.bar.position === "left" ? parent.left : undefined
spacing: Style.marginS * scaling
width: 360 * scaling
visible: true
@@ -92,11 +126,12 @@ Variants {
Repeater {
model: notificationModel
delegate: Rectangle {
width: 360 * scaling
height: Math.max(80 * scaling, contentColumn.implicitHeight + (Style.marginM * 2 * scaling))
Layout.preferredWidth: 360 * scaling
Layout.preferredHeight: notificationLayout.implicitHeight + (Style.marginL * 2 * scaling)
Layout.maximumHeight: Layout.preferredHeight
clip: true
radius: Style.radiusM * scaling
border.color: Color.mPrimary
radius: Style.radiusL * scaling
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
color: Color.mSurface
@@ -105,6 +140,17 @@ Variants {
property real opacityValue: 0.0
property bool isRemoving: false
// Right-click to dismiss
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: {
if (mouse.button === Qt.RightButton) {
animateOut()
}
}
}
// Scale and fade-in animation
scale: scaleValue
opacity: opacityValue
@@ -156,67 +202,137 @@ Variants {
}
}
Column {
id: contentColumn
ColumnLayout {
id: notificationLayout
anchors.fill: parent
anchors.margins: Style.marginM * scaling
spacing: Style.marginS * scaling
anchors.rightMargin: (Style.marginM + 32) * scaling // Leave space for close button
spacing: Style.marginM * scaling
// Header section with app name and timestamp
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS * scaling
NText {
text: (model.appName || model.desktopEntry) || "Unknown App"
text: `${(model.appName || model.desktopEntry) || "Unknown App"} · ${NotificationService.formatTimestamp(model.timestamp)}`
color: Color.mSecondary
font.pointSize: Style.fontSizeXS * scaling
}
Rectangle {
width: 6 * scaling
height: 6 * scaling
Layout.preferredWidth: 6 * scaling
Layout.preferredHeight: 6 * scaling
radius: Style.radiusXS * scaling
color: (model.urgency === NotificationUrgency.Critical) ? Color.mError : (model.urgency === NotificationUrgency.Low) ? Color.mOnSurface : Color.mPrimary
Layout.alignment: Qt.AlignVCenter
}
Item {
Layout.fillWidth: true
}
NText {
text: NotificationService.formatTimestamp(model.timestamp)
color: Color.mOnSurface
font.pointSize: Style.fontSizeXS * scaling
}
// Main content section
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
// Image
NImageCircled {
Layout.preferredWidth: 40 * scaling
Layout.preferredHeight: 40 * scaling
Layout.alignment: Qt.AlignTop
imagePath: model.image && model.image !== "" ? model.image : ""
borderColor: Color.transparent
borderWidth: 0
visible: (model.image && model.image !== "")
}
// Text content
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginS * scaling
NText {
text: model.summary || "No summary"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightMedium
color: Color.mOnSurface
textFormat: Text.PlainText
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Layout.fillWidth: true
maximumLineCount: 3
elide: Text.ElideRight
}
NText {
text: model.body || ""
font.pointSize: Style.fontSizeM * scaling
color: Color.mOnSurface
textFormat: Text.PlainText
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Layout.fillWidth: true
maximumLineCount: 5
elide: Text.ElideRight
visible: text.length > 0
}
}
}
NText {
text: model.summary || "No summary"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
wrapMode: Text.Wrap
width: 300 * scaling
maximumLineCount: 3
elide: Text.ElideRight
}
// Notification actions
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS * scaling
visible: model.rawNotification && model.rawNotification.actions && model.rawNotification.actions.length > 0
NText {
text: model.body || ""
font.pointSize: Style.fontSizeXS * scaling
color: Color.mOnSurface
wrapMode: Text.Wrap
width: 300 * scaling
maximumLineCount: 5
elide: Text.ElideRight
}
property var notificationActions: model.rawNotification ? model.rawNotification.actions : []
// Actions removed
Repeater {
model: parent.notificationActions
delegate: NButton {
text: {
var actionText = modelData.text || "Open"
// If text contains comma, take the part after the comma (the display text)
if (actionText.includes(",")) {
return actionText.split(",")[1] || actionText
}
return actionText
}
fontSize: Style.fontSizeS * scaling
backgroundColor: Color.mPrimary
textColor: Color.mOnPrimary
hoverColor: Color.mSecondary
pressColor: Color.mTertiary
outlined: false
customHeight: 32 * scaling
Layout.preferredHeight: 32 * scaling
onClicked: {
if (modelData && modelData.invoke) {
modelData.invoke()
}
}
}
}
// Spacer to push buttons to the left if needed
Item {
Layout.fillWidth: true
}
}
}
// Close button positioned absolutely
NIconButton {
icon: "close"
tooltipText: "Close"
sizeRatio: 0.8
tooltipText: "Close."
baseSize: Style.baseWidgetSize * 0.6
anchors.top: parent.top
anchors.topMargin: Style.marginM * scaling
anchors.right: parent.right
anchors.margins: Style.marginS * scaling
anchors.rightMargin: Style.marginM * scaling
onClicked: {
animateOut()

View File

@@ -12,9 +12,9 @@ import qs.Widgets
NPanel {
id: root
panelWidth: 380 * scaling
panelHeight: 500 * scaling
panelAnchorRight: true
preferredWidth: 380
preferredHeight: 500
panelKeyboardFocus: true
panelContent: Rectangle {
id: notificationRect
@@ -25,12 +25,13 @@ NPanel {
anchors.margins: Style.marginL * scaling
spacing: Style.marginM * scaling
// Header section
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
NIcon {
text: "notifications"
icon: "bell"
font.pointSize: Style.fontSizeXXL * scaling
color: Color.mPrimary
}
@@ -44,16 +45,27 @@ NPanel {
}
NIconButton {
icon: "delete"
tooltipText: "Clear history"
sizeRatio: 0.8
onClicked: NotificationService.clearHistory()
icon: Settings.data.notifications.doNotDisturb ? "bell-off" : "bell"
tooltipText: Settings.data.notifications.doNotDisturb ? "'Do Not Disturb' is enabled." : "'Do Not Disturb' is disabled."
baseSize: Style.baseWidgetSize * 0.8
onClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
onRightClicked: Settings.data.notifications.doNotDisturb = !Settings.data.notifications.doNotDisturb
}
NIconButton {
icon: "trash"
tooltipText: "Clear history."
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
NotificationService.clearHistory()
root.close()
}
}
NIconButton {
icon: "close"
tooltipText: "Close"
sizeRatio: 0.8
tooltipText: "Close."
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
root.close()
}
@@ -65,42 +77,54 @@ NPanel {
}
// Empty state when no notifications
Item {
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignHCenter
visible: NotificationService.historyModel.count === 0
spacing: Style.marginL * scaling
ColumnLayout {
anchors.centerIn: parent
spacing: Style.marginM * scaling
Item {
Layout.fillHeight: true
}
NIcon {
text: "notifications_off"
font.pointSize: Style.fontSizeXXXL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NIcon {
icon: "bell-off"
font.pointSize: 64 * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "No notifications"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurface
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "No notifications"
font.pointSize: Style.fontSizeL * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Your notifications will show up here as they arrive."
font.pointSize: Style.fontSizeNormal * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
NText {
text: "Your notifications will show up here as they arrive."
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
}
Item {
Layout.fillHeight: true
}
}
ListView {
// Notification list
NListView {
id: notificationList
Layout.fillWidth: true
Layout.fillHeight: true
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AsNeeded
model: NotificationService.historyModel
spacing: Style.marginM * scaling
clip: true
@@ -108,32 +132,46 @@ NPanel {
visible: NotificationService.historyModel.count > 0
delegate: Rectangle {
width: notificationList ? notificationList.width : 380 * scaling
height: Math.max(80, notificationContent.height + 30)
width: notificationList.width
height: notificationLayout.implicitHeight + (Style.marginM * scaling * 2)
radius: Style.radiusM * scaling
color: notificationMouseArea.containsMouse ? Color.mSecondary : Color.mSurfaceVariant
color: Color.mSurfaceVariant
border.color: Qt.alpha(Color.mOutline, Style.opacityMedium)
border.width: Math.max(1, Style.borderS * scaling)
RowLayout {
anchors {
fill: parent
margins: Style.marginM * scaling
}
id: notificationLayout
anchors.fill: parent
anchors.margins: Style.marginM * scaling
spacing: Style.marginM * scaling
// Notification content
Column {
id: notificationContent
// App icon (same style as popup)
NImageCircled {
Layout.preferredWidth: 28 * scaling
Layout.preferredHeight: 28 * scaling
Layout.alignment: Qt.AlignVCenter
// Prefer stable themed icons over transient image paths
imagePath: (appIcon && appIcon !== "") ? (AppIcons.iconFromName(appIcon, "application-x-executable") || appIcon) : ((AppIcons.iconForAppId(desktopEntry || appName, "application-x-executable") || (image && image !== "" ? image : AppIcons.iconFromName("application-x-executable", "application-x-executable"))))
borderColor: Color.transparent
borderWidth: 0
visible: true
}
// Notification content column
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
Layout.maximumWidth: notificationList.width - (Style.marginM * scaling * 4) // Account for margins and delete button
spacing: Style.marginXXS * scaling
NText {
text: (summary || "No summary").substring(0, 100)
font.pointSize: Style.fontSizeM * scaling
font.weight: Font.Medium
color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mPrimary
color: Color.mPrimary
textFormat: Text.PlainText
wrapMode: Text.Wrap
width: parent.width - 60
Layout.fillWidth: true
maximumLineCount: 2
elide: Text.ElideRight
}
@@ -141,25 +179,29 @@ NPanel {
NText {
text: (body || "").substring(0, 150)
font.pointSize: Style.fontSizeXS * scaling
color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface
color: Color.mOnSurface
textFormat: Text.PlainText
wrapMode: Text.Wrap
width: parent.width - 60
Layout.fillWidth: true
maximumLineCount: 3
elide: Text.ElideRight
visible: text.length > 0
}
NText {
text: NotificationService.formatTimestamp(timestamp)
font.pointSize: Style.fontSizeXS * scaling
color: notificationMouseArea.containsMouse ? Color.mSurface : Color.mOnSurface
color: Color.mOnSurface
Layout.fillWidth: true
}
}
// Trash icon button
// Delete button
NIconButton {
icon: "delete"
tooltipText: "Delete notification"
sizeRatio: 0.7
icon: "trash"
tooltipText: "Delete notification."
baseSize: Style.baseWidgetSize * 0.7
Layout.alignment: Qt.AlignTop
onClicked: {
Logger.log("NotificationHistory", "Removing notification:", summary)
@@ -172,7 +214,7 @@ NPanel {
MouseArea {
id: notificationMouseArea
anchors.fill: parent
anchors.rightMargin: Style.marginL * 3 * scaling
anchors.rightMargin: Style.marginXL * scaling
hoverEnabled: true
}
}

View File

@@ -1,4 +1,5 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
@@ -12,10 +13,11 @@ import qs.Widgets
NPanel {
id: root
panelWidth: 440 * scaling
panelHeight: 380 * scaling
preferredWidth: 440
preferredHeight: 410
panelAnchorHorizontalCenter: true
panelAnchorVerticalCenter: true
panelKeyboardFocus: true
// Timer properties
property int timerDuration: 9000 // 9 seconds
@@ -23,9 +25,44 @@ NPanel {
property bool timerActive: false
property int timeRemaining: 0
// Cancel timer when panel is closing
// Navigation properties
property int selectedIndex: 0
readonly property var powerOptions: [{
"action": "lock",
"icon": "lock",
"title": "Lock",
"subtitle": "Lock your session"
}, {
"action": "suspend",
"icon": "suspend",
"title": "Suspend",
"subtitle": "Put the system to sleep"
}, {
"action": "reboot",
"icon": "reboot",
"title": "Reboot",
"subtitle": "Restart the system"
}, {
"action": "logout",
"icon": "logout",
"title": "Logout",
"subtitle": "End your session"
}, {
"action": "shutdown",
"icon": "shutdown",
"title": "Shutdown",
"subtitle": "Turn off the system",
"isShutdown": true
}]
// Lifecycle handlers
onOpened: {
selectedIndex = 0
}
onClosed: {
cancelTimer()
selectedIndex = 0
}
// Timer management
@@ -79,6 +116,38 @@ NPanel {
root.close()
}
// Navigation functions
function selectNext() {
if (powerOptions.length > 0) {
selectedIndex = Math.min(selectedIndex + 1, powerOptions.length - 1)
}
}
function selectPrevious() {
if (powerOptions.length > 0) {
selectedIndex = Math.max(selectedIndex - 1, 0)
}
}
function selectFirst() {
selectedIndex = 0
}
function selectLast() {
if (powerOptions.length > 0) {
selectedIndex = powerOptions.length - 1
} else {
selectedIndex = 0
}
}
function activate() {
if (powerOptions.length > 0 && powerOptions[selectedIndex]) {
const option = powerOptions[selectedIndex]
startTimer(option.action)
}
}
// Countdown timer
Timer {
id: countdownTimer
@@ -93,8 +162,93 @@ NPanel {
}
panelContent: Rectangle {
id: ui
color: Color.transparent
// Keyboard shortcuts
Shortcut {
sequence: "Ctrl+K"
onActivated: ui.selectPrevious()
enabled: root.opened
}
Shortcut {
sequence: "Ctrl+J"
onActivated: ui.selectNext()
enabled: root.opened
}
Shortcut {
sequence: "Up"
onActivated: ui.selectPrevious()
enabled: root.opened
}
Shortcut {
sequence: "Down"
onActivated: ui.selectNext()
enabled: root.opened
}
Shortcut {
sequence: "Home"
onActivated: ui.selectFirst()
enabled: root.opened
}
Shortcut {
sequence: "End"
onActivated: ui.selectLast()
enabled: root.opened
}
Shortcut {
sequence: "Return"
onActivated: ui.activate()
enabled: root.opened
}
Shortcut {
sequence: "Enter"
onActivated: ui.activate()
enabled: root.opened
}
Shortcut {
sequence: "Escape"
onActivated: {
if (timerActive) {
cancelTimer()
} else {
cancelTimer()
root.close()
}
}
context: Qt.WidgetShortcut
enabled: root.opened
}
// Navigation functions
function selectNext() {
root.selectNext()
}
function selectPrevious() {
root.selectPrevious()
}
function selectFirst() {
root.selectFirst()
}
function selectLast() {
root.selectLast()
}
function activate() {
root.activate()
}
ColumnLayout {
anchors.fill: parent
anchors.topMargin: Style.marginL * scaling
@@ -109,8 +263,7 @@ NPanel {
Layout.preferredHeight: Style.baseWidgetSize * 0.8 * scaling
NText {
text: timerActive ? `${pendingAction.charAt(0).toUpperCase() + pendingAction.slice(1)} in ${Math.ceil(
timeRemaining / 1000)} seconds...` : "Power Options"
text: timerActive ? `${pendingAction.charAt(0).toUpperCase() + pendingAction.slice(1)} in ${Math.ceil(timeRemaining / 1000)} seconds...` : "Power Menu"
font.weight: Style.fontWeightBold
font.pointSize: Style.fontSizeL * scaling
color: timerActive ? Color.mPrimary : Color.mOnSurface
@@ -123,10 +276,10 @@ NPanel {
}
NIconButton {
icon: timerActive ? "back_hand" : "close"
icon: timerActive ? "stop" : "close"
tooltipText: timerActive ? "Cancel Timer" : "Close"
Layout.alignment: Qt.AlignVCenter
colorBg: timerActive ? Color.applyOpacity(Color.mError, "20") : Color.transparent
colorBg: timerActive ? Qt.alpha(Color.mError, 0.08) : Color.transparent
colorFg: timerActive ? Color.mError : Color.mOnSurface
onClicked: {
if (timerActive) {
@@ -139,60 +292,30 @@ NPanel {
}
}
NDivider {
Layout.fillWidth: true
}
// Power options
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
// Lock Screen
PowerButton {
Layout.fillWidth: true
icon: "lock_outline"
title: "Lock"
subtitle: "Lock your session"
onClicked: startTimer("lock")
pending: timerActive && pendingAction === "lock"
}
// Suspend
PowerButton {
Layout.fillWidth: true
icon: "bedtime"
title: "Suspend"
subtitle: "Put the system to sleep"
onClicked: startTimer("suspend")
pending: timerActive && pendingAction === "suspend"
}
// Reboot
PowerButton {
Layout.fillWidth: true
icon: "refresh"
title: "Reboot"
subtitle: "Restart the system"
onClicked: startTimer("reboot")
pending: timerActive && pendingAction === "reboot"
}
// Logout
PowerButton {
Layout.fillWidth: true
icon: "exit_to_app"
title: "Logout"
subtitle: "End your session"
onClicked: startTimer("logout")
pending: timerActive && pendingAction === "logout"
}
// Shutdown
PowerButton {
Layout.fillWidth: true
icon: "power_settings_new"
title: "Shutdown"
subtitle: "Turn off the system"
onClicked: startTimer("shutdown")
pending: timerActive && pendingAction === "shutdown"
isShutdown: true
Repeater {
model: powerOptions
delegate: PowerButton {
Layout.fillWidth: true
icon: modelData.icon
title: modelData.title
subtitle: modelData.subtitle
isShutdown: modelData.isShutdown || false
isSelected: index === selectedIndex
onClicked: {
selectedIndex = index
startTimer(modelData.action)
}
pending: timerActive && pendingAction === modelData.action
}
}
}
}
@@ -207,16 +330,19 @@ NPanel {
property string subtitle: ""
property bool pending: false
property bool isShutdown: false
property bool isSelected: false
signal clicked
height: Style.baseWidgetSize * 1.6 * scaling
radius: Style.radiusS * scaling
color: {
if (pending)
return Color.applyOpacity(Color.mPrimary, "20")
if (mouseArea.containsMouse)
return Color.mSecondary
if (pending) {
return Qt.alpha(Color.mPrimary, 0.08)
}
if (isSelected || mouseArea.containsMouse) {
return Color.mTertiary
}
return Color.transparent
}
@@ -238,14 +364,13 @@ NPanel {
id: iconElement
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
text: buttonRoot.icon
icon: buttonRoot.icon
color: {
if (buttonRoot.pending)
return Color.mPrimary
if (buttonRoot.isShutdown && !mouseArea.containsMouse)
if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse)
return Color.mError
if (mouseArea.containsMouse)
if (buttonRoot.isSelected || mouseArea.containsMouse)
return Color.mOnTertiary
return Color.mOnSurface
}
@@ -262,7 +387,7 @@ NPanel {
}
// Text content in the middle
Column {
ColumnLayout {
anchors.left: iconElement.right
anchors.right: pendingIndicator.visible ? pendingIndicator.left : parent.right
anchors.verticalCenter: parent.verticalCenter
@@ -277,9 +402,9 @@ NPanel {
color: {
if (buttonRoot.pending)
return Color.mPrimary
if (buttonRoot.isShutdown && !mouseArea.containsMouse)
if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse)
return Color.mError
if (mouseArea.containsMouse)
if (buttonRoot.isSelected || mouseArea.containsMouse)
return Color.mOnTertiary
return Color.mOnSurface
}
@@ -302,9 +427,9 @@ NPanel {
color: {
if (buttonRoot.pending)
return Color.mPrimary
if (buttonRoot.isShutdown && !mouseArea.containsMouse)
if (buttonRoot.isShutdown && !buttonRoot.isSelected && !mouseArea.containsMouse)
return Color.mError
if (mouseArea.containsMouse)
if (buttonRoot.isSelected || mouseArea.containsMouse)
return Color.mOnTertiary
return Color.mOnSurfaceVariant
}

View File

@@ -0,0 +1,527 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Services
NBox {
id: root
property string sectionName: ""
property string sectionId: ""
property var widgetModel: []
property var availableWidgets: []
readonly property real miniButtonSize: Style.baseWidgetSize * 0.65
signal addWidget(string widgetId, string section)
signal removeWidget(string section, int index)
signal reorderWidget(string section, int fromIndex, int toIndex)
signal updateWidgetSettings(string section, int index, var settings)
signal dragPotentialStarted
signal dragPotentialEnded
color: Color.mSurface
Layout.fillWidth: true
Layout.minimumHeight: {
var widgetCount = widgetModel.length
if (widgetCount === 0)
return 140 * scaling
var availableWidth = parent.width
var avgWidgetWidth = 150 * scaling
var widgetsPerRow = Math.max(1, Math.floor(availableWidth / avgWidgetWidth))
var rows = Math.ceil(widgetCount / widgetsPerRow)
return (50 + 20 + (rows * 48) + ((rows - 1) * Style.marginS) + 20) * scaling
}
// Generate widget color from name checksum
function getWidgetColor(widget) {
const totalSum = JSON.stringify(widget).split('').reduce((acc, character) => {
return acc + character.charCodeAt(0)
}, 0)
switch (totalSum % 6) {
case 0:
return [Color.mPrimary, Color.mOnPrimary]
case 1:
return [Color.mSecondary, Color.mOnSecondary]
case 2:
return [Color.mTertiary, Color.mOnTertiary]
case 3:
return [Color.mError, Color.mOnError]
case 4:
return [Color.mOnSurface, Color.mSurface]
case 5:
return [Color.mOnSurfaceVariant, Color.mSurfaceVariant]
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginL * scaling
spacing: Style.marginM * scaling
RowLayout {
Layout.fillWidth: true
NText {
text: sectionName + " Section"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mOnSurface
Layout.alignment: Qt.AlignVCenter
}
Item {
Layout.fillWidth: true
}
NComboBox {
id: comboBox
model: availableWidgets
label: ""
description: ""
placeholder: "Select a widget to add..."
onSelected: key => comboBox.currentKey = key
popupHeight: 340 * scaling
Layout.alignment: Qt.AlignVCenter
}
NIconButton {
icon: "add"
colorBg: Color.mPrimary
colorFg: Color.mOnPrimary
colorBgHover: Color.mSecondary
colorFgHover: Color.mOnSecondary
enabled: comboBox.currentKey !== ""
tooltipText: "Add widget to section"
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: Style.marginS * scaling
onClicked: {
if (comboBox.currentKey !== "") {
addWidget(comboBox.currentKey, sectionId)
comboBox.currentKey = ""
}
}
}
}
// Drag and Drop Widget Area
Item {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 65 * scaling
clip: false // Don't clip children so ghost can move freely
Flow {
id: widgetFlow
anchors.fill: parent
spacing: Style.marginS * scaling
flow: Flow.LeftToRight
Repeater {
model: widgetModel
delegate: Rectangle {
id: widgetItem
required property int index
required property var modelData
width: widgetContent.implicitWidth + Style.marginL * scaling
height: Style.baseWidgetSize * 1.15 * scaling
radius: Style.radiusL * scaling
color: root.getWidgetColor(modelData)[0]
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
// Store the widget index for drag operations
property int widgetIndex: index
readonly property int buttonsWidth: Math.round(20 * scaling)
readonly property int buttonsCount: 1 + BarWidgetRegistry.widgetHasUserSettings(modelData.id)
// Visual feedback during drag
opacity: flowDragArea.draggedIndex === index ? 0.5 : 1.0
scale: flowDragArea.draggedIndex === index ? 0.95 : 1.0
z: flowDragArea.draggedIndex === index ? 1000 : 0
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
}
}
Behavior on scale {
NumberAnimation {
duration: Style.animationFast
}
}
RowLayout {
id: widgetContent
anchors.centerIn: parent
spacing: Style.marginXXS * scaling
NText {
text: modelData.id
font.pointSize: Style.fontSizeS * scaling
color: root.getWidgetColor(modelData)[1]
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
Layout.preferredWidth: 80 * scaling
}
RowLayout {
spacing: 0
Layout.preferredWidth: buttonsCount * buttonsWidth
Loader {
active: BarWidgetRegistry.widgetHasUserSettings(modelData.id)
sourceComponent: NIconButton {
icon: "settings"
baseSize: miniButtonSize
colorBorder: Qt.alpha(Color.mOutline, Style.opacityLight)
colorBg: Color.mOnSurface
colorFg: Color.mOnPrimary
colorBgHover: Qt.alpha(Color.mOnPrimary, Style.opacityLight)
colorFgHover: Color.mOnPrimary
onClicked: {
var component = Qt.createComponent(Qt.resolvedUrl("BarWidgetSettingsDialog.qml"))
function instantiateAndOpen() {
var dialog = component.createObject(root, {
"widgetIndex": index,
"widgetData": modelData,
"widgetId": modelData.id,
"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())
}
})
}
}
}
}
NIconButton {
icon: "close"
baseSize: miniButtonSize
colorBorder: Qt.alpha(Color.mOutline, Style.opacityLight)
colorBg: Color.mOnSurface
colorFg: Color.mOnPrimary
colorBgHover: Qt.alpha(Color.mOnPrimary, Style.opacityLight)
colorFgHover: Color.mOnPrimary
onClicked: {
removeWidget(sectionId, index)
}
}
}
}
}
}
}
// Ghost/Clone widget for dragging
Rectangle {
id: dragGhost
width: 0
height: Style.baseWidgetSize * 1.15 * scaling
radius: Style.radiusL * scaling
color: Color.transparent
border.color: Color.mOutline
border.width: Math.max(1, Style.borderS * scaling)
opacity: 0.7
visible: flowDragArea.dragStarted
z: 2000
clip: false // Ensure ghost isn't clipped
Text {
id: ghostText
anchors.centerIn: parent
font.pointSize: Style.fontSizeS * scaling
color: Color.mOnPrimary
}
}
// Drop indicator - visual feedback for where the widget will be inserted
Rectangle {
id: dropIndicator
width: 3 * scaling
height: Style.baseWidgetSize * 1.15 * scaling
radius: width / 2
color: Color.mPrimary
opacity: 0
visible: opacity > 0
z: 1999
SequentialAnimation on opacity {
id: pulseAnimation
running: false
loops: Animation.Infinite
NumberAnimation {
to: 1
duration: 400
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 0.6
duration: 400
easing.type: Easing.InOutQuad
}
}
Behavior on x {
NumberAnimation {
duration: 100
easing.type: Easing.OutCubic
}
}
Behavior on y {
NumberAnimation {
duration: 100
easing.type: Easing.OutCubic
}
}
}
// MouseArea for drag and drop
MouseArea {
id: flowDragArea
anchors.fill: parent
z: -1
acceptedButtons: Qt.LeftButton
preventStealing: false
propagateComposedEvents: false
hoverEnabled: true // Always track mouse for drag operations
property point startPos: Qt.point(0, 0)
property bool dragStarted: false
property bool potentialDrag: false // Track if we're in a potential drag interaction
property int draggedIndex: -1
property real dragThreshold: 15 * scaling
property Item draggedWidget: null
property int dropTargetIndex: -1
property var draggedModelData: null
// Drop position calculation
function updateDropIndicator(mouseX, mouseY) {
if (!dragStarted || draggedIndex === -1) {
dropIndicator.opacity = 0
pulseAnimation.running = false
return
}
let bestIndex = -1
let bestPosition = null
let minDistance = Infinity
// Check position relative to each widget
for (var i = 0; i < widgetModel.length; i++) {
if (i === draggedIndex)
continue
const widget = widgetFlow.children[i]
if (!widget || widget.widgetIndex === undefined)
continue
// Check distance to left edge (insert before)
const leftDist = Math.sqrt(Math.pow(mouseX - widget.x, 2) + Math.pow(mouseY - (widget.y + widget.height / 2), 2))
// Check distance to right edge (insert after)
const rightDist = Math.sqrt(Math.pow(mouseX - (widget.x + widget.width), 2) + Math.pow(mouseY - (widget.y + widget.height / 2), 2))
if (leftDist < minDistance) {
minDistance = leftDist
bestIndex = i
bestPosition = Qt.point(widget.x - dropIndicator.width / 2 - Style.marginXS * scaling, widget.y)
}
if (rightDist < minDistance) {
minDistance = rightDist
bestIndex = i + 1
bestPosition = Qt.point(widget.x + widget.width + Style.marginXS * scaling - dropIndicator.width / 2, widget.y)
}
}
// Check if we should insert at position 0 (very beginning)
if (widgetModel.length > 0 && draggedIndex !== 0) {
const firstWidget = widgetFlow.children[0]
if (firstWidget) {
const dist = Math.sqrt(Math.pow(mouseX, 2) + Math.pow(mouseY - firstWidget.y, 2))
if (dist < minDistance && mouseX < firstWidget.x + firstWidget.width / 2) {
minDistance = dist
bestIndex = 0
bestPosition = Qt.point(Math.max(0, firstWidget.x - dropIndicator.width - Style.marginS * scaling), firstWidget.y)
}
}
}
// Only show indicator if we're close enough and it's a different position
if (minDistance < 80 * scaling && bestIndex !== -1) {
// Adjust index if we're moving forward
let adjustedIndex = bestIndex
if (bestIndex > draggedIndex) {
adjustedIndex = bestIndex - 1
}
// Don't show if it's the same position
if (adjustedIndex === draggedIndex) {
dropIndicator.opacity = 0
pulseAnimation.running = false
dropTargetIndex = -1
return
}
dropTargetIndex = adjustedIndex
if (bestPosition) {
dropIndicator.x = bestPosition.x
dropIndicator.y = bestPosition.y
dropIndicator.opacity = 1
if (!pulseAnimation.running) {
pulseAnimation.running = true
}
}
} else {
dropIndicator.opacity = 0
pulseAnimation.running = false
dropTargetIndex = -1
}
}
onPressed: mouse => {
startPos = Qt.point(mouse.x, mouse.y)
dragStarted = false
potentialDrag = false
draggedIndex = -1
draggedWidget = null
dropTargetIndex = -1
draggedModelData = null
// Find which widget was clicked
for (var i = 0; i < widgetModel.length; i++) {
const widget = widgetFlow.children[i]
if (widget && widget.widgetIndex !== undefined) {
if (mouse.x >= widget.x && mouse.x <= widget.x + widget.width && mouse.y >= widget.y && mouse.y <= widget.y + widget.height) {
const localX = mouse.x - widget.x
const buttonsStartX = widget.width - (widget.buttonsCount * widget.buttonsWidth)
if (localX < buttonsStartX) {
// This is a draggable area - prevent panel close immediately
draggedIndex = widget.widgetIndex
draggedWidget = widget
draggedModelData = widget.modelData
potentialDrag = true
preventStealing = true
// Signal that interaction started (prevents panel close)
root.dragPotentialStarted()
break
} else {
// This is a button area - let the click through
mouse.accepted = false
return
}
}
}
}
}
onPositionChanged: mouse => {
if (draggedIndex !== -1 && potentialDrag) {
const deltaX = mouse.x - startPos.x
const deltaY = mouse.y - startPos.y
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
if (!dragStarted && distance > dragThreshold) {
dragStarted = true
// Setup ghost widget
if (draggedWidget) {
dragGhost.width = draggedWidget.width
dragGhost.color = root.getWidgetColor(draggedModelData)[0]
ghostText.text = draggedModelData.id
}
}
if (dragStarted) {
// Move ghost widget
dragGhost.x = mouse.x - dragGhost.width / 2
dragGhost.y = mouse.y - dragGhost.height / 2
// Update drop indicator
updateDropIndicator(mouse.x, mouse.y)
}
}
}
onReleased: mouse => {
if (dragStarted && dropTargetIndex !== -1 && dropTargetIndex !== draggedIndex) {
// Perform the reorder
reorderWidget(sectionId, draggedIndex, dropTargetIndex)
}
// Always signal end of interaction if we started one
if (potentialDrag) {
root.dragPotentialEnded()
}
// Reset everything
dragStarted = false
potentialDrag = false
draggedIndex = -1
draggedWidget = null
dropTargetIndex = -1
draggedModelData = null
preventStealing = false
dropIndicator.opacity = 0
pulseAnimation.running = false
dragGhost.width = 0
}
onExited: {
if (dragStarted) {
// Hide drop indicator when mouse leaves, but keep ghost visible
dropIndicator.opacity = 0
pulseAnimation.running = false
}
}
onCanceled: {
// Handle cancel (e.g., ESC key pressed during drag)
if (potentialDrag) {
root.dragPotentialEnded()
}
// Reset everything
dragStarted = false
potentialDrag = false
draggedIndex = -1
draggedWidget = null
dropTargetIndex = -1
draggedModelData = null
preventStealing = false
dropIndicator.opacity = 0
pulseAnimation.running = false
dragGhost.width = 0
}
}
}
}
}

View File

@@ -0,0 +1,136 @@
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: 500 * 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",
"KeyboardLayout": "WidgetSettings/KeyboardLayoutSettings.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
spacing: 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,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
// Local state
property string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
property int valueWarningThreshold: widgetData.warningThreshold !== undefined ? widgetData.warningThreshold : widgetMetadata.warningThreshold
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.displayMode = valueDisplayMode
settings.warningThreshold = valueWarningThreshold
return settings
}
NComboBox {
label: "Display mode"
description: "Choose how you'd like this value to appear."
minimumWidth: 134 * scaling
model: ListModel {
ListElement {
key: "onhover"
name: "On Hover"
}
ListElement {
key: "alwaysShow"
name: "Always Show"
}
ListElement {
key: "alwaysHide"
name: "Always Hide"
}
}
currentKey: root.valueDisplayMode
onSelected: key => root.valueDisplayMode = key
}
NSpinBox {
label: "Low battery warning threshold"
description: "Show a warning when battery falls below this percentage."
value: valueWarningThreshold
suffix: "%"
minimum: 5
maximum: 50
onValueChanged: valueWarningThreshold = value
}
}

View File

@@ -0,0 +1,46 @@
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 string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.displayMode = valueDisplayMode
return settings
}
NComboBox {
label: "Display mode"
description: "Choose how you'd like this value to appear."
minimumWidth: 134 * scaling
model: ListModel {
ListElement {
key: "onhover"
name: "On Hover"
}
ListElement {
key: "alwaysShow"
name: "Always Show"
}
ListElement {
key: "alwaysHide"
name: "Always Hide"
}
}
currentKey: valueDisplayMode
onSelected: key => valueDisplayMode = key
}
}

View File

@@ -0,0 +1,49 @@
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 string valueDisplayFormat: widgetData.displayFormat !== undefined ? widgetData.displayFormat : widgetMetadata.displayFormat
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.displayFormat = valueDisplayFormat
return settings
}
NComboBox {
label: "Display Format"
model: ListModel {
ListElement {
key: "time"
name: "HH:mm"
}
ListElement {
key: "time-seconds"
name: "HH:mm:ss"
}
ListElement {
key: "time-date"
name: "HH:mm - Date"
}
ListElement {
key: "time-date-short"
name: "HH:mm - Short Date"
}
}
currentKey: valueDisplayFormat
onSelected: key => valueDisplayFormat = key
minimumWidth: 230 * scaling
}
}

View File

@@ -0,0 +1,262 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Window
import qs.Commons
import qs.Widgets
import qs.Services
ColumnLayout {
id: root
spacing: Style.marginM * scaling
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
settings.textCommand = textCommandInput.text
settings.textIntervalMs = parseInt(textIntervalInput.text || textIntervalInput.placeholderText, 10)
return settings
}
NTextInput {
id: iconInput
Layout.fillWidth: true
label: "Icon Name"
description: "Select an icon from the library."
placeholderText: "Enter icon name (e.g., cat, gear, house, ...)"
text: widgetData?.icon || widgetMetadata.icon
}
RowLayout {
spacing: Style.marginS * scaling
Layout.alignment: Qt.AlignLeft
NIcon {
Layout.alignment: Qt.AlignVCenter
icon: iconInput.text
visible: iconInput.text !== ""
}
NButton {
text: "Browse"
onClicked: iconPicker.open()
}
}
Popup {
id: iconPicker
modal: true
width: {
var w = Math.round(Math.max(screen.width * 0.35, 900) * scaling)
w = Math.min(w, screen.width - Style.marginL * 2)
return w
}
height: {
var h = Math.round(Math.max(screen.height * 0.65, 700) * scaling)
h = Math.min(h, screen.height - Style.barHeight * scaling - Style.marginL * 2)
return h
}
anchors.centerIn: Overlay.overlay
padding: Style.marginXL * scaling
property string query: ""
property string selectedIcon: ""
property var allIcons: Object.keys(Icons.icons)
property var filteredIcons: allIcons.filter(function (name) {
return query === "" || name.toLowerCase().indexOf(query.toLowerCase()) !== -1
})
readonly property int columns: 6
readonly property int cellW: Math.floor(grid.width / columns)
readonly property int cellH: Math.round(cellW * 0.7 + 36 * scaling)
background: Rectangle {
color: Color.mSurface
radius: Style.radiusL * scaling
border.color: Color.mPrimary
border.width: Style.borderM * scaling
}
ColumnLayout {
anchors.fill: parent
spacing: Style.marginM * scaling
// Title row
RowLayout {
Layout.fillWidth: true
NText {
text: "Icon Picker"
font.pointSize: Style.fontSizeL * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
Layout.fillWidth: true
}
NIconButton {
icon: "close"
onClicked: iconPicker.close()
}
}
NDivider {
Layout.fillWidth: true
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginS * scaling
NTextInput {
Layout.fillWidth: true
label: "Search"
placeholderText: "Search (e.g., arrow, battery, cloud)"
text: iconPicker.query
onTextChanged: iconPicker.query = text.trim().toLowerCase()
}
}
// Icon grid
NScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
horizontalPolicy: ScrollBar.AlwaysOff
verticalPolicy: ScrollBar.AlwaysOn
GridView {
id: grid
anchors.fill: parent
anchors.margins: Style.marginM * scaling
cellWidth: iconPicker.cellW
cellHeight: iconPicker.cellH
model: iconPicker.filteredIcons
delegate: Rectangle {
width: grid.cellWidth
height: grid.cellHeight
radius: Style.radiusS * scaling
clip: true
color: (iconPicker.selectedIcon === modelData) ? Qt.alpha(Color.mPrimary, 0.15) : "transparent"
border.color: (iconPicker.selectedIcon === modelData) ? Color.mPrimary : Qt.rgba(0, 0, 0, 0)
border.width: (iconPicker.selectedIcon === modelData) ? Style.borderS * scaling : 0
MouseArea {
anchors.fill: parent
onClicked: iconPicker.selectedIcon = modelData
onDoubleClicked: {
iconPicker.selectedIcon = modelData
iconInput.text = iconPicker.selectedIcon
iconPicker.close()
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginS * scaling
spacing: Style.marginS * scaling
Item {
Layout.fillHeight: true
Layout.preferredHeight: 4 * scaling
}
NIcon {
Layout.alignment: Qt.AlignHCenter
icon: modelData
font.pointSize: 42 * scaling
}
NText {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.topMargin: Style.marginXS * scaling
elide: Text.ElideRight
wrapMode: Text.NoWrap
maximumLineCount: 1
horizontalAlignment: Text.AlignHCenter
color: Color.mOnSurfaceVariant
font.pointSize: Style.fontSizeXS * scaling
text: modelData
}
Item {
Layout.fillHeight: true
}
}
}
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginM * scaling
Item {
Layout.fillWidth: true
}
NButton {
text: "Cancel"
outlined: true
onClicked: iconPicker.close()
}
NButton {
text: "Apply"
icon: "check"
enabled: iconPicker.selectedIcon !== ""
onClicked: {
iconInput.text = iconPicker.selectedIcon
iconPicker.close()
}
}
}
}
}
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
}
NDivider {
Layout.fillWidth: true
}
NText {
text: "Dynamic Text"
font.pointSize: Style.fontSizeM * scaling
font.weight: Style.fontWeightBold
color: Color.mPrimary
}
NTextInput {
id: textCommandInput
Layout.fillWidth: true
label: "Text Command"
description: "Shell command to run periodically (first line becomes the text)."
placeholderText: "echo \"Hello World\""
text: widgetData?.textCommand || widgetMetadata.textCommand
}
NTextInput {
id: textIntervalInput
Layout.fillWidth: true
label: "Refresh Interval"
description: "Interval in milliseconds."
placeholderText: String(widgetMetadata.textIntervalMs || 3000)
text: widgetData && widgetData.textIntervalMs !== undefined ? String(widgetData.textIntervalMs) : ""
}
}

View File

@@ -0,0 +1,46 @@
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 string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.displayMode = valueDisplayMode
return settings
}
NComboBox {
label: "Display mode"
description: "Choose how you'd like this value to appear."
minimumWidth: 134 * scaling
model: ListModel {
ListElement {
key: "onhover"
name: "On Hover"
}
ListElement {
key: "forceOpen"
name: "Force Open"
}
ListElement {
key: "alwaysHide"
name: "Always Hide"
}
}
currentKey: valueDisplayMode
onSelected: key => valueDisplayMode = key
}
}

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,46 @@
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 string valueDisplayMode: widgetData.displayMode !== undefined ? widgetData.displayMode : widgetMetadata.displayMode
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.displayMode = valueDisplayMode
return settings
}
NComboBox {
label: "Display mode"
description: "Choose how you'd like this value to appear."
minimumWidth: 134 * scaling
model: ListModel {
ListElement {
key: "onhover"
name: "On Hover"
}
ListElement {
key: "alwaysShow"
name: "Always Show"
}
ListElement {
key: "alwaysHide"
name: "Always Hide"
}
}
currentKey: valueDisplayMode
onSelected: key => valueDisplayMode = key
}
}

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,82 @@
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
property bool valueShowDiskUsage: widgetData.showDiskUsage !== undefined ? widgetData.showDiskUsage : widgetMetadata.showDiskUsage
function saveSettings() {
var settings = Object.assign({}, widgetData || {})
settings.showCpuUsage = valueShowCpuUsage
settings.showCpuTemp = valueShowCpuTemp
settings.showMemoryUsage = valueShowMemoryUsage
settings.showMemoryAsPercent = valueShowMemoryAsPercent
settings.showNetworkStats = valueShowNetworkStats
settings.showDiskUsage = valueShowDiskUsage
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: "Memory as percentage"
checked: valueShowMemoryAsPercent
onToggled: checked => valueShowMemoryAsPercent = checked
}
NToggle {
id: showNetworkStats
Layout.fillWidth: true
label: "Network traffic"
checked: valueShowNetworkStats
onToggled: checked => valueShowNetworkStats = checked
}
NToggle {
id: showDiskUsage
Layout.fillWidth: true
label: "Storage usage"
checked: valueShowDiskUsage
onToggled: checked => valueShowDiskUsage = checked
}
}

Some files were not shown because too many files have changed in this diff Show More