1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-05 03:52:07 -04:00

Compare commits

...

155 Commits

Author SHA1 Message Date
bbedward
9798d78300 core: allow RO commands to run as root 2026-04-07 15:04:04 -04:00
purian23
1f64bb8031 notifications(Settings): Update notifs popout settings overflow 2026-03-20 19:59:45 -04:00
purian23
eea7d12c0b dankinstall(Arch): improve AUR package installation logic 2026-03-20 17:50:24 -04:00
Linken Quy Dinh
85173126f4 fix: multi-monitor wallpaper cycling not working (#2042)
Fixed a QML property binding timing issue where dynamically created timers
and processes for per-monitor wallpaper cycling were being assigned to
properties and then immediately read back, which could return undefined
or stale values.

The fix stores the created object in a local variable before assigning
to the property map, ensuring a valid reference is always used.

Affected functions:
- startMonitorCycling() - timer creation
- cycleToNextWallpaper() - process creation
- cycleToPrevWallpaper() - process creation
2026-03-20 17:40:52 -04:00
bbedward
222187d8a6 niri: set com.danklinux.dms window rule for future compat 2026-03-20 10:05:29 -04:00
bbedward
bef3f65f63 popout: avoid calling functions on stale references 2026-03-20 09:36:38 -04:00
Dimariqe
bff83fe563 fix: redraw wallpaper after DMS lock screen is dismissed (#2037)
After unlocking the screen (startup lock or wake from sleep), the desktop
showed Hyprland's background color instead of the wallpaper.

WallpaperBackground disables QML updates via updatesEnabled after a 1-second
settle timer. While WlSessionLock is active, Hyprland does not composite the
background layer, so when the lock is released it needs a fresh Wayland buffer
— but none is committed because the render loop is already paused.

The previous attempt used SessionService.sessionUnlocked, which is unreliable
for the startup lock case: DMSService is not yet connected when lock() is
called at startup, so notifyLoginctl is a no-op and the loginctl state never
transitions, meaning sessionUnlocked never fires.

Fix by tracking the shell lock state directly from Lock.qml's shouldLock via
a new IdleService.isShellLocked property. WallpaperBackground watches this and
re-enables rendering for 1 second on unlock, ensuring a fresh buffer is
committed to Wayland before the compositor resumes displaying the layer.
2026-03-20 09:36:31 -04:00
bbedward
cbf00d133a wallpaper: tweak binding again for updatesEnabled 2026-03-20 09:25:04 -04:00
purian23
347f06b758 refactor(Notepad): Streamline hide behavior & auto-save function 2026-03-19 21:42:19 -04:00
bbedward
9070903512 cleanup settings tabs 2026-03-19 20:02:46 -04:00
purian23
e9d030f6d8 (greeter): Revise dir perms and add validations 2026-03-19 19:56:18 -04:00
bbedward
fbf9e6d1b9 greeter: remove variable assignments 2026-03-19 19:55:47 -04:00
purian23
e803812344 theme(greeter): fix auto theme accent variants & update selections 2026-03-19 19:55:43 -04:00
nick-linux8
9a64f2acf0 Fix(Greeter): Fixes #1992 Changed Greetd logic to include registryThemeVariants to pull in accent color (#2000) 2026-03-19 19:55:40 -04:00
zion
c647eafadc fix(nix/greeter): skip invalid customThemeFile in preStart (#1997)
* fix(nix/greeter): skip invalid customThemeFile in preStart

Avoid attempting to copy a null/empty/missing customThemeFile path by validating the jq result and file existence before cp.

Update distro/nix/greeter.nix

Co-authored-by: Lucas <43530291+LuckShiba@users.noreply.github.com>

* nix/greeter: update customTheme verification

---------

Co-authored-by: Lucas <43530291+LuckShiba@users.noreply.github.com>
Co-authored-by: LuckShiba <luckshiba@protonmail.com>
2026-03-19 19:55:35 -04:00
purian23
720ec07d13 (greeter): Trial fix for 30s auth delay & wireplumber state dir 2026-03-19 19:55:31 -04:00
purian23
4b4334e611 dms(policy): Restore dms greeter sync in immutable distros 2026-03-19 19:55:27 -04:00
purian23
b69a96e80b fix(greeter): add wireplumber state directory & update U2F env variables 2026-03-19 19:55:23 -04:00
purian23
1e6a73fd60 greeter(auth): Enhance fingerprint/U2F auth support w/Quickshell PAM
- Split auth capability state by lock screen and greeter
- Share detection between settings UI and lock runtime
- Broaden greeter PAM include detection across supported distros
2026-03-19 19:55:18 -04:00
purian23
60b6280750 greeter(distros): Move comps to Suggests on Debian/OpenSUSE 2026-03-19 19:53:58 -04:00
purian23
9e079f8a4b fix(greeter): Dup crash handlers 2026-03-19 19:53:54 -04:00
purian23
62c2e858ef (settings): Enhance authentication checks in Greeter & LockScreen tabs 2026-03-19 19:53:49 -04:00
purian23
78357d45bb fix(greeter): Allow empty password submits to reach PAM 2026-03-19 19:53:12 -04:00
purian23
3ff9564c9b (greeter): PAM auth improvements and defaults update 2026-03-19 19:53:09 -04:00
purian23
b0989cecad fix(Greeter): Multi-distro reliability updates
- Merge duplicate niri input/output KDL nodes instead of appending. Allows more overrides
- Guard AppArmor install/uninstall behind IsAppArmorEnabled() check
2026-03-19 19:53:04 -04:00
purian23
47be6a1033 fix(Greeter): Don't stop greeter immediately upon uninstallation 2026-03-19 19:53:01 -04:00
purian23
31b415b086 feat(Greeter): Add install/uninstall/activate cli commands & new UI opts
- AppArmor profile management
- Introduced `dms greeter uninstall` command to remove DMS greeter configuration and restore previous display manager.
- Implemented AppArmor profile installation and uninstallation for enhanced security.
2026-03-19 19:52:57 -04:00
purian23
7156e1e299 feat: Implement immutable DMS command policy
- Added pre-run checks for greeter and setup commands to enforce policy restrictions
- Created cli-policy.default.json to define blocked commands and user messages for immutable environments.
2026-03-19 19:52:52 -04:00
purian23
c72c9bfb08 greeter: New Greeter Settings UI & Sync fixes
- Add PAM Auth via GUI
- Added new sync flags
- Refactored cache directory management & many others
- Fix for wireplumber permissions
- Fix for polkit auth w/icon
- Add pam_fprintd timeout=5 to prevent 30s auth blocks when using password
2026-03-19 19:52:48 -04:00
purian23
73c75fcc2c refactor(greeter): Update auth flows and add configurable opts
- Finally fix debug info logs before dms greeter loads
- prevent greeter/lockscreen auth stalls with timeout recovery and unlock-state sync
2026-03-19 19:50:58 -04:00
bbedward
2ff42eba41 greeter: sync power menu options 2026-03-19 19:49:26 -04:00
purian23
9f13465cd7 feat: Add independent power action confirmation settings for dms greeter 2026-03-19 19:49:23 -04:00
purian23
366a98e0cc dms-greeter: Enhance DMS Greeter dankinstall & packaging across distros
- Added support for Debian, Ubuntu, Fedora, Arch, and OpenSUSE on dankinstall / dms greeter install
2026-03-19 19:48:20 -04:00
bbedward
31aeb8dc4b wallpaper: fixes for updatesEnable handling 2026-03-19 14:24:01 -04:00
bbedward
c4e7f3d62f workspaces: ignore X scroll events
fixes #2029
2026-03-19 13:22:07 -04:00
purian23
a1d13f276a dankinstall(debian): Minor update to ARM64 support 2026-03-18 09:27:52 -04:00
bbedward
dbf132d633 launcher v2: simplify screen change bindings 2026-03-18 09:27:47 -04:00
bbedward
59451890f1 popout: fix focusing of password prompts when popout is open
undesired effect of closing the popout but its probably the best solution
2026-03-16 11:37:21 -04:00
bbedward
e633c9e039 focused app: fallback to app name if no title in compact mode fixes #2005 2026-03-16 11:37:15 -04:00
bbedward
6c1fff2df1 cc: fix invalid number displays on percentages fixes #2010 2026-03-16 11:37:10 -04:00
bbedward
3891d125d1 dankbar: guard against nil screen names 2026-03-16 11:35:11 -04:00
bbedward
997011e008 fix: missing import in Hyprland service 2026-03-13 13:26:02 -04:00
dms-ci[bot]
2504396435 nix: update vendorHash for go.mod changes 2026-03-13 16:24:54 +00:00
bbedward
d206723b36 ci: fix hardcoded branch in vendor workflow 2026-03-13 12:22:46 -04:00
bbedward
a0ec3d59b8 nix: update flake 2026-03-13 12:18:38 -04:00
bbedward
17ef08aa58 nix: fix go regex matching 2026-03-13 12:18:38 -04:00
bbedward
57279d1c53 nix: dynamically resolve go version in flake 2026-03-13 12:18:38 -04:00
bbedward
8b003ac9cd ci: reveal errors in nix vendor hash update 2026-03-13 12:17:38 -04:00
Nek
0ea10b0ad2 fix(wallpaper): preserve per-monitor cycling when changing interval (#1981)
(#1816)
2026-03-13 11:46:14 -04:00
nick-linux8
2db4c9daa0 Added Better Handling In Event Dispatcher Function (#1980) 2026-03-13 11:45:02 -04:00
bbedward
363964e90b fix(udev): avoid event loop termination core: bump go to 1.26 2026-03-13 11:45:02 -04:00
Nek
a7b49eba70 fix(matugen): detect Zed Linux binary aliases (#1982) 2026-03-13 11:44:10 -04:00
bbedward
4ae334f60f settings: allow custom json to render all theme options 2026-03-13 11:44:05 -04:00
bbedward
86c0064ff9 fix(settings): fix animation speed binding in notifications tab fixes #1974 2026-03-12 11:45:36 -04:00
Adarsh219
5a6b52f07f fix(matugen): use single quotes for zed template paths (#1972) 2026-03-12 11:45:36 -04:00
Adarsh219
5aaa56853f feat: Add Zed editor theming support (#1954)
* feat: Add Zed editor theming support

* fix formatting and switch to CONFIG_DIR
2026-03-12 11:45:31 -04:00
bbedward
35913c22f5 fix(idle): ensure timeouts can never be 0 2026-03-11 18:55:44 -04:00
purian23
d7b560573c fix(settings): Improve error handling for plugin settings loading 2026-03-11 18:55:44 -04:00
bbedward
02a274ebe2 fix(launcher): select first file search result by default fixes #1967 2026-03-11 12:47:39 -04:00
nick-linux8
fc7b61c20b Issue:(Settings)Switched Neovim Mutagen Theme To Default False (#1964)
* Issue:(Settings)Switched Neovim Mutagen Theme To Default False

* also set to false in settingsData
- this is the case when file fails to parse

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-03-11 12:44:14 -04:00
bbedward
5880043f56 fix: dsearch references 2026-03-11 12:08:24 -04:00
Triệu Kha
fee3b7f2a7 fix(appdrawer): launcher launched via appdrawer doesnt respect size (#1960)
setting
2026-03-11 10:56:21 -04:00
bbedward
c0b0339fca plugins: fix list delegates 2026-03-11 09:58:24 -04:00
bbedward
26c1e62204 fix(dankbar): use ID as tie breaker 2026-03-10 11:48:33 -04:00
purian23
7b2d4dbe30 dankinstall: Update Arch/Quickshell installation 2026-03-10 11:05:25 -04:00
CaptainSpof
78c5d46c6b fix(wallpaper): follow symlinks when scanning wallpaper directory (#1947) 2026-03-10 11:05:25 -04:00
purian23
3fb85df504 fix(Clipboard) remove unused copyServe logic 2026-03-10 11:05:00 -04:00
micko
227dd24726 update deprecated syntax (#1928) 2026-03-10 11:05:00 -04:00
purian23
ae6a656899 fix(Clipboard): Epic RAM Growth - Closes #1920 2026-03-10 11:05:00 -04:00
Connor Welsh
a4055e0f01 fix(Calendar): add missing qs.Common import (#1926)
fixes calendar events getting dropped
2026-03-10 11:05:00 -04:00
Lucas
6d98c229ef flake: allow extra QT packages in dms-shell package (#1903) 2026-03-10 11:04:01 -04:00
Michael Erdely
71d93ad85e Not everyone uses paru or yay on Arch: Support pacman command (#1900)
* Not everyone uses paru or yay on Arch: Support pacman command
* Handle sudo properly when using pacman
* Move pacman to bottom per Purian23
* Remote duplicate which -- thanks Purian23!
2026-03-10 11:04:01 -04:00
Triệu Kha
4ec21fcd3d fix(dock): Dock flickering when having cursor floating by the side (#1897) 2026-03-10 11:04:01 -04:00
Lucas
0a2fe03fee ipc: update DankBar selection (#1894)
* ipc: update DankBar selection

* ipc: use getPreferredBar in dash open

* ipc: don't toggle dash on dash open
2026-03-10 11:04:01 -04:00
Triệu Kha
4f4745609b fix(osd): play/pause icon flipped in MediaPlaybackOSD (#1889) 2026-03-10 11:03:23 -04:00
purian23
a69cd515fb fix(dbar): Fixes autohide + click through edge case 2026-03-10 11:03:23 -04:00
purian23
06c4b97a6b fix(notifications): Allow duplicate history entry management w/unique IDs & source tracking 2026-03-10 11:03:23 -04:00
purian23
a6cf71a190 fix(notifications): Apply appIdSubs to iconFrImage fallback path - Consistent with the appIcon PR changes in #1880. 2026-03-10 11:01:34 -04:00
odt
21750156dc refactor(icons): centralize icon resolution into Paths.resolveIconPath/resolveIconUrl (#1880)
Supersedes #1878. Rather than duplicating the moddedAppId + file path
substitution pattern inline across 8 files, this introduces two
centralized functions in Paths.qml:

- resolveIconPath(iconName): for Quickshell.iconPath() callsites,
  with DesktopService.resolveIconPath() fallback
- resolveIconUrl(iconName): for image://icon/ URL callsites

All consumer files now use one-line calls. When no substitutions are
configured, moddedAppId() returns the original name unchanged (zero
cost), so this has no impact on users who don't use the feature.

Affected components:
- AppIconRenderer (8 lines → 1)
- NotificationCard, NotificationPopup, HistoryNotificationCard
- DockContextMenu, AppsDockContextMenu
- LauncherContent, LauncherTab (×3)

Co-authored-by: odtgit <odtgit@taliops.com>
2026-03-10 11:00:57 -04:00
supposede
f9b737f543 Update toolbar button styles with primary color (#1879) 2026-03-10 10:55:58 -04:00
odt
246b59f3b9 fix(icons): apply file path substitutions in launcher icon resolution (#1877)
Follow-up to #1867. The launcher's AppIconRenderer used its own
Quickshell.iconPath() call without going through appIdSubstitutions,
so PWA icons configured via regex file path rules were not resolved
in the app launcher.

Co-authored-by: odtgit <odtgit@taliops.com>
2026-03-10 10:55:53 -04:00
bbedward
dcda81ea64 wallpaper: bump render settle timer 2026-03-01 10:27:10 -05:00
bbedward
9909b665cd blurred wallpaper: defer update disabling much longer 2026-02-28 15:40:18 -05:00
bbedward
4bcd786be3 wallpaper: defer updatesEnabled binding 2026-02-28 01:10:26 -05:00
bbedward
64c9222000 loginctl: add fallbacks for session discovery 2026-02-27 10:12:25 -05:00
Iris
12acf2dd51 Change IsPluggedIn logic (#1859)
Co-authored-by: Iris <iris@raidev.eu>
2026-02-27 10:12:22 -05:00
Jan Greimann
fea97b4aad Adjust SystemUpdate process (#1845)
This fixes the problem that the system update terminal closes when the package manager encounters a problem (exit code != 0), allowing the user to understand the problem.

Signed-off-by: Jan Phillip Greimann <jan.greimann@ionos.com>
2026-02-27 10:12:19 -05:00
Kangheng Liu
c6d398eeac Systray: call context menu fallback for legacy protocol (#1839)
* systray: add call contextmenu fallback

directly call dbus contextmenu method. needs refactoring to be more
robust.

* add TODO

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-02-27 10:12:16 -05:00
bbedward
7a74be83d7 greeter: sync power menu options 2026-02-25 14:50:47 -05:00
bbedward
67a6427418 dankdash: fix menu overlays 2026-02-25 14:50:47 -05:00
purian23
18b20d3225 feat: Add independent power action confirmation settings for dms greeter 2026-02-25 14:50:47 -05:00
bbedward
8a76885fb6 desktop widgets: fix deactive loaders when widgets disabled fixes #1813 2026-02-25 12:34:47 -05:00
bbedward
69b1e61ab7 stage 1.4.3 2026-02-25 12:34:43 -05:00
bbedward
3f24cf37ca settings: make horizontal change more smart 2026-02-24 20:49:02 -05:00
bbedward
01218f34cb settings: restore notifyHorizontalBarChanged 2026-02-24 19:43:01 -05:00
purian23
9da58d8296 fix: Update HTML rendering injections 2026-02-24 19:43:01 -05:00
purian23
af0038e634 dbar: Refactor to memoize dbar & widget state via json 2026-02-24 19:20:30 -05:00
purian23
05c312b9eb cpu widget: Fix monitor binding 2026-02-24 19:20:30 -05:00
bbedward
89d5c958c4 settings: use Image in theme colors tab wp preview 2026-02-24 15:23:07 -05:00
bbedward
e4d86ad595 popout: fully unload popout layers on close 2026-02-24 15:20:00 -05:00
bbedward
532b54a028 wallpaper: handle initial load better, add dms randr command for quick physical scale retrieval 2026-02-24 15:20:00 -05:00
bbedward
504d027c3f privacy indicator: fix width when not active 2026-02-24 13:59:58 -05:00
bbedward
e8f95f4533 settings: use Image for per-mode previews 2026-02-24 13:37:34 -05:00
bbedward
b83256c83a matugen: skip theme refreshes if no colors changed 2026-02-24 13:37:34 -05:00
bbedward
8e2cd21be8 dock: fix tooltip positioning 2026-02-24 13:37:34 -05:00
bbedward
c5413608da dankbar: fix some defaults in reset 2026-02-24 13:37:34 -05:00
bbedward
586bcad442 widgets: set updatesEnabled false on background layers, if qs supports it 2026-02-24 13:37:34 -05:00
bbedward
3b3d10f730 widgets: fix moddedAppID consistency 2026-02-24 13:37:34 -05:00
purian23
4834891b36 settings: Re-adjust dbar layout 2026-02-24 13:37:34 -05:00
purian23
f60e65aecb settings: Dankbar layout updates 2026-02-24 13:37:34 -05:00
purian23
01387b0123 fix: Clipboard button widget alignment 2026-02-24 13:37:34 -05:00
bbedward
1476658c23 dankbar: fix syncing settings to new bars 2026-02-24 13:37:34 -05:00
bbedward
7861c6e316 i18n: term sync 2026-02-24 10:52:35 -05:00
bbedward
d2247d7b24 dankbar: restore horizontal change debounce 2026-02-24 10:52:35 -05:00
bbedward
2ff78d4a02 dpms: disable fade overlay in onRequestMonitorOn 2026-02-24 10:52:35 -05:00
bbedward
785243ce5f dankbar: optimize bindings in bar window 2026-02-24 10:52:35 -05:00
bbedward
0e1b868384 widgets: fix undefined icon warnings 2026-02-24 10:52:35 -05:00
null
2b08e800e8 feat: improve icon resolution and align switcher fallback styling (#1823)
- Implement deep search icon resolution in DesktopService with runtime caching.
- Update Paths.getAppIcon to utilize enhanced resolution for mismatched app IDs.
- Align Workspace Switcher fallback icons with AppsDock visual style.
- Synchronize fallback text logic between Switcher and Dock using app names.
2026-02-24 10:52:35 -05:00
purian23
74e4f8ea1e display: Fix output config on delete & popup height 2026-02-24 10:52:35 -05:00
purian23
9c58569b4c template: Refine bug report tracker 2026-02-24 10:52:35 -05:00
purian23
29de677e00 feat: Refactor DankBar w/New granular options - New background toggles - New maxIcon & maxText widget sizes (global) - Dedicated M3 padding slider - New independent icon scale options - Updated logic to improve performance on single & dual bar modes 2026-02-24 10:52:35 -05:00
purian23
fae4944845 fix: Animated Image warnings 2026-02-24 10:52:34 -05:00
Lucas
07a0ac4b7d doctor: fix imageformats detection (#1811) 2026-02-23 19:45:10 -05:00
bbedward
b2d8f4d73b keybinds: preserve scroll position of expanded item on list change fixes #1766 2026-02-23 19:33:29 -05:00
bbedward
fe58c45233 widgets: fallback when AnimatedImage probe fails to static Image 2026-02-23 19:03:48 -05:00
bbedward
3ea4e389eb thememode: connect to loginctl PrepareForSleep event 2026-02-23 19:03:48 -05:00
purian23
7276f295fc dms-greeter: Update dankinstall greeter automation w/distro packages 2026-02-23 18:53:29 -05:00
bbedward
93ed96a789 launcher: don't tie unload to visibility 2026-02-23 18:53:29 -05:00
purian23
bea325e94c audio: Sync audio hide opts w/dash Output devices 2026-02-23 18:53:29 -05:00
bbedward
2f8f1c30ad audio: fix cycle output, improve icon resolution for sink fixes #1808 2026-02-23 18:53:29 -05:00
Lucas
f859a14173 nix: update flake.lock (#1809) 2026-02-23 18:53:29 -05:00
bbedward
153f39da48 audio: disable effects when mpris player is playing 2026-02-23 18:53:29 -05:00
bbedward
e4accdd1c7 launcher: implement memory for selected tab fixes #1806 2026-02-23 10:20:48 -05:00
dms-ci[bot]
a2c89e0a8c nix: update vendorHash for go.mod changes 2026-02-23 10:20:48 -05:00
bbedward
e282831c2e widgets: make AnimatedImage conditional in DankCircularImage - Cut potential overhead of always using AnimatedImage 2026-02-23 10:20:48 -05:00
bbedward
5c5ff6195a osd: disable media playback OSD by default 2026-02-23 10:20:48 -05:00
Triệu Kha
c4bbf54679 clipboard: fix html elements get parsed in clipboard entry (#1798)
* clipboard: fix html elements get parsed in clipboard entry

* Revert "clipboard: fix html elements get parsed in clipboard entry"

This reverts commit 52b11eeb98.

* clipboard: fix html elements get parsed in clipboard entry
2026-02-23 10:20:48 -05:00
Jonas Bloch
98acafb4b8 fix(notepad): decode path URI when saving/creating a file (#1805) 2026-02-23 10:20:48 -05:00
Jonas Bloch
da20681fc0 feat: add support for animated gifs as profile pictures (#1804) 2026-02-23 10:20:48 -05:00
purian23
b38cb961b2 dms-greeter: Enable greetd via dms greeter install all-in-one cmd 2026-02-23 10:20:48 -05:00
bbedward
7a0bb07518 matugen: unconditionally run portal sync even if matugen errors 2026-02-22 23:09:18 -05:00
purian23
403e3e90a2 dms-greeter: Enhance DMS Greeter dankinstall & packaging across distros - Added support for Debian, Ubuntu, Fedora, Arch, and OpenSUSE on dankinstall / dms greeter install 2026-02-22 23:09:18 -05:00
bbedward
50b91f14b6 launcher: fix frecency ranking in search results fixes #1799 2026-02-22 23:09:18 -05:00
bbedward
b3df47fce0 scripts: fix shellcheck 2026-02-22 23:09:18 -05:00
bbedward
09bd65d746 bluetooth: expose trust/untrust on devices 2026-02-22 23:09:18 -05:00
长夜月玩Fedora
020d56ab7f Add support for 'evernight' distribution in Fedora (#1786) 2026-02-22 23:09:18 -05:00
Triệu Kha
f3bee65da9 Fix dock visible when theres no app (#1797)
* clipboard: improve image thumbnail
- thumbnail image is now bigger
- circular mask has been replaced with rounded rectangular mask

* dock: fix dock still visible when there's no app
2026-02-22 23:09:18 -05:00
purian23
b14b0946e2 feat: DMS Greeter packaging for Debian/OpenSUSE on OBS 2026-02-22 23:09:18 -05:00
Lucas
ca44205f1c zen: add more commands to detection (#1792) 2026-02-22 23:09:18 -05:00
purian23
2d39e8fd2a ipc: Fix DankDash Wallpaper call 2026-02-22 23:09:18 -05:00
purian23
6d4df6e927 theme: Fix Light/Dark mode portal sync 2026-02-22 23:09:18 -05:00
Connor Welsh
b8ab86e6c0 distro: add cups-pk-helper as suggested dependency (#1670) 2026-02-22 23:09:18 -05:00
bbedward
837329a6d8 window rules: default to fixed for width/height part of #1774 2026-02-22 23:09:18 -05:00
purian23
8c6c2ffd23 ubuntu: Fix dms-git Go versioning to restore builds 2026-02-22 23:09:18 -05:00
bbedward
ad3c8b6755 v1.4.3 version file 2026-02-22 23:07:18 -05:00
201 changed files with 13201 additions and 3205 deletions

View File

@@ -7,7 +7,7 @@ body:
attributes: attributes:
value: | value: |
## DankMaterialShell Bug Report ## DankMaterialShell Bug Report
Limit your report to one issue per submission unless closely related Limit your report to one issue per submission unless similarly related
- type: dropdown - type: dropdown
id: compositor id: compositor
attributes: attributes:
@@ -53,9 +53,9 @@ body:
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
id: original_installation_method_different id: original_installation_method
attributes: attributes:
label: Was your original Installation method different? label: Was this your original Installation method?
options: options:
- "Yes" - "Yes"
- No (specify below) - No (specify below)
@@ -73,7 +73,7 @@ body:
id: dms_doctor id: dms_doctor
attributes: attributes:
label: dms doctor -vC label: dms doctor -vC
description: Output of `dms doctor -vC` command — paste between the lines below to keep it collapsed in the issue description: Output of `dms doctor -vC` command — paste between the details tags below to keep it collapsed in the issue
placeholder: Paste the output of `dms doctor -vC` here placeholder: Paste the output of `dms doctor -vC` here
value: | value: |
<details> <details>

View File

@@ -10,6 +10,7 @@ on:
options: options:
- dms - dms
- dms-git - dms-git
- dms-greeter
- all - all
default: "dms" default: "dms"
rebuild_release: rebuild_release:
@@ -72,12 +73,27 @@ jobs:
fi fi
} }
# Helper function to check dms-greeter stable tag
check_dms_greeter_stable() {
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:danklinux/dms-greeter/dms-greeter.spec" 2>/dev/null || echo "")
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs | sed 's/^v//')
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "v$OBS_VERSION" ]]; then
echo "📋 dms-greeter: Tag $LATEST_TAG already exists, skipping"
return 1 # No update needed
else
echo "📋 dms-greeter: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})"
return 0 # Update needed
fi
}
# Main logic # Main logic
REBUILD="${{ github.event.inputs.rebuild_release }}" REBUILD="${{ github.event.inputs.rebuild_release }}"
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then
# Tag selected or pushed - always update stable package # Run from tag with no package specified - update both stable packages
echo "packages=dms" >> $GITHUB_OUTPUT echo "packages=dms dms-greeter" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}" VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT echo "has_updates=true" >> $GITHUB_OUTPUT
@@ -112,6 +128,10 @@ jobs:
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi fi
fi fi
if check_dms_greeter_stable; then
PACKAGES_TO_UPDATE+=("dms-greeter")
[[ -n "$LATEST_TAG" ]] && echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
@@ -144,6 +164,18 @@ jobs:
echo "has_updates=false" >> $GITHUB_OUTPUT echo "has_updates=false" >> $GITHUB_OUTPUT
fi fi
elif [[ "$PKG" == "dms-greeter" ]]; then
if check_dms_greeter_stable; then
echo "packages=$PKG" >> $GITHUB_OUTPUT
echo "has_updates=true" >> $GITHUB_OUTPUT
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
fi
else
echo "packages=" >> $GITHUB_OUTPUT
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
else else
# Unknown package - proceed anyway # Unknown package - proceed anyway
echo "packages=$PKG" >> $GITHUB_OUTPUT echo "packages=$PKG" >> $GITHUB_OUTPUT
@@ -171,15 +203,8 @@ jobs:
- name: Determine packages to update - name: Determine packages to update
id: packages id: packages
run: | run: |
# Check if GITHUB_REF points to a tag (works for both push events and workflow_dispatch with tag selected) # Use check-updates outputs when available
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then if [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
# Tag selected or pushed - use the tag from GITHUB_REF
echo "packages=dms" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Using tag from GITHUB_REF: $VERSION"
# Check if check-updates already determined a version (from auto-detection)
elif [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
# Use version from check-updates job # Use version from check-updates job
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
@@ -191,40 +216,16 @@ jobs:
elif [[ -n "${{ github.event.inputs.package }}" ]]; then elif [[ -n "${{ github.event.inputs.package }}" ]]; then
# Manual workflow dispatch # Manual workflow dispatch
# Determine version for dms stable # Determine version for dms stable and dms-greeter using the API
if [[ "${{ github.event.inputs.package }}" == "dms" ]]; then # GITHUB_REF is unreliable when "Use workflow from" a tag; API works from any ref
# Use github.ref if tag selected, otherwise auto-detect latest if [[ "${{ github.event.inputs.package }}" == "dms" ]] || [[ "${{ github.event.inputs.package }}" == "dms-greeter" ]] || [[ "${{ github.event.inputs.package }}" == "all" ]]; then
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
VERSION="${GITHUB_REF#refs/tags/}" if [[ -n "$LATEST_TAG" ]]; then
echo "version=$VERSION" >> $GITHUB_OUTPUT echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
echo "Using tag from GITHUB_REF: $VERSION" echo "Using latest release from API: $LATEST_TAG"
else else
# Auto-detect latest release for dms echo "ERROR: Could not fetch latest release from API"
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "") exit 1
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
echo "Auto-detected latest release: $LATEST_TAG"
else
echo "ERROR: Could not auto-detect latest release"
exit 1
fi
fi
elif [[ "${{ github.event.inputs.package }}" == "all" ]]; then
# Use github.ref if tag selected, otherwise auto-detect latest
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Using tag from GITHUB_REF: $VERSION"
else
# Auto-detect latest release for "all"
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
if [[ -n "$LATEST_TAG" ]]; then
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
echo "Auto-detected latest release: $LATEST_TAG"
else
echo "ERROR: Could not auto-detect latest release"
exit 1
fi
fi fi
fi fi
@@ -283,55 +284,66 @@ jobs:
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE" echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
} > "distro/debian/dms-git/debian/changelog" } > "distro/debian/dms-git/debian/changelog"
- name: Update dms stable version - name: Update stable version (dms + dms-greeter)
if: steps.packages.outputs.version != '' if: steps.packages.outputs.version != ''
run: | run: |
VERSION="${{ steps.packages.outputs.version }}" VERSION="${{ steps.packages.outputs.version }}"
VERSION_NO_V="${VERSION#v}" VERSION_NO_V="${VERSION#v}"
PACKAGES="${{ steps.packages.outputs.packages }}"
echo "==> Updating packaging files to version: $VERSION_NO_V" echo "==> Updating packaging files to version: $VERSION_NO_V"
# Update spec file # Update dms spec and changelog when dms is in the upload list
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec if [[ "$PACKAGES" == *"dms"* ]]; then
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
UPDATED_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1)
echo "✓ dms spec now shows Version: $UPDATED_VERSION"
# Verify the update DATE_STR=$(date "+%a %b %d %Y")
UPDATED_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1) LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms.spec)
echo "✓ Spec file now shows Version: $UPDATED_VERSION" {
echo "$LOCAL_SPEC_HEAD"
echo "%changelog"
echo "* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1"
echo "- Update to stable $VERSION release"
} > distro/opensuse/dms.spec
# Single changelog entry (full history on OBS website) if [[ -f "distro/debian/dms/debian/changelog" ]]; then
DATE_STR=$(date "+%a %b %d %Y") CHANGELOG_DATE=$(date -R)
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms.spec) {
{ echo "dms (${VERSION_NO_V}db1) stable; urgency=medium"
echo "$LOCAL_SPEC_HEAD" echo ""
echo "%changelog" echo " * Update to $VERSION stable release"
echo "* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1" echo ""
echo "- Update to stable $VERSION release" echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
} > distro/opensuse/dms.spec } > "distro/debian/dms/debian/changelog"
echo "✓ Updated dms changelog to ${VERSION_NO_V}db1"
# Update Debian _service files (both tar_scm and download_url formats)
for service in distro/debian/*/_service; do
if [[ -f "$service" ]]; then
# Update tar_scm revision parameter (for dms-git)
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
# Update download_url paths (for dms stable)
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
fi fi
done fi
# Update Debian changelog for dms stable (single entry, history on OBS website) # Update dms-greeter changelog when dms-greeter is in the upload list
if [[ -f "distro/debian/dms/debian/changelog" ]]; then if [[ "$PACKAGES" == *"dms-greeter"* ]] && [[ -f "distro/debian/dms-greeter/debian/changelog" ]]; then
CHANGELOG_DATE=$(date -R) CHANGELOG_DATE=$(date -R)
{ {
echo "dms (${VERSION_NO_V}db1) stable; urgency=medium" echo "dms-greeter (${VERSION_NO_V}db1) unstable; urgency=medium"
echo "" echo ""
echo " * Update to $VERSION stable release" echo " * Update to $VERSION stable release"
echo "" echo ""
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE" echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
} > "distro/debian/dms/debian/changelog" } > "distro/debian/dms-greeter/debian/changelog"
echo "✓ Updated Debian changelog to ${VERSION_NO_V}db1" echo "✓ Updated dms-greeter changelog to ${VERSION_NO_V}db1"
fi fi
# Update Debian _service files for packages in upload list (download_url paths)
for service in distro/debian/*/_service; do
if [[ -f "$service" ]]; then
# Update tar_scm revision parameter (for dms-git)
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
# Update download_url paths (for dms, dms-greeter stable)
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
fi
done
- name: Install Go - name: Install Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
@@ -354,6 +366,7 @@ jobs:
chmod 600 ~/.config/osc/oscrc chmod 600 ~/.config/osc/oscrc
- name: Upload to OBS - name: Upload to OBS
id: upload
env: env:
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }} REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
TAG_VERSION: ${{ steps.packages.outputs.version }} TAG_VERSION: ${{ steps.packages.outputs.version }}
@@ -362,6 +375,8 @@ jobs:
if [[ -z "$PACKAGES" ]]; then if [[ -z "$PACKAGES" ]]; then
echo "✓ No packages need uploading. All up to date!" echo "✓ No packages need uploading. All up to date!"
echo "uploaded_packages=" >> $GITHUB_OUTPUT
echo "skipped_packages=" >> $GITHUB_OUTPUT
exit 0 exit 0
fi fi
@@ -371,6 +386,9 @@ jobs:
echo "==> Version being uploaded: ${{ steps.packages.outputs.version }}" echo "==> Version being uploaded: ${{ steps.packages.outputs.version }}"
fi fi
UPLOADED_PACKAGES=()
SKIPPED_PACKAGES=()
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check) # PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
# Loop through each package and upload # Loop through each package and upload
for PKG in $PACKAGES; do for PKG in $PACKAGES; do
@@ -382,13 +400,37 @@ jobs:
fi fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
LOG_FILE=$(mktemp)
set +e
if [[ "$PKG" == "dms-git" ]]; then if [[ "$PKG" == "dms-git" ]]; then
bash distro/scripts/obs-upload.sh dms-git "Automated git update" bash distro/scripts/obs-upload.sh dms-git "Automated git update" >"$LOG_FILE" 2>&1
else else
bash distro/scripts/obs-upload.sh "$PKG" "$MESSAGE" bash distro/scripts/obs-upload.sh "$PKG" "$MESSAGE" >"$LOG_FILE" 2>&1
fi fi
STATUS=$?
set -e
cat "$LOG_FILE"
if [[ $STATUS -ne 0 ]]; then
rm -f "$LOG_FILE"
echo "❌ Upload failed for $PKG"
exit $STATUS
fi
if grep -Eq "Exiting gracefully \(no changes needed\)|No changes needed for this package\. Exiting gracefully\." "$LOG_FILE"; then
echo " $PKG is already up to date. Skipped."
SKIPPED_PACKAGES+=("$PKG")
else
UPLOADED_PACKAGES+=("$PKG")
fi
rm -f "$LOG_FILE"
done done
echo "uploaded_packages=${UPLOADED_PACKAGES[*]}" >> $GITHUB_OUTPUT
echo "skipped_packages=${SKIPPED_PACKAGES[*]}" >> $GITHUB_OUTPUT
- name: Summary - name: Summary
if: always() if: always()
run: | run: |
@@ -402,20 +444,59 @@ jobs:
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
echo "All packages are current. Run completed successfully." >> $GITHUB_STEP_SUMMARY echo "All packages are current. Run completed successfully." >> $GITHUB_STEP_SUMMARY
else else
echo "**Packages Uploaded:**" >> $GITHUB_STEP_SUMMARY UPLOADED_PACKAGES="${{ steps.upload.outputs.uploaded_packages }}"
SKIPPED_PACKAGES="${{ steps.upload.outputs.skipped_packages }}"
TOTAL_COUNT=$(wc -w <<<"$PACKAGES" | tr -d ' ')
UPLOADED_COUNT=0
SKIPPED_COUNT=0
if [[ -n "$UPLOADED_PACKAGES" ]]; then
UPLOADED_COUNT=$(wc -w <<<"$UPLOADED_PACKAGES" | tr -d ' ')
fi
if [[ -n "$SKIPPED_PACKAGES" ]]; then
SKIPPED_COUNT=$(wc -w <<<"$SKIPPED_PACKAGES" | tr -d ' ')
fi
in_list() {
local item="$1"
local list="$2"
[[ " $list " == *" $item "* ]]
}
if [[ "${{ job.status }}" == "success" ]]; then
echo "**Status:** ✅ Completed successfully" >> $GITHUB_STEP_SUMMARY
else
echo "**Status:** ❌ Completed with errors" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Processed:** $TOTAL_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
echo "**Uploaded:** $UPLOADED_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
echo "**Skipped (up to date):** $SKIPPED_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Packages:**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
for PKG in $PACKAGES; do for PKG in $PACKAGES; do
STATUS_ICON="✅"
STATUS_TEXT="uploaded"
if in_list "$PKG" "$SKIPPED_PACKAGES"; then
STATUS_ICON=""
STATUS_TEXT="up to date (skipped)"
elif ! in_list "$PKG" "$UPLOADED_PACKAGES"; then
STATUS_ICON="❌"
STATUS_TEXT="failed"
fi
case "$PKG" in case "$PKG" in
dms) dms)
echo "- ✅ **dms** → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms/dms)" >> $GITHUB_STEP_SUMMARY echo "- $STATUS_ICON **dms** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms/dms)" >> $GITHUB_STEP_SUMMARY
;; ;;
dms-git) dms-git)
echo "- ✅ **dms-git** → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY echo "- $STATUS_ICON **dms-git** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
;;
dms-greeter)
echo "- $STATUS_ICON **dms-greeter** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:danklinux/dms-greeter)" >> $GITHUB_STEP_SUMMARY
;; ;;
esac esac
done done
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then

View File

@@ -40,7 +40,7 @@ jobs:
echo "Build succeeded, no hash update needed" echo "Build succeeded, no hash update needed"
exit 0 exit 0
fi fi
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1) new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1 || true)
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; } [ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix) current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; } [ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
@@ -59,8 +59,8 @@ jobs:
git config user.email "dms-ci[bot]@users.noreply.github.com" git config user.email "dms-ci[bot]@users.noreply.github.com"
git add flake.nix git add flake.nix
git commit -m "nix: update vendorHash for go.mod changes" || exit 0 git commit -m "nix: update vendorHash for go.mod changes" || exit 0
git pull --rebase origin master git pull --rebase origin ${{ github.ref_name }}
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:${{ github.ref_name }}
else else
echo "No changes to flake.nix" echo "No changes to flake.nix"
fi fi

View File

@@ -0,0 +1,10 @@
{
"policy_version": 1,
"blocked_commands": [
"greeter install",
"greeter enable",
"greeter uninstall",
"setup"
],
"message": "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes."
}

View File

@@ -222,16 +222,19 @@ func init() {
func runClipCopy(cmd *cobra.Command, args []string) { func runClipCopy(cmd *cobra.Command, args []string) {
var data []byte var data []byte
copyFromStdin := false
switch { switch {
case len(args) > 0: case len(args) > 0:
data = []byte(args[0]) data = []byte(args[0])
default: case clipCopyDownload || clipCopyType == "__multi__":
var err error var err error
data, err = io.ReadAll(os.Stdin) data, err = io.ReadAll(os.Stdin)
if err != nil { if err != nil {
log.Fatalf("read stdin: %v", err) log.Fatalf("read stdin: %v", err)
} }
default:
copyFromStdin = true
} }
if clipCopyDownload { if clipCopyDownload {
@@ -257,6 +260,13 @@ func runClipCopy(cmd *cobra.Command, args []string) {
return return
} }
if copyFromStdin {
if err := clipboard.CopyReader(os.Stdin, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
log.Fatalf("copy: %v", err)
}
return
}
if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil { if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
log.Fatalf("copy: %v", err) log.Fatalf("copy: %v", err)
} }

View File

@@ -525,5 +525,6 @@ func getCommonCommands() []*cobra.Command {
doctorCmd, doctorCmd,
configCmd, configCmd,
dlCmd, dlCmd,
randrCmd,
} }
} }

View File

@@ -652,16 +652,14 @@ func checkI2CAvailability() checkResult {
func checkImageFormatPlugins() []checkResult { func checkImageFormatPlugins() []checkResult {
url := doctorDocsURL + "#optional-features" url := doctorDocsURL + "#optional-features"
pluginDir := findQtPluginDir() pluginDirs := findQtPluginDirs()
if pluginDir == "" { if len(pluginDirs) == 0 {
return []checkResult{ return []checkResult{
{catOptionalFeatures, "qt6-imageformats", statusInfo, "Cannot detect (plugin dir not found)", "WebP, TIFF, JP2 support", url}, {catOptionalFeatures, "qt6-imageformats", statusInfo, "Cannot detect (plugin dir not found)", "WebP, TIFF, JP2 support", url},
{catOptionalFeatures, "kimageformats", statusInfo, "Cannot detect (plugin dir not found)", "AVIF, HEIF, JXL support", url}, {catOptionalFeatures, "kimageformats", statusInfo, "Cannot detect (plugin dir not found)", "AVIF, HEIF, JXL support", url},
} }
} }
imageFormatsDir := filepath.Join(pluginDir, "imageformats")
type pluginCheck struct { type pluginCheck struct {
name string name string
desc string desc string
@@ -695,9 +693,18 @@ func checkImageFormatPlugins() []checkResult {
var results []checkResult var results []checkResult
for _, c := range checks { for _, c := range checks {
var found []string var found []string
for _, p := range c.plugins { var foundDirs []string
if _, err := os.Stat(filepath.Join(imageFormatsDir, p.file)); err == nil { for _, pluginDir := range pluginDirs {
found = append(found, p.format) imageFormatsDir := filepath.Join(pluginDir, "imageformats")
for _, p := range c.plugins {
if _, err := os.Stat(filepath.Join(imageFormatsDir, p.file)); err == nil {
if !slices.Contains(found, p.format) {
found = append(found, p.format)
}
if !slices.Contains(foundDirs, imageFormatsDir) {
foundDirs = append(foundDirs, imageFormatsDir)
}
}
} }
} }
@@ -708,7 +715,7 @@ func checkImageFormatPlugins() []checkResult {
default: default:
details := "" details := ""
if doctorVerbose { if doctorVerbose {
details = fmt.Sprintf("Formats: %s (%s)", strings.Join(found, ", "), imageFormatsDir) details = fmt.Sprintf("Formats: %s (%s)", strings.Join(found, ", "), strings.Join(foundDirs, ":"))
} }
result = checkResult{catOptionalFeatures, c.name, statusOK, fmt.Sprintf("Installed (%d formats)", len(found)), details, url} result = checkResult{catOptionalFeatures, c.name, statusOK, fmt.Sprintf("Installed (%d formats)", len(found)), details, url}
} }
@@ -718,22 +725,28 @@ func checkImageFormatPlugins() []checkResult {
return results return results
} }
func findQtPluginDir() string { func findQtPluginDirs() []string {
// Check QT_PLUGIN_PATH env var first (used by NixOS and custom setups) var dirs []string
addDir := func(dir string) {
if dir != "" {
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
dirs = append(dirs, dir)
}
}
}
// Check all paths in QT_PLUGIN_PATH env var (used by NixOS and custom setups)
if envPath := os.Getenv("QT_PLUGIN_PATH"); envPath != "" { if envPath := os.Getenv("QT_PLUGIN_PATH"); envPath != "" {
for dir := range strings.SplitSeq(envPath, ":") { for dir := range strings.SplitSeq(envPath, ":") {
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil { addDir(dir)
return dir
}
} }
} }
// Try qtpaths // Try qtpaths
for _, cmd := range []string{"qtpaths6", "qtpaths"} { for _, cmd := range []string{"qtpaths6", "qtpaths"} {
if output, err := exec.Command(cmd, "-query", "QT_INSTALL_PLUGINS").Output(); err == nil { if output, err := exec.Command(cmd, "-query", "QT_INSTALL_PLUGINS").Output(); err == nil {
if dir := strings.TrimSpace(string(output)); dir != "" { addDir(strings.TrimSpace(string(output)))
return dir
}
} }
} }
@@ -744,12 +757,10 @@ func findQtPluginDir() string {
"/usr/lib/x86_64-linux-gnu/qt6/plugins", "/usr/lib/x86_64-linux-gnu/qt6/plugins",
"/usr/lib/aarch64-linux-gnu/qt6/plugins", "/usr/lib/aarch64-linux-gnu/qt6/plugins",
} { } {
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil { addDir(dir)
return dir
}
} }
return "" return dirs
} }
func detectNetworkBackend(stackResult *network.DetectResult) string { func detectNetworkBackend(stackResult *network.DetectResult) string {

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,9 @@ package main
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"os"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
@@ -95,7 +97,11 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
func runMatugenGenerate(cmd *cobra.Command, args []string) { func runMatugenGenerate(cmd *cobra.Command, args []string) {
opts := buildMatugenOptions(cmd) opts := buildMatugenOptions(cmd)
if err := matugen.Run(opts); err != nil { err := matugen.Run(opts)
switch {
case errors.Is(err, matugen.ErrNoChanges):
os.Exit(2)
case err != nil:
log.Fatalf("Theme generation failed: %v", err) log.Fatalf("Theme generation failed: %v", err)
} }
} }
@@ -129,7 +135,11 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
if !wait { if !wait {
if err := sendServerRequestFireAndForget(request); err != nil { if err := sendServerRequestFireAndForget(request); err != nil {
log.Info("Server unavailable, running synchronously") log.Info("Server unavailable, running synchronously")
if err := matugen.Run(opts); err != nil { err := matugen.Run(opts)
switch {
case errors.Is(err, matugen.ErrNoChanges):
os.Exit(2)
case err != nil:
log.Fatalf("Theme generation failed: %v", err) log.Fatalf("Theme generation failed: %v", err)
} }
return return
@@ -146,11 +156,15 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
resp, ok := tryServerRequest(request) resp, ok := tryServerRequest(request)
if !ok { if !ok {
log.Info("Server unavailable, running synchronously") log.Info("Server unavailable, running synchronously")
if err := matugen.Run(opts); err != nil { err := matugen.Run(opts)
switch {
case errors.Is(err, matugen.ErrNoChanges):
resultCh <- matugen.ErrNoChanges
case err != nil:
resultCh <- err resultCh <- err
return default:
resultCh <- nil
} }
resultCh <- nil
return return
} }
if resp.Error != "" { if resp.Error != "" {
@@ -162,7 +176,10 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
select { select {
case err := <-resultCh: case err := <-resultCh:
if err != nil { switch {
case errors.Is(err, matugen.ErrNoChanges):
os.Exit(2)
case err != nil:
log.Fatalf("Theme generation failed: %v", err) log.Fatalf("Theme generation failed: %v", err)
} }
fmt.Println("Theme generation completed") fmt.Println("Theme generation completed")

View File

@@ -0,0 +1,58 @@
package main
import (
"encoding/json"
"fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/spf13/cobra"
)
var randrCmd = &cobra.Command{
Use: "randr",
Short: "Query output display information",
Long: "Query Wayland compositor for output names, scales, resolutions and refresh rates via zwlr-output-management",
Run: runRandr,
}
func init() {
randrCmd.Flags().Bool("json", false, "Output in JSON format")
}
type randrJSON struct {
Outputs []randrOutput `json:"outputs"`
}
func runRandr(cmd *cobra.Command, args []string) {
outputs, err := queryRandr()
if err != nil {
log.Fatalf("%v", err)
}
jsonFlag, _ := cmd.Flags().GetBool("json")
if jsonFlag {
data, err := json.Marshal(randrJSON{Outputs: outputs})
if err != nil {
log.Fatalf("failed to marshal JSON: %v", err)
}
fmt.Println(string(data))
return
}
for i, out := range outputs {
if i > 0 {
fmt.Println()
}
status := "enabled"
if !out.Enabled {
status = "disabled"
}
fmt.Printf("%s (%s)\n", out.Name, status)
fmt.Printf(" Scale: %.4g\n", out.Scale)
fmt.Printf(" Resolution: %dx%d\n", out.Width, out.Height)
if out.Refresh > 0 {
fmt.Printf(" Refresh: %.2f Hz\n", float64(out.Refresh)/1000.0)
}
}
}

View File

@@ -16,9 +16,10 @@ import (
) )
var setupCmd = &cobra.Command{ var setupCmd = &cobra.Command{
Use: "setup", Use: "setup",
Short: "Deploy DMS configurations", Short: "Deploy DMS configurations",
Long: "Deploy compositor and terminal configurations with interactive prompts", Long: "Deploy compositor and terminal configurations with interactive prompts",
PersistentPreRunE: requireMutableSystemCommand,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if err := runSetup(); err != nil { if err := runSetup(); err != nil {
log.Fatalf("Error during setup: %v", err) log.Fatalf("Error during setup: %v", err)

View File

@@ -0,0 +1,271 @@
package main
import (
"bufio"
_ "embed"
"encoding/json"
"fmt"
"os"
"strings"
"sync"
"github.com/spf13/cobra"
)
const (
cliPolicyPackagedPath = "/usr/share/dms/cli-policy.json"
cliPolicyAdminPath = "/etc/dms/cli-policy.json"
)
var (
immutablePolicyOnce sync.Once
immutablePolicy immutableCommandPolicy
immutablePolicyErr error
)
//go:embed assets/cli-policy.default.json
var defaultCLIPolicyJSON []byte
type immutableCommandPolicy struct {
ImmutableSystem bool
ImmutableReason string
BlockedCommands []string
Message string
}
type cliPolicyFile struct {
PolicyVersion int `json:"policy_version"`
ImmutableSystem *bool `json:"immutable_system"`
BlockedCommands *[]string `json:"blocked_commands"`
Message *string `json:"message"`
}
func normalizeCommandSpec(raw string) string {
normalized := strings.ToLower(strings.TrimSpace(raw))
normalized = strings.TrimPrefix(normalized, "dms ")
return strings.Join(strings.Fields(normalized), " ")
}
func normalizeBlockedCommands(raw []string) []string {
normalized := make([]string, 0, len(raw))
seen := make(map[string]bool)
for _, cmd := range raw {
spec := normalizeCommandSpec(cmd)
if spec == "" || seen[spec] {
continue
}
seen[spec] = true
normalized = append(normalized, spec)
}
return normalized
}
func commandBlockedByPolicy(commandPath string, blocked []string) bool {
normalizedPath := normalizeCommandSpec(commandPath)
if normalizedPath == "" {
return false
}
for _, entry := range blocked {
spec := normalizeCommandSpec(entry)
if spec == "" {
continue
}
if normalizedPath == spec || strings.HasPrefix(normalizedPath, spec+" ") {
return true
}
}
return false
}
func loadPolicyFile(path string) (*cliPolicyFile, error) {
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("failed to read %s: %w", path, err)
}
var policy cliPolicyFile
if err := json.Unmarshal(data, &policy); err != nil {
return nil, fmt.Errorf("failed to parse %s: %w", path, err)
}
return &policy, nil
}
func mergePolicyFile(base *immutableCommandPolicy, path string) error {
policyFile, err := loadPolicyFile(path)
if err != nil {
return err
}
if policyFile == nil {
return nil
}
if policyFile.ImmutableSystem != nil {
base.ImmutableSystem = *policyFile.ImmutableSystem
}
if policyFile.BlockedCommands != nil {
base.BlockedCommands = normalizeBlockedCommands(*policyFile.BlockedCommands)
}
if policyFile.Message != nil {
msg := strings.TrimSpace(*policyFile.Message)
if msg != "" {
base.Message = msg
}
}
return nil
}
func readOSReleaseMap(path string) map[string]string {
values := make(map[string]string)
file, err := os.Open(path)
if err != nil {
return values
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.ToUpper(strings.TrimSpace(parts[0]))
value := strings.Trim(strings.TrimSpace(parts[1]), "\"")
values[key] = strings.ToLower(value)
}
return values
}
func hasAnyToken(text string, tokens ...string) bool {
if text == "" {
return false
}
for _, token := range tokens {
if strings.Contains(text, token) {
return true
}
}
return false
}
func detectImmutableSystem() (bool, string) {
if _, err := os.Stat("/run/ostree-booted"); err == nil {
return true, "/run/ostree-booted is present"
}
osRelease := readOSReleaseMap("/etc/os-release")
if len(osRelease) == 0 {
return false, ""
}
id := osRelease["ID"]
idLike := osRelease["ID_LIKE"]
variantID := osRelease["VARIANT_ID"]
name := osRelease["NAME"]
prettyName := osRelease["PRETTY_NAME"]
immutableIDs := map[string]bool{
"bluefin": true,
"bazzite": true,
"silverblue": true,
"kinoite": true,
"sericea": true,
"onyx": true,
"aurora": true,
"fedora-iot": true,
"fedora-coreos": true,
}
if immutableIDs[id] {
return true, "os-release ID=" + id
}
markers := []string{"silverblue", "kinoite", "sericea", "onyx", "bazzite", "bluefin", "aurora", "ostree", "atomic"}
if hasAnyToken(variantID, markers...) {
return true, "os-release VARIANT_ID=" + variantID
}
if hasAnyToken(idLike, "ostree", "rpm-ostree") {
return true, "os-release ID_LIKE=" + idLike
}
if hasAnyToken(name, markers...) || hasAnyToken(prettyName, markers...) {
return true, "os-release identifies an atomic/ostree variant"
}
return false, ""
}
func getImmutablePolicy() (*immutableCommandPolicy, error) {
immutablePolicyOnce.Do(func() {
detectedImmutable, reason := detectImmutableSystem()
immutablePolicy = immutableCommandPolicy{
ImmutableSystem: detectedImmutable,
ImmutableReason: reason,
BlockedCommands: []string{"greeter install", "greeter enable", "setup"},
Message: "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes.",
}
var defaultPolicy cliPolicyFile
if err := json.Unmarshal(defaultCLIPolicyJSON, &defaultPolicy); err != nil {
immutablePolicyErr = fmt.Errorf("failed to parse embedded default CLI policy: %w", err)
return
}
if defaultPolicy.BlockedCommands != nil {
immutablePolicy.BlockedCommands = normalizeBlockedCommands(*defaultPolicy.BlockedCommands)
}
if defaultPolicy.Message != nil {
msg := strings.TrimSpace(*defaultPolicy.Message)
if msg != "" {
immutablePolicy.Message = msg
}
}
if err := mergePolicyFile(&immutablePolicy, cliPolicyPackagedPath); err != nil {
immutablePolicyErr = err
return
}
if err := mergePolicyFile(&immutablePolicy, cliPolicyAdminPath); err != nil {
immutablePolicyErr = err
return
}
})
if immutablePolicyErr != nil {
return nil, immutablePolicyErr
}
return &immutablePolicy, nil
}
func requireMutableSystemCommand(cmd *cobra.Command, _ []string) error {
policy, err := getImmutablePolicy()
if err != nil {
return err
}
if !policy.ImmutableSystem {
return nil
}
commandPath := normalizeCommandSpec(cmd.CommandPath())
if !commandBlockedByPolicy(commandPath, policy.BlockedCommands) {
return nil
}
reason := ""
if policy.ImmutableReason != "" {
reason = "Detected immutable system: " + policy.ImmutableReason + "\n"
}
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath)
}

View File

@@ -16,19 +16,10 @@ func init() {
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)") runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().MarkHidden("daemon-child") runCmd.Flags().MarkHidden("daemon-child")
// Add subcommands to greeter greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
// Add subcommands to setup
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
// Add subcommands to update
updateCmd.AddCommand(updateCheckCmd) updateCmd.AddCommand(updateCheckCmd)
// Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
// Add common commands to root
rootCmd.AddCommand(getCommonCommands()...) rootCmd.AddCommand(getCommonCommands()...)
rootCmd.AddCommand(updateCmd) rootCmd.AddCommand(updateCmd)
@@ -37,7 +28,7 @@ func init() {
} }
func main() { func main() {
if os.Geteuid() == 0 { if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
log.Fatal("This program should not be run as root. Exiting.") log.Fatal("This program should not be run as root. Exiting.")
} }

View File

@@ -11,30 +11,21 @@ import (
var Version = "dev" var Version = "dev"
func init() { func init() {
// Add flags
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode") runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process") runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)") runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().MarkHidden("daemon-child") runCmd.Flags().MarkHidden("daemon-child")
// Add subcommands to greeter greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
greeterCmd.AddCommand(greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
// Add subcommands to setup
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
// Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
// Add common commands to root
rootCmd.AddCommand(getCommonCommands()...) rootCmd.AddCommand(getCommonCommands()...)
rootCmd.SetHelpTemplate(getHelpTemplate()) rootCmd.SetHelpTemplate(getHelpTemplate())
} }
func main() { func main() {
// Block root if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
if os.Geteuid() == 0 {
log.Fatal("This program should not be run as root. Exiting.") log.Fatal("This program should not be run as root. Exiting.")
} }

View File

@@ -0,0 +1,172 @@
package main
import (
"fmt"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
)
type randrOutput struct {
Name string `json:"name"`
Scale float64 `json:"scale"`
Width int32 `json:"width"`
Height int32 `json:"height"`
Refresh int32 `json:"refresh"`
Enabled bool `json:"enabled"`
}
type randrHead struct {
name string
enabled bool
scale float64
currentModeID uint32
modeIDs []uint32
}
type randrMode struct {
width int32
height int32
refresh int32
}
type randrClient struct {
display *wlclient.Display
ctx *wlclient.Context
manager *wlr_output_management.ZwlrOutputManagerV1
heads map[uint32]*randrHead
modes map[uint32]*randrMode
done bool
err error
}
func queryRandr() ([]randrOutput, error) {
display, err := wlclient.Connect("")
if err != nil {
return nil, fmt.Errorf("failed to connect to Wayland: %w", err)
}
c := &randrClient{
display: display,
ctx: display.Context(),
heads: make(map[uint32]*randrHead),
modes: make(map[uint32]*randrMode),
}
defer c.ctx.Close()
registry, err := display.GetRegistry()
if err != nil {
return nil, fmt.Errorf("failed to get registry: %w", err)
}
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
if e.Interface == wlr_output_management.ZwlrOutputManagerV1InterfaceName {
mgr := wlr_output_management.NewZwlrOutputManagerV1(c.ctx)
version := min(e.Version, 4)
mgr.SetHeadHandler(func(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) {
c.handleHead(e)
})
mgr.SetDoneHandler(func(e wlr_output_management.ZwlrOutputManagerV1DoneEvent) {
c.done = true
})
if err := registry.Bind(e.Name, e.Interface, version, mgr); err == nil {
c.manager = mgr
}
}
})
// First roundtrip: discover globals and bind manager
syncCallback, err := display.Sync()
if err != nil {
return nil, fmt.Errorf("failed to sync display: %w", err)
}
syncCallback.SetDoneHandler(func(e wlclient.CallbackDoneEvent) {
if c.manager == nil {
c.err = fmt.Errorf("zwlr_output_manager_v1 protocol not supported by compositor")
c.done = true
}
// Otherwise wait for manager's DoneHandler
})
for !c.done {
if err := c.ctx.Dispatch(); err != nil {
return nil, fmt.Errorf("dispatch error: %w", err)
}
}
if c.err != nil {
return nil, c.err
}
return c.buildOutputs(), nil
}
func (c *randrClient) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) {
handle := e.Head
headID := handle.ID()
head := &randrHead{
modeIDs: make([]uint32, 0),
}
c.heads[headID] = head
handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) {
head.name = e.Name
})
handle.SetEnabledHandler(func(e wlr_output_management.ZwlrOutputHeadV1EnabledEvent) {
head.enabled = e.Enabled != 0
})
handle.SetScaleHandler(func(e wlr_output_management.ZwlrOutputHeadV1ScaleEvent) {
head.scale = e.Scale
})
handle.SetCurrentModeHandler(func(e wlr_output_management.ZwlrOutputHeadV1CurrentModeEvent) {
head.currentModeID = e.Mode.ID()
})
handle.SetModeHandler(func(e wlr_output_management.ZwlrOutputHeadV1ModeEvent) {
modeHandle := e.Mode
modeID := modeHandle.ID()
head.modeIDs = append(head.modeIDs, modeID)
mode := &randrMode{}
c.modes[modeID] = mode
modeHandle.SetSizeHandler(func(e wlr_output_management.ZwlrOutputModeV1SizeEvent) {
mode.width = e.Width
mode.height = e.Height
})
modeHandle.SetRefreshHandler(func(e wlr_output_management.ZwlrOutputModeV1RefreshEvent) {
mode.refresh = e.Refresh
})
})
}
func (c *randrClient) buildOutputs() []randrOutput {
outputs := make([]randrOutput, 0, len(c.heads))
for _, head := range c.heads {
out := randrOutput{
Name: head.name,
Scale: head.scale,
Enabled: head.enabled,
}
if mode, ok := c.modes[head.currentModeID]; ok {
out.Width = mode.width
out.Height = mode.height
out.Refresh = mode.refresh
}
outputs = append(outputs, out)
}
return outputs
}

View File

@@ -7,12 +7,20 @@ import (
"strings" "strings"
) )
func findCommandPath(cmd string) (string, error) { // isReadOnlyCommand returns true if the CLI args indicate a command that is
path, err := exec.LookPath(cmd) // safe to run as root (e.g. shell completion, help).
if err != nil { func isReadOnlyCommand(args []string) bool {
return "", fmt.Errorf("command '%s' not found in PATH", cmd) for _, arg := range args[1:] {
if strings.HasPrefix(arg, "-") {
continue
}
switch arg {
case "completion", "help", "__complete":
return true
}
return false
} }
return path, nil return false
} }
func isArchPackageInstalled(packageName string) bool { func isArchPackageInstalled(packageName string) bool {

View File

@@ -1,10 +1,12 @@
package clipboard package clipboard
import ( import (
"bytes"
"fmt" "fmt"
"io" "io"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"syscall" "syscall"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
@@ -12,17 +14,37 @@ import (
) )
func Copy(data []byte, mimeType string) error { func Copy(data []byte, mimeType string) error {
return CopyOpts(data, mimeType, false, false) return CopyReader(bytes.NewReader(data), mimeType, false, false)
} }
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error { func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
if foreground {
return copyServeWithWriter(func(writer io.Writer) error {
total := 0
for total < len(data) {
n, err := writer.Write(data[total:])
total += n
if err != nil {
return err
}
}
if total != len(data) {
return io.ErrShortWrite
}
return nil
}, mimeType, pasteOnce)
}
return CopyReader(bytes.NewReader(data), mimeType, foreground, pasteOnce)
}
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
if !foreground { if !foreground {
return copyFork(data, mimeType, pasteOnce) return copyFork(data, mimeType, pasteOnce)
} }
return copyServe(data, mimeType, pasteOnce) return copyServeReader(data, mimeType, pasteOnce)
} }
func copyFork(data []byte, mimeType string, pasteOnce bool) error { func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
args := []string{os.Args[0], "cl", "copy", "--foreground"} args := []string{os.Args[0], "cl", "copy", "--foreground"}
if pasteOnce { if pasteOnce {
args = append(args, "--paste-once") args = append(args, "--paste-once")
@@ -30,11 +52,15 @@ func copyFork(data []byte, mimeType string, pasteOnce bool) error {
args = append(args, "--type", mimeType) args = append(args, "--type", mimeType)
cmd := exec.Command(args[0], args[1:]...) cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = nil
cmd.Stdout = nil cmd.Stdout = nil
cmd.Stderr = nil cmd.Stderr = nil
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
if stdinSource, ok := data.(*os.File); ok {
cmd.Stdin = stdinSource
return cmd.Start()
}
stdin, err := cmd.StdinPipe() stdin, err := cmd.StdinPipe()
if err != nil { if err != nil {
return fmt.Errorf("stdin pipe: %w", err) return fmt.Errorf("stdin pipe: %w", err)
@@ -44,16 +70,66 @@ func copyFork(data []byte, mimeType string, pasteOnce bool) error {
return fmt.Errorf("start: %w", err) return fmt.Errorf("start: %w", err)
} }
if _, err := stdin.Write(data); err != nil { if _, err := io.Copy(stdin, data); err != nil {
stdin.Close() stdin.Close()
return fmt.Errorf("write stdin: %w", err) return fmt.Errorf("write stdin: %w", err)
} }
stdin.Close() if err := stdin.Close(); err != nil {
return fmt.Errorf("close stdin: %w", err)
}
return nil return nil
} }
func copyServe(data []byte, mimeType string, pasteOnce bool) error { func copyServeReader(data io.Reader, mimeType string, pasteOnce bool) error {
cachedData, err := createClipboardCacheFile()
if err != nil {
return fmt.Errorf("create clipboard cache file: %w", err)
}
defer os.Remove(cachedData.Name())
if _, err := io.Copy(cachedData, data); err != nil {
return fmt.Errorf("cache clipboard data: %w", err)
}
if err := cachedData.Close(); err != nil {
return fmt.Errorf("close temp cache file: %w", err)
}
return copyServeWithWriter(func(writer io.Writer) error {
cachedFile, err := os.Open(cachedData.Name())
if err != nil {
return fmt.Errorf("open temp cache file: %w", err)
}
defer cachedFile.Close()
if _, err := io.Copy(writer, cachedFile); err != nil {
return fmt.Errorf("write clipboard data: %w", err)
}
return nil
}, mimeType, pasteOnce)
}
func createClipboardCacheFile() (*os.File, error) {
preferredDirs := []string{}
if cacheDir, err := os.UserCacheDir(); err == nil {
preferredDirs = append(preferredDirs, filepath.Join(cacheDir, "dms", "clipboard"))
}
preferredDirs = append(preferredDirs, "/var/tmp/dms/clipboard")
for _, dir := range preferredDirs {
if err := os.MkdirAll(dir, 0o700); err != nil {
continue
}
cachedData, err := os.CreateTemp(dir, "dms-clipboard-*")
if err == nil {
return cachedData, nil
}
}
return os.CreateTemp("", "dms-clipboard-*")
}
func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOnce bool) error {
display, err := wlclient.Connect("") display, err := wlclient.Connect("")
if err != nil { if err != nil {
return fmt.Errorf("wayland connect: %w", err) return fmt.Errorf("wayland connect: %w", err)
@@ -139,12 +215,18 @@ func copyServe(data []byte, mimeType string, pasteOnce bool) error {
cancelled := make(chan struct{}) cancelled := make(chan struct{})
pasted := make(chan struct{}, 1) pasted := make(chan struct{}, 1)
sendErr := make(chan error, 1)
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) { source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
defer syscall.Close(e.Fd) defer syscall.Close(e.Fd)
file := os.NewFile(uintptr(e.Fd), "pipe") file := os.NewFile(uintptr(e.Fd), "pipe")
defer file.Close() defer file.Close()
file.Write(data) if err := writeTo(file); err != nil {
select {
case sendErr <- err:
default:
}
}
select { select {
case pasted <- struct{}{}: case pasted <- struct{}{}:
default: default:
@@ -165,6 +247,8 @@ func copyServe(data []byte, mimeType string, pasteOnce bool) error {
select { select {
case <-cancelled: case <-cancelled:
return nil return nil
case err := <-sendErr:
return err
case <-pasted: case <-pasted:
if pasteOnce { if pasteOnce {
return nil return nil

View File

@@ -252,6 +252,7 @@ window-rule {
// Open dms windows as floating by default // Open dms windows as floating by default
window-rule { window-rule {
match app-id=r#"org.quickshell$"# match app-id=r#"org.quickshell$"#
match app-id=r#"com.danklinux.dms$"#
open-floating true open-floating true
} }
debug { debug {

View File

@@ -97,6 +97,7 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
dependencies = append(dependencies, a.detectGit()) dependencies = append(dependencies, a.detectGit())
dependencies = append(dependencies, a.detectWindowManager(wm)) dependencies = append(dependencies, a.detectWindowManager(wm))
dependencies = append(dependencies, a.detectQuickshell()) dependencies = append(dependencies, a.detectQuickshell())
dependencies = append(dependencies, a.detectDMSGreeter())
dependencies = append(dependencies, a.detectXDGPortal()) dependencies = append(dependencies, a.detectXDGPortal())
dependencies = append(dependencies, a.detectAccountsService()) dependencies = append(dependencies, a.detectAccountsService())
@@ -124,12 +125,52 @@ func (a *ArchDistribution) detectAccountsService() deps.Dependency {
return a.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", a.packageInstalled("accountsservice")) return a.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", a.packageInstalled("accountsservice"))
} }
func (a *ArchDistribution) detectDMSGreeter() deps.Dependency {
return a.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", a.packageInstalled("greetd-dms-greeter-git"))
}
func (a *ArchDistribution) packageInstalled(pkg string) bool { func (a *ArchDistribution) packageInstalled(pkg string) bool {
cmd := exec.Command("pacman", "-Q", pkg) cmd := exec.Command("pacman", "-Q", pkg)
err := cmd.Run() err := cmd.Run()
return err == nil return err == nil
} }
// parseSRCINFODeps reads a .SRCINFO file and returns runtime dep and makedep package
func parseSRCINFODeps(srcinfoPath string) (deps []string, makedeps []string, err error) {
data, err := os.ReadFile(srcinfoPath)
if err != nil {
return nil, nil, err
}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
var pkg string
var target *[]string
switch {
case strings.HasPrefix(line, "makedepends = "):
pkg = strings.TrimPrefix(line, "makedepends = ")
target = &makedeps
case strings.HasPrefix(line, "depends = "):
pkg = strings.TrimPrefix(line, "depends = ")
target = &deps
default:
continue
}
// Strip version constraint (>=, <=, >, <, =) and colon-descriptions
if idx := strings.IndexAny(pkg, "><:="); idx >= 0 {
pkg = pkg[:idx]
}
pkg = strings.TrimSpace(pkg)
if pkg != "" {
*target = append(*target, pkg)
}
}
return deps, makedeps, nil
}
func (a *ArchDistribution) isInSystemRepo(pkg string) bool {
return exec.Command("pacman", "-Si", pkg).Run() == nil
}
func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant)) return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
} }
@@ -139,6 +180,7 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
"dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]), "dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]),
"git": {Name: "git", Repository: RepoTypeSystem}, "git": {Name: "git", Repository: RepoTypeSystem},
"quickshell": a.getQuickshellMapping(variants["quickshell"]), "quickshell": a.getQuickshellMapping(variants["quickshell"]),
"dms-greeter": {Name: "greetd-dms-greeter-git", Repository: RepoTypeAUR},
"matugen": a.getMatugenMapping(variants["matugen"]), "matugen": a.getMatugenMapping(variants["matugen"]),
"dgop": {Name: "dgop", Repository: RepoTypeSystem}, "dgop": {Name: "dgop", Repository: RepoTypeSystem},
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem}, "ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
@@ -434,29 +476,10 @@ func (a *ArchDistribution) installAURPackages(ctx context.Context, packages []st
a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", "))) a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", ")))
hasNiri := false hasNiri := false
hasQuickshell := false
for _, pkg := range packages { for _, pkg := range packages {
if pkg == "niri-git" { if pkg == "niri-git" {
hasNiri = true hasNiri = true
} }
if pkg == "quickshell" || pkg == "quickshell-git" {
hasQuickshell = true
}
}
// If quickshell is in the list, always reinstall google-breakpad first
if hasQuickshell {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.63,
Step: "Reinstalling google-breakpad for quickshell...",
IsComplete: false,
CommandInfo: "Reinstalling prerequisite AUR package for quickshell",
}
if err := a.installSingleAURPackage(ctx, "google-breakpad", sudoPassword, progressChan, 0.63, 0.65); err != nil {
return fmt.Errorf("failed to reinstall google-breakpad prerequisite for quickshell: %w", err)
}
} }
// If niri is in the list, install makepkg-git-lfs-proto first if not already installed // If niri is in the list, install makepkg-git-lfs-proto first if not already installed
@@ -537,6 +560,16 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
} }
func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error { func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error {
return a.installSingleAURPackageInternal(ctx, pkg, sudoPassword, progressChan, startProgress, endProgress, make(map[string]bool))
}
func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64, visited map[string]bool) error {
if visited[pkg] {
a.log(fmt.Sprintf("Skipping %s (already being installed, cycle detected)", pkg))
return nil
}
visited[pkg] = true
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err) return fmt.Errorf("failed to get user home directory: %w", err)
@@ -610,48 +643,8 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err) return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err)
} }
// Skip dependency installation for dms-shell-git and dms-shell-bin srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
// since we manually manage those dependencies if pkg == "dms-shell-bin" {
if pkg != "dms-shell-git" && pkg != "dms-shell-bin" {
// Pre-install dependencies from .SRCINFO
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.3*(endProgress-startProgress),
Step: fmt.Sprintf("Installing dependencies for %s...", pkg),
IsComplete: false,
CommandInfo: "Installing package dependencies and makedepends",
}
// Install dependencies and makedepends explicitly
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
depsCmd := exec.CommandContext(ctx, "bash", "-c",
fmt.Sprintf(`
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
if [[ "%s" == *"quickshell"* ]]; then
deps=$(echo "$deps" | sed 's/google-breakpad//g' | sed 's/ / /g' | sed 's/^ *//g' | sed 's/ *$//g')
fi
if [ ! -z "$deps" ] && [ "$deps" != " " ]; then
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
fi
`, srcinfoPath, pkg, sudoPassword))
if err := a.runWithProgress(depsCmd, progressChan, PhaseAURPackages, startProgress+0.3*(endProgress-startProgress), startProgress+0.35*(endProgress-startProgress)); err != nil {
return fmt.Errorf("FAILED to install runtime dependencies for %s: %w", pkg, err)
}
makedepsCmd := exec.CommandContext(ctx, "bash", "-c",
fmt.Sprintf(`
makedeps=$(grep -E "^[[:space:]]*makedepends = " "%s" | sed 's/^[[:space:]]*makedepends = //' | tr '\n' ' ')
if [ ! -z "$makedeps" ]; then
echo '%s' | sudo -S pacman -S --needed --noconfirm $makedeps
fi
`, srcinfoPath, sudoPassword))
if err := a.runWithProgress(makedepsCmd, progressChan, PhaseAURPackages, startProgress+0.35*(endProgress-startProgress), startProgress+0.4*(endProgress-startProgress)); err != nil {
return fmt.Errorf("FAILED to install make dependencies for %s: %w", pkg, err)
}
} else {
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages, Phase: PhaseAURPackages,
Progress: startProgress + 0.35*(endProgress-startProgress), Progress: startProgress + 0.35*(endProgress-startProgress),
@@ -659,6 +652,66 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
IsComplete: false, IsComplete: false,
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg), LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
} }
} else {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.3*(endProgress-startProgress),
Step: fmt.Sprintf("Resolving dependencies for %s...", pkg),
IsComplete: false,
CommandInfo: "Classifying dependencies as system or AUR",
}
runtimeDeps, makeDeps, err := parseSRCINFODeps(srcinfoPath)
if err != nil {
return fmt.Errorf("failed to parse .SRCINFO for %s: %w", pkg, err)
}
seen := make(map[string]bool)
var systemPkgs []string
var aurPkgs []string
for _, dep := range append(runtimeDeps, makeDeps...) {
if seen[dep] || a.packageInstalled(dep) {
continue
}
seen[dep] = true
if a.isInSystemRepo(dep) {
systemPkgs = append(systemPkgs, dep)
} else {
aurPkgs = append(aurPkgs, dep)
}
}
if len(systemPkgs) > 0 {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.32*(endProgress-startProgress),
Step: fmt.Sprintf("Installing %d system dependencies for %s...", len(systemPkgs), pkg),
IsComplete: false,
CommandInfo: fmt.Sprintf("sudo pacman -S --needed --noconfirm %s", strings.Join(systemPkgs, " ")),
}
if err := a.installSystemPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install system dependencies for %s: %w", pkg, err)
}
}
for _, aurDep := range aurPkgs {
a.log(fmt.Sprintf("Dependency %s is AUR-only, building from source...", aurDep))
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.35*(endProgress-startProgress),
Step: fmt.Sprintf("Installing AUR dependency %s for %s...", aurDep, pkg),
IsComplete: false,
CommandInfo: fmt.Sprintf("Building AUR dependency: %s", aurDep),
}
if err := a.installSingleAURPackageInternal(ctx, aurDep, sudoPassword, progressChan,
startProgress+0.35*(endProgress-startProgress),
startProgress+0.39*(endProgress-startProgress),
visited,
); err != nil {
return fmt.Errorf("failed to install AUR dependency %s for %s: %w", aurDep, pkg, err)
}
}
} }
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
@@ -671,7 +724,7 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm") buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm")
buildCmd.Dir = packageDir buildCmd.Dir = packageDir
buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar") // Disable compression for speed buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar")
if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil { if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil {
return fmt.Errorf("failed to build %s: %w", pkg, err) return fmt.Errorf("failed to build %s: %w", pkg, err)

View File

@@ -102,6 +102,19 @@ func (b *BaseDistribution) detectPackage(name, description string, installed boo
} }
} }
func (b *BaseDistribution) detectOptionalPackage(name, description string, installed bool) deps.Dependency {
status := deps.StatusMissing
if installed {
status = deps.StatusInstalled
}
return deps.Dependency{
Name: name,
Status: status,
Description: description,
Required: false,
}
}
func (b *BaseDistribution) detectGit() deps.Dependency { func (b *BaseDistribution) detectGit() deps.Dependency {
return b.detectCommand("git", "Version control system") return b.detectCommand("git", "Version control system")
} }

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"os/exec" "os/exec"
"runtime"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
@@ -61,6 +60,7 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, d.detectGit()) dependencies = append(dependencies, d.detectGit())
dependencies = append(dependencies, d.detectWindowManager(wm)) dependencies = append(dependencies, d.detectWindowManager(wm))
dependencies = append(dependencies, d.detectQuickshell()) dependencies = append(dependencies, d.detectQuickshell())
dependencies = append(dependencies, d.detectDMSGreeter())
dependencies = append(dependencies, d.detectXDGPortal()) dependencies = append(dependencies, d.detectXDGPortal())
dependencies = append(dependencies, d.detectAccountsService()) dependencies = append(dependencies, d.detectAccountsService())
@@ -86,12 +86,27 @@ func (d *DebianDistribution) detectAccountsService() deps.Dependency {
return d.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", d.packageInstalled("accountsservice")) return d.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", d.packageInstalled("accountsservice"))
} }
func (d *DebianDistribution) detectDMSGreeter() deps.Dependency {
return d.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", d.packageInstalled("dms-greeter"))
}
func (d *DebianDistribution) packageInstalled(pkg string) bool { func (d *DebianDistribution) packageInstalled(pkg string) bool {
cmd := exec.Command("dpkg", "-l", pkg) cmd := exec.Command("dpkg", "-l", pkg)
err := cmd.Run() err := cmd.Run()
return err == nil return err == nil
} }
func debianRepoArchitecture(arch string) string {
switch arch {
case "amd64", "x86_64":
return "amd64"
case "arm64", "aarch64":
return "arm64"
default:
return arch
}
}
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
return d.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant)) return d.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
} }
@@ -108,6 +123,7 @@ func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
// DMS packages from OBS with variant support // DMS packages from OBS with variant support
"dms (DankMaterialShell)": d.getDmsMapping(variants["dms (DankMaterialShell)"]), "dms (DankMaterialShell)": d.getDmsMapping(variants["dms (DankMaterialShell)"]),
"quickshell": d.getQuickshellMapping(variants["quickshell"]), "quickshell": d.getQuickshellMapping(variants["quickshell"]),
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
@@ -430,7 +446,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
} }
// Add repository // Add repository
repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, runtime.GOARCH, baseURL) repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, debianRepoArchitecture(osInfo.Architecture), baseURL)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages, Phase: PhaseSystemPackages,

View File

@@ -13,6 +13,9 @@ func init() {
Register("fedora", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution { Register("fedora", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan) return NewFedoraDistribution(config, logChan)
}) })
Register("evernight", "#72B8DC", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan)
})
Register("nobara", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution { Register("nobara", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan) return NewFedoraDistribution(config, logChan)
}) })
@@ -75,6 +78,7 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, f.detectGit()) dependencies = append(dependencies, f.detectGit())
dependencies = append(dependencies, f.detectWindowManager(wm)) dependencies = append(dependencies, f.detectWindowManager(wm))
dependencies = append(dependencies, f.detectQuickshell()) dependencies = append(dependencies, f.detectQuickshell())
dependencies = append(dependencies, f.detectDMSGreeter())
dependencies = append(dependencies, f.detectXDGPortal()) dependencies = append(dependencies, f.detectXDGPortal())
dependencies = append(dependencies, f.detectAccountsService()) dependencies = append(dependencies, f.detectAccountsService())
@@ -120,6 +124,7 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
// COPR packages // COPR packages
"quickshell": f.getQuickshellMapping(variants["quickshell"]), "quickshell": f.getQuickshellMapping(variants["quickshell"]),
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}, "matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
"dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]), "dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]),
"dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"}, "dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
@@ -191,6 +196,10 @@ func (f *FedoraDistribution) detectAccountsService() deps.Dependency {
} }
} }
func (f *FedoraDistribution) detectDMSGreeter() deps.Dependency {
return f.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", f.packageInstalled("dms-greeter"))
}
func (f *FedoraDistribution) getPrerequisites() []string { func (f *FedoraDistribution) getPrerequisites() []string {
return []string{ return []string{
"dnf-plugins-core", "dnf-plugins-core",

View File

@@ -55,6 +55,7 @@ const (
PhaseAURPackages PhaseAURPackages
PhaseCursorTheme PhaseCursorTheme
PhaseConfiguration PhaseConfiguration
PhaseGreeterSetup
PhaseComplete PhaseComplete
) )

View File

@@ -71,6 +71,7 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
dependencies = append(dependencies, o.detectGit()) dependencies = append(dependencies, o.detectGit())
dependencies = append(dependencies, o.detectWindowManager(wm)) dependencies = append(dependencies, o.detectWindowManager(wm))
dependencies = append(dependencies, o.detectQuickshell()) dependencies = append(dependencies, o.detectQuickshell())
dependencies = append(dependencies, o.detectDMSGreeter())
dependencies = append(dependencies, o.detectXDGPortal()) dependencies = append(dependencies, o.detectXDGPortal())
dependencies = append(dependencies, o.detectAccountsService()) dependencies = append(dependencies, o.detectAccountsService())
@@ -100,6 +101,10 @@ func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool {
return err == nil return err == nil
} }
func (o *OpenSUSEDistribution) detectDMSGreeter() deps.Dependency {
return o.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", o.packageInstalled("dms-greeter"))
}
func (o *OpenSUSEDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { func (o *OpenSUSEDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
return o.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant)) return o.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
} }
@@ -116,6 +121,7 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
// DMS packages from OBS // DMS packages from OBS
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]), "dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
"quickshell": o.getQuickshellMapping(variants["quickshell"]), "quickshell": o.getQuickshellMapping(variants["quickshell"]),
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"}, "dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},

View File

@@ -63,6 +63,7 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
dependencies = append(dependencies, u.detectGit()) dependencies = append(dependencies, u.detectGit())
dependencies = append(dependencies, u.detectWindowManager(wm)) dependencies = append(dependencies, u.detectWindowManager(wm))
dependencies = append(dependencies, u.detectQuickshell()) dependencies = append(dependencies, u.detectQuickshell())
dependencies = append(dependencies, u.detectDMSGreeter())
dependencies = append(dependencies, u.detectXDGPortal()) dependencies = append(dependencies, u.detectXDGPortal())
dependencies = append(dependencies, u.detectAccountsService()) dependencies = append(dependencies, u.detectAccountsService())
@@ -94,6 +95,10 @@ func (u *UbuntuDistribution) detectAccountsService() deps.Dependency {
return u.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", u.packageInstalled("accountsservice")) return u.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", u.packageInstalled("accountsservice"))
} }
func (u *UbuntuDistribution) detectDMSGreeter() deps.Dependency {
return u.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", u.packageInstalled("dms-greeter"))
}
func (u *UbuntuDistribution) packageInstalled(pkg string) bool { func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
cmd := exec.Command("dpkg", "-l", pkg) cmd := exec.Command("dpkg", "-l", pkg)
err := cmd.Run() err := cmd.Run()
@@ -116,6 +121,7 @@ func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
// DMS packages from PPAs // DMS packages from PPAs
"dms (DankMaterialShell)": u.getDmsMapping(variants["dms (DankMaterialShell)"]), "dms (DankMaterialShell)": u.getDmsMapping(variants["dms (DankMaterialShell)"]),
"quickshell": u.getQuickshellMapping(variants["quickshell"]), "quickshell": u.getQuickshellMapping(variants["quickshell"]),
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}, "matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}, "dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"}, "ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},

View File

@@ -0,0 +1,91 @@
# AppArmor profile for dms-greeter
#
# Managed by DMS — regenerated on every `dms greeter install` / `dms greeter sync`.
# Manual edits will be overwritten on next sync.
#
# Mode: complain (denials are logged, nothing is blocked)
# To switch to enforce after validating with `aa-logprof`:
# sudo aa-enforce /etc/apparmor.d/usr.bin.dms-greeter
#
#include <tunables/global>
profile dms-greeter /usr/bin/dms-greeter flags=(complain) {
#include <abstractions/base>
#include <abstractions/bash>
# The launcher script itself
/usr/bin/dms-greeter r,
# Cache directory — created by dms greeter sync/enable with greeter:greeter ownership
/var/cache/dms-greeter/ rw,
/var/cache/dms-greeter/** rwlk,
# DMS config — packaged path
/usr/share/quickshell/dms-greeter/ r,
/usr/share/quickshell/dms-greeter/** r,
/usr/share/quickshell/ r,
/usr/share/quickshell/** r,
# DMS config — system and user overrides
/etc/dms/ r,
/etc/dms/** r,
/usr/share/dms/ r,
/usr/share/dms/** r,
/home/*/.config/quickshell/ r,
/home/*/.config/quickshell/** r,
/root/.config/quickshell/ r,
/root/.config/quickshell/** r,
# greetd / PAM — read-only for session setup
/etc/greetd/ r,
/etc/greetd/** r,
/etc/pam.d/ r,
/etc/pam.d/** r,
/usr/lib/pam.d/ r,
/usr/lib/pam.d/** r,
# Compositor binaries — run unconfined so each compositor uses its own profile
/usr/bin/niri Ux,
/usr/bin/hyprland Ux,
/usr/bin/Hyprland Ux,
/usr/bin/sway Ux,
/usr/bin/labwc Ux,
/usr/bin/scroll Ux,
/usr/bin/miracle-wm Ux,
/usr/bin/mango Ux,
# Quickshell — run unconfined (has its own compositor profile on some distros)
/usr/bin/qs Ux,
/usr/bin/quickshell Ux,
# Wayland / XDG runtime (pipewire, wireplumber, wayland socket)
/run/user/[0-9]*/ rw,
/run/user/[0-9]*/** rw,
# DRM / GPU devices (required for Wayland compositor startup)
/dev/dri/ r,
/dev/dri/* rw,
/dev/udmabuf rw,
# Input devices
/dev/input/ r,
/dev/input/* r,
# Systemd journal / logging
/run/systemd/journal/socket rw,
/dev/log rw,
# Shell helper binaries invoked by the launcher script
/usr/bin/env ix,
/usr/bin/mkdir ix,
/usr/bin/cat ix,
/usr/bin/grep ix,
/usr/bin/dirname ix,
/usr/bin/basename ix,
/usr/bin/command ix,
/bin/env ix,
/bin/mkdir ix,
# Signal management (compositor lifecycle)
signal (send, receive) set=("term", "int", "hup", "kill"),
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,98 @@
package greeter
import (
"os"
"path/filepath"
"testing"
)
func writeTestJSON(t *testing.T, path string, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("failed to create parent dir for %s: %v", path, err)
}
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatalf("failed to write %s: %v", path, err)
}
}
func TestResolveGreeterThemeSyncState(t *testing.T) {
t.Parallel()
tests := []struct {
name string
settingsJSON string
sessionJSON string
wantSourcePath string
wantResolvedWallpaper string
wantDynamicOverrideUsed bool
}{
{
name: "dynamic theme with greeter wallpaper override uses generated greeter colors",
settingsJSON: `{
"currentThemeName": "dynamic",
"greeterWallpaperPath": "Pictures/blue.jpg",
"matugenScheme": "scheme-tonal-spot",
"iconTheme": "Papirus"
}`,
sessionJSON: `{"isLightMode":true}`,
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "greeter-colors", "dms-colors.json"),
wantResolvedWallpaper: filepath.Join("Pictures", "blue.jpg"),
wantDynamicOverrideUsed: true,
},
{
name: "dynamic theme without override uses desktop colors",
settingsJSON: `{
"currentThemeName": "dynamic",
"greeterWallpaperPath": ""
}`,
sessionJSON: `{"isLightMode":false}`,
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
wantResolvedWallpaper: "",
wantDynamicOverrideUsed: false,
},
{
name: "non-dynamic theme keeps desktop colors even with override wallpaper",
settingsJSON: `{
"currentThemeName": "purple",
"greeterWallpaperPath": "/tmp/blue.jpg"
}`,
sessionJSON: `{"isLightMode":false}`,
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
wantResolvedWallpaper: "/tmp/blue.jpg",
wantDynamicOverrideUsed: false,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
homeDir := t.TempDir()
writeTestJSON(t, filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"), tt.settingsJSON)
writeTestJSON(t, filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"), tt.sessionJSON)
state, err := resolveGreeterThemeSyncState(homeDir)
if err != nil {
t.Fatalf("resolveGreeterThemeSyncState returned error: %v", err)
}
if got := state.effectiveColorsSource(homeDir); got != filepath.Join(homeDir, tt.wantSourcePath) {
t.Fatalf("effectiveColorsSource = %q, want %q", got, filepath.Join(homeDir, tt.wantSourcePath))
}
wantResolvedWallpaper := tt.wantResolvedWallpaper
if wantResolvedWallpaper != "" && !filepath.IsAbs(wantResolvedWallpaper) {
wantResolvedWallpaper = filepath.Join(homeDir, wantResolvedWallpaper)
}
if state.ResolvedGreeterWallpaperPath != wantResolvedWallpaper {
t.Fatalf("ResolvedGreeterWallpaperPath = %q, want %q", state.ResolvedGreeterWallpaperPath, wantResolvedWallpaper)
}
if state.UsesDynamicWallpaperOverride != tt.wantDynamicOverrideUsed {
t.Fatalf("UsesDynamicWallpaperOverride = %v, want %v", state.UsesDynamicWallpaperOverride, tt.wantDynamicOverrideUsed)
}
})
}
}

View File

@@ -1,7 +1,9 @@
package matugen package matugen
import ( import (
"bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"math" "math"
"os" "os"
@@ -19,6 +21,8 @@ import (
"github.com/lucasb-eyer/go-colorful" "github.com/lucasb-eyer/go-colorful"
) )
var ErrNoChanges = errors.New("no color changes")
type ColorMode string type ColorMode string
const ( const (
@@ -54,7 +58,7 @@ var templateRegistry = []TemplateDef{
{ID: "qt6ct", Commands: []string{"qt6ct"}, ConfigFile: "qt6ct.toml"}, {ID: "qt6ct", Commands: []string{"qt6ct"}, ConfigFile: "qt6ct.toml"},
{ID: "firefox", Commands: []string{"firefox"}, ConfigFile: "firefox.toml"}, {ID: "firefox", Commands: []string{"firefox"}, ConfigFile: "firefox.toml"},
{ID: "pywalfox", Commands: []string{"pywalfox"}, ConfigFile: "pywalfox.toml"}, {ID: "pywalfox", Commands: []string{"pywalfox"}, ConfigFile: "pywalfox.toml"},
{ID: "zenbrowser", Commands: []string{"zen", "zen-browser"}, Flatpaks: []string{"app.zen_browser.zen"}, ConfigFile: "zenbrowser.toml"}, {ID: "zenbrowser", Commands: []string{"zen", "zen-browser", "zen-beta", "zen-twilight"}, Flatpaks: []string{"app.zen_browser.zen"}, ConfigFile: "zenbrowser.toml"},
{ID: "vesktop", Commands: []string{"vesktop"}, Flatpaks: []string{"dev.vencord.Vesktop"}, ConfigFile: "vesktop.toml"}, {ID: "vesktop", Commands: []string{"vesktop"}, Flatpaks: []string{"dev.vencord.Vesktop"}, ConfigFile: "vesktop.toml"},
{ID: "equibop", Commands: []string{"equibop"}, ConfigFile: "equibop.toml"}, {ID: "equibop", Commands: []string{"equibop"}, ConfigFile: "equibop.toml"},
{ID: "ghostty", Commands: []string{"ghostty"}, ConfigFile: "ghostty.toml", Kind: TemplateKindTerminal}, {ID: "ghostty", Commands: []string{"ghostty"}, ConfigFile: "ghostty.toml", Kind: TemplateKindTerminal},
@@ -67,6 +71,7 @@ var templateRegistry = []TemplateDef{
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true}, {ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
{ID: "vscode", Kind: TemplateKindVSCode}, {ID: "vscode", Kind: TemplateKindVSCode},
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs}, {ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs},
{ID: "zed", Commands: []string{"zed", "zeditor", "zedit"}, ConfigFile: "zed.toml"},
} }
func (c *ColorMode) GTKTheme() string { func (c *ColorMode) GTKTheme() string {
@@ -95,6 +100,7 @@ type Options struct {
IconTheme string IconTheme string
MatugenType string MatugenType string
RunUserTemplates bool RunUserTemplates bool
ColorsOnly bool
StockColors string StockColors string
SyncModeWithPortal bool SyncModeWithPortal bool
TerminalsAlwaysDark bool TerminalsAlwaysDark bool
@@ -160,8 +166,14 @@ func Run(opts Options) error {
log.Infof("Building theme: %s %s (%s)", opts.Kind, opts.Value, opts.Mode) log.Infof("Building theme: %s %s (%s)", opts.Kind, opts.Value, opts.Mode)
if err := buildOnce(&opts); err != nil { changed, buildErr := buildOnce(&opts)
return err if buildErr != nil {
return buildErr
}
if !changed {
log.Info("No color changes detected, skipping refresh")
return ErrNoChanges
} }
if opts.SyncModeWithPortal { if opts.SyncModeWithPortal {
@@ -172,25 +184,27 @@ func Run(opts Options) error {
return nil return nil
} }
func buildOnce(opts *Options) error { func buildOnce(opts *Options) (bool, error) {
cfgFile, err := os.CreateTemp("", "matugen-config-*.toml") cfgFile, err := os.CreateTemp("", "matugen-config-*.toml")
if err != nil { if err != nil {
return fmt.Errorf("failed to create temp config: %w", err) return false, fmt.Errorf("failed to create temp config: %w", err)
} }
defer os.Remove(cfgFile.Name()) defer os.Remove(cfgFile.Name())
defer cfgFile.Close() defer cfgFile.Close()
tmpDir, err := os.MkdirTemp("", "matugen-templates-*") tmpDir, err := os.MkdirTemp("", "matugen-templates-*")
if err != nil { if err != nil {
return fmt.Errorf("failed to create temp dir: %w", err) return false, fmt.Errorf("failed to create temp dir: %w", err)
} }
defer os.RemoveAll(tmpDir) defer os.RemoveAll(tmpDir)
if err := buildMergedConfig(opts, cfgFile, tmpDir); err != nil { if err := buildMergedConfig(opts, cfgFile, tmpDir); err != nil {
return fmt.Errorf("failed to build config: %w", err) return false, fmt.Errorf("failed to build config: %w", err)
} }
cfgFile.Close() cfgFile.Close()
oldColors, _ := os.ReadFile(opts.ColorsOutput())
var primaryDark, primaryLight, surface string var primaryDark, primaryLight, surface string
var dank16JSON string var dank16JSON string
var importArgs []string var importArgs []string
@@ -202,7 +216,7 @@ func buildOnce(opts *Options) error {
surface = extractNestedColor(opts.StockColors, "surface", "dark") surface = extractNestedColor(opts.StockColors, "surface", "dark")
if primaryDark == "" { if primaryDark == "" {
return fmt.Errorf("failed to extract primary dark from stock colors") return false, fmt.Errorf("failed to extract primary dark from stock colors")
} }
if primaryLight == "" { if primaryLight == "" {
primaryLight = primaryDark primaryLight = primaryDark
@@ -216,14 +230,14 @@ func buildOnce(opts *Options) error {
args := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()} args := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()}
args = append(args, importArgs...) args = append(args, importArgs...)
if err := runMatugen(args); err != nil { if err := runMatugen(args); err != nil {
return err return false, err
} }
} else { } else {
log.Infof("Using dynamic theme from %s: %s", opts.Kind, opts.Value) log.Infof("Using dynamic theme from %s: %s", opts.Kind, opts.Value)
matJSON, err := runMatugenDryRun(opts) matJSON, err := runMatugenDryRun(opts)
if err != nil { if err != nil {
return fmt.Errorf("matugen dry-run failed: %w", err) return false, fmt.Errorf("matugen dry-run failed: %w", err)
} }
primaryDark = extractMatugenColor(matJSON, "primary", "dark") primaryDark = extractMatugenColor(matJSON, "primary", "dark")
@@ -231,7 +245,7 @@ func buildOnce(opts *Options) error {
surface = extractMatugenColor(matJSON, "surface", "dark") surface = extractMatugenColor(matJSON, "surface", "dark")
if primaryDark == "" { if primaryDark == "" {
return fmt.Errorf("failed to extract primary color") return false, fmt.Errorf("failed to extract primary color")
} }
if primaryLight == "" { if primaryLight == "" {
primaryLight = primaryDark primaryLight = primaryDark
@@ -252,10 +266,19 @@ func buildOnce(opts *Options) error {
args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()) args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name())
args = append(args, importArgs...) args = append(args, importArgs...)
if err := runMatugen(args); err != nil { if err := runMatugen(args); err != nil {
return err return false, err
} }
} }
newColors, _ := os.ReadFile(opts.ColorsOutput())
if bytes.Equal(oldColors, newColors) && len(oldColors) > 0 {
return false, nil
}
if opts.ColorsOnly {
return true, nil
}
if isDMSGTKActive(opts.ConfigDir) { if isDMSGTKActive(opts.ConfigDir) {
switch opts.Mode { switch opts.Mode {
case ColorModeLight: case ColorModeLight:
@@ -273,7 +296,7 @@ func buildOnce(opts *Options) error {
signalTerminals(opts) signalTerminals(opts)
return nil return true, nil
} }
func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error { func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error {
@@ -313,6 +336,10 @@ output_path = '%s'
`, opts.ShellDir, opts.ColorsOutput()) `, opts.ShellDir, opts.ColorsOutput())
if opts.ColorsOnly {
return nil
}
homeDir, _ := os.UserHomeDir() homeDir, _ := os.UserHomeDir()
for _, tmpl := range templateRegistry { for _, tmpl := range templateRegistry {
if opts.ShouldSkipTemplate(tmpl.ID) { if opts.ShouldSkipTemplate(tmpl.ID) {
@@ -579,10 +606,10 @@ func detectMatugenVersionLocked() (matugenFlags, error) {
matugenVersionOK = true matugenVersionOK = true
if matugenSupportsCOE { if matugenSupportsCOE {
log.Infof("Matugen %s supports --continue-on-error", versionStr) log.Debugf("Matugen %s detected: continue-on-error support enabled", versionStr)
} }
if matugenIsV4 { if matugenIsV4 {
log.Infof("Matugen %s: using v4 flags", versionStr) log.Debugf("Matugen %s detected: using v4 compatibility flags", versionStr)
} }
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
} }

View File

@@ -3,6 +3,7 @@ package matugen
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils" mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils"
@@ -392,3 +393,51 @@ func TestSubstituteVars(t *testing.T) {
}) })
} }
} }
func TestBuildMergedConfigColorsOnly(t *testing.T) {
tempDir := t.TempDir()
shellDir := filepath.Join(tempDir, "shell")
configsDir := filepath.Join(shellDir, "matugen", "configs")
if err := os.MkdirAll(configsDir, 0o755); err != nil {
t.Fatalf("failed to create configs dir: %v", err)
}
baseConfig := "[config]\ncustom_keywords = []\n"
if err := os.WriteFile(filepath.Join(configsDir, "base.toml"), []byte(baseConfig), 0o644); err != nil {
t.Fatalf("failed to write base config: %v", err)
}
cfgFile, err := os.CreateTemp(tempDir, "merged-*.toml")
if err != nil {
t.Fatalf("failed to create temp config: %v", err)
}
defer os.Remove(cfgFile.Name())
defer cfgFile.Close()
opts := &Options{
ShellDir: shellDir,
ConfigDir: filepath.Join(tempDir, "config"),
StateDir: filepath.Join(tempDir, "state"),
ColorsOnly: true,
}
if err := buildMergedConfig(opts, cfgFile, filepath.Join(tempDir, "templates")); err != nil {
t.Fatalf("buildMergedConfig failed: %v", err)
}
if err := cfgFile.Close(); err != nil {
t.Fatalf("failed to close merged config: %v", err)
}
output, err := os.ReadFile(cfgFile.Name())
if err != nil {
t.Fatalf("failed to read merged config: %v", err)
}
content := string(output)
assert.Contains(t, content, "[templates.dank]")
assert.Contains(t, content, "output_path = '"+filepath.Join(opts.StateDir, "dms-colors.json")+"'")
assert.NotContains(t, content, "[templates.gtk]")
assert.False(t, strings.Contains(content, "output_path = 'CONFIG_DIR/"), "colors-only config should not emit app template outputs")
}

View File

@@ -2,6 +2,7 @@ package matugen
import ( import (
"context" "context"
"errors"
"sync" "sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
@@ -93,10 +94,13 @@ func (q *Queue) runWorker() {
err := Run(job.Options) err := Run(job.Options)
var result Result var result Result
if err != nil { switch {
result = Result{Success: false, Error: err} case err == nil:
} else {
result = Result{Success: true} result = Result{Success: true}
case errors.Is(err, ErrNoChanges):
result = Result{Success: true}
default:
result = Result{Success: false, Error: err}
} }
q.finishJob(result) q.finishJob(result)

View File

@@ -311,6 +311,10 @@ func (m *Manager) handleDevicePropertiesChanged(path dbus.ObjectPath, changed ma
select { select {
case m.eventQueue <- func() { case m.eventQueue <- func() {
time.Sleep(300 * time.Millisecond) time.Sleep(300 * time.Millisecond)
log.Infof("[Bluetooth] Auto-trusting newly paired device: %s", devicePath)
if err := m.TrustDevice(devicePath, true); err != nil {
log.Warnf("[Bluetooth] Auto-trust failed: %v", err)
}
log.Infof("[Bluetooth] Auto-connecting newly paired device: %s", devicePath) log.Infof("[Bluetooth] Auto-connecting newly paired device: %s", devicePath)
if err := m.ConnectDevice(devicePath); err != nil { if err := m.ConnectDevice(devicePath); err != nil {
log.Warnf("[Bluetooth] Auto-connect failed: %v", err) log.Warnf("[Bluetooth] Auto-connect failed: %v", err)

View File

@@ -5,5 +5,6 @@ const (
dbusPath = "/org/freedesktop/login1" dbusPath = "/org/freedesktop/login1"
dbusManagerInterface = "org.freedesktop.login1.Manager" dbusManagerInterface = "org.freedesktop.login1.Manager"
dbusSessionInterface = "org.freedesktop.login1.Session" dbusSessionInterface = "org.freedesktop.login1.Session"
dbusUserInterface = "org.freedesktop.login1.User"
dbusPropsInterface = "org.freedesktop.DBus.Properties" dbusPropsInterface = "org.freedesktop.DBus.Properties"
) )

View File

@@ -17,15 +17,8 @@ func NewManager() (*Manager, error) {
return nil, fmt.Errorf("failed to connect to system bus: %w", err) return nil, fmt.Errorf("failed to connect to system bus: %w", err)
} }
sessionID := os.Getenv("XDG_SESSION_ID")
if sessionID == "" {
sessionID = "self"
}
m := &Manager{ m := &Manager{
state: &SessionState{ state: &SessionState{},
SessionID: sessionID,
},
stateMutex: sync.RWMutex{}, stateMutex: sync.RWMutex{},
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
@@ -60,12 +53,13 @@ func (m *Manager) initialize() error {
m.initializeFallbackDelay() m.initializeFallbackDelay()
sessionPath, err := m.getSession(m.state.SessionID) sessionID, sessionPath, err := m.discoverSession()
if err != nil { if err != nil {
return fmt.Errorf("failed to get session path: %w", err) return fmt.Errorf("failed to get session path: %w", err)
} }
m.stateMutex.Lock() m.stateMutex.Lock()
m.state.SessionID = sessionID
m.state.SessionPath = string(sessionPath) m.state.SessionPath = string(sessionPath)
m.sessionPath = sessionPath m.sessionPath = sessionPath
m.stateMutex.Unlock() m.stateMutex.Unlock()
@@ -79,6 +73,41 @@ func (m *Manager) initialize() error {
return nil return nil
} }
func (m *Manager) discoverSession() (string, dbus.ObjectPath, error) {
// 1. Explicit XDG_SESSION_ID
if id := os.Getenv("XDG_SESSION_ID"); id != "" {
if path, err := m.getSession(id); err == nil {
fmt.Fprintf(os.Stderr, "loginctl: using XDG_SESSION_ID=%s\n", id)
return id, path, nil
}
}
// 2. PID-based lookup (works when caller is inside a session cgroup)
if id, path, err := m.getSessionByPID(uint32(os.Getpid())); err == nil {
fmt.Fprintf(os.Stderr, "loginctl: found session %s via PID\n", id)
return id, path, nil
}
// 3. User's primary display session (handles UWSM and similar)
if id, path, err := m.getUserDisplaySession(); err == nil {
fmt.Fprintf(os.Stderr, "loginctl: found session %s via User.Display\n", id)
return id, path, nil
}
// 4. Score all sessions for current UID
if id, path, err := m.findBestSession(); err == nil {
fmt.Fprintf(os.Stderr, "loginctl: found session %s via ListSessions scoring\n", id)
return id, path, nil
}
// 5. Last resort: "self"
path, err := m.getSession("self")
if err != nil {
return "", "", fmt.Errorf("%w", err)
}
return "self", path, nil
}
func (m *Manager) getSession(id string) (dbus.ObjectPath, error) { func (m *Manager) getSession(id string) (dbus.ObjectPath, error) {
var out dbus.ObjectPath var out dbus.ObjectPath
err := m.managerObj.Call(dbusManagerInterface+".GetSession", 0, id).Store(&out) err := m.managerObj.Call(dbusManagerInterface+".GetSession", 0, id).Store(&out)
@@ -88,6 +117,166 @@ func (m *Manager) getSession(id string) (dbus.ObjectPath, error) {
return out, nil return out, nil
} }
func (m *Manager) getSessionByPID(pid uint32) (string, dbus.ObjectPath, error) {
var path dbus.ObjectPath
if err := m.managerObj.Call(dbusManagerInterface+".GetSessionByPID", 0, pid).Store(&path); err != nil {
return "", "", err
}
sessionObj := m.conn.Object(dbusDest, path)
var id dbus.Variant
if err := sessionObj.Call(dbusPropsInterface+".Get", 0, dbusSessionInterface, "Id").Store(&id); err != nil {
return "", "", err
}
return id.Value().(string), path, nil
}
func (m *Manager) getUserDisplaySession() (string, dbus.ObjectPath, error) {
uid := uint32(os.Getuid())
var userPath dbus.ObjectPath
if err := m.managerObj.Call(dbusManagerInterface+".GetUser", 0, uid).Store(&userPath); err != nil {
return "", "", err
}
userObj := m.conn.Object(dbusDest, userPath)
var display dbus.Variant
if err := userObj.Call(dbusPropsInterface+".Get", 0, dbusUserInterface, "Display").Store(&display); err != nil {
return "", "", err
}
pair, ok := display.Value().([]any)
if !ok || len(pair) < 2 {
return "", "", fmt.Errorf("unexpected Display format")
}
sessionID, _ := pair[0].(string)
sessionPath, _ := pair[1].(dbus.ObjectPath)
if sessionID == "" || sessionPath == "" {
return "", "", fmt.Errorf("empty Display session")
}
return sessionID, sessionPath, nil
}
type sessionCandidate struct {
id string
path dbus.ObjectPath
}
func (m *Manager) findBestSession() (string, dbus.ObjectPath, error) {
// ListSessions returns a(susso): [][]any where each entry is [id, uid, name, seat, path]
var raw [][]any
if err := m.managerObj.Call(dbusManagerInterface+".ListSessions", 0).Store(&raw); err != nil {
return "", "", err
}
uid := uint32(os.Getuid())
var candidates []sessionCandidate
for _, entry := range raw {
if len(entry) < 5 {
continue
}
entryUID, _ := entry[1].(uint32)
if entryUID != uid {
continue
}
id, _ := entry[0].(string)
path, _ := entry[4].(dbus.ObjectPath)
if id != "" && path != "" {
candidates = append(candidates, sessionCandidate{id: id, path: path})
}
}
if len(candidates) == 0 {
return "", "", fmt.Errorf("no sessions for uid %d", uid)
}
bestScore := -1
var best sessionCandidate
for _, c := range candidates {
score := m.scoreSession(c.path)
if score > bestScore {
bestScore = score
best = c
}
}
if bestScore < 0 {
return "", "", fmt.Errorf("no viable session found")
}
return best.id, best.path, nil
}
func (m *Manager) scoreSession(path dbus.ObjectPath) int {
obj := m.conn.Object(dbusDest, path)
var props map[string]dbus.Variant
if err := obj.Call(dbusPropsInterface+".GetAll", 0, dbusSessionInterface).Store(&props); err != nil {
return -1
}
getStr := func(key string) string {
if v, ok := props[key]; ok {
if s, ok := v.Value().(string); ok {
return s
}
}
return ""
}
getBool := func(key string) bool {
if v, ok := props[key]; ok {
if b, ok := v.Value().(bool); ok {
return b
}
}
return false
}
getUint32 := func(key string) uint32 {
if v, ok := props[key]; ok {
if u, ok := v.Value().(uint32); ok {
return u
}
}
return 0
}
class := getStr("Class")
if class != "user" {
return -1
}
if getBool("Remote") {
return -1
}
score := 0
if getBool("Active") {
score += 100
}
switch getStr("Type") {
case "wayland", "x11":
score += 80
case "tty":
score += 10
}
if v, ok := props["Seat"]; ok {
if seatArr, ok := v.Value().([]any); ok && len(seatArr) >= 1 {
if seat, ok := seatArr[0].(string); ok && seat != "" {
score += 40
if seat == "seat0" {
score += 10
}
}
}
}
if getUint32("VTNr") > 0 {
score += 20
}
return score
}
func (m *Manager) refreshSessionBinding() error { func (m *Manager) refreshSessionBinding() error {
if m.managerObj == nil || m.conn == nil { if m.managerObj == nil || m.conn == nil {
return fmt.Errorf("manager not fully initialized") return fmt.Errorf("manager not fully initialized")

View File

@@ -1,6 +1,7 @@
package network package network
import ( import (
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -28,7 +29,13 @@ func TestDetectResult_HasNetworkdField(t *testing.T) {
func TestDetectNetworkStack_Integration(t *testing.T) { func TestDetectNetworkStack_Integration(t *testing.T) {
result, err := DetectNetworkStack() result, err := DetectNetworkStack()
if err != nil && strings.Contains(err.Error(), "connect system bus") {
t.Skipf("system D-Bus unavailable: %v", err)
}
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, result) if assert.NotNil(t, result) {
assert.NotEmpty(t, result.ChosenReason) assert.NotEmpty(t, result.ChosenReason)
}
} }

View File

@@ -1599,6 +1599,13 @@ func Start(printDocs bool) error {
log.Warnf("Theme mode manager unavailable: %v", err) log.Warnf("Theme mode manager unavailable: %v", err)
} else { } else {
notifyCapabilityChange() notifyCapabilityChange()
go func() {
<-loginctlReady
if loginctlManager == nil {
return
}
themeModeManager.WatchLoginctl(loginctlManager)
}()
} }
fatalErrChan := make(chan error, 1) fatalErrChan := make(chan error, 1)

View File

@@ -5,6 +5,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland" "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
@@ -187,6 +188,29 @@ func (m *Manager) Close() {
}) })
} }
func (m *Manager) WatchLoginctl(lm *loginctl.Manager) {
ch := lm.Subscribe("thememode")
m.wg.Add(1)
go func() {
defer m.wg.Done()
defer lm.Unsubscribe("thememode")
for {
select {
case <-m.stopChan:
return
case state, ok := <-ch:
if !ok {
return
}
if state.PreparingForSleep {
continue
}
m.TriggerUpdate()
}
}
}()
}
func (m *Manager) schedulerLoop() { func (m *Manager) schedulerLoop() {
defer m.wg.Done() defer m.wg.Done()
@@ -327,10 +351,12 @@ func statesEqual(a, b *State) bool {
} }
func (m *Manager) computeSchedule(now time.Time, config Config) (bool, time.Time) { func (m *Manager) computeSchedule(now time.Time, config Config) (bool, time.Time) {
if config.Mode == "location" { switch config.Mode {
case "location":
return m.computeLocationSchedule(now, config) return m.computeLocationSchedule(now, config)
default:
return computeTimeSchedule(now, config)
} }
return computeTimeSchedule(now, config)
} }
func computeTimeSchedule(now time.Time, config Config) (bool, time.Time) { func computeTimeSchedule(now time.Time, config Config) (bool, time.Time) {
@@ -381,10 +407,10 @@ func (m *Manager) computeLocationSchedule(now time.Time, config Config) (bool, t
} }
times, cond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, now, config.ElevationTwilight, config.ElevationDaylight) times, cond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, now, config.ElevationTwilight, config.ElevationDaylight)
if cond != wayland.SunNormal { switch cond {
if cond == wayland.SunMidnightSun { case wayland.SunMidnightSun:
return true, startOfNextDay(now) return true, startOfNextDay(now)
} case wayland.SunPolarNight:
return false, startOfNextDay(now) return false, startOfNextDay(now)
} }
@@ -397,10 +423,10 @@ func (m *Manager) computeLocationSchedule(now time.Time, config Config) (bool, t
nextDay := startOfNextDay(now) nextDay := startOfNextDay(now)
nextTimes, nextCond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, nextDay, config.ElevationTwilight, config.ElevationDaylight) nextTimes, nextCond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, nextDay, config.ElevationTwilight, config.ElevationDaylight)
if nextCond != wayland.SunNormal { switch nextCond {
if nextCond == wayland.SunMidnightSun { case wayland.SunMidnightSun:
return true, startOfNextDay(nextDay) return true, startOfNextDay(nextDay)
} case wayland.SunPolarNight:
return false, startOfNextDay(nextDay) return false, startOfNextDay(nextDay)
} }
@@ -413,13 +439,7 @@ func startOfNextDay(t time.Time) time.Time {
} }
func validateHourMinute(hour, minute int) bool { func validateHourMinute(hour, minute int) bool {
if hour < 0 || hour > 23 { return hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59
return false
}
if minute < 0 || minute > 59 {
return false
}
return true
} }
func (m *Manager) ValidateSchedule(startHour, startMinute, endHour, endMinute int) error { func (m *Manager) ValidateSchedule(startHour, startMinute, endHour, endMinute int) error {

View File

@@ -2,10 +2,10 @@ package wlcontext
import ( import (
"fmt" "fmt"
"golang.org/x/sys/unix"
"os" "os"
"sync" "sync"
"time"
"golang.org/x/sys/unix"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
@@ -123,6 +123,9 @@ func (sc *SharedContext) eventDispatcher() {
{Fd: int32(sc.wakeR), Events: unix.POLLIN}, {Fd: int32(sc.wakeR), Events: unix.POLLIN},
} }
consecutiveErrors := 0
const maxConsecutiveErrors = 20
for { for {
sc.drainCmdQueue() sc.drainCmdQueue()
@@ -153,9 +156,19 @@ func (sc *SharedContext) eventDispatcher() {
} }
if err := ctx.Dispatch(); err != nil && !os.IsTimeout(err) { if err := ctx.Dispatch(); err != nil && !os.IsTimeout(err) {
log.Errorf("Wayland connection error: %v", err) consecutiveErrors++
return log.Warnf("Wayland connection error (%d/%d): %v", consecutiveErrors, maxConsecutiveErrors, err)
if consecutiveErrors >= maxConsecutiveErrors {
log.Errorf("Fatal: Wayland connection unrecoverable after %d attempts. Exiting dispatcher.", maxConsecutiveErrors)
return
}
time.Sleep(100 * time.Millisecond * time.Duration(consecutiveErrors))
continue
} }
consecutiveErrors = 0
} }
} }

View File

@@ -7,6 +7,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps" "github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros" "github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
@@ -80,19 +81,24 @@ func (m Model) viewDependencyReview() string {
} }
} }
note := ""
if dep.Name == "dms-greeter" {
note = m.styles.Subtle.Render(" (selection replaces your current display manager)")
}
var line string var line string
if i == m.selectedDep { if i == m.selectedDep {
line = fmt.Sprintf("▶ %s%s%-25s %s", reinstallMarker, variantMarker, dep.Name, status) line = fmt.Sprintf("▶ %s%s%-25s %s", reinstallMarker, variantMarker, dep.Name, status)
if dep.Version != "" { if dep.Version != "" {
line += fmt.Sprintf(" (%s)", dep.Version) line += fmt.Sprintf(" (%s)", dep.Version)
} }
line = m.styles.SelectedOption.Render(line) line = m.styles.SelectedOption.Render(line) + note
} else { } else {
line = fmt.Sprintf(" %s%s%-25s %s", reinstallMarker, variantMarker, dep.Name, status) line = fmt.Sprintf(" %s%s%-25s %s", reinstallMarker, variantMarker, dep.Name, status)
if dep.Version != "" { if dep.Version != "" {
line += fmt.Sprintf(" (%s)", dep.Version) line += fmt.Sprintf(" (%s)", dep.Version)
} }
line = m.styles.Normal.Render(line) line = m.styles.Normal.Render(line) + note
} }
b.WriteString(line) b.WriteString(line)
@@ -115,6 +121,13 @@ func (m Model) updateDetectingDepsState(msg tea.Msg) (tea.Model, tea.Cmd) {
m.state = StateError m.state = StateError
} else { } else {
m.dependencies = depsMsg.deps m.dependencies = depsMsg.deps
// dms-greeter is opt-in skipped by default
for _, dep := range depsMsg.deps {
if dep.Name == "dms-greeter" {
m.disabledItems["dms-greeter"] = true
break
}
}
m.state = StateDependencyReview m.state = StateDependencyReview
} }
return m, m.listenForLogs() return m, m.listenForLogs()
@@ -230,6 +243,41 @@ func (m Model) installPackages() tea.Cmd {
// Convert installer messages to TUI messages // Convert installer messages to TUI messages
go func() { go func() {
for msg := range installerProgressChan { for msg := range installerProgressChan {
// Run optional greeter setup
if msg.Phase == distros.PhaseComplete && msg.IsComplete && msg.Error == nil {
greeterSelected := false
for _, dep := range m.dependencies {
if dep.Name == "dms-greeter" && !m.disabledItems["dms-greeter"] {
greeterSelected = true
break
}
}
if greeterSelected {
compositorName := "niri"
if m.selectedWM == 1 {
compositorName = "Hyprland"
}
m.packageProgressChan <- packageInstallProgressMsg{
progress: 0.92,
step: "Configuring DMS greeter...",
logOutput: "Starting automated greeter setup...",
}
greeterLogFunc := func(line string) {
m.packageProgressChan <- packageInstallProgressMsg{
progress: 0.94,
step: "Configuring DMS greeter...",
logOutput: line,
}
}
if err := greeter.AutoSetupGreeter(compositorName, m.sudoPassword, greeterLogFunc); err != nil {
m.packageProgressChan <- packageInstallProgressMsg{
progress: 0.96,
step: "Greeter setup warning",
logOutput: fmt.Sprintf("⚠ Greeter auto-setup warning (non-fatal): %v", err),
}
}
}
}
tuiMsg := packageInstallProgressMsg{ tuiMsg := packageInstallProgressMsg{
progress: msg.Progress, progress: msg.Progress,
step: msg.Step, step: msg.Step,

View File

@@ -864,10 +864,12 @@ func (p *NiriWritableProvider) formatRule(rule windowrules.WindowRule) string {
func formatSizeProperty(name, value string) string { func formatSizeProperty(name, value string) string {
parts := strings.SplitN(value, " ", 2) parts := strings.SplitN(value, " ", 2)
if len(parts) != 2 { if len(parts) == 2 {
return fmt.Sprintf(" %s { }", name) return fmt.Sprintf(" %s { %s %s; }", name, parts[0], parts[1])
} }
sizeType := parts[0] // Bare number without type prefix — default to "fixed"
sizeValue := parts[1] if _, err := strconv.Atoi(value); err == nil {
return fmt.Sprintf(" %s { %s %s; }", name, sizeType, sizeValue) return fmt.Sprintf(" %s { fixed %s; }", name, value)
}
return fmt.Sprintf(" %s { }", name)
} }

View File

@@ -29,6 +29,7 @@ Depends: ${misc:Depends},
qml6-module-qtquick-templates, qml6-module-qtquick-templates,
qml6-module-qtquick-window, qml6-module-qtquick-window,
qt6ct qt6ct
Suggests: cups-pk-helper
Provides: dms Provides: dms
Conflicts: dms Conflicts: dms
Replaces: dms Replaces: dms

View File

@@ -0,0 +1,9 @@
<services>
<!-- Download dms-qml source tarball from GitHub releases (greeter + quickshell content) -->
<service name="download_url">
<param name="protocol">https</param>
<param name="host">github.com</param>
<param name="path">/AvengeMedia/DankMaterialShell/releases/download/v1.4.2/dms-qml.tar.gz</param>
<param name="filename">dms-qml.tar.gz</param>
</service>
</services>

View File

@@ -0,0 +1,6 @@
dms-greeter (1.4.2db8) unstable; urgency=medium
* Initial Debian OBS package
* Port from Ubuntu/Fedora packaging
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 21 Feb 2026 00:00:00 +0000

View File

@@ -0,0 +1,23 @@
Source: dms-greeter
Section: x11
Priority: optional
Maintainer: Avenge Media <AvengeMedia.US@gmail.com>
Build-Depends: debhelper-compat (= 13)
Standards-Version: 4.6.2
Homepage: https://github.com/AvengeMedia/DankMaterialShell
Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell
Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
Package: dms-greeter
Architecture: any
Depends: ${misc:Depends},
greetd,
quickshell-git | quickshell
Suggests: niri | hyprland | sway
Description: DankMaterialShell greeter for greetd
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
inspired greeter interface built with Quickshell for Wayland compositors.
.
Supports multiple compositors including Niri, Hyprland, and Sway with automatic
compositor detection and configuration. Features session selection, user
authentication, and dynamic theming.

View File

@@ -0,0 +1,27 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: dms-greeter
Upstream-Contact: Avenge Media LLC <AvengeMedia.US@gmail.com>
Source: https://github.com/AvengeMedia/DankMaterialShell
Files: *
Copyright: 2026 Avenge Media LLC
License: MIT
License: MIT
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.

View File

@@ -0,0 +1,108 @@
#!/bin/sh
set -e
case "$1" in
configure)
# Create greeter user/group if they don't exist
if ! getent group greeter >/dev/null; then
addgroup --system greeter
fi
if ! getent passwd greeter >/dev/null; then
adduser --system --ingroup greeter --home /var/lib/greeter \
--shell /bin/bash --gecos "System Greeter" greeter
fi
if [ -d /var/cache/dms-greeter ]; then
chown -R greeter:greeter /var/cache/dms-greeter 2>/dev/null || true
fi
if [ -d /var/lib/greeter ]; then
chown -R greeter:greeter /var/lib/greeter 2>/dev/null || true
fi
# Check and set graphical.target as default
CURRENT_TARGET=$(systemctl get-default 2>/dev/null || echo "unknown")
if [ "$CURRENT_TARGET" != "graphical.target" ]; then
systemctl set-default graphical.target >/dev/null 2>&1 || true
TARGET_STATUS="Set to graphical.target (was: $CURRENT_TARGET) ✓"
else
TARGET_STATUS="Already graphical.target ✓"
fi
GREETD_CONFIG="/etc/greetd/config.toml"
CONFIG_STATUS="Not modified (already configured)"
# Check if niri or hyprland exists
COMPOSITOR="niri"
if ! command -v niri >/dev/null 2>&1; then
if command -v Hyprland >/dev/null 2>&1; then
COMPOSITOR="hyprland"
fi
fi
# If config doesn't exist, create a default one
if [ ! -f "$GREETD_CONFIG" ]; then
mkdir -p /etc/greetd
cat > "$GREETD_CONFIG" << 'GREETD_EOF'
[terminal]
vt = 1
[default_session]
user = "greeter"
command = "/usr/bin/dms-greeter --command COMPOSITOR_PLACEHOLDER"
GREETD_EOF
sed -i "s|COMPOSITOR_PLACEHOLDER|$COMPOSITOR|" "$GREETD_CONFIG"
CONFIG_STATUS="Created new config with $COMPOSITOR ✓"
elif ! grep -q "dms-greeter" "$GREETD_CONFIG"; then
# Backup existing config
BACKUP_FILE="${GREETD_CONFIG}.backup-$(date +%Y%m%d-%H%M%S)"
cp "$GREETD_CONFIG" "$BACKUP_FILE" 2>/dev/null || true
# Update command in default_session section
sed -i "/^\[default_session\]/,/^\[/ s|^command =.*|command = \"/usr/bin/dms-greeter --command $COMPOSITOR\"|" "$GREETD_CONFIG"
sed -i '/^\[default_session\]/,/^\[/ s|^user =.*|user = "greeter"|' "$GREETD_CONFIG"
CONFIG_STATUS="Updated existing config (backed up) with $COMPOSITOR ✓"
fi
# Only show banner on initial install
if [ -z "$2" ]; then
cat << 'EOF'
=========================================================================
DMS Greeter Installation Complete!
=========================================================================
Status:
EOF
echo " ✓ Greetd config: $CONFIG_STATUS"
echo " ✓ Default target: $TARGET_STATUS"
cat << 'EOF'
✓ Greeter user: Created
✓ Greeter directories: /var/cache/dms-greeter, /var/lib/greeter
Next steps:
1. Enable the greeter:
dms greeter enable
(This will automatically disable conflicting display managers,
set graphical.target, and enable greetd)
2. Sync your theme with the greeter (optional):
dms greeter sync
3. Check your setup:
dms greeter status
Ready to test? Run: sudo systemctl start greetd
Documentation: https://danklinux.com/docs/dankgreeter/
=========================================================================
EOF
fi
;;
esac
#DEBHELPER#
exit 0

View File

@@ -0,0 +1,14 @@
#!/bin/sh
set -e
case "$1" in
purge)
# Remove greeter cache directory on purge
rm -rf /var/cache/dms-greeter 2>/dev/null || true
;;
esac
#DEBHELPER#
exit 0

View File

@@ -0,0 +1,48 @@
#!/usr/bin/make -f
export DH_VERBOSE = 1
DEB_VERSION := $(shell dpkg-parsechangelog -S Version)
UPSTREAM_VERSION := $(shell echo $(DEB_VERSION) | sed 's/-[^-]*$$//')
%:
dh $@
override_dh_auto_build:
: nothing to build, we use prebuilt tarball content
override_dh_auto_install:
# Same pattern as dms: upstream from combined tarball (native format)
# Build root is either . (we're inside dms-qml) or has dms-qml/ subdir
SOURCE_DIR=""; \
if [ -d dms-qml ]; then SOURCE_DIR="dms-qml"; \
elif [ -f Modules/Greetd/assets/dms-greeter ]; then SOURCE_DIR="."; \
fi; \
if [ -n "$$SOURCE_DIR" ]; then \
mkdir -p debian/dms-greeter/usr/share/quickshell/dms-greeter && \
( cd $$SOURCE_DIR && tar cf - --exclude=debian . ) | \
( cd debian/dms-greeter/usr/share/quickshell/dms-greeter && tar xf - ) && \
install -Dm755 $$SOURCE_DIR/Modules/Greetd/assets/dms-greeter \
debian/dms-greeter/usr/bin/dms-greeter && \
install -Dm644 $$SOURCE_DIR/Modules/Greetd/README.md \
debian/dms-greeter/usr/share/doc/dms-greeter/README.md && \
install -Dm644 $$SOURCE_DIR/LICENSE \
debian/dms-greeter/usr/share/doc/dms-greeter/LICENSE && \
install -Dpm0644 $$SOURCE_DIR/systemd/tmpfiles-dms-greeter.conf \
debian/dms-greeter/usr/lib/tmpfiles.d/dms-greeter.conf; \
else \
echo "ERROR: No upstream source (dms-qml or Modules/Greetd/assets/dms-greeter)!" && \
echo "Contents of current directory:" && ls -la && exit 1; \
fi
# Remove build and development files
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/core
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/distro
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/.git*
rm -f debian/dms-greeter/usr/share/quickshell/dms-greeter/.gitignore
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/.github
override_dh_auto_clean:
rm -rf dms-qml
# When build root is dms-qml itself, we're inside it - nothing extra to remove
dh_auto_clean

View File

@@ -0,0 +1 @@
3.0 (native)

View File

@@ -0,0 +1 @@
# OBS _service downloads dms-qml.tar.gz; no extra excludes needed

View File

@@ -9,7 +9,7 @@ Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell
Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
Package: dms Package: dms
Architecture: amd64 Architecture: amd64 arm64
Depends: ${misc:Depends}, Depends: ${misc:Depends},
quickshell | quickshell-git, quickshell | quickshell-git,
accountsservice, accountsservice,
@@ -28,6 +28,7 @@ Depends: ${misc:Depends},
qml6-module-qtquick-templates, qml6-module-qtquick-templates,
qml6-module-qtquick-window, qml6-module-qtquick-window,
qt6ct qt6ct
Suggests: cups-pk-helper
Conflicts: dms-git Conflicts: dms-git
Replaces: dms-git Replaces: dms-git
Description: DankMaterialShell - Modern Wayland Desktop Shell Description: DankMaterialShell - Modern Wayland Desktop Shell

View File

@@ -1,2 +1,3 @@
dms-distropkg-amd64.gz dms-distropkg-amd64.gz
dms-distropkg-arm64.gz
dms-source.tar.gz dms-source.tar.gz

View File

@@ -1,4 +1,5 @@
# Include files that are normally excluded by .gitignore # Include files that are normally excluded by .gitignore
# These are needed for the build process on Launchpad # These are needed for the build process on Launchpad
tar-ignore = !dms-distropkg-amd64.gz tar-ignore = !dms-distropkg-amd64.gz
tar-ignore = !dms-distropkg-arm64.gz
tar-ignore = !dms-source.tar.gz tar-ignore = !dms-source.tar.gz

View File

@@ -37,6 +37,7 @@ Recommends: quickshell-git
# Recommended system packages # Recommended system packages
Recommends: NetworkManager Recommends: NetworkManager
Recommends: qt6-qtmultimedia Recommends: qt6-qtmultimedia
Suggests: cups-pk-helper
Suggests: qt6ct Suggests: qt6ct
%description %description

View File

@@ -28,6 +28,7 @@ Recommends: danksearch
Recommends: matugen Recommends: matugen
Recommends: NetworkManager Recommends: NetworkManager
Recommends: qt6-qtmultimedia Recommends: qt6-qtmultimedia
Suggests: cups-pk-helper
Suggests: qt6ct Suggests: qt6ct
%description %description

View File

@@ -23,6 +23,7 @@ let
lib.makeBinPath [ lib.makeBinPath [
cfg.quickshell.package cfg.quickshell.package
compositorPackage compositorPackage
pkgs.glib # provides gdbus, used by the fprintd hardware probe in GreeterContent.qml
] ]
} }
${ ${
@@ -179,7 +180,9 @@ in
fi fi
if [ -f settings.json ]; then if [ -f settings.json ]; then
if cp "$(${jq} -r '.customThemeFile' settings.json)" custom-theme.json; then theme_file="$(${jq} -r '.customThemeFile // empty' settings.json)"
if [ -f "$theme_file" ] && [ -r "$theme_file" ]; then
cp "$theme_file" custom-theme.json
mv settings.json settings.orig.json mv settings.json settings.orig.json
${jq} '.customThemeFile = "${cacheDir}/custom-theme.json"' settings.orig.json > settings.json ${jq} '.customThemeFile = "${cacheDir}/custom-theme.json"' settings.orig.json > settings.json
fi fi

View File

@@ -25,6 +25,7 @@ Recommends: matugen
Recommends: quickshell-git Recommends: quickshell-git
Recommends: NetworkManager Recommends: NetworkManager
Recommends: qt6-qtmultimedia Recommends: qt6-qtmultimedia
Suggests: cups-pk-helper
Suggests: qt6ct Suggests: qt6ct
Provides: dms Provides: dms

View File

@@ -0,0 +1,322 @@
# Spec for DMS Greeter - OpenSUSE/OBS
%global debug_package %{nil}
%global version VERSION_PLACEHOLDER
%global pkg_summary DankMaterialShell greeter for greetd
Name: dms-greeter
Version: %{version}
Release: RELEASE_PLACEHOLDER%{?dist}
Summary: %{pkg_summary}
License: MIT
URL: https://github.com/AvengeMedia/DankMaterialShell
Source0: dms-qml.tar.gz
BuildRequires: gzip
BuildRequires: wget
BuildRequires: systemd-rpm-macros
Requires: greetd
Requires: (quickshell-git or quickshell)
Requires(post): /usr/sbin/useradd
Requires(post): /usr/sbin/groupadd
Recommends: policycoreutils-python-utils
Recommends: acl
Suggests: niri
Suggests: hyprland
Suggests: sway
%description
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
inspired greeter interface built with Quickshell for Wayland compositors.
Supports multiple compositors including Niri, Hyprland, and Sway with automatic
compositor detection and configuration. Features session selection, user
authentication, and dynamic theming.
%prep
%setup -q -c -n dms-qml
%build
%install
# Install greeter files to shared data location
install -dm755 %{buildroot}%{_datadir}/quickshell/dms-greeter
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms-greeter/
install -Dm755 %{_builddir}/dms-qml/Modules/Greetd/assets/dms-greeter %{buildroot}%{_bindir}/dms-greeter
install -Dm644 %{_builddir}/dms-qml/Modules/Greetd/README.md %{buildroot}%{_docdir}/dms-greeter/README.md
install -Dpm0644 %{_builddir}/dms-qml/systemd/tmpfiles-dms-greeter.conf %{buildroot}%{_tmpfilesdir}/dms-greeter.conf
install -Dm644 %{_builddir}/dms-qml/LICENSE %{buildroot}%{_docdir}/dms-greeter/LICENSE
install -dm755 %{buildroot}%{_sharedstatedir}/greeter
# Remove build and development files
rm -rf %{buildroot}%{_datadir}/quickshell/dms-greeter/.git*
rm -f %{buildroot}%{_datadir}/quickshell/dms-greeter/.gitignore
rm -rf %{buildroot}%{_datadir}/quickshell/dms-greeter/.github
rm -rf %{buildroot}%{_datadir}/quickshell/dms-greeter/distro
%posttrans
if [ -d "%{_sysconfdir}/xdg/quickshell/dms-greeter" ]; then
rmdir "%{_sysconfdir}/xdg/quickshell/dms-greeter" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
fi
%files
%dir %{_docdir}/dms-greeter
%license %{_docdir}/dms-greeter/LICENSE
%doc %{_docdir}/dms-greeter/README.md
%{_bindir}/dms-greeter
%dir %{_datadir}/quickshell
%{_datadir}/quickshell/dms-greeter/
%{_tmpfilesdir}/%{name}.conf
%pre
# Create greeter user/group if they don't exist
getent group greeter >/dev/null || groupadd -r greeter
getent passwd greeter >/dev/null || \
useradd -r -g greeter -d %{_sharedstatedir}/greeter -s /bin/bash \
-c "System Greeter" greeter
exit 0
%post
# SELinux contexts (no-op on OpenSUSE - semanage/restorecon not present)
if [ -x /usr/sbin/semanage ] && [ -x /usr/sbin/restorecon ]; then
semanage fcontext -a -t bin_t '%{_bindir}/dms-greeter' >/dev/null 2>&1 || true
restorecon %{_bindir}/dms-greeter >/dev/null 2>&1 || true
semanage fcontext -a -t user_home_dir_t '%{_sharedstatedir}/greeter(/.*)?' >/dev/null 2>&1 || true
restorecon -R %{_sharedstatedir}/greeter >/dev/null 2>&1 || true
semanage fcontext -a -t cache_home_t '%{_localstatedir}/cache/dms-greeter(/.*)?' >/dev/null 2>&1 || true
restorecon -R %{_localstatedir}/cache/dms-greeter >/dev/null 2>&1 || true
semanage fcontext -a -t usr_t '%{_datadir}/quickshell/dms-greeter(/.*)?' >/dev/null 2>&1 || true
restorecon -R %{_datadir}/quickshell/dms-greeter >/dev/null 2>&1 || true
restorecon %{_sysconfdir}/pam.d/greetd >/dev/null 2>&1 || true
fi
# Resolve greeter runtime account/group for distro differences
GREETER_USER="greeter"
for candidate in greeter greetd _greeter; do
if getent passwd "$candidate" >/dev/null 2>&1; then
GREETER_USER="$candidate"
break
fi
done
GREETER_GROUP="$GREETER_USER"
if ! getent group "$GREETER_GROUP" >/dev/null 2>&1; then
for candidate in greeter greetd _greeter; do
if getent group "$candidate" >/dev/null 2>&1; then
GREETER_GROUP="$candidate"
break
fi
done
fi
# Ensure proper ownership of greeter directories
chown -R "$GREETER_USER:$GREETER_GROUP" %{_localstatedir}/cache/dms-greeter 2>/dev/null || true
chown -R "$GREETER_USER:$GREETER_GROUP" %{_sharedstatedir}/greeter 2>/dev/null || true
# Verify PAM configuration
PAM_CONFIG="/etc/pam.d/greetd"
write_greetd_pam_config() {
# openSUSE and Debian families usually expose PAM stacks as common-*
if [ -f /etc/pam.d/common-auth ] && [ -f /etc/pam.d/common-account ] && [ -f /etc/pam.d/common-password ] && [ -f /etc/pam.d/common-session ]; then
cat > "$PAM_CONFIG" << 'PAM_EOF'
#%PAM-1.0
auth include common-auth
account required pam_nologin.so
account include common-account
password include common-password
session required pam_loginuid.so
session optional pam_keyinit.so force revoke
session include common-session
PAM_EOF
return
fi
# Fedora/RHEL style system-auth/postlogin stack
if [ -f /etc/pam.d/system-auth ]; then
if [ -f /etc/pam.d/postlogin ]; then
cat > "$PAM_CONFIG" << 'PAM_EOF'
#%PAM-1.0
auth substack system-auth
auth include postlogin
account required pam_nologin.so
account include system-auth
password include system-auth
session required pam_loginuid.so
session optional pam_keyinit.so force revoke
session include system-auth
session include postlogin
PAM_EOF
else
cat > "$PAM_CONFIG" << 'PAM_EOF'
#%PAM-1.0
auth include system-auth
account required pam_nologin.so
account include system-auth
password include system-auth
session required pam_loginuid.so
session optional pam_keyinit.so force revoke
session include system-auth
PAM_EOF
fi
return
fi
# Last-resort conservative fallback
cat > "$PAM_CONFIG" << 'PAM_EOF'
#%PAM-1.0
auth required pam_unix.so nullok
account required pam_unix.so
password required pam_unix.so nullok sha512
session required pam_unix.so
PAM_EOF
}
if [ ! -f "$PAM_CONFIG" ]; then
write_greetd_pam_config
chmod 644 "$PAM_CONFIG"
[ "$1" -eq 1 ] && echo "Created PAM configuration for greetd"
else
NEEDS_PAM_UPDATE=0
if grep -q "common-auth" "$PAM_CONFIG"; then
if [ ! -f /etc/pam.d/common-auth ]; then
NEEDS_PAM_UPDATE=1
fi
elif grep -q "system-auth" "$PAM_CONFIG"; then
if [ ! -f /etc/pam.d/system-auth ]; then
NEEDS_PAM_UPDATE=1
fi
else
NEEDS_PAM_UPDATE=1
fi
if [ "$NEEDS_PAM_UPDATE" -eq 1 ]; then
cp "$PAM_CONFIG" "$PAM_CONFIG.backup-dms-greeter"
write_greetd_pam_config
chmod 644 "$PAM_CONFIG"
[ "$1" -eq 1 ] && echo "Updated PAM configuration (old config backed up to $PAM_CONFIG.backup-dms-greeter)"
fi
fi
# Auto-configure greetd config
GREETD_CONFIG="/etc/greetd/config.toml"
CONFIG_STATUS="Not modified (already configured)"
COMPOSITOR=""
for candidate in niri Hyprland sway; do
if command -v "$candidate" >/dev/null 2>&1; then
case "$candidate" in
Hyprland)
COMPOSITOR="hyprland"
;;
*)
COMPOSITOR="$candidate"
;;
esac
break
fi
done
if [ ! -f "$GREETD_CONFIG" ]; then
mkdir -p /etc/greetd
if [ -n "$COMPOSITOR" ]; then
cat > "$GREETD_CONFIG" << 'GREETD_EOF'
[terminal]
vt = 1
[default_session]
user = "GREETER_USER_PLACEHOLDER"
command = "/usr/bin/dms-greeter --command COMPOSITOR_PLACEHOLDER"
GREETD_EOF
sed -i "s|GREETER_USER_PLACEHOLDER|$GREETER_USER|" "$GREETD_CONFIG"
sed -i "s|COMPOSITOR_PLACEHOLDER|$COMPOSITOR|" "$GREETD_CONFIG"
CONFIG_STATUS="Created new config with $COMPOSITOR "
else
cat > "$GREETD_CONFIG" << 'GREETD_EOF'
[terminal]
vt = 1
[default_session]
user = "GREETER_USER_PLACEHOLDER"
command = "agreety --cmd /bin/login"
GREETD_EOF
sed -i "s|GREETER_USER_PLACEHOLDER|$GREETER_USER|" "$GREETD_CONFIG"
CONFIG_STATUS="Created safe fallback config (no supported compositor detected)"
fi
elif ! grep -q "dms-greeter" "$GREETD_CONFIG"; then
if [ -n "$COMPOSITOR" ]; then
BACKUP_FILE="${GREETD_CONFIG}.backup-$(date +%%Y%%m%%d-%%H%%M%%S)"
cp "$GREETD_CONFIG" "$BACKUP_FILE" 2>/dev/null || true
sed -i "/^\[default_session\]/,/^\[/ s|^command =.*|command = \"/usr/bin/dms-greeter --command $COMPOSITOR\"|" "$GREETD_CONFIG"
sed -i "/^\[default_session\]/,/^\[/ s|^user =.*|user = \"$GREETER_USER\"|" "$GREETD_CONFIG"
CONFIG_STATUS="Updated existing config (backed up) with $COMPOSITOR "
else
CONFIG_STATUS="Skipped dms-greeter command update (no supported compositor detected)"
fi
fi
# Set graphical.target as default
CURRENT_TARGET=$(systemctl get-default 2>/dev/null || echo "unknown")
if [ "$CURRENT_TARGET" != "graphical.target" ]; then
systemctl set-default graphical.target >/dev/null 2>&1 || true
TARGET_STATUS="Set to graphical.target (was: $CURRENT_TARGET) "
else
TARGET_STATUS="Already graphical.target "
fi
if [ "$1" -eq 1 ]; then
cat << 'EOF'
=========================================================================
DMS Greeter Installation Complete!
=========================================================================
Status:
EOF
echo " Greetd config: $CONFIG_STATUS"
echo " Default target: $TARGET_STATUS"
cat << 'EOF'
Greeter user: Created
Greeter directories: /var/cache/dms-greeter, /var/lib/greeter
SELinux contexts: Applied (if applicable)
Next steps:
1. Enable the greeter:
dms greeter enable
2. Sync your theme with the greeter (optional):
dms greeter sync
3. Check your setup:
dms greeter status
Ready to test? Run: sudo systemctl start greetd
Documentation: https://danklinux.com/docs/dankgreeter/
=========================================================================
EOF
fi
%postun
if [ "$1" -eq 0 ] && [ -x /usr/sbin/semanage ]; then
semanage fcontext -d '%{_bindir}/dms-greeter' 2>/dev/null || true
semanage fcontext -d '%{_sharedstatedir}/greeter(/.*)?' 2>/dev/null || true
semanage fcontext -d '%{_localstatedir}/cache/dms-greeter(/.*)?' 2>/dev/null || true
semanage fcontext -d '%{_datadir}/quickshell/dms-greeter(/.*)?' 2>/dev/null || true
fi
%changelog
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER
- Stable release VERSION_PLACEHOLDER
- Initial OpenSUSE/OBS port from Fedora

View File

@@ -27,6 +27,7 @@ Recommends: danksearch
Recommends: matugen Recommends: matugen
Recommends: NetworkManager Recommends: NetworkManager
Recommends: qt6-qtmultimedia Recommends: qt6-qtmultimedia
Suggests: cups-pk-helper
Suggests: qt6ct Suggests: qt6ct
%description %description

View File

@@ -11,7 +11,7 @@
OBS_BASE_PROJECT="home:AvengeMedia" OBS_BASE_PROJECT="home:AvengeMedia"
OBS_BASE="$HOME/.cache/osc-checkouts" OBS_BASE="$HOME/.cache/osc-checkouts"
ALL_PACKAGES=(dms dms-git) ALL_PACKAGES=(dms dms-git dms-greeter)
REPOS=("Debian_13" "openSUSE_Tumbleweed" "16.0") REPOS=("Debian_13" "openSUSE_Tumbleweed" "16.0")
ARCHES=("x86_64" "aarch64") ARCHES=("x86_64" "aarch64")
@@ -41,6 +41,9 @@ for pkg in "${PACKAGES[@]}"; do
dms-git) dms-git)
PROJECT="$OBS_BASE_PROJECT:dms-git" PROJECT="$OBS_BASE_PROJECT:dms-git"
;; ;;
dms-greeter)
PROJECT="$OBS_BASE_PROJECT:danklinux"
;;
*) *)
echo "Error: Unknown package '$pkg'" echo "Error: Unknown package '$pkg'"
continue continue
@@ -74,11 +77,15 @@ for pkg in "${PACKAGES[@]}"; do
COLOR="\033[0;32m" # Green COLOR="\033[0;32m" # Green
SYMBOL="✅" SYMBOL="✅"
;; ;;
failed) failed|broken|broken*)
COLOR="\033[0;31m" # Red COLOR="\033[0;31m" # Red
SYMBOL="❌" SYMBOL="❌"
FAILED_BUILDS+=("$repo $arch") FAILED_BUILDS+=("$repo $arch")
;; ;;
blocked)
COLOR="\033[0;33m" # Yellow
SYMBOL="⏸️"
;;
unresolvable) unresolvable)
COLOR="\033[0;33m" # Yellow COLOR="\033[0;33m" # Yellow
SYMBOL="⚠️" SYMBOL="⚠️"

View File

@@ -68,13 +68,14 @@ fi
OBS_BASE_PROJECT="home:AvengeMedia" OBS_BASE_PROJECT="home:AvengeMedia"
OBS_BASE="$HOME/.cache/osc-checkouts" OBS_BASE="$HOME/.cache/osc-checkouts"
AVAILABLE_PACKAGES=(dms dms-git) AVAILABLE_PACKAGES=(dms dms-git dms-greeter)
if [[ -z "$PACKAGE" ]]; then if [[ -z "$PACKAGE" ]]; then
echo "Available packages:" echo "Available packages:"
echo "" echo ""
echo " 1. dms - Stable DMS" echo " 1. dms - Stable DMS"
echo " 2. dms-git - Nightly DMS" echo " 2. dms-git - Nightly DMS"
echo " 3. dms-greeter - DMS greeter for greetd"
echo " a. all" echo " a. all"
echo "" echo ""
read -r -p "Select package (1-${#AVAILABLE_PACKAGES[@]}, a): " selection read -r -p "Select package (1-${#AVAILABLE_PACKAGES[@]}, a): " selection
@@ -141,7 +142,12 @@ check_obs_version_exists() {
return 0 return 0
fi fi
else else
echo "⚠️ Could not fetch OBS spec (API may be unavailable), proceeding anyway" # Empty/invalid response: expected on first upload (no spec on server yet), or actual API failure
if [[ -z "$OBS_SPEC" ]]; then
echo " No existing spec on OBS (first upload?) - proceeding"
else
echo "⚠️ Could not fetch OBS spec (API may be unavailable), proceeding anyway"
fi
return 1 return 1
fi fi
return 1 return 1
@@ -168,6 +174,22 @@ update_debian_dms_service() {
sed -i "s|/releases/download/v[0-9][^\"]*/dms-distropkg-arm64\.gz|/releases/download/v${base_version}/dms-distropkg-arm64.gz|" "$service_path" sed -i "s|/releases/download/v[0-9][^\"]*/dms-distropkg-arm64\.gz|/releases/download/v${base_version}/dms-distropkg-arm64.gz|" "$service_path"
} }
update_debian_dms_greeter_service() {
local service_path="$1"
if [[ -z "$service_path" || ! -f "$service_path" ]]; then
return 0
fi
if [[ -z "$CHANGELOG_VERSION" ]]; then
return 0
fi
local base_version
base_version=$(echo "$CHANGELOG_VERSION" | sed -E 's/^([0-9]+(\.[0-9]+)*).*/\1/')
if [[ -z "$base_version" ]]; then
return 0
fi
sed -i "s|/releases/download/v[0-9][^\"]*/dms-qml\.tar\.gz|/releases/download/v${base_version}/dms-qml.tar.gz|" "$service_path"
}
update_opensuse_git_spec() { update_opensuse_git_spec() {
local spec_path="$1" local spec_path="$1"
if [[ -z "$spec_path" || ! -f "$spec_path" ]]; then if [[ -z "$spec_path" || ! -f "$spec_path" ]]; then
@@ -248,6 +270,9 @@ dms)
dms-git) dms-git)
PROJECT="dms-git" PROJECT="dms-git"
;; ;;
dms-greeter)
PROJECT="danklinux"
;;
*) *)
echo "Error: Unknown package '$PACKAGE'" echo "Error: Unknown package '$PACKAGE'"
exit 1 exit 1
@@ -329,9 +354,11 @@ if [[ -d "distro/debian/$PACKAGE/debian" ]]; then
echo " - Applied rebuild suffix: $CHANGELOG_VERSION" echo " - Applied rebuild suffix: $CHANGELOG_VERSION"
fi fi
# Keep Debian dms _service in sync with changelog version # Keep Debian _service in sync with changelog version
if [[ "$PACKAGE" == "dms" ]] && [[ -f "distro/debian/$PACKAGE/_service" ]]; then if [[ "$PACKAGE" == "dms" ]] && [[ -f "distro/debian/$PACKAGE/_service" ]]; then
update_debian_dms_service "distro/debian/$PACKAGE/_service" update_debian_dms_service "distro/debian/$PACKAGE/_service"
elif [[ "$PACKAGE" == "dms-greeter" ]] && [[ -f "distro/debian/$PACKAGE/_service" ]]; then
update_debian_dms_greeter_service "distro/debian/$PACKAGE/_service"
fi fi
# Check if this version already exists in OBS # Check if this version already exists in OBS
@@ -341,6 +368,12 @@ if [[ -d "distro/debian/$PACKAGE/debian" ]]; then
if [[ "$PACKAGE" == *"-git" ]]; then if [[ "$PACKAGE" == *"-git" ]]; then
echo "==> Error: This commit is already uploaded to OBS" echo "==> Error: This commit is already uploaded to OBS"
echo " The same git commit ($(echo "$CHANGELOG_VERSION" | grep -oP '[a-f0-9]{8}' | tail -1)) already exists on OBS." echo " The same git commit ($(echo "$CHANGELOG_VERSION" | grep -oP '[a-f0-9]{8}' | tail -1)) already exists on OBS."
if [[ -n "${GITHUB_ACTIONS:-}" ]] || [[ -n "${CI:-}" ]]; then
echo " CI run detected: skipping upload as a no-op (already up to date)."
echo " If you need to force rebuild this same commit, set REBUILD_RELEASE (e.g. 2, 3, ...)."
echo "✓ Exiting gracefully (no changes needed)"
exit 0
fi
echo " To rebuild the same commit, specify a rebuild number:" echo " To rebuild the same commit, specify a rebuild number:"
echo " ./distro/scripts/obs-upload.sh $PACKAGE 2" echo " ./distro/scripts/obs-upload.sh $PACKAGE 2"
echo " ./distro/scripts/obs-upload.sh $PACKAGE 3" echo " ./distro/scripts/obs-upload.sh $PACKAGE 3"
@@ -379,6 +412,13 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ -f "distro/opensuse/$PACKAGE.spec" ]];
if [[ "$PACKAGE" == *"-git" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then if [[ "$PACKAGE" == *"-git" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then
update_opensuse_git_spec "$WORK_DIR/$PACKAGE.spec" update_opensuse_git_spec "$WORK_DIR/$PACKAGE.spec"
elif [[ "$PACKAGE" == "dms-greeter" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then
DMS_GREETER_BASE_VERSION=$(echo "$CHANGELOG_VERSION" | sed -E 's/^([0-9]+(\.[0-9]+)*).*/\1/')
DMS_GREETER_RELEASE=$(echo "$CHANGELOG_VERSION" | sed -E 's/.*db([0-9]+)$/\1/' || echo "1")
CHANGELOG_DATE=$(date '+%a %b %d %Y')
sed -i "s/VERSION_PLACEHOLDER/${DMS_GREETER_BASE_VERSION}/g" "$WORK_DIR/$PACKAGE.spec"
sed -i "s/RELEASE_PLACEHOLDER/${DMS_GREETER_RELEASE}/g" "$WORK_DIR/$PACKAGE.spec"
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" "$WORK_DIR/$PACKAGE.spec"
fi fi
if [[ -f "$WORK_DIR/.osc/$PACKAGE.spec" ]]; then if [[ -f "$WORK_DIR/.osc/$PACKAGE.spec" ]]; then
@@ -444,6 +484,24 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ "$UPLOAD_DEBIAN" == false ]] && [[ -f
fi fi
fi fi
# For dms-greeter: download dms-qml.tar.gz from _service URL
if [[ -z "${SOURCE_DIR:-}" ]] && [[ "$PACKAGE" == "dms-greeter" ]] && [[ -f "distro/debian/$PACKAGE/_service" ]]; then
DMS_GREETER_URL=$(grep -A 5 'name="download_url"' "distro/debian/$PACKAGE/_service" | grep "path" | sed 's/.*<param name="path">\(.*\)<\/param>.*/\1/' | head -1)
if [[ -n "$DMS_GREETER_URL" ]]; then
DMS_GREETER_FULL_URL="https://github.com${DMS_GREETER_URL}"
echo " Downloading dms-greeter source from: $DMS_GREETER_FULL_URL"
if wget -q -O "$TEMP_DIR/dms-qml.tar.gz" "$DMS_GREETER_FULL_URL" 2>/dev/null || \
curl -L -f -s -o "$TEMP_DIR/dms-qml.tar.gz" "$DMS_GREETER_FULL_URL" 2>/dev/null; then
cd "$TEMP_DIR"
tar -xzf dms-qml.tar.gz
if [[ -f "Modules/Greetd/assets/dms-greeter" ]]; then
SOURCE_DIR="$TEMP_DIR"
fi
cd "$REPO_ROOT"
fi
fi
fi
if [[ -n "$SOURCE_DIR" && -d "$SOURCE_DIR" ]]; then if [[ -n "$SOURCE_DIR" && -d "$SOURCE_DIR" ]]; then
SOURCE0=$(grep "^Source0:" "distro/opensuse/$PACKAGE.spec" | awk '{print $2}' | head -1) SOURCE0=$(grep "^Source0:" "distro/opensuse/$PACKAGE.spec" | awk '{print $2}' | head -1)
@@ -452,6 +510,15 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ "$UPLOAD_DEBIAN" == false ]] && [[ -f
cd "$OBS_TARBALL_DIR" cd "$OBS_TARBALL_DIR"
case "$PACKAGE" in case "$PACKAGE" in
dms-greeter)
EXPECTED_DIR="dms-qml"
echo " Creating $SOURCE0 (directory: $EXPECTED_DIR)"
mkdir -p "$EXPECTED_DIR"
cp -a "$SOURCE_DIR"/. "$EXPECTED_DIR/"
tar -czf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
rm -rf "$EXPECTED_DIR"
echo " Created $SOURCE0 ($(stat -c%s "$WORK_DIR/$SOURCE0" 2>/dev/null || echo 0) bytes)"
;;
dms) dms)
DMS_VERSION=$(grep "^Version:" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec" | sed 's/^Version:[[:space:]]*//' | head -1) DMS_VERSION=$(grep "^Version:" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec" | sed 's/^Version:[[:space:]]*//' | head -1)
EXPECTED_DIR="DankMaterialShell-${DMS_VERSION}" EXPECTED_DIR="DankMaterialShell-${DMS_VERSION}"
@@ -584,6 +651,17 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
if [[ -z "$SOURCE_DIR" ]]; then if [[ -z "$SOURCE_DIR" ]]; then
SOURCE_DIR=$(find . -maxdepth 1 -type d ! -name "." | head -1) SOURCE_DIR=$(find . -maxdepth 1 -type d ! -name "." | head -1)
fi fi
# dms-qml.tar.gz extracts flat (quickshell contents, no top-level dir)
# Create dms-qml wrapper so combined tarball has correct top-level dir (like dms)
if [[ "$PACKAGE" == "dms-greeter" ]] && [[ -f "Modules/Greetd/assets/dms-greeter" ]]; then
mkdir -p dms-qml
for f in *; do
if [[ -e "$f" && "$f" != "dms-qml" && "$f" != "source-archive" ]]; then
mv "$f" dms-qml/ 2>/dev/null || true
fi
done
SOURCE_DIR="dms-qml"
fi
if [[ -z "$SOURCE_DIR" || ! -d "$SOURCE_DIR" ]]; then if [[ -z "$SOURCE_DIR" || ! -d "$SOURCE_DIR" ]]; then
echo "Error: Failed to extract source archive or find source directory" echo "Error: Failed to extract source archive or find source directory"
echo "Contents of $TEMP_DIR:" echo "Contents of $TEMP_DIR:"
@@ -660,6 +738,21 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
cd "$OBS_TARBALL_DIR" cd "$OBS_TARBALL_DIR"
case "$PACKAGE" in case "$PACKAGE" in
dms-greeter)
EXPECTED_DIR="dms-qml"
echo " Creating $SOURCE0 (directory: $EXPECTED_DIR)"
mkdir -p "$EXPECTED_DIR"
cp -a "$SOURCE_DIR"/. "$EXPECTED_DIR/"
if [[ "$SOURCE0" == *.tar.xz ]]; then
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cJf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
elif [[ "$SOURCE0" == *.tar.bz2 ]]; then
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
else
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
fi
rm -rf "$EXPECTED_DIR"
echo " Created $SOURCE0 ($(stat -c%s "$WORK_DIR/$SOURCE0" 2>/dev/null || echo 0) bytes)"
;;
dms) dms)
DMS_VERSION=$(grep "^Version:" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec" | sed 's/^Version:[[:space:]]*//' | head -1) DMS_VERSION=$(grep "^Version:" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec" | sed 's/^Version:[[:space:]]*//' | head -1)
EXPECTED_DIR="DankMaterialShell-${DMS_VERSION}" EXPECTED_DIR="DankMaterialShell-${DMS_VERSION}"
@@ -709,10 +802,17 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
echo " - OpenSUSE source tarballs created" echo " - OpenSUSE source tarballs created"
fi fi
# Copy and update OpenSUSE spec file with the correct version (for -git packages) # Copy and update OpenSUSE spec file with the correct version
cp "distro/opensuse/$PACKAGE.spec" "$WORK_DIR/" cp "distro/opensuse/$PACKAGE.spec" "$WORK_DIR/"
if [[ "$PACKAGE" == *"-git" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then if [[ "$PACKAGE" == *"-git" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then
update_opensuse_git_spec "$WORK_DIR/$PACKAGE.spec" update_opensuse_git_spec "$WORK_DIR/$PACKAGE.spec"
elif [[ "$PACKAGE" == "dms-greeter" ]] && [[ -n "$CHANGELOG_VERSION" ]]; then
DMS_GREETER_BASE_VERSION=$(echo "$CHANGELOG_VERSION" | sed -E 's/^([0-9]+(\.[0-9]+)*).*/\1/')
DMS_GREETER_RELEASE=$(echo "$CHANGELOG_VERSION" | sed -E 's/.*db([0-9]+)$/\1/' || echo "1")
CHANGELOG_DATE=$(date '+%a %b %d %Y')
sed -i "s/VERSION_PLACEHOLDER/${DMS_GREETER_BASE_VERSION}/g" "$WORK_DIR/$PACKAGE.spec"
sed -i "s/RELEASE_PLACEHOLDER/${DMS_GREETER_RELEASE}/g" "$WORK_DIR/$PACKAGE.spec"
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" "$WORK_DIR/$PACKAGE.spec"
fi fi
fi fi
@@ -839,8 +939,48 @@ EOF
echo " - Quilt format detected: creating debian.tar.gz" echo " - Quilt format detected: creating debian.tar.gz"
tar -czf "$WORK_DIR/debian.tar.gz" -C "distro/debian/$PACKAGE" debian/ tar -czf "$WORK_DIR/debian.tar.gz" -C "distro/debian/$PACKAGE" debian/
# For dms-greeter: create orig tarball so Debian build gets upstream (OBS only passes .dsc Files to Debian)
DSC_FILES_DEBIAN=""
if [[ "$PACKAGE" == "dms-greeter" ]]; then
UPSTREAM_VER=$(echo "$VERSION" | sed 's/-[^-]*$//')
ORIG_TARBALL="${PACKAGE}_${UPSTREAM_VER}.orig.tar.gz"
ORIG_DIR="${PACKAGE}-${UPSTREAM_VER}"
if [[ -f "distro/debian/$PACKAGE/_service" ]] && grep -q "download_url" "distro/debian/$PACKAGE/_service"; then
DG_TEMP=$(mktemp -d)
DMS_GREETER_PATH=$(grep -A 5 'name="download_url"' "distro/debian/$PACKAGE/_service" | grep "path" | sed 's/.*<param name="path">\(.*\)<\/param>.*/\1/' | head -1)
if [[ -n "$DMS_GREETER_PATH" ]]; then
DG_URL="https://github.com${DMS_GREETER_PATH}"
echo " - Downloading dms-greeter source for orig tarball: $DG_URL"
if wget -q -O "$DG_TEMP/dms-qml.tar.gz" "$DG_URL" 2>/dev/null || curl -L -f -s -o "$DG_TEMP/dms-qml.tar.gz" "$DG_URL" 2>/dev/null; then
( cd "$DG_TEMP" && tar --no-same-owner -xzf dms-qml.tar.gz && mkdir -p "$ORIG_DIR" && \
for f in *; do [[ "$f" != "dms-qml.tar.gz" && "$f" != "$ORIG_DIR" ]] && mv "$f" "$ORIG_DIR/"; done )
if [[ -d "$DG_TEMP/$ORIG_DIR/Modules" ]] || [[ -f "$DG_TEMP/$ORIG_DIR/LICENSE" ]]; then
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$ORIG_TARBALL" -C "$DG_TEMP" "$ORIG_DIR"
ORIG_MD5=$(md5sum "$WORK_DIR/$ORIG_TARBALL" | cut -d' ' -f1)
ORIG_SIZE=$(stat -c%s "$WORK_DIR/$ORIG_TARBALL" 2>/dev/null || stat -f%z "$WORK_DIR/$ORIG_TARBALL" 2>/dev/null)
DSC_FILES_DEBIAN=" $ORIG_MD5 $ORIG_SIZE $ORIG_TARBALL
"
echo " - Created $ORIG_TARBALL for Debian orig"
fi
rm -rf "$DG_TEMP"
fi
fi
fi
fi
DEBIAN_MD5=$(md5sum "$WORK_DIR/debian.tar.gz" | cut -d' ' -f1)
DEBIAN_SIZE=$(stat -c%s "$WORK_DIR/debian.tar.gz" 2>/dev/null || stat -f%z "$WORK_DIR/debian.tar.gz" 2>/dev/null)
echo " - Generating $PACKAGE.dsc for quilt format" echo " - Generating $PACKAGE.dsc for quilt format"
cat >"$WORK_DIR/$PACKAGE.dsc" <<EOF # debtransform: DEBTRANSFORM-TAR = orig (upstream), DEBTRANSFORM-FILES-TAR = debian archive
DEBTRANSFORM_EXTRA=""
if [[ -n "$DSC_FILES_DEBIAN" ]] && [[ -n "$ORIG_TARBALL" ]]; then
DEBTRANSFORM_EXTRA="DEBTRANSFORM-TAR: $ORIG_TARBALL
DEBTRANSFORM-FILES-TAR: debian.tar.gz
"
fi
cat >"$WORK_DIR/$PACKAGE.dsc" <<DSCEOF
Format: 3.0 (quilt) Format: 3.0 (quilt)
Source: $PACKAGE Source: $PACKAGE
Binary: $PACKAGE Binary: $PACKAGE
@@ -848,10 +988,9 @@ Architecture: any
Version: $VERSION Version: $VERSION
Maintainer: Avenge Media <AvengeMedia.US@gmail.com> Maintainer: Avenge Media <AvengeMedia.US@gmail.com>
Build-Depends: debhelper-compat (= 13), wget, gzip Build-Depends: debhelper-compat (= 13), wget, gzip
DEBTRANSFORM-TAR: debian.tar.gz ${DEBTRANSFORM_EXTRA}Files:${DSC_FILES_DEBIAN}
Files: $DEBIAN_MD5 $DEBIAN_SIZE debian.tar.gz
00000000000000000000000000000000 1 debian.tar.gz DSCEOF
EOF
fi fi
fi fi
fi fi
@@ -882,6 +1021,13 @@ if [[ -n "$OBS_FILES" ]]; then
continue continue
fi fi
# Keep current orig tarball for dms-greeter (Debian 3.0 quilt needs it)
UPSTREAM_VER_CLEAN=$(echo "$CHANGELOG_VERSION" | sed 's/-[^-]*$//' 2>/dev/null)
if [[ "$PACKAGE" == "dms-greeter" ]] && [[ "$old_file" == "${PACKAGE}_${UPSTREAM_VER_CLEAN}.orig.tar.gz" ]]; then
echo " - Keeping orig tarball: $old_file"
continue
fi
if [[ "$old_file" == "${PACKAGE}-source.tar.gz" ]]; then if [[ "$old_file" == "${PACKAGE}-source.tar.gz" ]]; then
echo " - Keeping source tarball: $old_file" echo " - Keeping source tarball: $old_file"
continue continue

View File

@@ -28,8 +28,17 @@ override_dh_auto_build:
# Launchpad build environment has no internet access # Launchpad build environment has no internet access
test -d dms-git-repo || (echo "ERROR: dms-git-repo directory not found!" && exit 1) test -d dms-git-repo || (echo "ERROR: dms-git-repo directory not found!" && exit 1)
# Patch go.mod to use Go 1.24 base version (Ubuntu has 1.24.4, project requires 1.24.6) # Patch go.mod for Launchpad: align go directive w/latest Go toolchain
sed -i 's/^go 1\.24\.[0-9]*/go 1.24/' dms-git-repo/core/go.mod GO_VERSION=$$(go env GOVERSION | sed -E 's/^go([0-9]+\.[0-9]+).*/\1/'); \
if [ -n "$$GO_VERSION" ]; then \
sed -E -i "s/^go 1\.[0-9]+(\.[0-9]+)?/go $$GO_VERSION/" dms-git-repo/core/go.mod; \
if [ -f dms-git-repo/core/vendor/modules.txt ]; then \
sed -E -i "s/^(## explicit; go )1\.[0-9]+(\.[0-9]+)?$$/\1$$GO_VERSION/" dms-git-repo/core/vendor/modules.txt; \
fi; \
else \
echo "Warning: Could not detect Go version, leaving go.mod go directive unchanged"; \
fi
sed -E -i '/^toolchain go[0-9.]+$$/d' dms-git-repo/core/go.mod
# Build dms-cli from source # Build dms-cli from source
# Detect architecture # Detect architecture

View File

@@ -13,7 +13,7 @@ Architecture: any
Depends: ${misc:Depends}, Depends: ${misc:Depends},
greetd, greetd,
quickshell-git | quickshell quickshell-git | quickshell
Recommends: niri | hyprland | sway Suggests: niri | hyprland | sway
Description: DankMaterialShell greeter for greetd Description: DankMaterialShell greeter for greetd
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3 DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
inspired greeter interface built with Quickshell for Wayland compositors. inspired greeter interface built with Quickshell for Wayland compositors.

6
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1769018530, "lastModified": 1771369470,
"narHash": "sha256-MJ27Cy2NtBEV5tsK+YraYr2g851f3Fl1LpNHDzDX15c=", "narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "88d3861acdd3d2f0e361767018218e51810df8a1", "rev": "0182a361324364ae3f436a63005877674cf45efb",
"type": "github" "type": "github"
}, },
"original": { "original": {

139
flake.nix
View File

@@ -17,6 +17,25 @@
... ...
}: }:
let let
goModVersion =
let
content = builtins.readFile ./core/go.mod;
lines = builtins.filter builtins.isString (builtins.split "\n" content);
goLines = builtins.filter (l: builtins.match "go [0-9]+\\..*" l != null) lines;
matched =
if goLines != [ ] then builtins.match "go ([0-9]+)\\.([0-9]+).*" (builtins.head goLines) else null;
in
if matched != null then
{
major = builtins.elemAt matched 0;
minor = builtins.elemAt matched 1;
}
else
{
major = "1";
minor = "25";
};
goForPkgs = pkgs: pkgs.${"go_${goModVersion.major}_${goModVersion.minor}"};
forEachSystem = forEachSystem =
fn: fn:
nixpkgs.lib.genAttrs [ "aarch64-darwin" "aarch64-linux" "x86_64-darwin" "x86_64-linux" ] ( nixpkgs.lib.genAttrs [ "aarch64-darwin" "aarch64-linux" "x86_64-darwin" "x86_64-linux" ] (
@@ -72,76 +91,82 @@
"${cleanVersion}${dateSuffix}${revSuffix}"; "${cleanVersion}${dateSuffix}${revSuffix}";
in in
{ {
dms-shell = pkgs.buildGoModule ( dms-shell = pkgs.lib.makeOverridable (
let
rootSrc = ./.;
in
{ {
inherit version; extraQtPackages ? [ ],
pname = "dms-shell"; }:
src = ./core; (pkgs.buildGoModule.override { go = goForPkgs pkgs; }) (
vendorHash = "sha256-cVUJXgzYMRSM0od1xzDVkMTdxHu3OIQX2bQ8AJbGQ1Q="; let
rootSrc = ./.;
qtPackages = (qmlPkgs pkgs) ++ extraQtPackages;
in
{
inherit version;
pname = "dms-shell";
src = ./core;
vendorHash = "sha256-cVUJXgzYMRSM0od1xzDVkMTdxHu3OIQX2bQ8AJbGQ1Q=";
subPackages = [ "cmd/dms" ]; subPackages = [ "cmd/dms" ];
ldflags = [ ldflags = [
"-s" "-s"
"-w" "-w"
"-X 'main.Version=${version}'" "-X 'main.Version=${version}'"
]; ];
nativeBuildInputs = with pkgs; [ nativeBuildInputs = with pkgs; [
installShellFiles installShellFiles
makeWrapper makeWrapper
]; ];
postInstall = '' postInstall = ''
mkdir -p $out/share/quickshell/dms mkdir -p $out/share/quickshell/dms
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/ cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
chmod u+w $out/share/quickshell/dms/VERSION chmod u+w $out/share/quickshell/dms/VERSION
echo "${version}" > $out/share/quickshell/dms/VERSION echo "${version}" > $out/share/quickshell/dms/VERSION
# Install desktop file and icon # Install desktop file and icon
install -D ${rootSrc}/assets/dms-open.desktop \ install -D ${rootSrc}/assets/dms-open.desktop \
$out/share/applications/dms-open.desktop $out/share/applications/dms-open.desktop
install -D ${rootSrc}/core/assets/danklogo.svg \ install -D ${rootSrc}/core/assets/danklogo.svg \
$out/share/hicolor/scalable/apps/danklogo.svg $out/share/hicolor/scalable/apps/danklogo.svg
wrapProgram $out/bin/dms \ wrapProgram $out/bin/dms \
--add-flags "-c $out/share/quickshell/dms" \ --add-flags "-c $out/share/quickshell/dms" \
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs (qmlPkgs pkgs)}" \ --prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs qtPackages}" \
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs (qmlPkgs pkgs)}" --prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs qtPackages}"
install -Dm644 ${rootSrc}/assets/systemd/dms.service \ install -Dm644 ${rootSrc}/assets/systemd/dms.service \
$out/lib/systemd/user/dms.service $out/lib/systemd/user/dms.service
substituteInPlace $out/lib/systemd/user/dms.service \ substituteInPlace $out/lib/systemd/user/dms.service \
--replace-fail /usr/bin/dms $out/bin/dms \ --replace-fail /usr/bin/dms $out/bin/dms \
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill --replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \ substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash --replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \ substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so --replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
installShellCompletion --cmd dms \ installShellCompletion --cmd dms \
--bash <($out/bin/dms completion bash) \ --bash <($out/bin/dms completion bash) \
--fish <($out/bin/dms completion fish) \ --fish <($out/bin/dms completion fish) \
--zsh <($out/bin/dms completion zsh) --zsh <($out/bin/dms completion zsh)
''; '';
meta = { meta = {
description = "Desktop shell for wayland compositors built with Quickshell & GO"; description = "Desktop shell for wayland compositors built with Quickshell & GO";
homepage = "https://danklinux.com"; homepage = "https://danklinux.com";
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}"; changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}";
license = pkgs.lib.licenses.mit; license = pkgs.lib.licenses.mit;
mainProgram = "dms"; mainProgram = "dms";
platforms = pkgs.lib.platforms.linux; platforms = pkgs.lib.platforms.linux;
}; };
} }
); )
) { };
quickshell = quickshell.packages.${system}.default; quickshell = quickshell.packages.${system}.default;
@@ -181,7 +206,7 @@
buildInputs = buildInputs =
with pkgs; with pkgs;
[ [
go_1_25 (goForPkgs pkgs)
gopls gopls
delve delve
go-tools go-tools

View File

@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import QtCore import QtCore
import qs.Services
Singleton { Singleton {
id: root id: root
@@ -70,20 +71,50 @@ Singleton {
return appId; return appId;
} }
function resolveIconPath(iconName: string): string {
if (!iconName) return "";
const moddedId = moddedAppId(iconName);
if (moddedId !== iconName) {
if (moddedId.startsWith("~") || moddedId.startsWith("/"))
return toFileUrl(expandTilde(moddedId));
if (moddedId.startsWith("file://"))
return moddedId;
return Quickshell.iconPath(moddedId, true);
}
return Quickshell.iconPath(iconName, true) || DesktopService.resolveIconPath(iconName);
}
function resolveIconUrl(iconName: string): string {
if (!iconName) return "";
const moddedId = moddedAppId(iconName);
if (moddedId !== iconName) {
if (moddedId.startsWith("~") || moddedId.startsWith("/"))
return toFileUrl(expandTilde(moddedId));
if (moddedId.startsWith("file://"))
return moddedId;
return "image://icon/" + moddedId;
}
return "image://icon/" + iconName;
}
function getAppIcon(appId: string, desktopEntry: var): string { function getAppIcon(appId: string, desktopEntry: var): string {
if (appId === "org.quickshell") { if (appId === "org.quickshell") {
return Qt.resolvedUrl("../assets/danklogo.svg"); return Qt.resolvedUrl("../assets/danklogo.svg");
} }
const moddedId = moddedAppId(appId); const moddedId = moddedAppId(appId);
if (moddedId !== appId) { if (moddedId !== appId)
return Quickshell.iconPath(moddedId, true); return resolveIconPath(appId);
}
if (desktopEntry && desktopEntry.icon) { if (desktopEntry && desktopEntry.icon) {
return Quickshell.iconPath(desktopEntry.icon, true); return Quickshell.iconPath(desktopEntry.icon, true);
} }
return Quickshell.iconPath(appId, true);
const icon = Quickshell.iconPath(appId, true);
if (icon && icon !== "")
return icon;
return DesktopService.resolveIconPath(appId);
} }
function getAppName(appId: string, desktopEntry: var): string { function getAppName(appId: string, desktopEntry: var): string {

View File

@@ -12,6 +12,27 @@ Singleton {
signal popoutOpening signal popoutOpening
signal popoutChanged signal popoutChanged
function _closePopout(popout) {
switch (true) {
case popout.dashVisible !== undefined:
popout.dashVisible = false;
return;
case popout.notificationHistoryVisible !== undefined:
popout.notificationHistoryVisible = false;
return;
default:
popout.close();
}
}
function _isStale(popout) {
try {
return !popout || !("shouldBeVisible" in popout);
} catch (e) {
return true;
}
}
function showPopout(popout) { function showPopout(popout) {
if (!popout || !popout.screen) if (!popout || !popout.screen)
return; return;
@@ -23,13 +44,11 @@ Singleton {
const otherPopout = currentPopoutsByScreen[otherScreenName]; const otherPopout = currentPopoutsByScreen[otherScreenName];
if (!otherPopout || otherPopout === popout) if (!otherPopout || otherPopout === popout)
continue; continue;
if (otherPopout.dashVisible !== undefined) { if (_isStale(otherPopout)) {
otherPopout.dashVisible = false; currentPopoutsByScreen[otherScreenName] = null;
} else if (otherPopout.notificationHistoryVisible !== undefined) { continue;
otherPopout.notificationHistoryVisible = false;
} else {
otherPopout.close();
} }
_closePopout(otherPopout);
} }
currentPopoutsByScreen[screenName] = popout; currentPopoutsByScreen[screenName] = popout;
@@ -51,15 +70,9 @@ Singleton {
function closeAllPopouts() { function closeAllPopouts() {
for (const screenName in currentPopoutsByScreen) { for (const screenName in currentPopoutsByScreen) {
const popout = currentPopoutsByScreen[screenName]; const popout = currentPopoutsByScreen[screenName];
if (!popout) if (!popout || _isStale(popout))
continue; continue;
if (popout.dashVisible !== undefined) { _closePopout(popout);
popout.dashVisible = false;
} else if (popout.notificationHistoryVisible !== undefined) {
popout.notificationHistoryVisible = false;
} else {
popout.close();
}
} }
currentPopoutsByScreen = {}; currentPopoutsByScreen = {};
} }
@@ -90,6 +103,12 @@ Singleton {
if (!otherPopout) if (!otherPopout)
continue; continue;
if (_isStale(otherPopout)) {
currentPopoutsByScreen[otherScreenName] = null;
currentPopoutTriggers[otherScreenName] = null;
continue;
}
if (otherPopout === popout) { if (otherPopout === popout) {
movedFromOtherScreen = true; movedFromOtherScreen = true;
currentPopoutsByScreen[otherScreenName] = null; currentPopoutsByScreen[otherScreenName] = null;
@@ -97,45 +116,26 @@ Singleton {
continue; continue;
} }
if (otherPopout.dashVisible !== undefined) { _closePopout(otherPopout);
otherPopout.dashVisible = false;
} else if (otherPopout.notificationHistoryVisible !== undefined) {
otherPopout.notificationHistoryVisible = false;
} else {
otherPopout.close();
}
} }
if (currentPopout && currentPopout !== popout) { if (currentPopout && currentPopout !== popout) {
if (currentPopout.dashVisible !== undefined) { if (_isStale(currentPopout)) {
currentPopout.dashVisible = false; currentPopoutsByScreen[screenName] = null;
} else if (currentPopout.notificationHistoryVisible !== undefined) { currentPopoutTriggers[screenName] = null;
currentPopout.notificationHistoryVisible = false;
} else { } else {
currentPopout.close(); _closePopout(currentPopout);
} }
} }
if (currentPopout === popout && popout.shouldBeVisible && !movedFromOtherScreen) { if (currentPopout === popout && popout.shouldBeVisible && !movedFromOtherScreen) {
if (triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId) { if (triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId) {
if (popout.dashVisible !== undefined) { _closePopout(popout);
popout.dashVisible = false;
} else if (popout.notificationHistoryVisible !== undefined) {
popout.notificationHistoryVisible = false;
} else {
popout.close();
}
return; return;
} }
if (triggerId === undefined) { if (triggerId === undefined) {
if (popout.dashVisible !== undefined) { _closePopout(popout);
popout.dashVisible = false;
} else if (popout.notificationHistoryVisible !== undefined) {
popout.notificationHistoryVisible = false;
} else {
popout.close();
}
return; return;
} }

View File

@@ -126,6 +126,10 @@ Singleton {
property var hiddenOutputDeviceNames: [] property var hiddenOutputDeviceNames: []
property var hiddenInputDeviceNames: [] property var hiddenInputDeviceNames: []
property string launcherLastMode: "all"
property string appDrawerLastMode: "apps"
property string niriOverviewLastMode: "apps"
Component.onCompleted: { Component.onCompleted: {
if (!isGreeterMode) { if (!isGreeterMode) {
loadSettings(); loadSettings();
@@ -571,14 +575,7 @@ Singleton {
} }
} }
if (!newSettings[identifier]) { newSettings[identifier] = getMonitorCyclingSettings(screenName);
newSettings[identifier] = {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
};
}
newSettings[identifier].enabled = enabled; newSettings[identifier].enabled = enabled;
monitorCyclingSettings = newSettings; monitorCyclingSettings = newSettings;
saveSettings(); saveSettings();
@@ -609,14 +606,7 @@ Singleton {
} }
} }
if (!newSettings[identifier]) { newSettings[identifier] = getMonitorCyclingSettings(screenName);
newSettings[identifier] = {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
};
}
newSettings[identifier].mode = mode; newSettings[identifier].mode = mode;
monitorCyclingSettings = newSettings; monitorCyclingSettings = newSettings;
saveSettings(); saveSettings();
@@ -647,14 +637,7 @@ Singleton {
} }
} }
if (!newSettings[identifier]) { newSettings[identifier] = getMonitorCyclingSettings(screenName);
newSettings[identifier] = {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
};
}
newSettings[identifier].interval = interval; newSettings[identifier].interval = interval;
monitorCyclingSettings = newSettings; monitorCyclingSettings = newSettings;
saveSettings(); saveSettings();
@@ -685,14 +668,7 @@ Singleton {
} }
} }
if (!newSettings[identifier]) { newSettings[identifier] = getMonitorCyclingSettings(screenName);
newSettings[identifier] = {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
};
}
newSettings[identifier].time = time; newSettings[identifier].time = time;
monitorCyclingSettings = newSettings; monitorCyclingSettings = newSettings;
saveSettings(); saveSettings();
@@ -1100,6 +1076,21 @@ Singleton {
saveSettings(); saveSettings();
} }
function setLauncherLastMode(mode) {
launcherLastMode = mode;
saveSettings();
}
function setAppDrawerLastMode(mode) {
appDrawerLastMode = mode;
saveSettings();
}
function setNiriOverviewLastMode(mode) {
niriOverviewLastMode = mode;
saveSettings();
}
function syncWallpaperForCurrentMode() { function syncWallpaperForCurrentMode() {
if (!perModeWallpaper) if (!perModeWallpaper)
return; return;
@@ -1186,7 +1177,7 @@ Singleton {
"time": "06:00" "time": "06:00"
}; };
var value = _findMonitorValue(monitorCyclingSettings, screenName); var value = _findMonitorValue(monitorCyclingSettings, screenName);
return value !== undefined ? value : defaults; return Object.assign({}, defaults, value !== undefined ? value : {});
} }
FileView { FileView {
@@ -1213,7 +1204,7 @@ Singleton {
id: greeterSessionFile id: greeterSessionFile
path: { path: {
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms"; const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
return greetCfgDir + "/session.json"; return greetCfgDir + "/session.json";
} }
preload: isGreeterMode preload: isGreeterMode

View File

@@ -294,6 +294,17 @@ Singleton {
property string centeringMode: "index" property string centeringMode: "index"
property string clockDateFormat: "" property string clockDateFormat: ""
property string lockDateFormat: "" property string lockDateFormat: ""
property bool greeterRememberLastSession: true
property bool greeterRememberLastUser: true
property bool greeterEnableFprint: false
property bool greeterEnableU2f: false
property string greeterWallpaperPath: ""
property bool greeterUse24HourClock: true
property bool greeterShowSeconds: false
property bool greeterPadHours12Hour: false
property string greeterLockDateFormat: ""
property string greeterFontFamily: ""
property string greeterWallpaperFillMode: ""
property int mediaSize: 1 property int mediaSize: 1
property string appLauncherViewMode: "list" property string appLauncherViewMode: "list"
@@ -437,13 +448,14 @@ Singleton {
property bool matugenTemplateGhostty: true property bool matugenTemplateGhostty: true
property bool matugenTemplateKitty: true property bool matugenTemplateKitty: true
property bool matugenTemplateFoot: true property bool matugenTemplateFoot: true
property bool matugenTemplateNeovim: true property bool matugenTemplateNeovim: false
property bool matugenTemplateAlacritty: true property bool matugenTemplateAlacritty: true
property bool matugenTemplateWezterm: true property bool matugenTemplateWezterm: true
property bool matugenTemplateDgop: true property bool matugenTemplateDgop: true
property bool matugenTemplateKcolorscheme: true property bool matugenTemplateKcolorscheme: true
property bool matugenTemplateVscode: true property bool matugenTemplateVscode: true
property bool matugenTemplateEmacs: true property bool matugenTemplateEmacs: true
property bool matugenTemplateZed: true
property bool showDock: false property bool showDock: false
property bool dockAutoHide: false property bool dockAutoHide: false
@@ -494,6 +506,23 @@ Singleton {
property bool enableFprint: false property bool enableFprint: false
property int maxFprintTries: 15 property int maxFprintTries: 15
property bool fprintdAvailable: false property bool fprintdAvailable: false
property bool lockFingerprintCanEnable: false
property bool lockFingerprintReady: false
property string lockFingerprintReason: "probe_failed"
property bool greeterFingerprintCanEnable: false
property bool greeterFingerprintReady: false
property string greeterFingerprintReason: "probe_failed"
property string greeterFingerprintSource: "none"
property bool enableU2f: false
property string u2fMode: "or"
property bool u2fAvailable: false
property bool lockU2fCanEnable: false
property bool lockU2fReady: false
property string lockU2fReason: "probe_failed"
property bool greeterU2fCanEnable: false
property bool greeterU2fReady: false
property string greeterU2fReason: "probe_failed"
property string greeterU2fSource: "none"
property string lockScreenActiveMonitor: "all" property string lockScreenActiveMonitor: "all"
property string lockScreenInactiveColor: "#000000" property string lockScreenInactiveColor: "#000000"
property int lockScreenNotificationMode: 0 property int lockScreenNotificationMode: 0
@@ -518,7 +547,7 @@ Singleton {
property int osdPosition: SettingsData.Position.BottomCenter property int osdPosition: SettingsData.Position.BottomCenter
property bool osdVolumeEnabled: true property bool osdVolumeEnabled: true
property bool osdMediaVolumeEnabled: true property bool osdMediaVolumeEnabled: true
property bool osdMediaPlaybackEnabled: true property bool osdMediaPlaybackEnabled: false
property bool osdBrightnessEnabled: true property bool osdBrightnessEnabled: true
property bool osdIdleInhibitorEnabled: true property bool osdIdleInhibitorEnabled: true
property bool osdMicMuteEnabled: true property bool osdMicMuteEnabled: true
@@ -572,6 +601,10 @@ Singleton {
"widgetTransparency": 1.0, "widgetTransparency": 1.0,
"squareCorners": false, "squareCorners": false,
"noBackground": false, "noBackground": false,
"maximizeWidgetIcons": false,
"maximizeWidgetText": false,
"removeWidgetPadding": false,
"widgetPadding": 8,
"gothCornersEnabled": false, "gothCornersEnabled": false,
"gothCornerRadiusOverride": false, "gothCornerRadiusOverride": false,
"gothCornerRadiusValue": 12, "gothCornerRadiusValue": 12,
@@ -584,6 +617,7 @@ Singleton {
"widgetOutlineOpacity": 1.0, "widgetOutlineOpacity": 1.0,
"widgetOutlineThickness": 1, "widgetOutlineThickness": 1,
"fontScale": 1.0, "fontScale": 1.0,
"iconScale": 1.0,
"autoHide": false, "autoHide": false,
"autoHideDelay": 250, "autoHideDelay": 250,
"showOnWindowsOpen": false, "showOnWindowsOpen": false,
@@ -971,12 +1005,19 @@ Singleton {
signal widgetDataChanged signal widgetDataChanged
signal workspaceIconsUpdated signal workspaceIconsUpdated
function refreshAuthAvailability() {
if (isGreeterMode)
return;
Processes.settingsRoot = root;
Processes.detectAuthCapabilities();
}
Component.onCompleted: { Component.onCompleted: {
if (!isGreeterMode) { if (!isGreeterMode) {
Processes.settingsRoot = root; Processes.settingsRoot = root;
loadSettings(); loadSettings();
initializeListModels(); initializeListModels();
Processes.detectFprintd(); refreshAuthAvailability();
Processes.checkPluginSettings(); Processes.checkPluginSettings();
} }
} }
@@ -1026,6 +1067,7 @@ Singleton {
elif command -v dconf >/dev/null 2>&1; then elif command -v dconf >/dev/null 2>&1; then
dconf read /org/gnome/desktop/interface/icon-theme 2>/dev/null | sed "s/'//g" dconf read /org/gnome/desktop/interface/icon-theme 2>/dev/null | sed "s/'//g"
fi`; fi`;
Proc.runCommand("detectCosmicIconTheme", ["sh", "-c", detectScript], (output, exitCode) => { Proc.runCommand("detectCosmicIconTheme", ["sh", "-c", detectScript], (output, exitCode) => {
if (exitCode !== 0) if (exitCode !== 0)
return; return;
@@ -1215,10 +1257,47 @@ Singleton {
return JSON.stringify(Store.toJson(root), null, 2); return JSON.stringify(Store.toJson(root), null, 2);
} }
function _resetPluginSettings() {
_pluginParseError = false;
pluginSettings = {};
}
function _pluginSettingsErrorCode(error) {
if (typeof error === "number")
return error;
if (error && typeof error === "object") {
if (typeof error.code === "number")
return error.code;
if (typeof error.errno === "number")
return error.errno;
}
const msg = String(error || "").trim();
if (/^\d+$/.test(msg))
return Number(msg);
return -1;
}
function _isMissingPluginSettingsError(error) {
if (_pluginSettingsErrorCode(error) === 2)
return true;
const msg = String(error || "").toLowerCase();
return msg.indexOf("file does not exist") !== -1
|| msg.indexOf("no such file") !== -1
|| msg.indexOf("enoent") !== -1;
}
function loadPluginSettings() { function loadPluginSettings() {
_pluginSettingsLoading = true; try {
parsePluginSettings(pluginSettingsFile.text()); parsePluginSettings(pluginSettingsFile.text());
_pluginSettingsLoading = false; } catch (e) {
const msg = e.message || String(e);
if (!_isMissingPluginSettingsError(e))
console.warn("SettingsData: Failed to load plugin_settings.json. Error:", msg);
_resetPluginSettings();
}
} }
function parsePluginSettings(content) { function parsePluginSettings(content) {
@@ -2654,6 +2733,7 @@ Singleton {
blockLoading: true blockLoading: true
blockWrites: true blockWrites: true
atomicWrites: true atomicWrites: true
printErrors: false
watchChanges: !isGreeterMode watchChanges: !isGreeterMode
onLoaded: { onLoaded: {
if (!isGreeterMode) { if (!isGreeterMode) {
@@ -2662,7 +2742,10 @@ Singleton {
} }
onLoadFailed: error => { onLoadFailed: error => {
if (!isGreeterMode) { if (!isGreeterMode) {
pluginSettings = {}; const msg = String(error || "");
if (!_isMissingPluginSettingsError(error))
console.warn("SettingsData: Failed to load plugin_settings.json. Error:", msg);
_resetPluginSettings();
} }
} }
} }

View File

@@ -858,7 +858,7 @@ Singleton {
property string fontFamily: { property string fontFamily: {
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") { if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
return GreetdSettings.fontFamily; return GreetdSettings.getEffectiveFontFamily();
} }
return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable"; return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable";
} }
@@ -961,7 +961,6 @@ Singleton {
} }
if (!isGreeterMode) { if (!isGreeterMode) {
// Skip with matugen because, our script runner will do it.
if (!matugenAvailable) { if (!matugenAvailable) {
PortalService.setLightMode(light); PortalService.setLightMode(light);
} }
@@ -1023,7 +1022,11 @@ Singleton {
if (themeData.variants.type === "multi" && themeData.variants.flavors && themeData.variants.accents) { if (themeData.variants.type === "multi" && themeData.variants.flavors && themeData.variants.accents) {
const defaults = themeData.variants.defaults || {}; const defaults = themeData.variants.defaults || {};
const modeDefaults = defaults[colorMode] || defaults.dark || {}; const modeDefaults = defaults[colorMode] || defaults.dark || {};
const stored = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults, colorMode) : modeDefaults; const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
const stored = isGreeterMode ?
(GreetdSettings.registryThemeVariants[themeId]?.[colorMode] || modeDefaults) :
(typeof SettingsData !== "undefined" ?
SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults, colorMode) : modeDefaults);
var flavorId = stored.flavor || modeDefaults.flavor || ""; var flavorId = stored.flavor || modeDefaults.flavor || "";
const accentId = stored.accent || modeDefaults.accent || ""; const accentId = stored.accent || modeDefaults.accent || "";
var flavor = findVariant(themeData.variants.flavors, flavorId); var flavor = findVariant(themeData.variants.flavors, flavorId);
@@ -1049,7 +1052,10 @@ Singleton {
} }
if (themeData.variants.options && themeData.variants.options.length > 0) { if (themeData.variants.options && themeData.variants.options.length > 0) {
const selectedVariantId = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, themeData.variants.default) : themeData.variants.default; const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
const selectedVariantId = isGreeterMode
? (typeof GreetdSettings.registryThemeVariants[themeId] === "string" ? GreetdSettings.registryThemeVariants[themeId] : themeData.variants.default)
: (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, themeData.variants.default) : themeData.variants.default);
const variant = findVariant(themeData.variants.options, selectedVariantId); const variant = findVariant(themeData.variants.options, selectedVariantId);
if (variant) { if (variant) {
const variantColors = variant[colorMode] || variant.dark || variant.light || {}; const variantColors = variant[colorMode] || variant.dark || variant.light || {};
@@ -1170,21 +1176,23 @@ Singleton {
return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5; return (0.299 * c.r + 0.587 * c.g + 0.114 * c.b) < 0.5;
} }
function barIconSize(barThickness, offset, noBackground) { function barIconSize(barThickness, offset, maximizeIcon, iconScale) {
const defaultOffset = offset !== undefined ? offset : -6; const defaultOffset = offset !== undefined ? offset : -6;
const size = (noBackground ?? false) ? iconSizeLarge : iconSize; const size = (maximizeIcon ?? false) ? iconSizeLarge : iconSize;
const s = iconScale !== undefined ? iconScale : 1.0;
return Math.round((barThickness / 48) * (size + defaultOffset)); return Math.round((barThickness / 48) * (size + defaultOffset) * s);
} }
function barTextSize(barThickness, fontScale) { function barTextSize(barThickness, fontScale, maximizeText) {
const scale = barThickness / 48; const scale = barThickness / 48;
const dankBarScale = fontScale !== undefined ? fontScale : 1.0; const dankBarScale = fontScale !== undefined ? fontScale : 1.0;
const maxScale = (maximizeText ?? false) ? 1.5 : 1.0;
if (scale <= 0.75) if (scale <= 0.75)
return Math.round(fontSizeSmall * 0.9 * dankBarScale); return Math.round(fontSizeSmall * 0.9 * dankBarScale * maxScale);
if (scale >= 1.25) if (scale >= 1.25)
return Math.round(fontSizeMedium * dankBarScale); return Math.round(fontSizeMedium * dankBarScale * maxScale);
return Math.round(fontSizeSmall * dankBarScale); return Math.round(fontSizeSmall * dankBarScale * maxScale);
} }
function getBatteryIcon(level, isCharging, batteryAvailable) { function getBatteryIcon(level, isCharging, batteryAvailable) {
@@ -1324,7 +1332,7 @@ Singleton {
if (typeof SettingsData !== "undefined") { if (typeof SettingsData !== "undefined") {
const skipTemplates = []; const skipTemplates = [];
if (!SettingsData.runDmsMatugenTemplates) { if (!SettingsData.runDmsMatugenTemplates) {
skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode", "emacs"); skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode", "emacs", "zed");
} else { } else {
if (!SettingsData.matugenTemplateGtk) if (!SettingsData.matugenTemplateGtk)
skipTemplates.push("gtk"); skipTemplates.push("gtk");
@@ -1368,6 +1376,8 @@ Singleton {
skipTemplates.push("vscode"); skipTemplates.push("vscode");
if (!SettingsData.matugenTemplateEmacs) if (!SettingsData.matugenTemplateEmacs)
skipTemplates.push("emacs"); skipTemplates.push("emacs");
if (!SettingsData.matugenTemplateZed)
skipTemplates.push("zed");
} }
if (skipTemplates.length > 0) { if (skipTemplates.length > 0) {
args.push("--skip-templates", skipTemplates.join(",")); args.push("--skip-templates", skipTemplates.join(","));
@@ -1417,8 +1427,13 @@ Singleton {
const defaults = customThemeRawData.variants.defaults || {}; const defaults = customThemeRawData.variants.defaults || {};
const darkDefaults = defaults.dark || {}; const darkDefaults = defaults.dark || {};
const lightDefaults = defaults.light || defaults.dark || {}; const lightDefaults = defaults.light || defaults.dark || {};
const storedDark = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults, "dark") : darkDefaults; const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
const storedLight = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults; const storedDark = isGreeterMode
? (GreetdSettings.registryThemeVariants[themeId]?.dark || darkDefaults)
: (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults, "dark") : darkDefaults);
const storedLight = isGreeterMode
? (GreetdSettings.registryThemeVariants[themeId]?.light || lightDefaults)
: (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults);
const darkFlavorId = storedDark.flavor || darkDefaults.flavor || ""; const darkFlavorId = storedDark.flavor || darkDefaults.flavor || "";
const lightFlavorId = storedLight.flavor || lightDefaults.flavor || ""; const lightFlavorId = storedLight.flavor || lightDefaults.flavor || "";
const accentId = storedDark.accent || darkDefaults.accent || ""; const accentId = storedDark.accent || darkDefaults.accent || "";
@@ -1436,7 +1451,10 @@ Singleton {
lightTheme = mergeColors(lightTheme, accent[lightFlavor.id] || {}); lightTheme = mergeColors(lightTheme, accent[lightFlavor.id] || {});
} }
} else if (customThemeRawData.variants.options) { } else if (customThemeRawData.variants.options) {
const selectedVariantId = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, customThemeRawData.variants.default) : customThemeRawData.variants.default; const isGreeterMode = typeof SessionData !== "undefined" && SessionData.isGreeterMode;
const selectedVariantId = isGreeterMode
? (typeof GreetdSettings.registryThemeVariants[themeId] === "string" ? GreetdSettings.registryThemeVariants[themeId] : customThemeRawData.variants.default)
: (typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeVariant(themeId, customThemeRawData.variants.default) : customThemeRawData.variants.default);
const variant = findVariant(customThemeRawData.variants.options, selectedVariantId); const variant = findVariant(customThemeRawData.variants.options, selectedVariantId);
if (variant) { if (variant) {
darkTheme = mergeColors(darkTheme, variant.dark || {}); darkTheme = mergeColors(darkTheme, variant.dark || {});
@@ -1760,10 +1778,11 @@ Singleton {
FileView { FileView {
id: dynamicColorsFileView id: dynamicColorsFileView
path: { path: {
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms"; const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json"; const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json";
return colorsPath; return colorsPath;
} }
blockLoading: false
watchChanges: !SessionData.isGreeterMode watchChanges: !SessionData.isGreeterMode
function parseAndLoadColors() { function parseAndLoadColors() {

View File

@@ -10,18 +10,352 @@ Singleton {
property var settingsRoot: null property var settingsRoot: null
property string greetdPamText: ""
property string systemAuthPamText: ""
property string commonAuthPamText: ""
property string passwordAuthPamText: ""
property string systemLoginPamText: ""
property string systemLocalLoginPamText: ""
property string commonAuthPcPamText: ""
property string loginPamText: ""
property string dankshellU2fPamText: ""
property string u2fKeysText: ""
property string fingerprintProbeOutput: ""
property int fingerprintProbeExitCode: 0
property bool fingerprintProbeStreamFinished: false
property bool fingerprintProbeExited: false
property string fingerprintProbeState: "probe_failed"
property string pamSupportProbeOutput: ""
property bool pamSupportProbeStreamFinished: false
property bool pamSupportProbeExited: false
property int pamSupportProbeExitCode: 0
property bool pamFprintSupportDetected: false
property bool pamU2fSupportDetected: false
readonly property string homeDir: Quickshell.env("HOME") || ""
readonly property string u2fKeysPath: homeDir ? homeDir + "/.config/Yubico/u2f_keys" : ""
readonly property bool homeU2fKeysDetected: u2fKeysPath !== "" && u2fKeysWatcher.loaded && u2fKeysText.trim() !== ""
readonly property bool lockU2fCustomConfigDetected: pamModuleEnabled(dankshellU2fPamText, "pam_u2f")
readonly property bool greeterPamHasFprint: greeterPamStackHasModule("pam_fprintd")
readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f")
function envFlag(name) {
const value = (Quickshell.env(name) || "").trim().toLowerCase();
if (value === "1" || value === "true" || value === "yes" || value === "on")
return true;
if (value === "0" || value === "false" || value === "no" || value === "off")
return false;
return null;
}
readonly property var forcedFprintAvailable: envFlag("DMS_FORCE_FPRINT_AVAILABLE")
readonly property var forcedU2fAvailable: envFlag("DMS_FORCE_U2F_AVAILABLE")
function detectQtTools() { function detectQtTools() {
qtToolsDetectionProcess.running = true; qtToolsDetectionProcess.running = true;
} }
function detectAuthCapabilities() {
if (!settingsRoot)
return;
if (forcedFprintAvailable === null) {
fingerprintProbeOutput = "";
fingerprintProbeStreamFinished = false;
fingerprintProbeExited = false;
fingerprintProbeProcess.running = true;
} else {
fingerprintProbeState = forcedFprintAvailable ? "ready" : "probe_failed";
}
if (forcedFprintAvailable === null || forcedU2fAvailable === null) {
pamFprintSupportDetected = false;
pamU2fSupportDetected = false;
pamSupportProbeOutput = "";
pamSupportProbeStreamFinished = false;
pamSupportProbeExited = false;
pamSupportDetectionProcess.running = true;
}
recomputeAuthCapabilities();
}
function detectFprintd() { function detectFprintd() {
fprintdDetectionProcess.running = true; detectAuthCapabilities();
}
function detectU2f() {
detectAuthCapabilities();
} }
function checkPluginSettings() { function checkPluginSettings() {
pluginSettingsCheckProcess.running = true; pluginSettingsCheckProcess.running = true;
} }
function stripPamComment(line) {
if (!line)
return "";
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#"))
return "";
const hashIdx = trimmed.indexOf("#");
if (hashIdx >= 0)
return trimmed.substring(0, hashIdx).trim();
return trimmed;
}
function pamModuleEnabled(pamText, moduleName) {
if (!pamText || !moduleName)
return false;
const lines = pamText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (!line)
continue;
if (line.includes(moduleName))
return true;
}
return false;
}
function pamTextIncludesFile(pamText, filename) {
if (!pamText || !filename)
return false;
const lines = pamText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (!line)
continue;
if (line.includes(filename) && (line.includes("include") || line.includes("substack") || line.startsWith("@include")))
return true;
}
return false;
}
function greeterPamStackHasModule(moduleName) {
if (pamModuleEnabled(greetdPamText, moduleName))
return true;
const includedPamStacks = [
["system-auth", systemAuthPamText],
["common-auth", commonAuthPamText],
["password-auth", passwordAuthPamText],
["system-login", systemLoginPamText],
["system-local-login", systemLocalLoginPamText],
["common-auth-pc", commonAuthPcPamText],
["login", loginPamText]
];
for (let i = 0; i < includedPamStacks.length; i++) {
const stack = includedPamStacks[i];
if (pamTextIncludesFile(greetdPamText, stack[0]) && pamModuleEnabled(stack[1], moduleName))
return true;
}
return false;
}
function hasEnrolledFingerprintOutput(output) {
const lower = (output || "").toLowerCase();
if (lower.includes("has fingers enrolled") || lower.includes("has fingerprints enrolled"))
return true;
const lines = lower.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const trimmed = lines[i].trim();
if (trimmed.startsWith("finger:"))
return true;
if (trimmed.startsWith("- ") && trimmed.includes("finger"))
return true;
}
return false;
}
function hasMissingFingerprintEnrollmentOutput(output) {
const lower = (output || "").toLowerCase();
return lower.includes("no fingers enrolled")
|| lower.includes("no fingerprints enrolled")
|| lower.includes("no prints enrolled");
}
function hasMissingFingerprintReaderOutput(output) {
const lower = (output || "").toLowerCase();
return lower.includes("no devices available")
|| lower.includes("no device available")
|| lower.includes("no devices found")
|| lower.includes("list_devices failed")
|| lower.includes("no device");
}
function parseFingerprintProbe(exitCode, output) {
if (hasEnrolledFingerprintOutput(output))
return "ready";
if (hasMissingFingerprintEnrollmentOutput(output))
return "missing_enrollment";
if (hasMissingFingerprintReaderOutput(output))
return "missing_reader";
if (exitCode === 0)
return "missing_enrollment";
if (exitCode === 127 || (output || "").includes("__missing_command__"))
return "probe_failed";
return pamFprintSupportDetected ? "probe_failed" : "missing_pam_support";
}
function setLockFingerprintCapability(canEnable, ready, reason) {
settingsRoot.lockFingerprintCanEnable = canEnable;
settingsRoot.lockFingerprintReady = ready;
settingsRoot.lockFingerprintReason = reason;
}
function setLockU2fCapability(canEnable, ready, reason) {
settingsRoot.lockU2fCanEnable = canEnable;
settingsRoot.lockU2fReady = ready;
settingsRoot.lockU2fReason = reason;
}
function setGreeterFingerprintCapability(canEnable, ready, reason, source) {
settingsRoot.greeterFingerprintCanEnable = canEnable;
settingsRoot.greeterFingerprintReady = ready;
settingsRoot.greeterFingerprintReason = reason;
settingsRoot.greeterFingerprintSource = source;
}
function setGreeterU2fCapability(canEnable, ready, reason, source) {
settingsRoot.greeterU2fCanEnable = canEnable;
settingsRoot.greeterU2fReady = ready;
settingsRoot.greeterU2fReason = reason;
settingsRoot.greeterU2fSource = source;
}
function recomputeFingerprintCapabilities() {
if (forcedFprintAvailable !== null) {
const reason = forcedFprintAvailable ? "ready" : "probe_failed";
const source = forcedFprintAvailable ? "dms" : "none";
setLockFingerprintCapability(forcedFprintAvailable, forcedFprintAvailable, reason);
setGreeterFingerprintCapability(forcedFprintAvailable, forcedFprintAvailable, reason, source);
return;
}
const state = fingerprintProbeState;
switch (state) {
case "ready":
setLockFingerprintCapability(true, true, "ready");
break;
case "missing_enrollment":
setLockFingerprintCapability(true, false, "missing_enrollment");
break;
case "missing_reader":
setLockFingerprintCapability(false, false, "missing_reader");
break;
case "missing_pam_support":
setLockFingerprintCapability(false, false, "missing_pam_support");
break;
default:
setLockFingerprintCapability(false, false, "probe_failed");
break;
}
if (greeterPamHasFprint) {
switch (state) {
case "ready":
setGreeterFingerprintCapability(true, true, "configured_externally", "pam");
break;
case "missing_enrollment":
setGreeterFingerprintCapability(true, false, "missing_enrollment", "pam");
break;
case "missing_reader":
setGreeterFingerprintCapability(false, false, "missing_reader", "pam");
break;
default:
setGreeterFingerprintCapability(true, false, "probe_failed", "pam");
break;
}
return;
}
switch (state) {
case "ready":
setGreeterFingerprintCapability(true, true, "ready", "dms");
break;
case "missing_enrollment":
setGreeterFingerprintCapability(true, false, "missing_enrollment", "dms");
break;
case "missing_reader":
setGreeterFingerprintCapability(false, false, "missing_reader", "none");
break;
case "missing_pam_support":
setGreeterFingerprintCapability(false, false, "missing_pam_support", "none");
break;
default:
setGreeterFingerprintCapability(false, false, "probe_failed", "none");
break;
}
}
function recomputeU2fCapabilities() {
if (forcedU2fAvailable !== null) {
const reason = forcedU2fAvailable ? "ready" : "probe_failed";
const source = forcedU2fAvailable ? "dms" : "none";
setLockU2fCapability(forcedU2fAvailable, forcedU2fAvailable, reason);
setGreeterU2fCapability(forcedU2fAvailable, forcedU2fAvailable, reason, source);
return;
}
const lockReady = lockU2fCustomConfigDetected || homeU2fKeysDetected;
const lockCanEnable = lockReady || pamU2fSupportDetected;
const lockReason = lockReady ? "ready" : (lockCanEnable ? "missing_key_registration" : "missing_pam_support");
setLockU2fCapability(lockCanEnable, lockReady, lockReason);
if (greeterPamHasU2f) {
setGreeterU2fCapability(true, true, "configured_externally", "pam");
return;
}
const greeterReady = homeU2fKeysDetected;
const greeterCanEnable = greeterReady || pamU2fSupportDetected;
const greeterReason = greeterReady ? "ready" : (greeterCanEnable ? "missing_key_registration" : "missing_pam_support");
setGreeterU2fCapability(greeterCanEnable, greeterReady, greeterReason, greeterCanEnable ? "dms" : "none");
}
function recomputeAuthCapabilities() {
if (!settingsRoot)
return;
recomputeFingerprintCapabilities();
recomputeU2fCapabilities();
settingsRoot.fprintdAvailable = settingsRoot.lockFingerprintReady || settingsRoot.greeterFingerprintReady;
settingsRoot.u2fAvailable = settingsRoot.lockU2fReady || settingsRoot.greeterU2fReady;
}
function finalizeFingerprintProbe() {
if (!fingerprintProbeStreamFinished || !fingerprintProbeExited)
return;
fingerprintProbeState = parseFingerprintProbe(fingerprintProbeExitCode, fingerprintProbeOutput);
recomputeAuthCapabilities();
}
function finalizePamSupportProbe() {
if (!pamSupportProbeStreamFinished || !pamSupportProbeExited)
return;
pamFprintSupportDetected = false;
pamU2fSupportDetected = false;
const lines = (pamSupportProbeOutput || "").trim().split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const parts = lines[i].split(":");
if (parts.length !== 2)
continue;
if (parts[0] === "pam_fprintd.so")
pamFprintSupportDetected = parts[1] === "true";
else if (parts[0] === "pam_u2f.so")
pamU2fSupportDetected = parts[1] === "true";
}
if (forcedFprintAvailable === null && fingerprintProbeState === "missing_pam_support")
fingerprintProbeState = parseFingerprintProbe(fingerprintProbeExitCode, fingerprintProbeOutput);
recomputeAuthCapabilities();
}
property var qtToolsDetectionProcess: Process { property var qtToolsDetectionProcess: Process {
command: ["sh", "-c", "echo -n 'qt5ct:'; command -v qt5ct >/dev/null && echo 'true' || echo 'false'; echo -n 'qt6ct:'; command -v qt6ct >/dev/null && echo 'true' || echo 'false'; echo -n 'gtk:'; (command -v gsettings >/dev/null || command -v dconf >/dev/null) && echo 'true' || echo 'false'"] command: ["sh", "-c", "echo -n 'qt5ct:'; command -v qt5ct >/dev/null && echo 'true' || echo 'false'; echo -n 'qt6ct:'; command -v qt6ct >/dev/null && echo 'true' || echo 'false'; echo -n 'gtk:'; (command -v gsettings >/dev/null || command -v dconf >/dev/null) && echo 'true' || echo 'false'"]
running: false running: false
@@ -31,15 +365,15 @@ Singleton {
if (!settingsRoot) if (!settingsRoot)
return; return;
if (text && text.trim()) { if (text && text.trim()) {
var lines = text.trim().split('\n'); const lines = text.trim().split("\n");
for (var i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
var line = lines[i]; const line = lines[i];
if (line.startsWith('qt5ct:')) { if (line.startsWith("qt5ct:")) {
settingsRoot.qt5ctAvailable = line.split(':')[1] === 'true'; settingsRoot.qt5ctAvailable = line.split(":")[1] === "true";
} else if (line.startsWith('qt6ct:')) { } else if (line.startsWith("qt6ct:")) {
settingsRoot.qt6ctAvailable = line.split(':')[1] === 'true'; settingsRoot.qt6ctAvailable = line.split(":")[1] === "true";
} else if (line.startsWith('gtk:')) { } else if (line.startsWith("gtk:")) {
settingsRoot.gtkAvailable = line.split(':')[1] === 'true'; settingsRoot.gtkAvailable = line.split(":")[1] === "true";
} }
} }
} }
@@ -47,13 +381,181 @@ Singleton {
} }
} }
property var fprintdDetectionProcess: Process { property var fingerprintProbeProcess: Process {
command: ["sh", "-c", "command -v fprintd-list >/dev/null 2>&1"] command: ["sh", "-c", "if command -v fprintd-list >/dev/null 2>&1; then fprintd-list \"${USER:-$(id -un)}\" 2>&1; else printf '__missing_command__\\n'; exit 127; fi"]
running: false running: false
stdout: StdioCollector {
onStreamFinished: {
root.fingerprintProbeOutput = text || "";
root.fingerprintProbeStreamFinished = true;
root.finalizeFingerprintProbe();
}
}
onExited: function (exitCode) { onExited: function (exitCode) {
if (!settingsRoot) root.fingerprintProbeExitCode = exitCode;
return; root.fingerprintProbeExited = true;
settingsRoot.fprintdAvailable = (exitCode === 0); root.finalizeFingerprintProbe();
}
}
property var pamSupportDetectionProcess: Process {
command: ["sh", "-c", "for module in pam_fprintd.so pam_u2f.so; do found=false; for dir in /usr/lib64/security /usr/lib/security /lib/security /lib/x86_64-linux-gnu/security /usr/lib/x86_64-linux-gnu/security /usr/lib/aarch64-linux-gnu/security /run/current-system/sw/lib/security; do if [ -f \"$dir/$module\" ]; then found=true; break; fi; done; printf '%s:%s\\n' \"$module\" \"$found\"; done"]
running: false
stdout: StdioCollector {
onStreamFinished: {
root.pamSupportProbeOutput = text || "";
root.pamSupportProbeStreamFinished = true;
root.finalizePamSupportProbe();
}
}
onExited: function (exitCode) {
root.pamSupportProbeExitCode = exitCode;
root.pamSupportProbeExited = true;
root.finalizePamSupportProbe();
}
}
FileView {
id: greetdPamWatcher
path: "/etc/pam.d/greetd"
printErrors: false
onLoaded: {
root.greetdPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.greetdPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: systemAuthPamWatcher
path: "/etc/pam.d/system-auth"
printErrors: false
onLoaded: {
root.systemAuthPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.systemAuthPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: commonAuthPamWatcher
path: "/etc/pam.d/common-auth"
printErrors: false
onLoaded: {
root.commonAuthPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.commonAuthPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: passwordAuthPamWatcher
path: "/etc/pam.d/password-auth"
printErrors: false
onLoaded: {
root.passwordAuthPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.passwordAuthPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: systemLoginPamWatcher
path: "/etc/pam.d/system-login"
printErrors: false
onLoaded: {
root.systemLoginPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.systemLoginPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: systemLocalLoginPamWatcher
path: "/etc/pam.d/system-local-login"
printErrors: false
onLoaded: {
root.systemLocalLoginPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.systemLocalLoginPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: commonAuthPcPamWatcher
path: "/etc/pam.d/common-auth-pc"
printErrors: false
onLoaded: {
root.commonAuthPcPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.commonAuthPcPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: loginPamWatcher
path: "/etc/pam.d/login"
printErrors: false
onLoaded: {
root.loginPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.loginPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: dankshellU2fPamWatcher
path: "/etc/pam.d/dankshell-u2f"
printErrors: false
onLoaded: {
root.dankshellU2fPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.dankshellU2fPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: u2fKeysWatcher
path: root.u2fKeysPath
printErrors: false
onLoaded: {
root.u2fKeysText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.u2fKeysText = "";
root.recomputeAuthCapabilities();
} }
} }

View File

@@ -77,7 +77,11 @@ var SPEC = {
deviceMaxVolumes: { def: {} }, deviceMaxVolumes: { def: {} },
hiddenOutputDeviceNames: { def: [] }, hiddenOutputDeviceNames: { def: [] },
hiddenInputDeviceNames: { def: [] } hiddenInputDeviceNames: { def: [] },
launcherLastMode: { def: "all" },
appDrawerLastMode: { def: "apps" },
niriOverviewLastMode: { def: "apps" }
}; };
function getValidKeys() { function getValidKeys() {

View File

@@ -154,6 +154,17 @@ var SPEC = {
centeringMode: { def: "index" }, centeringMode: { def: "index" },
clockDateFormat: { def: "" }, clockDateFormat: { def: "" },
lockDateFormat: { def: "" }, lockDateFormat: { def: "" },
greeterRememberLastSession: { def: true },
greeterRememberLastUser: { def: true },
greeterEnableFprint: { def: false },
greeterEnableU2f: { def: false },
greeterWallpaperPath: { def: "" },
greeterUse24HourClock: { def: true },
greeterShowSeconds: { def: false },
greeterPadHours12Hour: { def: false },
greeterLockDateFormat: { def: "" },
greeterFontFamily: { def: "" },
greeterWallpaperFillMode: { def: "" },
mediaSize: { def: 1 }, mediaSize: { def: 1 },
appLauncherViewMode: { def: "list" }, appLauncherViewMode: { def: "list" },
@@ -262,12 +273,13 @@ var SPEC = {
matugenTemplateKitty: { def: true }, matugenTemplateKitty: { def: true },
matugenTemplateFoot: { def: true }, matugenTemplateFoot: { def: true },
matugenTemplateAlacritty: { def: true }, matugenTemplateAlacritty: { def: true },
matugenTemplateNeovim: { def: true }, matugenTemplateNeovim: { def: false },
matugenTemplateWezterm: { def: true }, matugenTemplateWezterm: { def: true },
matugenTemplateDgop: { def: true }, matugenTemplateDgop: { def: true },
matugenTemplateKcolorscheme: { def: true }, matugenTemplateKcolorscheme: { def: true },
matugenTemplateVscode: { def: true }, matugenTemplateVscode: { def: true },
matugenTemplateEmacs: { def: true }, matugenTemplateEmacs: { def: true },
matugenTemplateZed: { def: true },
showDock: { def: false }, showDock: { def: false },
dockAutoHide: { def: false }, dockAutoHide: { def: false },
@@ -317,6 +329,23 @@ var SPEC = {
enableFprint: { def: false }, enableFprint: { def: false },
maxFprintTries: { def: 15 }, maxFprintTries: { def: 15 },
fprintdAvailable: { def: false, persist: false }, fprintdAvailable: { def: false, persist: false },
lockFingerprintCanEnable: { def: false, persist: false },
lockFingerprintReady: { def: false, persist: false },
lockFingerprintReason: { def: "probe_failed", persist: false },
greeterFingerprintCanEnable: { def: false, persist: false },
greeterFingerprintReady: { def: false, persist: false },
greeterFingerprintReason: { def: "probe_failed", persist: false },
greeterFingerprintSource: { def: "none", persist: false },
enableU2f: { def: false },
u2fMode: { def: "or" },
u2fAvailable: { def: false, persist: false },
lockU2fCanEnable: { def: false, persist: false },
lockU2fReady: { def: false, persist: false },
lockU2fReason: { def: "probe_failed", persist: false },
greeterU2fCanEnable: { def: false, persist: false },
greeterU2fReady: { def: false, persist: false },
greeterU2fReason: { def: "probe_failed", persist: false },
greeterU2fSource: { def: "none", persist: false },
lockScreenActiveMonitor: { def: "all" }, lockScreenActiveMonitor: { def: "all" },
lockScreenInactiveColor: { def: "#000000" }, lockScreenInactiveColor: { def: "#000000" },
lockScreenNotificationMode: { def: 0 }, lockScreenNotificationMode: { def: 0 },
@@ -341,7 +370,7 @@ var SPEC = {
osdPosition: { def: 5 }, osdPosition: { def: 5 },
osdVolumeEnabled: { def: true }, osdVolumeEnabled: { def: true },
osdMediaVolumeEnabled: { def: true }, osdMediaVolumeEnabled: { def: true },
osdMediaPlaybackEnabled: { def: true }, osdMediaPlaybackEnabled: { def: false },
osdBrightnessEnabled: { def: true }, osdBrightnessEnabled: { def: true },
osdIdleInhibitorEnabled: { def: true }, osdIdleInhibitorEnabled: { def: true },
osdMicMuteEnabled: { def: true }, osdMicMuteEnabled: { def: true },
@@ -395,6 +424,10 @@ var SPEC = {
widgetTransparency: 1.0, widgetTransparency: 1.0,
squareCorners: false, squareCorners: false,
noBackground: false, noBackground: false,
maximizeWidgetIcons: false,
maximizeWidgetText: false,
removeWidgetPadding: false,
widgetPadding: 8,
gothCornersEnabled: false, gothCornersEnabled: false,
gothCornerRadiusOverride: false, gothCornerRadiusOverride: false,
gothCornerRadiusValue: 12, gothCornerRadiusValue: 12,
@@ -407,6 +440,7 @@ var SPEC = {
widgetOutlineOpacity: 1.0, widgetOutlineOpacity: 1.0,
widgetOutlineThickness: 1, widgetOutlineThickness: 1,
fontScale: 1.0, fontScale: 1.0,
iconScale: 1.0,
autoHide: false, autoHide: false,
autoHideDelay: 250, autoHideDelay: 250,
showOnWindowsOpen: false, showOnWindowsOpen: false,

View File

@@ -142,25 +142,45 @@ Item {
fadeDpmsWindowLoader.item.cancelFade(); fadeDpmsWindowLoader.item.cancelFade();
} }
} }
function onRequestMonitorOn() {
if (!fadeDpmsWindowLoader.item)
return;
fadeDpmsWindowLoader.item.cancelFade();
}
} }
} }
} }
property string _barLayoutStateJson: {
const configs = SettingsData.barConfigs;
const mapped = configs.map((c, i) => ({
id: c.id,
position: c.position,
autoHide: c.autoHide,
visible: c.visible,
_origIndex: i
})).sort((a, b) => {
const aVertical = a.position === SettingsData.Position.Left || a.position === SettingsData.Position.Right;
const bVertical = b.position === SettingsData.Position.Left || b.position === SettingsData.Position.Right;
if (aVertical !== bVertical)
return aVertical - bVertical;
return a._origIndex - b._origIndex;
});
return JSON.stringify(mapped);
}
on_BarLayoutStateJsonChanged: {
if (typeof dockRecreateDebounce !== "undefined") {
dockRecreateDebounce.restart();
}
}
Repeater { Repeater {
id: dankBarRepeater id: dankBarRepeater
model: ScriptModel { model: ScriptModel {
id: barRepeaterModel id: barRepeaterModel
values: { values: JSON.parse(root._barLayoutStateJson)
const configs = SettingsData.barConfigs;
return configs.map(c => ({
id: c.id,
position: c.position
})).sort((a, b) => {
const aVertical = a.position === SettingsData.Position.Left || a.position === SettingsData.Position.Right;
const bVertical = b.position === SettingsData.Position.Left || b.position === SettingsData.Position.Right;
return aVertical - bVertical;
});
}
} }
property var hyprlandOverviewLoaderRef: hyprlandOverviewLoader property var hyprlandOverviewLoaderRef: hyprlandOverviewLoader
@@ -207,13 +227,6 @@ Item {
PolkitService.polkitAvailable; PolkitService.polkitAvailable;
} }
Connections {
target: SettingsData
function onBarConfigsChanged() {
dockRecreateDebounce.restart();
}
}
Loader { Loader {
id: dockLoader id: dockLoader
active: root.dockEnabled active: root.dockEnabled
@@ -265,6 +278,7 @@ Item {
sourceComponent: Component { sourceComponent: Component {
DankDashPopout { DankDashPopout {
id: dankDashPopout id: dankDashPopout
onPopoutClosed: PopoutService.unloadDankDash()
} }
} }
} }
@@ -284,8 +298,13 @@ Item {
active: false active: false
Component.onCompleted: {
PopoutService.notificationCenterLoader = notificationCenterLoader;
}
NotificationCenterPopout { NotificationCenterPopout {
id: notificationCenter id: notificationCenter
onPopoutClosed: PopoutService.unloadNotificationCenter()
Component.onCompleted: { Component.onCompleted: {
PopoutService.notificationCenterPopout = notificationCenter; PopoutService.notificationCenterPopout = notificationCenter;
@@ -309,10 +328,15 @@ Item {
property var modalRef: colorPickerModal property var modalRef: colorPickerModal
property LazyLoader powerModalLoaderRef: powerMenuModalLoader property LazyLoader powerModalLoaderRef: powerMenuModalLoader
Component.onCompleted: {
PopoutService.controlCenterLoader = controlCenterLoader;
}
ControlCenterPopout { ControlCenterPopout {
id: controlCenterPopout id: controlCenterPopout
colorPickerModal: controlCenterLoader.modalRef colorPickerModal: controlCenterLoader.modalRef
powerMenuModalLoader: controlCenterLoader.powerModalLoaderRef powerMenuModalLoader: controlCenterLoader.powerModalLoaderRef
onPopoutClosed: PopoutService.unloadControlCenter()
onLockRequested: { onLockRequested: {
lock.activate(); lock.activate();
@@ -420,8 +444,13 @@ Item {
active: false active: false
Component.onCompleted: {
PopoutService.batteryPopoutLoader = batteryPopoutLoader;
}
BatteryPopout { BatteryPopout {
id: batteryPopout id: batteryPopout
onPopoutClosed: PopoutService.unloadBattery()
Component.onCompleted: { Component.onCompleted: {
PopoutService.batteryPopout = batteryPopout; PopoutService.batteryPopout = batteryPopout;
@@ -434,8 +463,13 @@ Item {
active: false active: false
Component.onCompleted: {
PopoutService.layoutPopoutLoader = layoutPopoutLoader;
}
DWLLayoutPopout { DWLLayoutPopout {
id: layoutPopout id: layoutPopout
onPopoutClosed: PopoutService.unloadLayoutPopout()
Component.onCompleted: { Component.onCompleted: {
PopoutService.layoutPopout = layoutPopout; PopoutService.layoutPopout = layoutPopout;
@@ -448,8 +482,13 @@ Item {
active: false active: false
Component.onCompleted: {
PopoutService.vpnPopoutLoader = vpnPopoutLoader;
}
VpnPopout { VpnPopout {
id: vpnPopout id: vpnPopout
onPopoutClosed: PopoutService.unloadVpn()
Component.onCompleted: { Component.onCompleted: {
PopoutService.vpnPopout = vpnPopout; PopoutService.vpnPopout = vpnPopout;
@@ -462,8 +501,13 @@ Item {
active: false active: false
Component.onCompleted: {
PopoutService.processListPopoutLoader = processListPopoutLoader;
}
ProcessListPopout { ProcessListPopout {
id: processListPopout id: processListPopout
onPopoutClosed: PopoutService.unloadProcessListPopout()
Component.onCompleted: { Component.onCompleted: {
PopoutService.processListPopout = processListPopout; PopoutService.processListPopout = processListPopout;
@@ -506,8 +550,13 @@ Item {
active: false active: false
Component.onCompleted: {
PopoutService.appDrawerLoader = appDrawerLoader;
}
AppDrawerPopout { AppDrawerPopout {
id: appDrawerPopout id: appDrawerPopout
onPopoutClosed: PopoutService.unloadAppDrawer()
Component.onCompleted: { Component.onCompleted: {
PopoutService.appDrawerPopout = appDrawerPopout; PopoutService.appDrawerPopout = appDrawerPopout;
@@ -539,8 +588,13 @@ Item {
active: false active: false
Component.onCompleted: {
PopoutService.clipboardHistoryPopoutLoader = clipboardHistoryPopoutLoader;
}
ClipboardHistoryPopout { ClipboardHistoryPopout {
id: clipboardHistoryPopout id: clipboardHistoryPopout
onPopoutClosed: PopoutService.unloadClipboardHistoryPopout()
Component.onCompleted: { Component.onCompleted: {
PopoutService.clipboardHistoryPopout = clipboardHistoryPopout; PopoutService.clipboardHistoryPopout = clipboardHistoryPopout;
@@ -715,8 +769,13 @@ Item {
active: false active: false
Component.onCompleted: {
PopoutService.systemUpdateLoader = systemUpdateLoader;
}
SystemUpdatePopout { SystemUpdatePopout {
id: systemUpdatePopout id: systemUpdatePopout
onPopoutClosed: PopoutService.unloadSystemUpdate()
Component.onCompleted: { Component.onCompleted: {
PopoutService.systemUpdatePopout = systemUpdatePopout; PopoutService.systemUpdatePopout = systemUpdatePopout;
@@ -739,9 +798,8 @@ Item {
content: Component { content: Component {
Notepad { Notepad {
onHideRequested: { slideout: notepadSlideout
notepadSlideout.hide(); onHideRequested: notepadSlideout.hide()
}
} }
} }

View File

@@ -21,11 +21,37 @@ Item {
required property var workspaceRenameModalLoader required property var workspaceRenameModalLoader
required property var windowRuleModalLoader required property var windowRuleModalLoader
function getFirstBar() { function getPreferredBar(refPropertyName) {
if (!root.dankBarRepeater || root.dankBarRepeater.count === 0) if (!root.dankBarRepeater || root.dankBarRepeater.count === 0)
return null; return null;
const firstLoader = root.dankBarRepeater.itemAt(0);
return firstLoader ? firstLoader.item : null; const focusedScreenName = BarWidgetService.getFocusedScreenName();
const loaders = Array.from({
length: root.dankBarRepeater.count
}, (_, i) => root.dankBarRepeater.itemAt(i));
let currentBar = null;
for (const loader of loaders) {
const instances = loader?.item?.barVariants?.instances || [];
for (const bar of instances) {
if (!bar)
continue;
const onFocusedScreen = focusedScreenName && bar.modelData?.name === focusedScreenName;
const hasRef = !refPropertyName || !!bar[refPropertyName];
if (hasRef) {
currentBar = bar;
if (onFocusedScreen)
break;
}
}
}
return currentBar;
} }
IpcHandler { IpcHandler {
@@ -97,9 +123,9 @@ Item {
IpcHandler { IpcHandler {
function open(): string { function open(): string {
const bar = root.getFirstBar(); const bar = root.getPreferredBar("controlCenterButtonRef");
if (bar) { if (bar) {
bar.triggerControlCenterOnFocusedScreen(); bar.triggerControlCenter();
return "CONTROL_CENTER_OPEN_SUCCESS"; return "CONTROL_CENTER_OPEN_SUCCESS";
} }
return "CONTROL_CENTER_OPEN_FAILED"; return "CONTROL_CENTER_OPEN_FAILED";
@@ -114,9 +140,14 @@ Item {
} }
function toggle(): string { function toggle(): string {
const bar = root.getFirstBar(); if (root.controlCenterLoader.item?.shouldBeVisible) {
root.controlCenterLoader.item.close();
return "CONTROL_CENTER_TOGGLE_SUCCESS";
}
const bar = root.getPreferredBar("controlCenterButtonRef");
if (bar) { if (bar) {
bar.triggerControlCenterOnFocusedScreen(); bar.triggerControlCenter();
return "CONTROL_CENTER_TOGGLE_SUCCESS"; return "CONTROL_CENTER_TOGGLE_SUCCESS";
} }
return "CONTROL_CENTER_TOGGLE_FAILED"; return "CONTROL_CENTER_TOGGLE_FAILED";
@@ -131,27 +162,37 @@ Item {
IpcHandler { IpcHandler {
function open(tab: string): string { function open(tab: string): string {
root.dankDashPopoutLoader.active = true; const bar = root.getPreferredBar("clockButtonRef");
if (root.dankDashPopoutLoader.item) { if (!bar)
switch (tab.toLowerCase()) { return "DASH_OPEN_FAILED";
case "media":
root.dankDashPopoutLoader.item.currentTabIndex = 1; const dash = root.dankDashPopoutLoader.item;
break; const onSameScreen = dash && dash.shouldBeVisible && dash.triggerScreen?.name === bar.screen?.name;
case "wallpaper":
root.dankDashPopoutLoader.item.currentTabIndex = 2; if (!onSameScreen) {
break; bar.triggerWallpaperBrowser();
case "weather":
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 3 : 0;
break;
default:
root.dankDashPopoutLoader.item.currentTabIndex = 0;
break;
}
root.dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen);
root.dankDashPopoutLoader.item.dashVisible = true;
return "DASH_OPEN_SUCCESS";
} }
return "DASH_OPEN_FAILED";
if (!root.dankDashPopoutLoader.item)
return "DASH_OPEN_FAILED";
switch (tab.toLowerCase()) {
case "media":
root.dankDashPopoutLoader.item.currentTabIndex = 1;
break;
case "wallpaper":
root.dankDashPopoutLoader.item.currentTabIndex = 2;
break;
case "weather":
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 3 : 0;
break;
default:
root.dankDashPopoutLoader.item.currentTabIndex = 0;
break;
}
root.dankDashPopoutLoader.item.dashVisible = true;
return "DASH_OPEN_SUCCESS";
} }
function close(): string { function close(): string {
@@ -163,8 +204,14 @@ Item {
} }
function toggle(tab: string): string { function toggle(tab: string): string {
const bar = root.getFirstBar(); if (root.dankDashPopoutLoader.item?.dashVisible) {
if (bar && bar.triggerWallpaperBrowserOnFocusedScreen()) { root.dankDashPopoutLoader.item.dashVisible = false;
return "DASH_TOGGLE_SUCCESS";
}
const bar = root.getPreferredBar("clockButtonRef");
if (bar) {
bar.triggerWallpaperBrowser();
if (root.dankDashPopoutLoader.item) { if (root.dankDashPopoutLoader.item) {
switch (tab.toLowerCase()) { switch (tab.toLowerCase()) {
case "media": case "media":
@@ -521,8 +568,9 @@ Item {
IpcHandler { IpcHandler {
function wallpaper(): string { function wallpaper(): string {
const bar = root.getFirstBar(); const bar = root.getPreferredBar("clockButtonRef");
if (bar && bar.triggerWallpaperBrowserOnFocusedScreen()) { if (bar) {
bar.triggerWallpaperBrowser();
return "SUCCESS: Toggled wallpaper browser"; return "SUCCESS: Toggled wallpaper browser";
} }
return "ERROR: Failed to toggle wallpaper browser"; return "ERROR: Failed to toggle wallpaper browser";

View File

@@ -135,6 +135,7 @@ Rectangle {
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
maximumLineCount: entryType === "long_text" ? 3 : 1 maximumLineCount: entryType === "long_text" ? 3 : 1
elide: Text.ElideRight elide: Text.ElideRight
textFormat: Text.PlainText
} }
} }
} }

View File

@@ -676,8 +676,6 @@ Item {
var apps = searchApps(searchQuery); var apps = searchApps(searchQuery);
for (var i = 0; i < apps.length; i++) { for (var i = 0; i < apps.length; i++) {
if (searchQuery)
apps[i]._preScored = 11000 - i;
allItems.push(apps[i]); allItems.push(apps[i]);
} }
@@ -685,14 +683,11 @@ Item {
if (searchQuery && searchQuery.length >= 2) { if (searchQuery && searchQuery.length >= 2) {
_pluginPhasePending = true; _pluginPhasePending = true;
_phase1Items = allItems.slice(); _phase1Items = allItems.slice();
_pluginPhaseForceFirst = shouldResetSelection;
pluginPhaseTimer.restart(); pluginPhaseTimer.restart();
if (allItems.length === 0) { isSearching = true;
_pluginPhaseForceFirst = shouldResetSelection; searchCompleted();
isSearching = true; return;
searchCompleted();
return;
}
_pluginPhaseForceFirst = false;
} else if (!searchQuery) { } else if (!searchQuery) {
var emptyTriggerOrdered = getEmptyTriggerPluginsOrdered(); var emptyTriggerOrdered = getEmptyTriggerPluginsOrdered();
for (var i = 0; i < emptyTriggerOrdered.length; i++) { for (var i = 0; i < emptyTriggerOrdered.length; i++) {
@@ -880,9 +875,7 @@ Item {
_applyHighlights(newSections, searchQuery); _applyHighlights(newSections, searchQuery);
flatModel = Scorer.flattenSections(newSections); flatModel = Scorer.flattenSections(newSections);
sections = newSections; sections = newSections;
if (selectedFlatIndex >= flatModel.length) { selectedFlatIndex = getFirstItemIndex();
selectedFlatIndex = getFirstItemIndex();
}
updateSelectedItem(); updateSelectedItem();
}); });
} }

View File

@@ -16,7 +16,6 @@ Item {
property var spotlightContent: launcherContentLoader.item property var spotlightContent: launcherContentLoader.item
property bool openedFromOverview: false property bool openedFromOverview: false
property bool isClosing: false property bool isClosing: false
property bool _windowEnabled: true
property bool _pendingInitialize: false property bool _pendingInitialize: false
property string _pendingQuery: "" property string _pendingQuery: ""
property string _pendingMode: "" property string _pendingMode: ""
@@ -102,7 +101,7 @@ Item {
spotlightContent.searchField.text = query; spotlightContent.searchField.text = query;
} }
if (spotlightContent.controller) { if (spotlightContent.controller) {
var targetMode = mode || "all"; var targetMode = mode || SessionData.launcherLastMode || "all";
spotlightContent.controller.searchMode = targetMode; spotlightContent.controller.searchMode = targetMode;
spotlightContent.controller.activePluginId = ""; spotlightContent.controller.activePluginId = "";
spotlightContent.controller.activePluginName = ""; spotlightContent.controller.activePluginName = "";
@@ -226,6 +225,15 @@ Item {
} }
} }
Connections {
target: spotlightContent?.controller ?? null
function onModeChanged(mode) {
if (spotlightContent.controller.autoSwitchedToFiles)
return;
SessionData.setLauncherLastMode(mode);
}
}
HyprlandFocusGrab { HyprlandFocusGrab {
id: focusGrab id: focusGrab
windows: [launcherWindow] windows: [launcherWindow]
@@ -253,38 +261,26 @@ Item {
if (Quickshell.screens.length === 0) if (Quickshell.screens.length === 0)
return; return;
const screen = launcherWindow.screen; const screenName = launcherWindow.screen?.name;
const screenName = screen?.name; if (screenName) {
let needsReset = !screen || !screenName;
if (!needsReset) {
needsReset = true;
for (let i = 0; i < Quickshell.screens.length; i++) { for (let i = 0; i < Quickshell.screens.length; i++) {
if (Quickshell.screens[i].name === screenName) { if (Quickshell.screens[i].name === screenName)
needsReset = false; return;
break;
}
} }
} }
if (!needsReset) if (spotlightOpen)
return; hide();
const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0]; const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0];
if (!newScreen) if (newScreen)
return; launcherWindow.screen = newScreen;
root._windowEnabled = false;
launcherWindow.screen = newScreen;
Qt.callLater(() => {
root._windowEnabled = true;
});
} }
} }
PanelWindow { PanelWindow {
id: launcherWindow id: launcherWindow
visible: root._windowEnabled && (!root.unloadContentOnClose || spotlightOpen || isClosing) visible: spotlightOpen || isClosing
color: "transparent" color: "transparent"
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore

View File

@@ -643,7 +643,7 @@ FocusScope {
Image { Image {
width: 40 width: 40
height: 40 height: 40
source: editingApp?.icon ? "image://icon/" + editingApp.icon : "image://icon/application-x-executable" source: Paths.resolveIconUrl(editingApp?.icon || "application-x-executable")
sourceSize.width: 40 sourceSize.width: 40
sourceSize.height: 40 sourceSize.height: 40
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit

View File

@@ -460,7 +460,7 @@ Item {
switch (mode) { switch (mode) {
case "files": case "files":
if (!DSearchService.dsearchAvailable) if (!DSearchService.dsearchAvailable)
return I18n.tr("File search requires dsearch\nInstall from github.com/morelazers/dsearch"); return I18n.tr("File search requires dsearch\nInstall from github.com/AvengeMedia/danksearch");
if (!hasQuery) if (!hasQuery)
return I18n.tr("Type to search files"); return I18n.tr("Type to search files");
if (root.controller.searchQuery.length < 2) if (root.controller.searchQuery.length < 2)

View File

@@ -3,7 +3,7 @@
const Weights = { const Weights = {
exactMatch: 10000, exactMatch: 10000,
prefixMatch: 5000, prefixMatch: 5000,
wordBoundary: 1000, wordBoundary: 3000,
substring: 500, substring: 500,
fuzzy: 100, fuzzy: 100,
frecency: 2000, frecency: 2000,
@@ -99,8 +99,8 @@ function getTimeBucketWeight(daysSinceUsed) {
function calculateTextScore(name, query) { function calculateTextScore(name, query) {
if (name === query) return Weights.exactMatch if (name === query) return Weights.exactMatch
if (name.startsWith(query)) return Weights.prefixMatch if (name.startsWith(query)) return Weights.prefixMatch
if (name.includes(query)) return Weights.substring
if (hasWordBoundaryMatch(name, query)) return Weights.wordBoundary if (hasWordBoundaryMatch(name, query)) return Weights.wordBoundary
if (name.includes(query)) return Weights.substring
if (query.length >= 3) { if (query.length >= 3) {
var fs = fuzzyScore(name, query) var fs = fuzzyScore(name, query)
@@ -140,7 +140,7 @@ function score(item, query, frecencyData) {
if (textScore === 0) return 0 if (textScore === 0) return 0
var usageBonus = frecencyData ? Math.min(frecencyData.usageCount * 10, Weights.frecency) : 0 var usageBonus = frecencyData ? Math.min(frecencyData.usageCount * 50, Weights.frecency) : 0
return textScore + usageBonus + typeBonus return textScore + usageBonus + typeBonus
} }

View File

@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -11,8 +12,45 @@ FloatingWindow {
property string passwordInput: "" property string passwordInput: ""
property var currentFlow: PolkitService.agent?.flow property var currentFlow: PolkitService.agent?.flow
property bool isLoading: false property bool isLoading: false
property bool awaitingFprintForPassword: false
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2 readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
property string polkitEtcPamText: ""
property string polkitLibPamText: ""
property string systemAuthPamText: ""
property string commonAuthPamText: ""
property string passwordAuthPamText: ""
readonly property bool polkitPamHasFprint: {
const polkitText = polkitEtcPamText !== "" ? polkitEtcPamText : polkitLibPamText;
if (!polkitText)
return false;
return pamModuleEnabled(polkitText, "pam_fprintd") || (polkitText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_fprintd")) || (polkitText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_fprintd")) || (polkitText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_fprintd"));
}
function stripPamComment(line) {
if (!line)
return "";
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#"))
return "";
const hashIdx = trimmed.indexOf("#");
if (hashIdx >= 0)
return trimmed.substring(0, hashIdx).trim();
return trimmed;
}
function pamModuleEnabled(pamText, moduleName) {
if (!pamText || !moduleName)
return false;
const lines = pamText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (line && line.includes(moduleName))
return true;
}
return false;
}
function focusPasswordField() { function focusPasswordField() {
passwordField.forceActiveFocus(); passwordField.forceActiveFocus();
} }
@@ -20,6 +58,7 @@ FloatingWindow {
function show() { function show() {
passwordInput = ""; passwordInput = "";
isLoading = false; isLoading = false;
awaitingFprintForPassword = false;
visible = true; visible = true;
Qt.callLater(focusPasswordField); Qt.callLater(focusPasswordField);
} }
@@ -28,17 +67,27 @@ FloatingWindow {
visible = false; visible = false;
} }
function _commitSubmit() {
isLoading = true;
awaitingFprintForPassword = false;
currentFlow.submit(passwordInput);
passwordInput = "";
}
function submitAuth() { function submitAuth() {
if (!currentFlow || isLoading) if (!currentFlow || isLoading)
return; return;
isLoading = true; if (!currentFlow.isResponseRequired) {
currentFlow.submit(passwordInput); awaitingFprintForPassword = true;
passwordInput = ""; return;
}
_commitSubmit();
} }
function cancelAuth() { function cancelAuth() {
if (isLoading) if (isLoading)
return; return;
awaitingFprintForPassword = false;
if (currentFlow) { if (currentFlow) {
currentFlow.cancelAuthenticationRequest(); currentFlow.cancelAuthenticationRequest();
return; return;
@@ -60,6 +109,7 @@ FloatingWindow {
} }
passwordInput = ""; passwordInput = "";
isLoading = false; isLoading = false;
awaitingFprintForPassword = false;
} }
Connections { Connections {
@@ -83,6 +133,11 @@ FloatingWindow {
function onIsResponseRequiredChanged() { function onIsResponseRequiredChanged() {
if (!currentFlow.isResponseRequired) if (!currentFlow.isResponseRequired)
return; return;
if (awaitingFprintForPassword && passwordInput !== "") {
_commitSubmit();
return;
}
awaitingFprintForPassword = false;
isLoading = false; isLoading = false;
passwordInput = ""; passwordInput = "";
passwordField.forceActiveFocus(); passwordField.forceActiveFocus();
@@ -101,6 +156,41 @@ FloatingWindow {
} }
} }
FileView {
path: "/etc/pam.d/polkit-1"
printErrors: false
onLoaded: root.polkitEtcPamText = text()
onLoadFailed: root.polkitEtcPamText = ""
}
FileView {
path: "/usr/lib/pam.d/polkit-1"
printErrors: false
onLoaded: root.polkitLibPamText = text()
onLoadFailed: root.polkitLibPamText = ""
}
FileView {
path: "/etc/pam.d/system-auth"
printErrors: false
onLoaded: root.systemAuthPamText = text()
onLoadFailed: root.systemAuthPamText = ""
}
FileView {
path: "/etc/pam.d/common-auth"
printErrors: false
onLoaded: root.commonAuthPamText = text()
onLoadFailed: root.commonAuthPamText = ""
}
FileView {
path: "/etc/pam.d/password-auth"
printErrors: false
onLoaded: root.passwordAuthPamText = text()
onLoadFailed: root.passwordAuthPamText = ""
}
FocusScope { FocusScope {
id: contentFocusScope id: contentFocusScope
@@ -205,36 +295,30 @@ FloatingWindow {
visible: text !== "" visible: text !== ""
} }
Rectangle { DankTextField {
id: passwordField
width: parent.width width: parent.width
height: inputFieldHeight height: inputFieldHeight
radius: Theme.cornerRadius backgroundColor: Theme.surfaceHover
color: Theme.surfaceHover normalBorderColor: Theme.outlineStrong
border.color: passwordField.activeFocus ? Theme.primary : Theme.outlineStrong focusedBorderColor: Theme.primary
border.width: passwordField.activeFocus ? 2 : 1 borderWidth: 1
focusedBorderWidth: 2
leftIconName: polkitPamHasFprint ? "fingerprint" : ""
leftIconSize: 20
leftIconColor: Theme.primary
leftIconFocusedColor: Theme.primary
opacity: isLoading ? 0.5 : 1 opacity: isLoading ? 0.5 : 1
font.pixelSize: Theme.fontSizeMedium
MouseArea { textColor: Theme.surfaceText
anchors.fill: parent text: passwordInput
enabled: !isLoading showPasswordToggle: !(currentFlow?.responseVisible ?? false)
onClicked: passwordField.forceActiveFocus() echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
} placeholderText: ""
enabled: !isLoading
DankTextField { onTextEdited: passwordInput = text
id: passwordField onAccepted: submitAuth()
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: passwordInput
showPasswordToggle: !(currentFlow?.responseVisible ?? false)
echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
placeholderText: ""
backgroundColor: "transparent"
enabled: !isLoading
onTextEdited: passwordInput = text
onAccepted: submitAuth()
}
} }
StyledText { StyledText {

View File

@@ -631,7 +631,7 @@ FloatingWindow {
anchors.fill: parent anchors.fill: parent
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
textColor: Theme.surfaceText textColor: Theme.surfaceText
placeholderText: "fixed 800" placeholderText: "800"
backgroundColor: "transparent" backgroundColor: "transparent"
enabled: root.visible enabled: root.visible
} }
@@ -658,7 +658,7 @@ FloatingWindow {
anchors.fill: parent anchors.fill: parent
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
textColor: Theme.surfaceText textColor: Theme.surfaceText
placeholderText: "fixed 600" placeholderText: "600"
backgroundColor: "transparent" backgroundColor: "transparent"
enabled: root.visible enabled: root.visible
} }

View File

@@ -8,6 +8,9 @@ DankPopout {
layerNamespace: "dms:app-launcher" layerNamespace: "dms:app-launcher"
readonly property real screenWidth: screen?.width ?? 1920
readonly property real screenHeight: screen?.height ?? 1080
property string _pendingMode: "" property string _pendingMode: ""
property string _pendingQuery: "" property string _pendingQuery: ""
@@ -41,8 +44,35 @@ DankPopout {
openWithQuery(query); openWithQuery(query);
} }
popupWidth: 560 readonly property int _baseWidth: {
popupHeight: 640 switch (SettingsData.dankLauncherV2Size) {
case "micro":
return 500;
case "medium":
return 720;
case "large":
return 860;
default:
return 620;
}
}
readonly property int _baseHeight: {
switch (SettingsData.dankLauncherV2Size) {
case "micro":
return 480;
case "medium":
return 720;
case "large":
return 860;
default:
return 600;
}
}
popupWidth: Math.min(_baseWidth, screenWidth - 100)
popupHeight: Math.min(_baseHeight, screenHeight - 100)
triggerWidth: 40 triggerWidth: 40
positioning: "" positioning: ""
contentHandlesKeys: contentLoader.item?.launcherContent?.editMode ?? false contentHandlesKeys: contentLoader.item?.launcherContent?.editMode ?? false
@@ -61,7 +91,7 @@ DankPopout {
return; return;
const query = _pendingQuery; const query = _pendingQuery;
const mode = _pendingMode || "apps"; const mode = _pendingMode || SessionData.appDrawerLastMode || "apps";
_pendingMode = ""; _pendingMode = "";
_pendingQuery = ""; _pendingQuery = "";
@@ -83,6 +113,15 @@ DankPopout {
lc.actionPanel?.hide(); lc.actionPanel?.hide();
} }
Connections {
target: contentLoader.item?.launcherContent?.controller ?? null
function onModeChanged(mode) {
if (contentLoader.item.launcherContent.controller.autoSwitchedToFiles)
return;
SessionData.setAppDrawerLastMode(mode);
}
}
content: Component { content: Component {
Rectangle { Rectangle {
id: contentContainer id: contentContainer

View File

@@ -85,12 +85,8 @@ Variants {
} }
Component.onCompleted: { Component.onCompleted: {
if (!source) { if (typeof blurWallpaperWindow.updatesEnabled !== "undefined")
isInitialized = true; blurWallpaperWindow.updatesEnabled = Qt.binding(() => !root.source || root.effectActive || root._renderSettling || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading);
return;
}
const formattedSource = source.startsWith("file://") ? source : encodeFileUrl(source);
setWallpaperImmediate(formattedSource);
isInitialized = true; isInitialized = true;
} }
@@ -98,8 +94,25 @@ Variants {
property real transitionProgress: 0 property real transitionProgress: 0
readonly property bool transitioning: transitionAnimation.running readonly property bool transitioning: transitionAnimation.running
property bool effectActive: false property bool effectActive: false
property bool _renderSettling: true
property bool useNextForEffect: false property bool useNextForEffect: false
Connections {
target: currentWallpaper
function onStatusChanged() {
if (currentWallpaper.status !== Image.Ready && currentWallpaper.status !== Image.Error)
return;
root._renderSettling = true;
renderSettleTimer.restart();
}
}
Timer {
id: renderSettleTimer
interval: 1000
onTriggered: root._renderSettling = false
}
onSourceChanged: { onSourceChanged: {
if (!source || source.startsWith("#")) { if (!source || source.startsWith("#")) {
setWallpaperImmediate(""); setWallpaperImmediate("");
@@ -124,6 +137,8 @@ Variants {
transitionAnimation.stop(); transitionAnimation.stop();
root.transitionProgress = 0.0; root.transitionProgress = 0.0;
root.effectActive = false; root.effectActive = false;
root._renderSettling = true;
renderSettleTimer.restart();
currentWallpaper.source = newSource; currentWallpaper.source = newSource;
nextWallpaper.source = ""; nextWallpaper.source = "";
} }
@@ -152,6 +167,8 @@ Variants {
transitionAnimation.stop(); transitionAnimation.stop();
root.transitionProgress = 0; root.transitionProgress = 0;
root.effectActive = false; root.effectActive = false;
root._renderSettling = true;
renderSettleTimer.restart();
currentWallpaper.source = nextWallpaper.source; currentWallpaper.source = nextWallpaper.source;
nextWallpaper.source = ""; nextWallpaper.source = "";
} }
@@ -160,6 +177,9 @@ Variants {
return; return;
} }
root._renderSettling = true;
renderSettleTimer.restart();
nextWallpaper.source = newPath; nextWallpaper.source = newPath;
if (nextWallpaper.status === Image.Ready) if (nextWallpaper.status === Image.Ready)
@@ -186,6 +206,7 @@ Variants {
visible: false visible: false
opacity: 1 opacity: 1
asynchronous: true asynchronous: true
retainWhileLoading: true
smooth: true smooth: true
cache: true cache: true
sourceSize: Qt.size(root.textureWidth, root.textureHeight) sourceSize: Qt.size(root.textureWidth, root.textureHeight)
@@ -198,6 +219,7 @@ Variants {
visible: false visible: false
opacity: 0 opacity: 0
asynchronous: true asynchronous: true
retainWhileLoading: true
smooth: true smooth: true
cache: true cache: true
sourceSize: Qt.size(root.textureWidth, root.textureHeight) sourceSize: Qt.size(root.textureWidth, root.textureHeight)
@@ -280,6 +302,8 @@ Variants {
root.useNextForEffect = false; root.useNextForEffect = false;
nextWallpaper.source = ""; nextWallpaper.source = "";
root.transitionProgress = 0.0; root.transitionProgress = 0.0;
root._renderSettling = true;
renderSettleTimer.restart();
root.effectActive = false; root.effectActive = false;
} }
} }

View File

@@ -70,6 +70,16 @@ DankPopout {
backgroundInteractive: !anyModalOpen backgroundInteractive: !anyModalOpen
onCredentialsPromptOpenChanged: {
if (credentialsPromptOpen && shouldBeVisible)
close();
}
onPolkitModalOpenChanged: {
if (polkitModalOpen && shouldBeVisible)
close();
}
customKeyboardFocus: { customKeyboardFocus: {
if (!shouldBeVisible) if (!shouldBeVisible)
return WlrKeyboardFocus.None; return WlrKeyboardFocus.None;

View File

@@ -234,16 +234,7 @@ Rectangle {
spacing: Theme.spacingS spacing: Theme.spacingS
DankIcon { DankIcon {
name: { name: AudioService.sinkIcon(modelData)
if (modelData.name.includes("bluez"))
return "headset";
else if (modelData.name.includes("hdmi"))
return "tv";
else if (modelData.name.includes("usb"))
return "headset";
else
return "speaker";
}
size: Theme.iconSize - 4 size: Theme.iconSize - 4
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter

View File

@@ -669,6 +669,30 @@ Rectangle {
} }
} }
MenuItem {
text: bluetoothContextMenu.currentDevice?.trusted ? I18n.tr("Untrust") : I18n.tr("Trust")
height: 32
contentItem: StyledText {
text: parent.text
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
leftPadding: Theme.spacingS
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius / 2
}
onTriggered: {
if (!bluetoothContextMenu.hasDevice)
return;
bluetoothContextMenu.currentDevice.trusted = !bluetoothContextMenu.currentDevice.trusted;
}
}
MenuItem { MenuItem {
text: I18n.tr("Forget Device") text: I18n.tr("Forget Device")
height: 32 height: 32

View File

@@ -21,73 +21,82 @@ Item {
property alias centerWidgetsModel: centerWidgetsModel property alias centerWidgetsModel: centerWidgetsModel
property alias rightWidgetsModel: rightWidgetsModel property alias rightWidgetsModel: rightWidgetsModel
property string _leftWidgetsJson: {
root.barConfig;
const leftWidgets = root.barConfig?.leftWidgets || [];
const mapped = leftWidgets.map((w, index) => {
if (typeof w === "string") {
return {
widgetId: w,
id: w + "_" + index,
enabled: true
};
} else {
const obj = Object.assign({}, w);
obj.widgetId = w.id || w.widgetId;
obj.id = (w.id || w.widgetId) + "_" + index;
obj.enabled = w.enabled !== false;
return obj;
}
});
return JSON.stringify(mapped);
}
property string _centerWidgetsJson: {
root.barConfig;
const centerWidgets = root.barConfig?.centerWidgets || [];
const mapped = centerWidgets.map((w, index) => {
if (typeof w === "string") {
return {
widgetId: w,
id: w + "_" + index,
enabled: true
};
} else {
const obj = Object.assign({}, w);
obj.widgetId = w.id || w.widgetId;
obj.id = (w.id || w.widgetId) + "_" + index;
obj.enabled = w.enabled !== false;
return obj;
}
});
return JSON.stringify(mapped);
}
property string _rightWidgetsJson: {
root.barConfig;
const rightWidgets = root.barConfig?.rightWidgets || [];
const mapped = rightWidgets.map((w, index) => {
if (typeof w === "string") {
return {
widgetId: w,
id: w + "_" + index,
enabled: true
};
} else {
const obj = Object.assign({}, w);
obj.widgetId = w.id || w.widgetId;
obj.id = (w.id || w.widgetId) + "_" + index;
obj.enabled = w.enabled !== false;
return obj;
}
});
return JSON.stringify(mapped);
}
ScriptModel { ScriptModel {
id: leftWidgetsModel id: leftWidgetsModel
values: { values: JSON.parse(root._leftWidgetsJson)
root.barConfig;
const leftWidgets = root.barConfig?.leftWidgets || [];
return leftWidgets.map((w, index) => {
if (typeof w === "string") {
return {
widgetId: w,
id: w + "_" + index,
enabled: true
};
} else {
const obj = Object.assign({}, w);
obj.widgetId = w.id || w.widgetId;
obj.id = (w.id || w.widgetId) + "_" + index;
obj.enabled = w.enabled !== false;
return obj;
}
});
}
} }
ScriptModel { ScriptModel {
id: centerWidgetsModel id: centerWidgetsModel
values: { values: JSON.parse(root._centerWidgetsJson)
root.barConfig;
const centerWidgets = root.barConfig?.centerWidgets || [];
return centerWidgets.map((w, index) => {
if (typeof w === "string") {
return {
widgetId: w,
id: w + "_" + index,
enabled: true
};
} else {
const obj = Object.assign({}, w);
obj.widgetId = w.id || w.widgetId;
obj.id = (w.id || w.widgetId) + "_" + index;
obj.enabled = w.enabled !== false;
return obj;
}
});
}
} }
ScriptModel { ScriptModel {
id: rightWidgetsModel id: rightWidgetsModel
values: { values: JSON.parse(root._rightWidgetsJson)
root.barConfig;
const rightWidgets = root.barConfig?.rightWidgets || [];
return rightWidgets.map((w, index) => {
if (typeof w === "string") {
return {
widgetId: w,
id: w + "_" + index,
enabled: true
};
} else {
const obj = Object.assign({}, w);
obj.widgetId = w.id || w.widgetId;
obj.id = (w.id || w.widgetId) + "_" + index;
obj.enabled = w.enabled !== false;
return obj;
}
});
}
} }
function triggerControlCenterOnFocusedScreen() { function triggerControlCenterOnFocusedScreen() {

View File

@@ -561,10 +561,7 @@ Item {
axis: barWindow.axis axis: barWindow.axis
section: topBarContent.getWidgetSection(parent) section: topBarContent.getWidgetSection(parent)
parentScreen: barWindow.screen parentScreen: barWindow.screen
popoutTarget: { popoutTarget: clipboardHistoryPopoutLoader.item ?? null
clipboardHistoryPopoutLoader.active = true;
return clipboardHistoryPopoutLoader.item;
}
function openClipboardPopout(initialTab) { function openClipboardPopout(initialTab) {
clipboardHistoryPopoutLoader.active = true; clipboardHistoryPopoutLoader.active = true;
@@ -759,10 +756,7 @@ Item {
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
section: topBarContent.getWidgetSection(parent) || "center" section: topBarContent.getWidgetSection(parent) || "center"
popoutTarget: { popoutTarget: dankDashPopoutLoader.item ?? null
dankDashPopoutLoader.active = true;
return dankDashPopoutLoader.item;
}
parentScreen: barWindow.screen parentScreen: barWindow.screen
Component.onCompleted: { Component.onCompleted: {
@@ -826,10 +820,7 @@ Item {
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
section: topBarContent.getWidgetSection(parent) || "center" section: topBarContent.getWidgetSection(parent) || "center"
popoutTarget: { popoutTarget: dankDashPopoutLoader.item ?? null
dankDashPopoutLoader.active = true;
return dankDashPopoutLoader.item;
}
parentScreen: barWindow.screen parentScreen: barWindow.screen
onClicked: { onClicked: {
dankDashPopoutLoader.active = true; dankDashPopoutLoader.active = true;
@@ -881,10 +872,7 @@ Item {
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
section: topBarContent.getWidgetSection(parent) || "center" section: topBarContent.getWidgetSection(parent) || "center"
popoutTarget: { popoutTarget: dankDashPopoutLoader.item ?? null
dankDashPopoutLoader.active = true;
return dankDashPopoutLoader.item;
}
parentScreen: barWindow.screen parentScreen: barWindow.screen
onClicked: { onClicked: {
dankDashPopoutLoader.active = true; dankDashPopoutLoader.active = true;
@@ -968,10 +956,7 @@ Item {
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
axis: barWindow.axis axis: barWindow.axis
section: topBarContent.getWidgetSection(parent) || "right" section: topBarContent.getWidgetSection(parent) || "right"
popoutTarget: { popoutTarget: processListPopoutLoader.item ?? null
processListPopoutLoader.active = true;
return processListPopoutLoader.item;
}
parentScreen: barWindow.screen parentScreen: barWindow.screen
widgetData: parent.widgetData widgetData: parent.widgetData
onCpuClicked: { onCpuClicked: {
@@ -1004,10 +989,7 @@ Item {
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
axis: barWindow.axis axis: barWindow.axis
section: topBarContent.getWidgetSection(parent) || "right" section: topBarContent.getWidgetSection(parent) || "right"
popoutTarget: { popoutTarget: processListPopoutLoader.item ?? null
processListPopoutLoader.active = true;
return processListPopoutLoader.item;
}
parentScreen: barWindow.screen parentScreen: barWindow.screen
widgetData: parent.widgetData widgetData: parent.widgetData
onRamClicked: { onRamClicked: {
@@ -1054,10 +1036,7 @@ Item {
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
axis: barWindow.axis axis: barWindow.axis
section: topBarContent.getWidgetSection(parent) || "right" section: topBarContent.getWidgetSection(parent) || "right"
popoutTarget: { popoutTarget: processListPopoutLoader.item ?? null
processListPopoutLoader.active = true;
return processListPopoutLoader.item;
}
parentScreen: barWindow.screen parentScreen: barWindow.screen
widgetData: parent.widgetData widgetData: parent.widgetData
onCpuTempClicked: { onCpuTempClicked: {
@@ -1090,10 +1069,7 @@ Item {
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
axis: barWindow.axis axis: barWindow.axis
section: topBarContent.getWidgetSection(parent) || "right" section: topBarContent.getWidgetSection(parent) || "right"
popoutTarget: { popoutTarget: processListPopoutLoader.item ?? null
processListPopoutLoader.active = true;
return processListPopoutLoader.item;
}
parentScreen: barWindow.screen parentScreen: barWindow.screen
widgetData: parent.widgetData widgetData: parent.widgetData
onGpuTempClicked: { onGpuTempClicked: {
@@ -1134,10 +1110,7 @@ Item {
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
axis: barWindow.axis axis: barWindow.axis
section: topBarContent.getWidgetSection(parent) || "right" section: topBarContent.getWidgetSection(parent) || "right"
popoutTarget: { popoutTarget: notificationCenterLoader.item ?? null
notificationCenterLoader.active = true;
return notificationCenterLoader.item;
}
parentScreen: barWindow.screen parentScreen: barWindow.screen
onClicked: { onClicked: {
notificationCenterLoader.active = true; notificationCenterLoader.active = true;
@@ -1172,10 +1145,7 @@ Item {
section: topBarContent.getWidgetSection(parent) || "right" section: topBarContent.getWidgetSection(parent) || "right"
barSpacing: barConfig?.spacing ?? 4 barSpacing: barConfig?.spacing ?? 4
barConfig: topBarContent.barConfig barConfig: topBarContent.barConfig
popoutTarget: { popoutTarget: batteryPopoutLoader.item ?? null
batteryPopoutLoader.active = true;
return batteryPopoutLoader.item;
}
parentScreen: barWindow.screen parentScreen: barWindow.screen
onToggleBatteryPopup: { onToggleBatteryPopup: {
batteryPopoutLoader.active = true; batteryPopoutLoader.active = true;
@@ -1208,10 +1178,7 @@ Item {
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
axis: barWindow.axis axis: barWindow.axis
section: topBarContent.getWidgetSection(parent) || "center" section: topBarContent.getWidgetSection(parent) || "center"
popoutTarget: { popoutTarget: layoutPopoutLoader.item ?? null
layoutPopoutLoader.active = true;
return layoutPopoutLoader.item;
}
parentScreen: barWindow.screen parentScreen: barWindow.screen
onToggleLayoutPopup: { onToggleLayoutPopup: {
layoutPopoutLoader.active = true; layoutPopoutLoader.active = true;
@@ -1244,10 +1211,7 @@ Item {
barSpacing: barConfig?.spacing ?? 4 barSpacing: barConfig?.spacing ?? 4
barConfig: topBarContent.barConfig barConfig: topBarContent.barConfig
isAutoHideBar: topBarContent.barConfig?.autoHide ?? false isAutoHideBar: topBarContent.barConfig?.autoHide ?? false
popoutTarget: { popoutTarget: vpnPopoutLoader.item ?? null
vpnPopoutLoader.active = true;
return vpnPopoutLoader.item;
}
parentScreen: barWindow.screen parentScreen: barWindow.screen
onToggleVpnPopup: { onToggleVpnPopup: {
vpnPopoutLoader.active = true; vpnPopoutLoader.active = true;
@@ -1281,10 +1245,7 @@ Item {
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
axis: barWindow.axis axis: barWindow.axis
section: topBarContent.getWidgetSection(parent) || "right" section: topBarContent.getWidgetSection(parent) || "right"
popoutTarget: { popoutTarget: controlCenterLoader.item ?? null
controlCenterLoader.active = true;
return controlCenterLoader.item;
}
parentScreen: barWindow.screen parentScreen: barWindow.screen
screenName: barWindow.screen?.name || "" screenName: barWindow.screen?.name || ""
screenModel: barWindow.screen?.model || "" screenModel: barWindow.screen?.model || ""
@@ -1434,10 +1395,7 @@ Item {
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
axis: barWindow.axis axis: barWindow.axis
section: topBarContent.getWidgetSection(parent) || "right" section: topBarContent.getWidgetSection(parent) || "right"
popoutTarget: { popoutTarget: systemUpdateLoader.item ?? null
systemUpdateLoader.active = true;
return systemUpdateLoader.item;
}
parentScreen: barWindow.screen parentScreen: barWindow.screen
onClicked: { onClicked: {
systemUpdateLoader.active = true; systemUpdateLoader.active = true;

View File

@@ -48,10 +48,11 @@ PanelWindow {
return; return;
} }
let section = "center";
if (clockButtonRef && clockButtonRef.visualContent && dankDashPopoutLoader.item.setTriggerPosition) { if (clockButtonRef && clockButtonRef.visualContent && dankDashPopoutLoader.item.setTriggerPosition) {
// Calculate barPosition from axis.edge // Calculate barPosition from axis.edge
const barPosition = axis?.edge === "left" ? 2 : (axis?.edge === "right" ? 3 : (axis?.edge === "top" ? 0 : 1)); const barPosition = axis?.edge === "left" ? 2 : (axis?.edge === "right" ? 3 : (axis?.edge === "top" ? 0 : 1));
const section = clockButtonRef.section || "center"; section = clockButtonRef.section || "center";
let triggerPos, triggerWidth; let triggerPos, triggerWidth;
if (section === "center") { if (section === "center") {
@@ -130,38 +131,55 @@ PanelWindow {
readonly property real _wingR: Math.max(0, wingtipsRadius) readonly property real _wingR: Math.max(0, wingtipsRadius)
readonly property color _surfaceContainer: Theme.surfaceContainer readonly property color _surfaceContainer: Theme.surfaceContainer
readonly property string _barId: barConfig?.id ?? "default" readonly property string _barId: barConfig?.id ?? "default"
readonly property var _liveBarConfig: SettingsData.barConfigs.find(c => c.id === _barId) || barConfig property real _backgroundAlpha: barConfig?.transparency ?? 1.0
readonly property real _backgroundAlpha: _liveBarConfig?.transparency ?? 1.0
readonly property color _bgColor: Theme.withAlpha(_surfaceContainer, _backgroundAlpha) readonly property color _bgColor: Theme.withAlpha(_surfaceContainer, _backgroundAlpha)
function _updateBackgroundAlpha() {
const live = SettingsData.barConfigs.find(c => c.id === _barId);
_backgroundAlpha = (live ?? barConfig)?.transparency ?? 1.0;
}
readonly property real _dpr: CompositorService.getScreenScale(barWindow.screen) readonly property real _dpr: CompositorService.getScreenScale(barWindow.screen)
property string screenName: modelData.name property string screenName: modelData.name
readonly property bool hasMaximizedToplevel: { property bool hasMaximizedToplevel: false
if (!(barConfig?.maximizeDetection ?? true)) property bool shouldHideForWindows: false
return false;
if (!CompositorService.isHyprland && !CompositorService.isNiri) function _updateHasMaximizedToplevel() {
return false; if (!(barConfig?.maximizeDetection ?? true)) {
hasMaximizedToplevel = false;
return;
}
if (!CompositorService.isHyprland && !CompositorService.isNiri) {
hasMaximizedToplevel = false;
return;
}
const filtered = CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName); const filtered = CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
for (let i = 0; i < filtered.length; i++) { for (let i = 0; i < filtered.length; i++) {
if (filtered[i]?.maximized) if (filtered[i]?.maximized) {
return true; hasMaximizedToplevel = true;
return;
}
} }
return false; hasMaximizedToplevel = false;
} }
readonly property bool shouldHideForWindows: { function _updateShouldHideForWindows() {
if (!(barConfig?.showOnWindowsOpen ?? false)) if (!(barConfig?.showOnWindowsOpen ?? false)) {
return false; shouldHideForWindows = false;
if (!(barConfig?.autoHide ?? false)) return;
return false; }
if (!CompositorService.isNiri && !CompositorService.isHyprland) if (!(barConfig?.autoHide ?? false)) {
return false; shouldHideForWindows = false;
return;
}
if (!CompositorService.isNiri && !CompositorService.isHyprland) {
shouldHideForWindows = false;
return;
}
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
NiriService.windows;
let currentWorkspaceId = null; let currentWorkspaceId = null;
for (let i = 0; i < NiriService.allWorkspaces.length; i++) { for (let i = 0; i < NiriService.allWorkspaces.length; i++) {
const ws = NiriService.allWorkspaces[i]; const ws = NiriService.allWorkspaces[i];
@@ -171,8 +189,10 @@ PanelWindow {
} }
} }
if (currentWorkspaceId === null) if (currentWorkspaceId === null) {
return false; shouldHideForWindows = false;
return;
}
let hasTiled = false; let hasTiled = false;
let hasFloatingTouchingBar = false; let hasFloatingTouchingBar = false;
@@ -216,14 +236,12 @@ PanelWindow {
} }
} }
if (hasTiled) shouldHideForWindows = hasTiled || hasFloatingTouchingBar;
return true; return;
return hasFloatingTouchingBar;
} }
const filtered = CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName); const filtered = CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
return filtered.length > 0; shouldHideForWindows = filtered.length > 0;
} }
property real effectiveSpacing: hasMaximizedToplevel ? 0 : (barConfig?.spacing ?? 4) property real effectiveSpacing: hasMaximizedToplevel ? 0 : (barConfig?.spacing ?? 4)
@@ -257,7 +275,7 @@ PanelWindow {
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
if (!onThisScreen) if (!onThisScreen)
return false; return false;
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name) if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
return false; return false;
return true; return true;
}); });
@@ -280,7 +298,7 @@ PanelWindow {
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
if (!onThisScreen) if (!onThisScreen)
return false; return false;
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name) if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
return false; return false;
return true; return true;
}); });
@@ -304,7 +322,7 @@ PanelWindow {
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
if (!onThisScreen) if (!onThisScreen)
return false; return false;
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name) if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
return false; return false;
return true; return true;
}); });
@@ -328,7 +346,7 @@ PanelWindow {
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
if (!onThisScreen) if (!onThisScreen)
return false; return false;
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name) if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
return false; return false;
return true; return true;
}); });
@@ -355,6 +373,9 @@ PanelWindow {
} }
updateGpuTempConfig(); updateGpuTempConfig();
_updateBackgroundAlpha();
_updateHasMaximizedToplevel();
_updateShouldHideForWindows();
inhibitorInitTimer.start(); inhibitorInitTimer.start();
} }
@@ -431,11 +452,37 @@ PanelWindow {
Connections { Connections {
function onBarConfigChanged() { function onBarConfigChanged() {
barWindow.updateGpuTempConfig(); barWindow.updateGpuTempConfig();
barWindow._updateBackgroundAlpha();
barWindow._updateHasMaximizedToplevel();
barWindow._updateShouldHideForWindows();
} }
target: rootWindow target: rootWindow
} }
Connections {
target: SettingsData
function onBarConfigsChanged() {
barWindow._updateBackgroundAlpha();
}
}
Connections {
target: CompositorService
function onToplevelsChanged() {
barWindow._updateHasMaximizedToplevel();
barWindow._updateShouldHideForWindows();
}
}
Connections {
target: NiriService
function onAllWorkspacesChanged() {
barWindow._updateHasMaximizedToplevel();
barWindow._updateShouldHideForWindows();
}
}
Connections { Connections {
function onNvidiaGpuTempEnabledChanged() { function onNvidiaGpuTempEnabledChanged() {
barWindow.updateGpuTempConfig(); barWindow.updateGpuTempConfig();
@@ -505,8 +552,9 @@ PanelWindow {
readonly property var _leftSection: topBarContent ? (barWindow.isVertical ? topBarContent.vLeftSection : topBarContent.hLeftSection) : null readonly property var _leftSection: topBarContent ? (barWindow.isVertical ? topBarContent.vLeftSection : topBarContent.hLeftSection) : null
readonly property var _centerSection: topBarContent ? (barWindow.isVertical ? topBarContent.vCenterSection : topBarContent.hCenterSection) : null readonly property var _centerSection: topBarContent ? (barWindow.isVertical ? topBarContent.vCenterSection : topBarContent.hCenterSection) : null
readonly property var _rightSection: topBarContent ? (barWindow.isVertical ? topBarContent.vRightSection : topBarContent.hRightSection) : null readonly property var _rightSection: topBarContent ? (barWindow.isVertical ? topBarContent.vRightSection : topBarContent.hRightSection) : null
readonly property real _revealProgress: topBarSlide.x + topBarSlide.y
function sectionRect(section, isCenter) { function sectionRect(section, isCenter, _dep) {
if (!section) if (!section)
return { return {
"x": 0, "x": 0,
@@ -535,7 +583,7 @@ PanelWindow {
item: clickThroughEnabled ? null : inputMask item: clickThroughEnabled ? null : inputMask
Region { Region {
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._leftSection, false) : { readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._leftSection, false, barWindow._revealProgress) : {
"x": 0, "x": 0,
"y": 0, "y": 0,
"w": 0, "w": 0,
@@ -548,7 +596,7 @@ PanelWindow {
} }
Region { Region {
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._centerSection, true) : { readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._centerSection, true, barWindow._revealProgress) : {
"x": 0, "x": 0,
"y": 0, "y": 0,
"w": 0, "w": 0,
@@ -561,7 +609,7 @@ PanelWindow {
} }
Region { Region {
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._rightSection, false) : { readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._rightSection, false, barWindow._revealProgress) : {
"x": 0, "x": 0,
"y": 0, "y": 0,
"w": 0, "w": 0,
@@ -572,6 +620,14 @@ PanelWindow {
width: r.w width: r.w
height: r.h height: r.h
} }
Region {
readonly property bool active: barWindow.clickThroughEnabled && !inputMask.showing
x: active ? inputMask.x : 0
y: active ? inputMask.y : 0
width: active ? inputMask.width : 0
height: active ? inputMask.height : 0
}
} }
Item { Item {
@@ -584,7 +640,7 @@ PanelWindow {
Timer { Timer {
id: revealHold id: revealHold
interval: barConfig?.autoHideDelay ?? 250 interval: barWindow.clickThroughEnabled ? Math.max((barConfig?.autoHideDelay ?? 250) * 6, 1500) : (barConfig?.autoHideDelay ?? 250)
repeat: false repeat: false
onTriggered: { onTriggered: {
if (!topBarMouseArea.containsMouse && !topBarCore.hasActivePopout) { if (!topBarMouseArea.containsMouse && !topBarCore.hasActivePopout) {
@@ -616,6 +672,7 @@ PanelWindow {
onHasActivePopoutChanged: evaluateReveal() onHasActivePopoutChanged: evaluateReveal()
function updateActivePopoutState() { function updateActivePopoutState() {
if (!barWindow.screen) return;
const screenName = barWindow.screen.name; const screenName = barWindow.screen.name;
const activePopout = PopoutManager.currentPopoutsByScreen[screenName]; const activePopout = PopoutManager.currentPopoutsByScreen[screenName];
const activeTrayMenu = TrayMenuManager.activeTrayMenus[screenName]; const activeTrayMenu = TrayMenuManager.activeTrayMenus[screenName];
@@ -642,7 +699,6 @@ PanelWindow {
Connections { Connections {
function onBarConfigChanged() { function onBarConfigChanged() {
topBarCore.autoHide = barConfig?.autoHide ?? false; topBarCore.autoHide = barConfig?.autoHide ?? false;
revealHold.interval = barConfig?.autoHideDelay ?? 250;
} }
target: rootWindow target: rootWindow

View File

@@ -400,7 +400,7 @@ BasePill {
Component.onCompleted: updateModel() Component.onCompleted: updateModel()
visible: dockItems.length > 0 visible: dockItems.length > 0
readonly property real iconCellSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground) + 6 readonly property real iconCellSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) + 6
content: Component { content: Component {
Item { Item {
@@ -584,8 +584,7 @@ BasePill {
property string tooltipText: { property string tooltipText: {
root._desktopEntriesUpdateTrigger; root._desktopEntriesUpdateTrigger;
const moddedId = Paths.moddedAppId(appId); const desktopEntry = appId ? DesktopEntries.heuristicLookup(appId) : null;
const desktopEntry = moddedId ? DesktopEntries.heuristicLookup(moddedId) : null;
const appName = appId ? Paths.getAppName(appId, desktopEntry) : "Unknown"; const appName = appId ? Paths.getAppName(appId, desktopEntry) : "Unknown";
if (modelData.type === "grouped" && windowCount > 1) { if (modelData.type === "grouped" && windowCount > 1) {
@@ -597,7 +596,7 @@ BasePill {
readonly property bool enlargeEnabled: (widgetData?.appsDockEnlargeOnHover !== undefined ? widgetData.appsDockEnlargeOnHover : SettingsData.appsDockEnlargeOnHover) readonly property bool enlargeEnabled: (widgetData?.appsDockEnlargeOnHover !== undefined ? widgetData.appsDockEnlargeOnHover : SettingsData.appsDockEnlargeOnHover)
readonly property real enlargeScale: enlargeEnabled && mouseArea.containsMouse ? (widgetData?.appsDockEnlargePercentage !== undefined ? widgetData.appsDockEnlargePercentage : SettingsData.appsDockEnlargePercentage) / 100.0 : 1.0 readonly property real enlargeScale: enlargeEnabled && mouseArea.containsMouse ? (widgetData?.appsDockEnlargePercentage !== undefined ? widgetData.appsDockEnlargePercentage : SettingsData.appsDockEnlargePercentage) / 100.0 : 1.0
readonly property real baseIconSizeMultiplier: (widgetData?.appsDockIconSizePercentage !== undefined ? widgetData.appsDockIconSizePercentage : SettingsData.appsDockIconSizePercentage) / 100.0 readonly property real baseIconSizeMultiplier: (widgetData?.appsDockIconSizePercentage !== undefined ? widgetData.appsDockIconSizePercentage : SettingsData.appsDockIconSizePercentage) / 100.0
readonly property real effectiveIconSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground) * baseIconSizeMultiplier readonly property real effectiveIconSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) * baseIconSizeMultiplier
readonly property color activeOverlayColor: { readonly property color activeOverlayColor: {
switch (SettingsData.appsDockActiveColorMode) { switch (SettingsData.appsDockActiveColorMode) {
@@ -690,9 +689,8 @@ BasePill {
if (!appItem.appId) if (!appItem.appId)
return ""; return "";
if (modelData.isCoreApp) if (modelData.isCoreApp)
return ""; // Explicitly skip if core app to avoid flickering or wrong look ups return "";
const moddedId = Paths.moddedAppId(appItem.appId); const desktopEntry = DesktopEntries.heuristicLookup(appItem.appId);
const desktopEntry = DesktopEntries.heuristicLookup(moddedId);
return Paths.getAppIcon(appItem.appId, desktopEntry); return Paths.getAppIcon(appItem.appId, desktopEntry);
} }
smooth: true smooth: true
@@ -749,8 +747,7 @@ BasePill {
root._desktopEntriesUpdateTrigger; root._desktopEntriesUpdateTrigger;
if (!appItem.appId) if (!appItem.appId)
return "?"; return "?";
const moddedId = Paths.moddedAppId(appItem.appId); const desktopEntry = DesktopEntries.heuristicLookup(appItem.appId);
const desktopEntry = DesktopEntries.heuristicLookup(moddedId);
const appName = Paths.getAppName(appItem.appId, desktopEntry); const appName = Paths.getAppName(appItem.appId, desktopEntry);
return appName.charAt(0).toUpperCase(); return appName.charAt(0).toUpperCase();
} }
@@ -795,7 +792,7 @@ BasePill {
anchors.rightMargin: Theme.spacingS anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: appItem.windowTitle || appItem.appId text: appItem.windowTitle || appItem.appId
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale) font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale, barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
elide: Text.ElideRight elide: Text.ElideRight
maximumLineCount: 1 maximumLineCount: 1

View File

@@ -273,7 +273,7 @@ PanelWindow {
IconImage { IconImage {
anchors.fill: parent anchors.fill: parent
source: modelData.icon ? Quickshell.iconPath(modelData.icon, true) : "" source: modelData.icon ? Paths.resolveIconPath(modelData.icon) : ""
smooth: true smooth: true
asynchronous: true asynchronous: true
visible: status === Image.Ready visible: status === Image.Ready

View File

@@ -42,7 +42,7 @@ BasePill {
DankIcon { DankIcon {
name: BatteryService.getBatteryIcon() name: BatteryService.getBatteryIcon()
size: Theme.barIconSize(battery.barThickness, undefined, battery.barConfig?.noBackground) size: Theme.barIconSize(battery.barThickness, undefined, battery.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: { color: {
if (!BatteryService.batteryAvailable) { if (!BatteryService.batteryAvailable) {
return Theme.widgetIconColor; return Theme.widgetIconColor;
@@ -63,7 +63,7 @@ BasePill {
StyledText { StyledText {
text: BatteryService.batteryLevel.toString() text: BatteryService.batteryLevel.toString()
font.pixelSize: Theme.barTextSize(battery.barThickness, battery.barConfig?.fontScale) font.pixelSize: Theme.barTextSize(battery.barThickness, battery.barConfig?.fontScale, battery.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
visible: BatteryService.batteryAvailable visible: BatteryService.batteryAvailable
@@ -78,7 +78,7 @@ BasePill {
DankIcon { DankIcon {
name: BatteryService.getBatteryIcon() name: BatteryService.getBatteryIcon()
size: Theme.barIconSize(battery.barThickness, -4, battery.barConfig?.noBackground) size: Theme.barIconSize(battery.barThickness, -4, battery.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: { color: {
if (!BatteryService.batteryAvailable) { if (!BatteryService.batteryAvailable) {
return Theme.widgetIconColor; return Theme.widgetIconColor;
@@ -99,7 +99,7 @@ BasePill {
StyledText { StyledText {
text: `${BatteryService.batteryLevel}%` text: `${BatteryService.batteryLevel}%`
font.pixelSize: Theme.barTextSize(battery.barThickness, battery.barConfig?.fontScale) font.pixelSize: Theme.barTextSize(battery.barThickness, battery.barConfig?.fontScale, battery.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: BatteryService.batteryAvailable visible: BatteryService.batteryAvailable

View File

@@ -54,7 +54,7 @@ BasePill {
id: icon id: icon
anchors.centerIn: parent anchors.centerIn: parent
name: "shift_lock" name: "shift_lock"
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground) size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: Theme.primary color: Theme.primary
} }
} }

View File

@@ -9,9 +9,6 @@ BasePill {
id: root id: root
property bool isActive: false property bool isActive: false
property var popoutTarget: null
property var parentScreen: null
property Item windowRoot: (Window.window ? Window.window.contentItem : null)
property bool isAutoHideBar: false property bool isAutoHideBar: false
signal clipboardClicked signal clipboardClicked
@@ -87,7 +84,7 @@ BasePill {
id: icon id: icon
anchors.centerIn: parent anchors.centerIn: parent
name: "content_paste" name: "content_paste"
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground) size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: Theme.widgetIconColor color: Theme.widgetIconColor
} }
} }

View File

@@ -38,7 +38,7 @@ BasePill {
const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours; const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
return String(display).padStart(2, '0').charAt(0); return String(display).padStart(2, '0').charAt(0);
} }
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
width: Math.round(font.pixelSize * 0.6) width: Math.round(font.pixelSize * 0.6)
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@@ -53,7 +53,7 @@ BasePill {
const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours; const display = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
return String(display).padStart(2, '0').charAt(1); return String(display).padStart(2, '0').charAt(1);
} }
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
width: Math.round(font.pixelSize * 0.6) width: Math.round(font.pixelSize * 0.6)
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@@ -67,7 +67,7 @@ BasePill {
StyledText { StyledText {
text: String(systemClock?.date?.getMinutes()).padStart(2, '0').charAt(0) text: String(systemClock?.date?.getMinutes()).padStart(2, '0').charAt(0)
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
width: Math.round(font.pixelSize * 0.6) width: Math.round(font.pixelSize * 0.6)
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@@ -76,7 +76,7 @@ BasePill {
StyledText { StyledText {
text: String(systemClock?.date?.getMinutes()).padStart(2, '0').charAt(1) text: String(systemClock?.date?.getMinutes()).padStart(2, '0').charAt(1)
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
width: Math.round(font.pixelSize * 0.6) width: Math.round(font.pixelSize * 0.6)
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@@ -91,7 +91,7 @@ BasePill {
StyledText { StyledText {
text: String(systemClock?.date?.getSeconds()).padStart(2, '0').charAt(0) text: String(systemClock?.date?.getSeconds()).padStart(2, '0').charAt(0)
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
width: Math.round(font.pixelSize * 0.6) width: Math.round(font.pixelSize * 0.6)
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@@ -100,7 +100,7 @@ BasePill {
StyledText { StyledText {
text: String(systemClock?.date?.getSeconds()).padStart(2, '0').charAt(1) text: String(systemClock?.date?.getSeconds()).padStart(2, '0').charAt(1)
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
width: Math.round(font.pixelSize * 0.6) width: Math.round(font.pixelSize * 0.6)
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@@ -135,7 +135,7 @@ BasePill {
const value = dayFirst ? String(systemClock?.date?.getDate()).padStart(2, '0') : String(systemClock?.date?.getMonth() + 1).padStart(2, '0'); const value = dayFirst ? String(systemClock?.date?.getDate()).padStart(2, '0') : String(systemClock?.date?.getMonth() + 1).padStart(2, '0');
return value.charAt(0); return value.charAt(0);
} }
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.primary color: Theme.primary
width: Math.round(font.pixelSize * 0.6) width: Math.round(font.pixelSize * 0.6)
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@@ -150,7 +150,7 @@ BasePill {
const value = dayFirst ? String(systemClock?.date?.getDate()).padStart(2, '0') : String(systemClock?.date?.getMonth() + 1).padStart(2, '0'); const value = dayFirst ? String(systemClock?.date?.getDate()).padStart(2, '0') : String(systemClock?.date?.getMonth() + 1).padStart(2, '0');
return value.charAt(1); return value.charAt(1);
} }
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.primary color: Theme.primary
width: Math.round(font.pixelSize * 0.6) width: Math.round(font.pixelSize * 0.6)
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@@ -171,7 +171,7 @@ BasePill {
const value = dayFirst ? String(systemClock?.date?.getMonth() + 1).padStart(2, '0') : String(systemClock?.date?.getDate()).padStart(2, '0'); const value = dayFirst ? String(systemClock?.date?.getMonth() + 1).padStart(2, '0') : String(systemClock?.date?.getDate()).padStart(2, '0');
return value.charAt(0); return value.charAt(0);
} }
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.primary color: Theme.primary
width: Math.round(font.pixelSize * 0.6) width: Math.round(font.pixelSize * 0.6)
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@@ -186,7 +186,7 @@ BasePill {
const value = dayFirst ? String(systemClock?.date?.getMonth() + 1).padStart(2, '0') : String(systemClock?.date?.getDate()).padStart(2, '0'); const value = dayFirst ? String(systemClock?.date?.getMonth() + 1).padStart(2, '0') : String(systemClock?.date?.getDate()).padStart(2, '0');
return value.charAt(1); return value.charAt(1);
} }
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.primary color: Theme.primary
width: Math.round(font.pixelSize * 0.6) width: Math.round(font.pixelSize * 0.6)
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@@ -201,7 +201,7 @@ BasePill {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingS spacing: Theme.spacingS
property real fontSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale) property real fontSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
property real digitWidth: fontSize * 0.6 property real digitWidth: fontSize * 0.6
property string hoursStr: { property string hoursStr: {

View File

@@ -19,7 +19,7 @@ BasePill {
id: icon id: icon
anchors.centerIn: parent anchors.centerIn: parent
name: "palette" name: "palette"
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground) size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: root.isActive ? Theme.primary : Theme.surfaceText color: root.isActive ? Theme.primary : Theme.surfaceText
} }
} }

View File

@@ -29,7 +29,7 @@ BasePill {
property real micAccumulator: 0 property real micAccumulator: 0
property real volumeAccumulator: 0 property real volumeAccumulator: 0
property real brightnessAccumulator: 0 property real brightnessAccumulator: 0
readonly property real vIconSize: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground) readonly property real vIconSize: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
property var _hRow: null property var _hRow: null
property var _vCol: null property var _vCol: null
property var _hAudio: null property var _hAudio: null
@@ -378,7 +378,7 @@ BasePill {
Item { Item {
width: parent.width width: parent.width
height: root.vIconSize + (root.showAudioPercent ? audioPercentV.implicitHeight + 2 : 0) height: root.vIconSize + (audioPercentV.visible ? audioPercentV.implicitHeight + 2 : 0)
visible: root.showAudioIcon visible: root.showAudioIcon
DankIcon { DankIcon {
@@ -392,9 +392,9 @@ BasePill {
StyledText { StyledText {
id: audioPercentV id: audioPercentV
visible: root.showAudioPercent visible: root.showAudioPercent && isFinite(AudioService.sink?.audio?.volume)
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%" text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: audioIconV.bottom anchors.top: audioIconV.bottom
@@ -404,7 +404,7 @@ BasePill {
Item { Item {
width: parent.width width: parent.width
height: root.vIconSize + (root.showMicPercent ? micPercentV.implicitHeight + 2 : 0) height: root.vIconSize + (micPercentV.visible ? micPercentV.implicitHeight + 2 : 0)
visible: root.showMicIcon visible: root.showMicIcon
DankIcon { DankIcon {
@@ -418,9 +418,9 @@ BasePill {
StyledText { StyledText {
id: micPercentV id: micPercentV
visible: root.showMicPercent visible: root.showMicPercent && isFinite(AudioService.source?.audio?.volume)
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%" text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: micIconV.bottom anchors.top: micIconV.bottom
@@ -430,7 +430,7 @@ BasePill {
Item { Item {
width: parent.width width: parent.width
height: root.vIconSize + (root.showBrightnessPercent ? brightnessPercentV.implicitHeight + 2 : 0) height: root.vIconSize + (brightnessPercentV.visible ? brightnessPercentV.implicitHeight + 2 : 0)
visible: root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice() visible: root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice()
DankIcon { DankIcon {
@@ -444,9 +444,9 @@ BasePill {
StyledText { StyledText {
id: brightnessPercentV id: brightnessPercentV
visible: root.showBrightnessPercent visible: root.showBrightnessPercent && isFinite(getBrightness())
text: Math.round(getBrightness() * 100) + "%" text: Math.round(getBrightness() * 100) + "%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: brightnessIconV.bottom anchors.top: brightnessIconV.bottom
@@ -502,7 +502,7 @@ BasePill {
DankIcon { DankIcon {
name: "screen_record" name: "screen_record"
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground) size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText color: NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: root.showScreenSharingIcon && NiriService.hasCasts visible: root.showScreenSharingIcon && NiriService.hasCasts
@@ -511,7 +511,7 @@ BasePill {
DankIcon { DankIcon {
id: networkIcon id: networkIcon
name: root.getNetworkIconName() name: root.getNetworkIconName()
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground) size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: root.getNetworkIconColor() color: root.getNetworkIconColor()
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: root.showNetworkIcon && NetworkService.networkAvailable visible: root.showNetworkIcon && NetworkService.networkAvailable
@@ -520,7 +520,7 @@ BasePill {
DankIcon { DankIcon {
id: vpnIcon id: vpnIcon
name: "vpn_lock" name: "vpn_lock"
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground) size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText color: NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected
@@ -529,7 +529,7 @@ BasePill {
DankIcon { DankIcon {
id: bluetoothIcon id: bluetoothIcon
name: "bluetooth" name: "bluetooth"
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground) size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: BluetoothService.connected ? Theme.primary : Theme.surfaceText color: BluetoothService.connected ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled
@@ -545,7 +545,7 @@ BasePill {
DankIcon { DankIcon {
id: audioIcon id: audioIcon
name: root.getVolumeIconName() name: root.getVolumeIconName()
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground) size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: Theme.widgetIconColor color: Theme.widgetIconColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left anchors.left: parent.left
@@ -554,9 +554,9 @@ BasePill {
StyledText { StyledText {
id: audioPercent id: audioPercent
visible: root.showAudioPercent visible: root.showAudioPercent && isFinite(AudioService.sink?.audio?.volume)
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%" text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: audioIcon.right anchors.left: audioIcon.right
@@ -574,7 +574,7 @@ BasePill {
DankIcon { DankIcon {
id: micIcon id: micIcon
name: root.getMicIconName() name: root.getMicIconName()
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground) size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: root.getMicIconColor() color: root.getMicIconColor()
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left anchors.left: parent.left
@@ -583,9 +583,9 @@ BasePill {
StyledText { StyledText {
id: micPercent id: micPercent
visible: root.showMicPercent visible: root.showMicPercent && isFinite(AudioService.source?.audio?.volume)
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%" text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: micIcon.right anchors.left: micIcon.right
@@ -603,7 +603,7 @@ BasePill {
DankIcon { DankIcon {
id: brightnessIcon id: brightnessIcon
name: root.getBrightnessIconName() name: root.getBrightnessIconName()
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground) size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: Theme.widgetIconColor color: Theme.widgetIconColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left anchors.left: parent.left
@@ -612,9 +612,9 @@ BasePill {
StyledText { StyledText {
id: brightnessPercent id: brightnessPercent
visible: root.showBrightnessPercent visible: root.showBrightnessPercent && isFinite(getBrightness())
text: Math.round(getBrightness() * 100) + "%" text: Math.round(getBrightness() * 100) + "%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.left: brightnessIcon.right anchors.left: brightnessIcon.right
@@ -625,7 +625,7 @@ BasePill {
DankIcon { DankIcon {
id: batteryIcon id: batteryIcon
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable) name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground) size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: root.getBatteryIconColor() color: root.getBatteryIconColor()
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: root.showBatteryIcon && BatteryService.batteryAvailable visible: root.showBatteryIcon && BatteryService.batteryAvailable
@@ -634,7 +634,7 @@ BasePill {
DankIcon { DankIcon {
id: printerIcon id: printerIcon
name: "print" name: "print"
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground) size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: Theme.primary color: Theme.primary
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs() visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs()
@@ -642,7 +642,7 @@ BasePill {
DankIcon { DankIcon {
name: "settings" name: "settings"
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.noBackground) size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: root.isActive ? Theme.primary : Theme.widgetIconColor color: root.isActive ? Theme.primary : Theme.widgetIconColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: root.hasNoVisibleIcons() visible: root.hasNoVisibleIcons()

View File

@@ -36,7 +36,7 @@ BasePill {
DankIcon { DankIcon {
name: "memory" name: "memory"
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground) size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: { color: {
if (DgopService.cpuUsage > 80) { if (DgopService.cpuUsage > 80) {
return Theme.tempDanger; return Theme.tempDanger;
@@ -59,7 +59,7 @@ BasePill {
return DgopService.cpuUsage.toFixed(0); return DgopService.cpuUsage.toFixed(0);
} }
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
} }
@@ -74,7 +74,7 @@ BasePill {
DankIcon { DankIcon {
id: cpuIcon id: cpuIcon
name: "memory" name: "memory"
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground) size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: { color: {
if (DgopService.cpuUsage > 80) { if (DgopService.cpuUsage > 80) {
return Theme.tempDanger; return Theme.tempDanger;
@@ -93,7 +93,7 @@ BasePill {
id: textBox id: textBox
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
implicitWidth: root.minimumWidth ? Math.max(cpuBaseline.width, cpuText.paintedWidth) : cpuText.paintedWidth implicitWidth: root.minimumWidth ? Math.max(cpuBaseline.width, cpuCurrent.width) : cpuCurrent.width
implicitHeight: cpuText.implicitHeight implicitHeight: cpuText.implicitHeight
width: implicitWidth width: implicitWidth
@@ -101,10 +101,16 @@ BasePill {
StyledTextMetrics { StyledTextMetrics {
id: cpuBaseline id: cpuBaseline
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
text: "88%" text: "88%"
} }
StyledTextMetrics {
id: cpuCurrent
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
text: cpuText.text
}
StyledText { StyledText {
id: cpuText id: cpuText
text: { text: {
@@ -114,7 +120,7 @@ BasePill {
} }
return v.toFixed(0) + "%"; return v.toFixed(0) + "%";
} }
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale) font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor color: Theme.widgetTextColor
anchors.fill: parent anchors.fill: parent

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