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

Compare commits

...

102 Commits

Author SHA1 Message Date
bbedward
db4de55338 popout: decouple shadow from content layer 2026-02-18 10:46:01 -05:00
bbedward
37ecbbbbde popout: disable layer after animation 2026-02-18 10:34:21 -05:00
purian23
d6a6d2a438 notifications: Maintain shadow during expansion 2026-02-18 10:34:21 -05:00
purian23
bf1c6eec74 notifications: Update initial popup height surfaces 2026-02-18 10:34:21 -05:00
bbedward
0ddae80584 running apps: fix scroll events being propagated fixes #1724 2026-02-18 10:34:21 -05:00
bbedward
5c96c03bfa matugen: make v4 detection more resilient 2026-02-18 09:57:35 -05:00
bbedward
dfe36e47d8 process list: fix scaling with fonts fixes #1721 2026-02-18 09:57:35 -05:00
purian23
63e1b75e57 dankinstall: Fix Debian ARM64 detection 2026-02-18 09:57:35 -05:00
bbedward
29efdd8598 matugen: detect emacs directory fixes #1720 2026-02-18 09:57:35 -05:00
bbedward
34d03cf11b osd: optimize bindings 2026-02-18 09:57:35 -05:00
bbedward
c339389d44 screenshot: adjust cursor CLI option to be more explicit 2026-02-17 22:28:46 -05:00
bbedward
af5f6eb656 settings: workaround crash 2026-02-17 22:20:19 -05:00
purian23
a6d28e2553 notifications: Tweak animation scale & settings 2026-02-17 22:07:36 -05:00
bbedward
6213267908 settings: guard internal writes from watcher 2026-02-17 22:03:57 -05:00
bbedward
d084114149 cc: fix plugin reloading in bar position changes 2026-02-17 17:25:19 -05:00
bbedward
f6d99eca0d popout: anchor height changing popout surfaces to top and bottom 2026-02-17 17:25:19 -05:00
bbedward
722eb3289e workspaces: fix named workspace icons 2026-02-17 17:25:19 -05:00
bbedward
b7f2bdcb2d dankinstall: no_anim on dms layers 2026-02-17 17:25:19 -05:00
bbedward
11c20db6e6 1.4.1 2026-02-17 14:08:15 -05:00
bbedward
8a4e3f8bb1 system updater: fix hide no update option 2026-02-17 14:08:04 -05:00
bbedward
bc8fe97c13 launcher: fix kb navigation not always showing last delegate in view 2026-02-17 14:08:04 -05:00
bbedward
47262155aa doctor: add qt6-imageformats check 2026-02-17 14:08:04 -05:00
bbedward
dd4c41a6b2 v1.4.0 2026-02-17 12:01:36 -05:00
bbedward
92a25fdb6a process list: add all/user/system filters 2026-02-17 11:25:05 -05:00
bbedward
d6650be008 dgop service: expose username 2026-02-17 11:04:55 -05:00
bbedward
2646e7b19a launcher v2: apply transparency to footer 2026-02-17 10:54:49 -05:00
bbedward
4133f11d82 changelog: remove text note 2026-02-17 10:50:01 -05:00
bbedward
22ed740394 ripple: small tweaks to shader 2026-02-17 10:39:32 -05:00
bbedward
063299a434 cc: network tab performance improvements 2026-02-17 10:25:19 -05:00
bbedward
44d836c975 ripple: use a shader for ripple effect 2026-02-17 09:27:18 -05:00
bbedward
da437e77fb keybinds: auto-focus cheatsheet search 2026-02-17 08:44:47 -05:00
Jonas Bloch
34a6bbfb32 feat: Keybinds cheatsheet search (#1706)
* feat(wip): add fuzzy finder in keybinds cheatsheet

* fix: replace GridLayout with RowLayout and don't use anchors in KeybindsModal

* fix: replace fuzzyfinder with simple inclusion criterion for keybind search

* fix: bring back categoryKeys (there was no reason to remove it)
2026-02-17 08:42:45 -05:00
Jonas Bloch
9ed53bac9e feat: Auto settings reload (#1707)
* feat: auto-reload settings json file

* fix: set settings file reload debounce to 50ms
2026-02-17 08:41:18 -05:00
purian23
3a6752c3d2 dock: Update indicator padding 2026-02-17 07:55:33 -05:00
purian23
ef19568dd7 audio: New ability to hide input/output devices
- Updated slider presets
- Disabled mouse wheel scrolling on list scroll
2026-02-17 00:54:32 -05:00
bbedward
f280cd9d3b keybinds: dont pass dirs 2026-02-16 23:55:11 -05:00
Divya Jain
cf4ce3c476 add support for globalprotect vpn using saml auth flow (#1689)
* add support for globalprotect vpn using saml auth flow

* go fmt

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-02-16 23:25:35 -05:00
bbedward
ae59e53c4c settings: fix wallpaper cycle buttons 2026-02-16 23:14:56 -05:00
dms-ci[bot]
7e0d661f63 nix: update vendorHash for go.mod changes 2026-02-17 04:02:14 +00:00
bbedward
0b33d3f905 miraclewm: add support for Miracle WM 2026-02-16 23:00:25 -05:00
purian23
d62bdda56b theme: Add Cosmic light/dark & icon theming support 2026-02-16 21:25:30 -05:00
Lucas
5841b38cd9 Update nix packaging (#1703)
* nix: add kimageformats to DMS qml dependencies

* nix: enable polkit by default in NixOS module
2026-02-16 20:47:40 -05:00
purian23
83e2b5a7a6 notifications: Tweak toast button padding 2026-02-16 19:29:57 -05:00
bbedward
2f863f64ee core: set qt platform to wayland;xcb by default 2026-02-16 18:28:31 -05:00
bbedward
1a8b397cfd weather: keep tab height consistent 2026-02-16 18:14:30 -05:00
bbedward
196c421b75 Squashed commit of the following:
commit 051b7576f7
Author: purian23 <purian23@gmail.com>
Date:   Sun Feb 15 16:38:45 2026 -0500

    Height for realz

commit 7784488a61
Author: purian23 <purian23@gmail.com>
Date:   Sun Feb 15 16:34:09 2026 -0500

    Fix height and truncate text/URLs

commit 31b328d428
Author: bbedward <bbedward@gmail.com>
Date:   Sun Feb 15 16:25:57 2026 -0500

    notifications: handle URL encoding in markdown2html

commit dbb04f74a2
Author: bbedward <bbedward@gmail.com>
Date:   Sun Feb 15 16:10:20 2026 -0500

    notifications: more comprehensive decoder

commit b29c7192c2
Author: bbedward <bbedward@gmail.com>
Date:   Sun Feb 15 15:51:37 2026 -0500

    notifications: html unescape

commit 8a48fa11ec
Author: purian23 <purian23@gmail.com>
Date:   Sun Feb 15 15:04:33 2026 -0500

    Add expressive curve on init toast

commit ee124f5e04
Author: purian23 <purian23@gmail.com>
Date:   Sun Feb 15 15:02:16 2026 -0500

    Expressive curves on swipe & btn height

commit 0fce904635
Author: purian23 <purian23@gmail.com>
Date:   Sun Feb 15 13:40:02 2026 -0500

    Provide bottom button clearance

commit 00d3829999
Author: bbedward <bbedward@gmail.com>
Date:   Sun Feb 15 13:24:31 2026 -0500

    notifications: cleanup popup display logic

commit fd05768059
Author: purian23 <purian23@gmail.com>
Date:   Sun Feb 15 01:00:55 2026 -0500

    Add Privacy Mode
    - Smoother notification expansions
    - Shadow & Privacy Toggles

commit 0dba11d845
Author: purian23 <purian23@gmail.com>
Date:   Sat Feb 14 22:48:46 2026 -0500

    Further M3 enhancements

commit 949c216964
Author: purian23 <purian23@gmail.com>
Date:   Sat Feb 14 19:59:38 2026 -0500

    Right-Click to set Rules on Notifications directly

commit 62bc25782c
Author: bbedward <bbedward@gmail.com>
Date:   Fri Feb 13 21:44:27 2026 -0500

    notifications: fix compact spacing, reveal header bar, add bottom center
    position, pointing hand cursor fix

commit ed495d4396
Author: purian23 <purian23@gmail.com>
Date:   Fri Feb 13 20:25:40 2026 -0500

    Tighten init toast

commit ebe38322a0
Author: purian23 <purian23@gmail.com>
Date:   Fri Feb 13 20:09:59 2026 -0500

    Update more m3 baselines & spacing

commit b1735bb701
Author: purian23 <purian23@gmail.com>
Date:   Fri Feb 13 14:10:05 2026 -0500

    Expand rules on-Click

commit 9f13546b4d
Author: purian23 <purian23@gmail.com>
Date:   Fri Feb 13 12:59:29 2026 -0500

    Add Notification Rules
    - Additional right-click ops
    - Allow for 3rd boy line on init notification popup

commit be133b73c7
Author: purian23 <purian23@gmail.com>
Date:   Fri Feb 13 10:10:03 2026 -0500

    Truncate long title in groups

commit 4fc275bead
Author: bbedward <bbedward@gmail.com>
Date:   Thu Feb 12 23:27:34 2026 -0500

    notification: expand/collapse animation adjustment

commit 00e6172a68
Author: purian23 <purian23@gmail.com>
Date:   Thu Feb 12 22:50:11 2026 -0500

    Fix global warnings

commit 0772f6deb7
Author: purian23 <purian23@gmail.com>
Date:   Thu Feb 12 22:46:40 2026 -0500

    Tweak expansion duration

commit 0ffeed3ff0
Author: purian23 <purian23@gmail.com>
Date:   Thu Feb 12 22:16:16 2026 -0500

    notifications: Update Material 3 baselines
    - New right-click to mute option
    - New independent Notification Animation settings
2026-02-16 17:57:13 -05:00
bbedward
8399d64c2d settings: drop beta from confiugration 2026-02-16 17:51:20 -05:00
bbedward
c530eab303 settings: fix dropped disconnected displays on save 2026-02-16 17:47:28 -05:00
xdenotte
45b6362dd3 fix: correct preview centering with scaling (#1701) 2026-02-16 17:47:21 -05:00
bbedward
50b77dcfc3 i18n: term update 2026-02-16 17:42:10 -05:00
bbedward
be8f3adf01 core/screensaver: add methods to introspect XML 2026-02-16 17:36:49 -05:00
bbedward
75a8c171ea launcher: remove double loader 2026-02-16 12:17:46 -05:00
bbedward
466ff59573 launcher: keep loaded default 2026-02-16 12:06:31 -05:00
bbedward
053bb91927 process list: fix clipped graphs
fixes #1697
2026-02-16 11:37:19 -05:00
bbedward
2c9b22c016 changelog: add and enable 1.4 changelog 2026-02-16 10:33:59 -05:00
Jon Rogers
a9ee91586e fix: preserve _preScored from plugin items to allow ordering control (#1696)
The _preScored property allows plugins to control the ordering of
launcher results. Previously, this property was being overwritten
with undefined during item transformation, preventing plugins from
controlling which items appear first.

This change preserves the _preScored value from plugin items in:
- transformBuiltInLauncherItem()
- transformPluginItem()

Required for: devnullvoid/dms-web-search#7
2026-02-16 00:38:10 -05:00
Kristoffer Grönlund
81bce74612 greeter: Add support for Debian greetd user/group name (#1685)
* greeter: Detect user and group used by greetd

On most distros greetd runs as user and group "greeter",
but on Debian the user and group "_greetd" are used.

* greeter: Use correct group in sync command

* greeter: more generic group detection

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-02-16 00:36:58 -05:00
purian23
f2a6d2c7da core: Fix DMS Greeter group check & add Cosmic support 2026-02-15 22:48:22 -05:00
bbedward
0a9a34912e wallpapers: support more image formats + case insensitivity
fixes #1694
fixes #1660
2026-02-15 16:22:27 -05:00
Higor Prado
abff670814 fix(niri): restore lazy overview spotlight lifecycle to reduce idle VRAM (#1693) 2026-02-15 15:49:55 -05:00
bbedward
0d49acaaa8 launcher: try a more targeted unload approach 2026-02-15 15:48:49 -05:00
Higor Prado
ebe1785411 fix(launcher): release DankLauncherV2 resources after close (#1692)
* fix(launcher): release DankLauncherV2 resources after close

* launcher: make unload on close optional

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-02-15 15:26:03 -05:00
bbedward
f9f0192b22 i18n: update terms 2026-02-15 13:40:34 -05:00
bbedward
e5cdbf4cf5 clipboard: option to paste on enter
fixes #1686
2026-02-15 13:36:19 -05:00
Higor Prado
13ef1efa7b fix(qml): optimize VRAM usage in DankRipple (#1691)
Replace layer+MultiEffect mask with GPU-native clipping for rounded
corners. The previous approach created offscreen textures for every
clickable element with rounded corners (used in 34+ files across the
UI), adding 100-300MB VRAM on NVIDIA GPUs.

The new approach uses clip: true on a Rectangle with the corner
radius, which is handled natively by the GPU without creating
intermediate textures.

Visual impact: Minimal - the ripple effect works identically.
The only theoretical difference is slightly less smooth edges on
rounded corners during the ripple animation, which is not noticeable
at 10% opacity during the quick animation.

VRAM improvement: Tested on NVIDIA, ~100-300MB reduction.
2026-02-15 13:25:45 -05:00
Sunny
fbd9301a2d fixed emacs template to work for both light and dark themes (#1682) 2026-02-15 12:22:23 -05:00
Artem
24e3024b57 fix(brightness): refresh sysfs cache on hotplug (#1674)
* fix(brightness): refresh sysfs cache on hotplug

The SysfsBackend used a cache that was never refreshed on display hot plug, causing new backlight devices to not appear in IPC until restart.

This adds Rescan() to SysfsBackend and calls it in Manager.Rescan(), matching the behavior of DDCBackend.

Fixes: hotplugged external monitor brightness control via IPC

* make fmt

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-02-14 14:00:01 -05:00
bbedward
52d5af11ba dankdash: fix triggering when clock widget isnt present on bar
fixes #1601
2026-02-14 13:54:51 -05:00
bbedward
44a45b00cf widgets: cleanup rectangles across popouts, modals, OSDs 2026-02-14 11:15:26 -05:00
bbedward
2b78fe5b9f popout: remove double rectangle artifact 2026-02-14 10:45:50 -05:00
bbedward
14f92669c6 doctor: add cups-pk-helper 2026-02-14 10:38:03 -05:00
bbedward
124106de87 scrollies: switch to frame animation for kinetic scroll 2026-02-13 22:26:36 -05:00
Connor Welsh
bb8e0d384f dock: resolve icons for pre-substituted app IDs (#1669) 2026-02-13 21:40:17 -05:00
bbedward
59d37847ec osd: allow overriding layer 2026-02-13 18:04:34 -05:00
chimera
acdc531dca MangoWC and Scroll Greeter Support for NixOS (#1647)
* add mangowc greeter to nix.

i am going to be suprised if this only needed this line

* point mangowc to mango

there is no way this works

* mango flake detection and maybe scroll support

* " "

* no mango flake dependency

* mango dependency remove too

i have got to add "parenthesis" to stuff more

* Final De-dependification of MangoWC

it works without the flake YES

* mangowc -> mango pt 1

* mangowc -> mango pt 2

necessary evil. will break inital greetd confs but works after change

* Preserve Compatibility
2026-02-13 17:38:21 -05:00
bbedward
ce75dac81b track art: use URLs directly 2026-02-13 17:31:51 -05:00
bbedward
b8d40761ff network: simplify connection handling 2026-02-13 17:24:58 -05:00
bbedward
3a7430f6da osd: reverse media playback icons and handle screen changes 2026-02-13 15:43:46 -05:00
bbedward
242660c51d theme: improve handling of custom themes with variants and accents in
light/dark mode (e.g. catpuccin will react to light/dark changes and
remember theme per-mode)
fixes #1094
2026-02-13 10:31:59 -05:00
bbedward
8a6c1e45ce themes: fix overflow of option button group
fixes #1399
2026-02-13 10:22:07 -05:00
bbedward
b8e5f9f3b1 matugen: support v4 2026-02-13 09:40:51 -05:00
bbedward
d60e70f9cc notifications: fix crash in modal 2026-02-12 23:15:22 -05:00
bbedward
cdb70fadb3 launcher v2: fix kb navigation to top of scroll 2026-02-12 22:41:40 -05:00
purian23
7867deef60 dock: Fix option to use custom logos 2026-02-12 21:02:06 -05:00
bbedward
a77c1adb32 matugen: dont signal terminals when disabled
fixes #1658
2026-02-12 16:57:21 -05:00
bbedward
da14d75a3b i18n: term update 2026-02-12 15:06:23 -05:00
Bernardo Gomes
7c66a34931 fix(i18n): capture missing strings and add pt-BR translations (#1654) 2026-02-12 15:05:44 -05:00
Bernardo Gomes
425715e0f0 feat(notifications): add configurable notification rules (#1655) 2026-02-12 15:04:02 -05:00
bbedward
a3baf8ce31 running apps: fix focusing of windows when grouped 2026-02-12 14:51:10 -05:00
bbedward
605e03b065 dankbar: fix spacing at scale of running apps, dock, and system tray 2026-02-12 14:38:31 -05:00
bbedward
0e9b21d359 plugins: add plugin state helpers 2026-02-12 14:04:56 -05:00
bbedward
ba5bf0cabc i18n: general RTL fixes 2026-02-12 11:58:32 -05:00
bbedward
96b9d7aab3 ci: update go version and golangci-lint version 2026-02-12 09:57:38 -05:00
dms-ci[bot]
750e4c4527 nix: update vendorHash for go.mod changes 2026-02-12 14:50:23 +00:00
bbedward
7417e26444 core: replace go-localereader directive 2026-02-12 09:48:10 -05:00
bbedward
00e1099912 weather: light redesign for dash card 2026-02-12 09:42:29 -05:00
bbedward
bd46d29ff0 settings: optimize sidebar bindings 2026-02-11 18:42:32 -05:00
bbedward
1a9d7684b9 wallpaper: fix per-monitor view modes
fixes #1582
2026-02-11 17:58:44 -05:00
bbedward
0133c19276 dock: fix auto-hide hit area
media osd: fix showing without album art
2026-02-11 17:51:29 -05:00
ArijanJ
46bb3b613b feat: add osd toggles to search index (#1652)
* feat: add osd toggles to search index
this commit also regenerates the search index

* add newline at end of translations file

* ran prek to fix the file :)
2026-02-11 13:28:04 -05:00
bbedward
5839a5de30 displays: add full screen only for hyprland and convert vrr to dropdown
fixes #1649
fixes #1548
2026-02-11 09:31:35 -05:00
bbedward
535d0bb0f0 lock/greeter: fix keyboard layout on Hyprland
fixes #1650
fixes #672
fixes #1600
2026-02-11 08:57:24 -05:00
201 changed files with 15986 additions and 5207 deletions

View File

@@ -45,9 +45,9 @@ body:
- type: textarea
id: dms_doctor
attributes:
label: dms doctor -v
description: Output of `dms doctor -v` command
placeholder: Paste the output of `dms doctor -v` here
label: dms doctor -vC
description: Output of `dms doctor -vC` command
placeholder: Paste the output of `dms doctor -vC` here
validations:
required: true
- type: textarea

View File

@@ -30,9 +30,9 @@ body:
- type: textarea
id: dms_doctor
attributes:
label: dms doctor -v
description: Output of `dms doctor -v` command
placeholder: Paste the output of `dms doctor -v` here
label: dms doctor -vC
description: Output of `dms doctor -vC` command
placeholder: Paste the output of `dms doctor -vC` here
validations:
required: false
- type: textarea

View File

@@ -191,6 +191,11 @@ jobs:
git fetch origin --force tag ${TAG}
git checkout ${TAG}
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: ./core/go.mod
- name: Download core artifacts
uses: actions/download-artifact@v4
with:
@@ -229,6 +234,7 @@ jobs:
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
- **`dankinstall-amd64.gz`** - Installer binary for x86_64 systems
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems
- **`dms-cli-<version>.tar.gz`** - Go source code with vendored modules (for distro packaging)
- **`dms-qml.tar.gz`** - QML source code only
### Checksums
@@ -387,6 +393,19 @@ jobs:
rm -rf _temp_full
done
- name: Generate vendored source tarball
run: |
set -euxo pipefail
VERSION_NUM=${TAG#v}
cd core
go mod vendor
cd ..
tar czf "_release_assets/dms-cli-${VERSION_NUM}.tar.gz" \
--transform "s,^core/,dms-cli-${VERSION_NUM}/," \
--exclude='core/.git' \
core/
(cd _release_assets && sha256sum "dms-cli-${VERSION_NUM}.tar.gz" > "dms-cli-${VERSION_NUM}.tar.gz.sha256")
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:

View File

@@ -335,7 +335,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: "1.24"
go-version-file: ./core/go.mod
- name: Install OSC
run: |

View File

@@ -158,7 +158,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.24"
go-version-file: ./core/go.mod
cache: false
- name: Install build dependencies

View File

@@ -19,7 +19,7 @@ Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
</div>
DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop.
DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), [Miracle WM](https://github.com/miracle-wm-org/miracle-wm), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop.
## Repository Structure
@@ -105,7 +105,7 @@ Extend functionality with the [plugin registry](https://plugins.danklinux.com).
## Supported Compositors
Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [labwc](https://labwc.github.io/), and [Scroll](https://github.com/dawsers/scroll) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features.
Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), and [Miracle WM](https://github.com/miracle-wm-org/miracle-wm) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features.
[Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors)

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/golangci/golangci-lint
rev: v2.6.2
rev: v2.9.0
hooks:
- id: golangci-lint-fmt
require_serial: true

View File

@@ -649,6 +649,109 @@ func checkI2CAvailability() checkResult {
return checkResult{catOptionalFeatures, "I2C/DDC", statusOK, fmt.Sprintf("%d monitor(s) detected", len(devices)), "External monitor brightness control", doctorDocsURL + "#optional-features"}
}
func checkImageFormatPlugins() []checkResult {
url := doctorDocsURL + "#optional-features"
pluginDir := findQtPluginDir()
if pluginDir == "" {
return []checkResult{
{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},
}
}
imageFormatsDir := filepath.Join(pluginDir, "imageformats")
type pluginCheck struct {
name string
desc string
plugins []struct{ file, format string }
}
checks := []pluginCheck{
{
name: "qt6-imageformats",
desc: "WebP, TIFF, GIF, JP2 support",
plugins: []struct{ file, format string }{
{"libqwebp.so", "WebP"},
{"libqtiff.so", "TIFF"},
{"libqgif.so", "GIF"},
{"libqjp2.so", "JP2"},
{"libqicns.so", "ICNS"},
},
},
{
name: "kimageformats",
desc: "AVIF, HEIF, JXL support",
plugins: []struct{ file, format string }{
{"kimg_avif.so", "AVIF"},
{"kimg_heif.so", "HEIF"},
{"kimg_jxl.so", "JXL"},
{"kimg_exr.so", "EXR"},
},
},
}
var results []checkResult
for _, c := range checks {
var found []string
for _, p := range c.plugins {
if _, err := os.Stat(filepath.Join(imageFormatsDir, p.file)); err == nil {
found = append(found, p.format)
}
}
var result checkResult
switch {
case len(found) == 0:
result = checkResult{catOptionalFeatures, c.name, statusWarn, "Not installed", c.desc, url}
default:
details := ""
if doctorVerbose {
details = fmt.Sprintf("Formats: %s (%s)", strings.Join(found, ", "), imageFormatsDir)
}
result = checkResult{catOptionalFeatures, c.name, statusOK, fmt.Sprintf("Installed (%d formats)", len(found)), details, url}
}
results = append(results, result)
}
return results
}
func findQtPluginDir() string {
// Check QT_PLUGIN_PATH env var first (used by NixOS and custom setups)
if envPath := os.Getenv("QT_PLUGIN_PATH"); envPath != "" {
for dir := range strings.SplitSeq(envPath, ":") {
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
return dir
}
}
}
// Try qtpaths
for _, cmd := range []string{"qtpaths6", "qtpaths"} {
if output, err := exec.Command(cmd, "-query", "QT_INSTALL_PLUGINS").Output(); err == nil {
if dir := strings.TrimSpace(string(output)); dir != "" {
return dir
}
}
}
// Fallback: common distro paths
for _, dir := range []string{
"/usr/lib/qt6/plugins",
"/usr/lib64/qt6/plugins",
"/usr/lib/x86_64-linux-gnu/qt6/plugins",
"/usr/lib/aarch64-linux-gnu/qt6/plugins",
} {
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
return dir
}
}
return ""
}
func detectNetworkBackend(stackResult *network.DetectResult) string {
switch stackResult.Backend {
case network.BackendNetworkManager:
@@ -689,7 +792,21 @@ func checkOptionalDependencies() []checkResult {
logindStatus, logindMsg := getOptionalDBusStatus("org.freedesktop.login1")
results = append(results, checkResult{catOptionalFeatures, "logind", logindStatus, logindMsg, "Session management", optionalFeaturesURL})
cupsPkHelperBus := "org.opensuse.CupsPkHelper.Mechanism"
var cupsPkStatus status
var cupsPkMsg string
switch {
case utils.IsDBusServiceAvailable(cupsPkHelperBus):
cupsPkStatus, cupsPkMsg = statusOK, "Running"
case utils.IsDBusServiceActivatable(cupsPkHelperBus):
cupsPkStatus, cupsPkMsg = statusOK, "Available"
default:
cupsPkStatus, cupsPkMsg = statusWarn, "Not available (install cups-pk-helper)"
}
results = append(results, checkResult{catOptionalFeatures, "cups-pk-helper", cupsPkStatus, cupsPkMsg, "Printer management", optionalFeaturesURL})
results = append(results, checkI2CAvailability())
results = append(results, checkImageFormatPlugins()...)
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {

View File

@@ -169,7 +169,8 @@ func syncGreeter() error {
return fmt.Errorf("greeter cache directory not found at %s\nPlease install the greeter first", cacheDir)
}
greeterGroupExists := checkGroupExists("greeter")
greeterGroup := greeter.DetectGreeterGroup()
greeterGroupExists := utils.HasGroup(greeterGroup)
if greeterGroupExists {
currentUser, err := user.Current()
if err != nil {
@@ -182,25 +183,27 @@ func syncGreeter() error {
return fmt.Errorf("failed to check groups: %w", err)
}
inGreeterGroup := strings.Contains(string(groupsOutput), "greeter")
inGreeterGroup := strings.Contains(string(groupsOutput), greeterGroup)
if !inGreeterGroup {
fmt.Println("\n⚠ Warning: You are not in the greeter group.")
fmt.Print("Would you like to add your user to the greeter group? (y/N): ")
fmt.Printf("\n⚠ Warning: You are not in the %s group.\n", greeterGroup)
fmt.Printf("Would you like to add your user to the %s group? (Y/n): ", greeterGroup)
var response string
fmt.Scanln(&response)
response = strings.ToLower(strings.TrimSpace(response))
if response == "y" || response == "yes" {
fmt.Println("\nAdding user to greeter group...")
addUserCmd := exec.Command("sudo", "usermod", "-aG", "greeter", currentUser.Username)
if response != "n" && response != "no" {
fmt.Printf("\nAdding user to %s group...\n", greeterGroup)
addUserCmd := exec.Command("sudo", "usermod", "-aG", greeterGroup, currentUser.Username)
addUserCmd.Stdout = os.Stdout
addUserCmd.Stderr = os.Stderr
if err := addUserCmd.Run(); err != nil {
return fmt.Errorf("failed to add user to greeter group: %w", err)
return fmt.Errorf("failed to add user to %s group: %w", greeterGroup, err)
}
fmt.Println("✓ User added to greeter group")
fmt.Printf("✓ User added to %s group\n", greeterGroup)
fmt.Println("⚠ You will need to log out and back in for the group change to take effect")
} else {
return fmt.Errorf("aborted: user must be in the greeter group before syncing")
}
}
}
@@ -243,21 +246,6 @@ func syncGreeter() error {
return nil
}
func checkGroupExists(groupName string) bool {
data, err := os.ReadFile("/etc/group")
if err != nil {
return false
}
lines := strings.SplitSeq(string(data), "\n")
for line := range lines {
if strings.HasPrefix(line, groupName+":") {
return true
}
}
return false
}
func disableDisplayManager(dmName string) (bool, error) {
state, err := getSystemdServiceState(dmName)
if err != nil {
@@ -389,7 +377,7 @@ func ensureGraphicalTarget() error {
func handleConflictingDisplayManagers() error {
fmt.Println("\n=== Checking for Conflicting Display Managers ===")
conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm"}
conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"}
disabledAny := false
var errors []string

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds/providers"
@@ -82,24 +83,35 @@ func init() {
func initializeProviders() {
registry := keybinds.GetDefaultRegistry()
hyprlandProvider := providers.NewHyprlandProvider("$HOME/.config/hypr")
hyprlandProvider := providers.NewHyprlandProvider("")
if err := registry.Register(hyprlandProvider); err != nil {
log.Warnf("Failed to register Hyprland provider: %v", err)
}
mangowcProvider := providers.NewMangoWCProvider("$HOME/.config/mango")
mangowcProvider := providers.NewMangoWCProvider("")
if err := registry.Register(mangowcProvider); err != nil {
log.Warnf("Failed to register MangoWC provider: %v", err)
}
scrollProvider := providers.NewSwayProvider("$HOME/.config/scroll")
if err := registry.Register(scrollProvider); err != nil {
log.Warnf("Failed to register Scroll provider: %v", err)
configDir, _ := os.UserConfigDir()
if configDir != "" {
scrollProvider := providers.NewSwayProvider(filepath.Join(configDir, "scroll"))
if err := registry.Register(scrollProvider); err != nil {
log.Warnf("Failed to register Scroll provider: %v", err)
}
}
swayProvider := providers.NewSwayProvider("$HOME/.config/sway")
if err := registry.Register(swayProvider); err != nil {
log.Warnf("Failed to register Sway provider: %v", err)
miracleProvider := providers.NewMiracleProvider("")
if err := registry.Register(miracleProvider); err != nil {
log.Warnf("Failed to register Miracle WM provider: %v", err)
}
if configDir != "" {
swayProvider := providers.NewSwayProvider(filepath.Join(configDir, "sway"))
if err := registry.Register(swayProvider); err != nil {
log.Warnf("Failed to register Sway provider: %v", err)
}
}
niriProvider := providers.NewNiriProvider("")
@@ -144,6 +156,8 @@ func makeProviderWithPath(name, path string) keybinds.Provider {
return providers.NewSwayProvider(path)
case "scroll":
return providers.NewSwayProvider(path)
case "miracle":
return providers.NewMiracleProvider(path)
case "niri":
return providers.NewNiriProvider(path)
default:

View File

@@ -13,16 +13,16 @@ import (
)
var (
ssOutputName string
ssIncludeCursor bool
ssFormat string
ssQuality int
ssOutputDir string
ssFilename string
ssNoClipboard bool
ssNoFile bool
ssNoNotify bool
ssStdout bool
ssOutputName string
ssCursor string
ssFormat string
ssQuality int
ssOutputDir string
ssFilename string
ssNoClipboard bool
ssNoFile bool
ssNoNotify bool
ssStdout bool
)
var screenshotCmd = &cobra.Command{
@@ -52,7 +52,7 @@ Examples:
dms screenshot last # Last region (pre-selected)
dms screenshot --no-clipboard # Save file only
dms screenshot --no-file # Clipboard only
dms screenshot --cursor # Include cursor
dms screenshot --cursor=on # Include cursor
dms screenshot -f jpg -q 85 # JPEG with quality 85`,
}
@@ -111,7 +111,7 @@ var notifyActionCmd = &cobra.Command{
func init() {
screenshotCmd.PersistentFlags().StringVarP(&ssOutputName, "output", "o", "", "Output name for 'output' mode")
screenshotCmd.PersistentFlags().BoolVar(&ssIncludeCursor, "cursor", false, "Include cursor in screenshot")
screenshotCmd.PersistentFlags().StringVar(&ssCursor, "cursor", "off", "Include cursor in screenshot (on/off)")
screenshotCmd.PersistentFlags().StringVarP(&ssFormat, "format", "f", "png", "Output format (png, jpg, ppm)")
screenshotCmd.PersistentFlags().IntVarP(&ssQuality, "quality", "q", 90, "JPEG quality (1-100)")
screenshotCmd.PersistentFlags().StringVarP(&ssOutputDir, "dir", "d", "", "Output directory")
@@ -136,7 +136,9 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
config := screenshot.DefaultConfig()
config.Mode = mode
config.OutputName = ssOutputName
config.IncludeCursor = ssIncludeCursor
if strings.EqualFold(ssCursor, "on") {
config.Cursor = screenshot.CursorOn
}
config.Clipboard = !ssNoClipboard
config.SaveFile = !ssNoFile
config.Notify = !ssNoNotify

View File

@@ -210,7 +210,7 @@ func runShellInteractive(session bool) {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
}
if os.Getenv("QT_QPA_PLATFORM") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland")
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
}
cmd.Stdin = os.Stdin
@@ -450,7 +450,7 @@ func runShellDaemon(session bool) {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
}
if os.Getenv("QT_QPA_PLATFORM") == "" {
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland")
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
}
devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)

View File

@@ -1,11 +1,11 @@
module github.com/AvengeMedia/DankMaterialShell/core
go 1.24.6
go 1.25.0
require (
github.com/Wifx/gonetworkmanager/v2 v2.2.0
github.com/alecthomas/chroma/v2 v2.23.1
github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/log v0.4.2
@@ -19,44 +19,43 @@ require (
github.com/yuin/goldmark v1.7.16
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.etcd.io/bbolt v1.4.3
golang.org/x/exp v0.0.0-20260112195511-716be5621a96
golang.org/x/image v0.35.0
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a
golang.org/x/image v0.36.0
)
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/clipperhouse/displaywidth v0.8.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.4.0 // indirect
github.com/clipperhouse/displaywidth v0.10.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg/v2 v2.0.2 // indirect
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc // indirect
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/kevinburke/ssh_config v1.4.0 // indirect
github.com/kevinburke/ssh_config v1.6.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.0 // indirect
)
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/colorprofile v0.4.2 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/x/ansi v0.11.4 // indirect
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-git/go-git/v6 v6.0.0-20260123133532-f99a98e81ce9
github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -70,7 +69,11 @@ require (
github.com/spf13/afero v1.15.0
github.com/spf13/pflag v1.0.10 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.40.0
golang.org/x/text v0.33.0
gopkg.in/yaml.v3 v3.0.1 // indirect
golang.org/x/sys v0.41.0
golang.org/x/text v0.34.0
gopkg.in/yaml.v3 v3.0.1
)
// v0.0.1 tag is missing a LICENSE file; master has it.
// See: https://github.com/mattn/go-localereader/issues/2
replace github.com/mattn/go-localereader v0.0.1 => github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75

View File

@@ -20,30 +20,28 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
github.com/charmbracelet/x/ansi v0.11.4 h1:6G65PLu6HjmE858CnTUQY1LXT3ZUWwfvqEROLF8vqHI=
github.com/charmbracelet/x/ansi v0.11.4/go.mod h1:/5AZ+UfWExW3int5H5ugnsG/PWjNcSQcwYsHBlPFQN4=
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.8.0 h1:/z8v+H+4XLluJKS7rAc7uHZTalT5Z+1430ld3lePSRI=
github.com/clipperhouse/displaywidth v0.8.0/go.mod h1:UpOXiIKep+TohQYwvAAM/VDU8v3Z5rnWTxiwueR0XvQ=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.4.0 h1:RXqE/l5EiAbA4u97giimKNlmpvkmz+GrBVTelsoXy9g=
github.com/clipperhouse/uax29/v2 v2.4.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -66,12 +64,12 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc h1:rhkjrnRkamkRC7woapp425E4CAH6RPcqsS9X8LA93IY=
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc/go.mod h1:X1oe0Z2qMsa9hkar3AAPuL9hu4Mi3ztXEjdqRhr6fcc=
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 h1:UU7oARtwQ5g85aFiCSwIUA6PBmAshYj0sytl/5CCBgs=
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3/go.mod h1:ZW9JC5gionMP1kv5uiaOaV23q0FFmNrVOV8VW+y/acc=
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67 h1:3hutPZF+/FBjR/9MdsLJ7e1mlt9pwHgwxMW7CrbmWII=
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67/go.mod h1:xKt0pNHST9tYHvbiLxSY27CQWFwgIxBJuDrOE0JvbZw=
github.com/go-git/go-git/v6 v6.0.0-20260123133532-f99a98e81ce9 h1:VzdR70t+SMjYnBgnbtNpq4ElZAAovLPMG+GFX8OBRtM=
github.com/go-git/go-git/v6 v6.0.0-20260123133532-f99a98e81ce9/go.mod h1:EWlxLBkiFCzXNCadvt05fT9PCAE2sUedgDsvUUIo18s=
github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f h1:TBkCJv9YwPOuXq1OG0r01bcxRrvs15Hp/DtZuPt4H6s=
github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f/go.mod h1:B88nWzfnhTlIikoJ4d84Nc9noKS5mJoA7SgDdkt0aPU=
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -88,8 +86,8 @@ github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 h1:B+A58zGFuDrvE
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -103,8 +101,8 @@ github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQ
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
@@ -152,24 +150,24 @@ github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.m
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I=
golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

View File

@@ -644,7 +644,7 @@ func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalC
if strings.HasPrefix(trimmed, "exec-once = systemctl --user start") {
startupSectionFound = true
result = append(result, "exec-once = dms run")
result = append(result, "env = QT_QPA_PLATFORM,wayland")
result = append(result, "env = QT_QPA_PLATFORM,wayland;xcb")
result = append(result, "env = ELECTRON_OZONE_PLATFORM_HINT,auto")
result = append(result, "env = QT_QPA_PLATFORMTHEME,gtk3")
result = append(result, "env = QT_QPA_PLATFORMTHEME_QT6,gtk3")
@@ -659,7 +659,7 @@ func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalC
if strings.Contains(line, "STARTUP APPS") {
insertLines := []string{
"exec-once = dms run",
"env = QT_QPA_PLATFORM,wayland",
"env = QT_QPA_PLATFORM,wayland;xcb",
"env = ELECTRON_OZONE_PLATFORM_HINT,auto",
"env = QT_QPA_PLATFORMTHEME,gtk3",
"env = QT_QPA_PLATFORMTHEME_QT6,gtk3",
@@ -677,7 +677,7 @@ func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalC
func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalCommand string) string {
envVars := fmt.Sprintf(`environment {
XDG_CURRENT_DESKTOP "niri"
QT_QPA_PLATFORM "wayland"
QT_QPA_PLATFORM "wayland;xcb"
ELECTRON_OZONE_PLATFORM_HINT "auto"
QT_QPA_PLATFORMTHEME "gtk3"
QT_QPA_PLATFORMTHEME_QT6 "gtk3"

View File

@@ -111,6 +111,7 @@ windowrule = float on, match:class ^(zoom)$
# windowrule = float on, match:class ^(org.quickshell)$
layerrule = no_anim on, match:namespace ^(quickshell)$
layerrule = no_anim on, match:namespace ^dms:.*
source = ./dms/colors.conf
source = ./dms/outputs.conf

View File

@@ -430,7 +430,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
}
// 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, runtime.GOARCH, baseURL)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,

View File

@@ -22,6 +22,21 @@ func DetectDMSPath() (string, error) {
return config.LocateDMSConfig()
}
func DetectGreeterGroup() string {
data, err := os.ReadFile("/etc/group")
if err != nil {
fmt.Fprintln(os.Stderr, "⚠ Warning: could not read /etc/group, defaulting to greeter")
return "greeter"
}
if group, found := utils.FindGroupData(string(data), "greeter", "greetd", "_greeter"); found {
return group
}
fmt.Fprintln(os.Stderr, "⚠ Warning: no greeter group found in /etc/group, defaulting to greeter")
return "greeter"
}
// DetectCompositors checks which compositors are installed
func DetectCompositors() []string {
var compositors []string
@@ -194,14 +209,17 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
return fmt.Errorf("failed to create cache directory: %w", err)
}
if err := runSudoCmd(sudoPassword, "chown", "greeter:greeter", cacheDir); err != nil {
group := DetectGreeterGroup()
owner := fmt.Sprintf("%s:%s", group, group)
if err := runSudoCmd(sudoPassword, "chown", owner, cacheDir); err != nil {
return fmt.Errorf("failed to set cache directory owner: %w", err)
}
if err := runSudoCmd(sudoPassword, "chmod", "755", cacheDir); err != nil {
return fmt.Errorf("failed to set cache directory permissions: %w", err)
}
logFunc(fmt.Sprintf("✓ Created cache directory %s (owner: greeter:greeter, permissions: 755)", cacheDir))
logFunc(fmt.Sprintf("✓ Created cache directory %s (owner: %s, permissions: 755)", cacheDir, owner))
return nil
}
@@ -234,6 +252,8 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
{filepath.Join(homeDir, ".local", "share"), ".local/share directory"},
}
owner := DetectGreeterGroup()
logFunc("\nSetting up parent directory ACLs for greeter user access...")
for _, dir := range parentDirs {
@@ -245,9 +265,9 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
}
// Set ACL to allow greeter user read+execute permission (for session discovery)
if err := runSudoCmd(sudoPassword, "setfacl", "-m", "u:greeter:rx", dir.path); err != nil {
if err := runSudoCmd(sudoPassword, "setfacl", "-m", fmt.Sprintf("u:%s:rx", owner), dir.path); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err))
logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m u:greeter:x %s", dir.path))
logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m u:%s:x %s", owner, dir.path))
continue
}
@@ -271,17 +291,19 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
return fmt.Errorf("failed to determine current user")
}
group := DetectGreeterGroup()
// Check if user is already in greeter group
groupsCmd := exec.Command("groups", currentUser)
groupsOutput, err := groupsCmd.Output()
if err == nil && strings.Contains(string(groupsOutput), "greeter") {
logFunc(fmt.Sprintf("✓ %s is already in greeter group", currentUser))
if err == nil && strings.Contains(string(groupsOutput), group) {
logFunc(fmt.Sprintf("✓ %s is already in %s group", currentUser, group))
} else {
// Add current user to greeter group for file access permissions
if err := runSudoCmd(sudoPassword, "usermod", "-aG", "greeter", currentUser); err != nil {
return fmt.Errorf("failed to add %s to greeter group: %w", currentUser, err)
if err := runSudoCmd(sudoPassword, "usermod", "-aG", group, currentUser); err != nil {
return fmt.Errorf("failed to add %s to %s group: %w", currentUser, group, err)
}
logFunc(fmt.Sprintf("✓ Added %s to greeter group (logout/login required for changes to take effect)", currentUser))
logFunc(fmt.Sprintf("✓ Added %s to %s group (logout/login required for changes to take effect)", currentUser, group))
}
configDirs := []struct {
@@ -304,7 +326,7 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
}
}
if err := runSudoCmd(sudoPassword, "chgrp", "-R", "greeter", dir.path); err != nil {
if err := runSudoCmd(sudoPassword, "chgrp", "-R", group, dir.path); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: Failed to set group for %s: %v", dir.desc, err))
continue
}
@@ -436,10 +458,11 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
}
greeterDir := "/etc/greetd/niri"
greeterGroup := DetectGreeterGroup()
if err := runSudoCmd(sudoPassword, "mkdir", "-p", greeterDir); err != nil {
return fmt.Errorf("failed to create greetd niri directory: %w", err)
}
if err := runSudoCmd(sudoPassword, "chown", "root:greeter", greeterDir); err != nil {
if err := runSudoCmd(sudoPassword, "chown", fmt.Sprintf("root:%s", greeterGroup), greeterDir); err != nil {
return fmt.Errorf("failed to set greetd niri directory ownership: %w", err)
}
if err := runSudoCmd(sudoPassword, "chmod", "755", greeterDir); err != nil {
@@ -464,7 +487,7 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
if err := backupFileIfExists(sudoPassword, dmsPath, ".backup"); err != nil {
return fmt.Errorf("failed to backup %s: %w", dmsPath, err)
}
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", "greeter", "-m", "0644", dmsTemp.Name(), dmsPath); err != nil {
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", dmsTemp.Name(), dmsPath); err != nil {
return fmt.Errorf("failed to install greetd niri dms config: %w", err)
}
@@ -487,7 +510,7 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
if err := backupFileIfExists(sudoPassword, mainPath, ".backup"); err != nil {
return fmt.Errorf("failed to backup %s: %w", mainPath, err)
}
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", "greeter", "-m", "0644", mainTemp.Name(), mainPath); err != nil {
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", mainTemp.Name(), mainPath); err != nil {
return fmt.Errorf("failed to install greetd niri main config: %w", err)
}
@@ -736,17 +759,19 @@ func ConfigureGreetd(dmsPath, compositor string, logFunc func(string), sudoPassw
logFunc(fmt.Sprintf("✓ Backed up existing config to %s", backupPath))
}
greeterUser := DetectGreeterGroup()
var configContent string
if data, err := os.ReadFile(configPath); err == nil {
configContent = string(data)
} else {
configContent = `[terminal]
configContent = fmt.Sprintf(`[terminal]
vt = 1
[default_session]
user = "greeter"
`
user = "%s"
`, greeterUser)
}
lines := strings.Split(configContent, "\n")
@@ -755,7 +780,7 @@ user = "greeter"
trimmed := strings.TrimSpace(line)
if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") {
if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") {
newLines = append(newLines, `user = "greeter"`)
newLines = append(newLines, fmt.Sprintf(`user = "%s"`, greeterUser))
} else {
newLines = append(newLines, line)
}
@@ -807,7 +832,7 @@ user = "greeter"
return fmt.Errorf("failed to move config to /etc/greetd: %w", err)
}
logFunc(fmt.Sprintf("✓ Updated greetd configuration (user: greeter, command: %s --command %s -p %s)", wrapperCmd, compositorLower, dmsPath))
logFunc(fmt.Sprintf("✓ Updated greetd configuration (user: %s, command: %s --command %s -p %s)", greeterUser, wrapperCmd, compositorLower, dmsPath))
return nil
}

View File

@@ -0,0 +1,95 @@
package providers
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
type MiracleProvider struct {
configPath string
}
func NewMiracleProvider(configPath string) *MiracleProvider {
if configPath == "" {
configDir, err := os.UserConfigDir()
if err == nil {
configPath = filepath.Join(configDir, "miracle-wm")
}
}
return &MiracleProvider{configPath: configPath}
}
func (m *MiracleProvider) Name() string {
return "miracle"
}
func (m *MiracleProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
config, err := ParseMiracleConfig(m.configPath)
if err != nil {
return nil, fmt.Errorf("failed to parse miracle-wm config: %w", err)
}
bindings := MiracleConfigToBindings(config)
categorizedBinds := make(map[string][]keybinds.Keybind)
for _, kb := range bindings {
category := m.categorizeAction(kb.Action)
bind := keybinds.Keybind{
Key: m.formatKey(kb),
Description: kb.Comment,
Action: kb.Action,
}
categorizedBinds[category] = append(categorizedBinds[category], bind)
}
return &keybinds.CheatSheet{
Title: "Miracle WM Keybinds",
Provider: m.Name(),
Binds: categorizedBinds,
}, nil
}
func (m *MiracleProvider) GetOverridePath() string {
expanded, err := utils.ExpandPath(m.configPath)
if err != nil {
return filepath.Join(m.configPath, "config.yaml")
}
return filepath.Join(expanded, "config.yaml")
}
func (m *MiracleProvider) formatKey(kb MiracleKeyBinding) string {
parts := make([]string, 0, len(kb.Mods)+1)
parts = append(parts, kb.Mods...)
parts = append(parts, kb.Key)
return strings.Join(parts, "+")
}
func (m *MiracleProvider) categorizeAction(action string) string {
switch {
case strings.HasPrefix(action, "select_workspace_") || strings.HasPrefix(action, "move_to_workspace_"):
return "Workspace"
case strings.Contains(action, "select_") || strings.Contains(action, "move_"):
return "Window"
case action == "toggle_resize" || strings.HasPrefix(action, "resize_"):
return "Window"
case action == "fullscreen" || action == "toggle_floating" || action == "quit_active_window" || action == "toggle_pinned_to_workspace":
return "Window"
case action == "toggle_tabbing" || action == "toggle_stacking" || action == "request_vertical" || action == "request_horizontal":
return "Layout"
case action == "quit_compositor":
return "System"
case action == "terminal":
return "Execute"
case strings.HasPrefix(action, "magnifier_"):
return "Accessibility"
case strings.HasPrefix(action, "dms ") || strings.Contains(action, "dms ipc"):
return "Execute"
default:
return "Execute"
}
}

View File

@@ -0,0 +1,320 @@
package providers
import (
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"gopkg.in/yaml.v3"
)
type MiracleConfig struct {
Terminal string `yaml:"terminal"`
ActionKey string `yaml:"action_key"`
DefaultActionOverrides []MiracleActionOverride `yaml:"default_action_overrides"`
CustomActions []MiracleCustomAction `yaml:"custom_actions"`
}
type MiracleActionOverride struct {
Name string `yaml:"name"`
Action string `yaml:"action"`
Modifiers []string `yaml:"modifiers"`
Key string `yaml:"key"`
}
type MiracleCustomAction struct {
Command string `yaml:"command"`
Action string `yaml:"action"`
Modifiers []string `yaml:"modifiers"`
Key string `yaml:"key"`
}
type MiracleKeyBinding struct {
Mods []string
Key string
Action string
Comment string
}
var miracleDefaultBinds = []MiracleKeyBinding{
{Mods: []string{"Super"}, Key: "Return", Action: "terminal", Comment: "Open terminal"},
{Mods: []string{"Super"}, Key: "v", Action: "request_vertical", Comment: "Layout windows vertically"},
{Mods: []string{"Super"}, Key: "h", Action: "request_horizontal", Comment: "Layout windows horizontally"},
{Mods: []string{"Super"}, Key: "Up", Action: "select_up", Comment: "Select window above"},
{Mods: []string{"Super"}, Key: "Down", Action: "select_down", Comment: "Select window below"},
{Mods: []string{"Super"}, Key: "Left", Action: "select_left", Comment: "Select window left"},
{Mods: []string{"Super"}, Key: "Right", Action: "select_right", Comment: "Select window right"},
{Mods: []string{"Super", "Shift"}, Key: "Up", Action: "move_up", Comment: "Move window up"},
{Mods: []string{"Super", "Shift"}, Key: "Down", Action: "move_down", Comment: "Move window down"},
{Mods: []string{"Super", "Shift"}, Key: "Left", Action: "move_left", Comment: "Move window left"},
{Mods: []string{"Super", "Shift"}, Key: "Right", Action: "move_right", Comment: "Move window right"},
{Mods: []string{"Super"}, Key: "r", Action: "toggle_resize", Comment: "Toggle resize mode"},
{Mods: []string{"Super"}, Key: "f", Action: "fullscreen", Comment: "Toggle fullscreen"},
{Mods: []string{"Super", "Shift"}, Key: "q", Action: "quit_active_window", Comment: "Close window"},
{Mods: []string{"Super", "Shift"}, Key: "e", Action: "quit_compositor", Comment: "Exit compositor"},
{Mods: []string{"Super"}, Key: "Space", Action: "toggle_floating", Comment: "Toggle floating"},
{Mods: []string{"Super", "Shift"}, Key: "p", Action: "toggle_pinned_to_workspace", Comment: "Toggle pinned to workspace"},
{Mods: []string{"Super"}, Key: "w", Action: "toggle_tabbing", Comment: "Toggle tabbing layout"},
{Mods: []string{"Super"}, Key: "s", Action: "toggle_stacking", Comment: "Toggle stacking layout"},
{Mods: []string{"Super"}, Key: "1", Action: "select_workspace_0", Comment: "Workspace 1"},
{Mods: []string{"Super"}, Key: "2", Action: "select_workspace_1", Comment: "Workspace 2"},
{Mods: []string{"Super"}, Key: "3", Action: "select_workspace_2", Comment: "Workspace 3"},
{Mods: []string{"Super"}, Key: "4", Action: "select_workspace_3", Comment: "Workspace 4"},
{Mods: []string{"Super"}, Key: "5", Action: "select_workspace_4", Comment: "Workspace 5"},
{Mods: []string{"Super"}, Key: "6", Action: "select_workspace_5", Comment: "Workspace 6"},
{Mods: []string{"Super"}, Key: "7", Action: "select_workspace_6", Comment: "Workspace 7"},
{Mods: []string{"Super"}, Key: "8", Action: "select_workspace_7", Comment: "Workspace 8"},
{Mods: []string{"Super"}, Key: "9", Action: "select_workspace_8", Comment: "Workspace 9"},
{Mods: []string{"Super"}, Key: "0", Action: "select_workspace_9", Comment: "Workspace 10"},
{Mods: []string{"Super", "Shift"}, Key: "1", Action: "move_to_workspace_0", Comment: "Move to workspace 1"},
{Mods: []string{"Super", "Shift"}, Key: "2", Action: "move_to_workspace_1", Comment: "Move to workspace 2"},
{Mods: []string{"Super", "Shift"}, Key: "3", Action: "move_to_workspace_2", Comment: "Move to workspace 3"},
{Mods: []string{"Super", "Shift"}, Key: "4", Action: "move_to_workspace_3", Comment: "Move to workspace 4"},
{Mods: []string{"Super", "Shift"}, Key: "5", Action: "move_to_workspace_4", Comment: "Move to workspace 5"},
{Mods: []string{"Super", "Shift"}, Key: "6", Action: "move_to_workspace_5", Comment: "Move to workspace 6"},
{Mods: []string{"Super", "Shift"}, Key: "7", Action: "move_to_workspace_6", Comment: "Move to workspace 7"},
{Mods: []string{"Super", "Shift"}, Key: "8", Action: "move_to_workspace_7", Comment: "Move to workspace 8"},
{Mods: []string{"Super", "Shift"}, Key: "9", Action: "move_to_workspace_8", Comment: "Move to workspace 9"},
{Mods: []string{"Super", "Shift"}, Key: "0", Action: "move_to_workspace_9", Comment: "Move to workspace 10"},
}
func ParseMiracleConfig(configPath string) (*MiracleConfig, error) {
expanded, err := utils.ExpandPath(configPath)
if err != nil {
return nil, err
}
info, err := os.Stat(expanded)
if err != nil {
return nil, err
}
var configFile string
if info.IsDir() {
configFile = filepath.Join(expanded, "config.yaml")
} else {
configFile = expanded
}
data, err := os.ReadFile(configFile)
if err != nil {
return nil, err
}
var config MiracleConfig
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, err
}
if config.ActionKey == "" {
config.ActionKey = "meta"
}
return &config, nil
}
func resolveMiracleModifier(mod, actionKey string) string {
switch mod {
case "primary":
return resolveActionKey(actionKey)
case "alt", "alt_left", "alt_right":
return "Alt"
case "shift", "shift_left", "shift_right":
return "Shift"
case "ctrl", "ctrl_left", "ctrl_right":
return "Ctrl"
case "meta", "meta_left", "meta_right":
return "Super"
default:
return mod
}
}
func resolveActionKey(actionKey string) string {
switch actionKey {
case "meta":
return "Super"
case "alt":
return "Alt"
case "ctrl":
return "Ctrl"
default:
return "Super"
}
}
func miracleKeyCodeToName(keyCode string) string {
name := strings.TrimPrefix(keyCode, "KEY_")
name = strings.ToLower(name)
switch name {
case "enter":
return "Return"
case "space":
return "Space"
case "up":
return "Up"
case "down":
return "Down"
case "left":
return "Left"
case "right":
return "Right"
case "tab":
return "Tab"
case "escape", "esc":
return "Escape"
case "delete":
return "Delete"
case "backspace":
return "BackSpace"
case "home":
return "Home"
case "end":
return "End"
case "pageup":
return "Page_Up"
case "pagedown":
return "Page_Down"
case "print":
return "Print"
case "pause":
return "Pause"
case "volumeup":
return "XF86AudioRaiseVolume"
case "volumedown":
return "XF86AudioLowerVolume"
case "mute":
return "XF86AudioMute"
case "micmute":
return "XF86AudioMicMute"
case "brightnessup":
return "XF86MonBrightnessUp"
case "brightnessdown":
return "XF86MonBrightnessDown"
case "kbdillumup":
return "XF86KbdBrightnessUp"
case "kbdillumdown":
return "XF86KbdBrightnessDown"
case "comma":
return "comma"
case "minus":
return "minus"
case "equal":
return "equal"
}
if len(name) == 1 {
return name
}
return name
}
func MiracleConfigToBindings(config *MiracleConfig) []MiracleKeyBinding {
overridden := make(map[string]bool)
var bindings []MiracleKeyBinding
for _, override := range config.DefaultActionOverrides {
mods := make([]string, 0, len(override.Modifiers))
for _, mod := range override.Modifiers {
mods = append(mods, resolveMiracleModifier(mod, config.ActionKey))
}
bindings = append(bindings, MiracleKeyBinding{
Mods: mods,
Key: miracleKeyCodeToName(override.Key),
Action: override.Name,
Comment: miracleActionDescription(override.Name),
})
overridden[override.Name] = true
}
for _, def := range miracleDefaultBinds {
if overridden[def.Action] {
continue
}
bindings = append(bindings, def)
}
for _, custom := range config.CustomActions {
mods := make([]string, 0, len(custom.Modifiers))
for _, mod := range custom.Modifiers {
mods = append(mods, resolveMiracleModifier(mod, config.ActionKey))
}
bindings = append(bindings, MiracleKeyBinding{
Mods: mods,
Key: miracleKeyCodeToName(custom.Key),
Action: custom.Command,
Comment: custom.Command,
})
}
return bindings
}
func miracleActionDescription(action string) string {
switch action {
case "terminal":
return "Open terminal"
case "request_vertical":
return "Layout windows vertically"
case "request_horizontal":
return "Layout windows horizontally"
case "select_up":
return "Select window above"
case "select_down":
return "Select window below"
case "select_left":
return "Select window left"
case "select_right":
return "Select window right"
case "move_up":
return "Move window up"
case "move_down":
return "Move window down"
case "move_left":
return "Move window left"
case "move_right":
return "Move window right"
case "toggle_resize":
return "Toggle resize mode"
case "fullscreen":
return "Toggle fullscreen"
case "quit_active_window":
return "Close window"
case "quit_compositor":
return "Exit compositor"
case "toggle_floating":
return "Toggle floating"
case "toggle_pinned_to_workspace":
return "Toggle pinned to workspace"
case "toggle_tabbing":
return "Toggle tabbing layout"
case "toggle_stacking":
return "Toggle stacking layout"
case "magnifier_on":
return "Enable magnifier"
case "magnifier_off":
return "Disable magnifier"
case "magnifier_increase_size":
return "Increase magnifier area"
case "magnifier_decrease_size":
return "Decrease magnifier area"
case "magnifier_increase_scale":
return "Increase magnifier scale"
case "magnifier_decrease_scale":
return "Decrease magnifier scale"
}
if num, ok := strings.CutPrefix(action, "select_workspace_"); ok {
return "Workspace " + num
}
if num, ok := strings.CutPrefix(action, "move_to_workspace_"); ok {
return "Move to workspace " + num
}
return action
}

View File

@@ -3,6 +3,7 @@ package providers
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
@@ -18,14 +19,21 @@ func NewSwayProvider(configPath string) *SwayProvider {
_, scrollEnvSet := os.LookupEnv("SCROLLSOCK")
if configPath == "" {
configDir, err := os.UserConfigDir()
if err != nil {
configDir = ""
}
if scrollEnvSet {
configPath = "$HOME/.config/scroll"
if configDir != "" {
configPath = filepath.Join(configDir, "scroll")
}
isScroll = true
} else {
configPath = "$HOME/.config/sway"
if configDir != "" {
configPath = filepath.Join(configDir, "sway")
}
}
} else {
// Determine isScroll based on the provided config path
isScroll = strings.Contains(configPath, "scroll")
}
@@ -36,16 +44,16 @@ func NewSwayProvider(configPath string) *SwayProvider {
}
func (s *SwayProvider) Name() string {
if s != nil && s.isScroll {
return "scroll"
}
if s == nil {
_, ok := os.LookupEnv("SCROLLSOCK")
if ok {
if os.Getenv("SCROLLSOCK") != "" {
return "scroll"
}
return "sway"
}
if s.isScroll {
return "scroll"
}
return "sway"
}

View File

@@ -15,8 +15,13 @@ func TestSwayProviderName(t *testing.T) {
func TestSwayProviderDefaultPath(t *testing.T) {
provider := NewSwayProvider("")
if provider.configPath != "$HOME/.config/sway" {
t.Errorf("configPath = %q, want %q", provider.configPath, "$HOME/.config/sway")
configDir, err := os.UserConfigDir()
if err != nil {
t.Skip("UserConfigDir not available")
}
expected := filepath.Join(configDir, "sway")
if provider.configPath != expected {
t.Errorf("configPath = %q, want %q", provider.configPath, expected)
}
}

View File

@@ -33,6 +33,7 @@ const (
TemplateKindTerminal
TemplateKindGTK
TemplateKindVSCode
TemplateKindEmacs
)
type TemplateDef struct {
@@ -65,7 +66,7 @@ var templateRegistry = []TemplateDef{
{ID: "dgop", Commands: []string{"dgop"}, ConfigFile: "dgop.toml"},
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
{ID: "vscode", Kind: TemplateKindVSCode},
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml"},
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs},
}
func (c *ColorMode) GTKTheme() string {
@@ -78,8 +79,10 @@ func (c *ColorMode) GTKTheme() string {
}
var (
matugenVersionOnce sync.Once
matugenVersionMu sync.Mutex
matugenVersionOK bool
matugenSupportsCOE bool
matugenIsV4 bool
)
type Options struct {
@@ -268,7 +271,7 @@ func buildOnce(opts *Options) error {
refreshQt6ct()
}
signalTerminals()
signalTerminals(opts)
return nil
}
@@ -333,6 +336,10 @@ output_path = '%s'
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "vscode-insiders", filepath.Join(homeDir, ".vscode-insiders/extensions"), opts.ShellDir)
case TemplateKindEmacs:
if utils.EmacsConfigDir() != "" {
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
}
default:
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
}
@@ -490,6 +497,9 @@ func substituteVars(content, shellDir string) string {
result = strings.ReplaceAll(result, "'CONFIG_DIR/", "'"+utils.XDGConfigHome()+"/")
result = strings.ReplaceAll(result, "'DATA_DIR/", "'"+utils.XDGDataHome()+"/")
result = strings.ReplaceAll(result, "'CACHE_DIR/", "'"+utils.XDGCacheHome()+"/")
if emacsDir := utils.EmacsConfigDir(); emacsDir != "" {
result = strings.ReplaceAll(result, "'EMACS_DIR/", "'"+emacsDir+"/")
}
return result
}
@@ -510,67 +520,160 @@ func extractTOMLSection(content, startMarker, endMarker string) string {
return content[startIdx : startIdx+endIdx]
}
func checkMatugenVersion() {
matugenVersionOnce.Do(func() {
cmd := exec.Command("matugen", "--version")
output, err := cmd.Output()
if err != nil {
return
}
versionStr := strings.TrimSpace(string(output))
versionStr = strings.TrimPrefix(versionStr, "matugen ")
parts := strings.Split(versionStr, ".")
if len(parts) < 2 {
return
}
major, err := strconv.Atoi(parts[0])
if err != nil {
return
}
minor, err := strconv.Atoi(parts[1])
if err != nil {
return
}
matugenSupportsCOE = major > 3 || (major == 3 && minor >= 1)
if matugenSupportsCOE {
log.Infof("Matugen %s supports --continue-on-error", versionStr)
}
})
type matugenFlags struct {
supportsCOE bool
isV4 bool
}
func runMatugen(args []string) error {
checkMatugenVersion()
func detectMatugenVersion() (matugenFlags, error) {
matugenVersionMu.Lock()
defer matugenVersionMu.Unlock()
if matugenSupportsCOE {
args = append([]string{"--continue-on-error"}, args...)
if matugenVersionOK {
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
}
return detectMatugenVersionLocked()
}
func redetectMatugenVersion(old matugenFlags) (matugenFlags, bool) {
matugenVersionMu.Lock()
defer matugenVersionMu.Unlock()
matugenVersionOK = false
flags, err := detectMatugenVersionLocked()
if err != nil {
return old, false
}
changed := flags.supportsCOE != old.supportsCOE || flags.isV4 != old.isV4
return flags, changed
}
func detectMatugenVersionLocked() (matugenFlags, error) {
cmd := exec.Command("matugen", "--version")
output, err := cmd.Output()
if err != nil {
return matugenFlags{}, fmt.Errorf("failed to get matugen version: %w", err)
}
versionStr := strings.TrimSpace(string(output))
versionStr = strings.TrimPrefix(versionStr, "matugen ")
parts := strings.Split(versionStr, ".")
if len(parts) < 2 {
return matugenFlags{}, fmt.Errorf("unexpected matugen version format: %q", versionStr)
}
major, err := strconv.Atoi(parts[0])
if err != nil {
return matugenFlags{}, fmt.Errorf("failed to parse matugen major version %q: %w", parts[0], err)
}
minor, err := strconv.Atoi(parts[1])
if err != nil {
return matugenFlags{}, fmt.Errorf("failed to parse matugen minor version %q: %w", parts[1], err)
}
matugenSupportsCOE = major > 3 || (major == 3 && minor >= 1)
matugenIsV4 = major >= 4
matugenVersionOK = true
if matugenSupportsCOE {
log.Infof("Matugen %s supports --continue-on-error", versionStr)
}
if matugenIsV4 {
log.Infof("Matugen %s: using v4 flags", versionStr)
}
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
}
func buildMatugenArgs(baseArgs []string, flags matugenFlags) []string {
args := make([]string, 0, len(baseArgs)+4)
if flags.supportsCOE {
args = append(args, "--continue-on-error")
}
args = append(args, baseArgs...)
if flags.isV4 {
args = append(args, "--source-color-index", "0")
}
return args
}
func runMatugen(baseArgs []string) error {
flags, err := detectMatugenVersion()
if err != nil {
return err
}
args := buildMatugenArgs(baseArgs, flags)
cmd := exec.Command("matugen", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
runErr := cmd.Run()
if runErr == nil {
return nil
}
log.Warnf("Matugen failed (v4=%v): %v", flags.isV4, runErr)
newFlags, changed := redetectMatugenVersion(flags)
if !changed {
return runErr
}
log.Warnf("Matugen version changed (v4: %v -> %v), retrying", flags.isV4, newFlags.isV4)
args = buildMatugenArgs(baseArgs, newFlags)
retryCmd := exec.Command("matugen", args...)
retryCmd.Stdout = os.Stdout
retryCmd.Stderr = os.Stderr
return retryCmd.Run()
}
func runMatugenDryRun(opts *Options) (string, error) {
var args []string
switch opts.Kind {
case "hex":
args = []string{"color", "hex", opts.Value}
default:
args = []string{opts.Kind, opts.Value}
}
args = append(args, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
cmd := exec.Command("matugen", args...)
output, err := cmd.Output()
flags, err := detectMatugenVersion()
if err != nil {
return "", err
}
output, dryErr := execDryRun(opts, flags)
if dryErr == nil {
return output, nil
}
log.Warnf("Matugen dry-run failed (v4=%v): %v", flags.isV4, dryErr)
newFlags, changed := redetectMatugenVersion(flags)
if !changed {
return "", dryErr
}
log.Warnf("Matugen version changed (v4: %v -> %v), retrying dry-run", flags.isV4, newFlags.isV4)
return execDryRun(opts, newFlags)
}
func execDryRun(opts *Options, flags matugenFlags) (string, error) {
var baseArgs []string
switch opts.Kind {
case "hex":
baseArgs = []string{"color", "hex", opts.Value}
default:
baseArgs = []string{opts.Kind, opts.Value}
}
baseArgs = append(baseArgs, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
if flags.isV4 {
baseArgs = append(baseArgs, "--source-color-index", "0", "--old-json-output")
}
cmd := exec.Command("matugen", baseArgs...)
var stderr strings.Builder
cmd.Stderr = &stderr
output, err := cmd.Output()
if err != nil {
if stderr.Len() > 0 {
return "", fmt.Errorf("matugen %v failed (v4=%v): %s", baseArgs, flags.isV4, strings.TrimSpace(stderr.String()))
}
return "", fmt.Errorf("matugen %v failed (v4=%v): %w", baseArgs, flags.isV4, err)
}
return strings.ReplaceAll(string(output), "\n", ""), nil
}
@@ -692,11 +795,15 @@ func refreshQt6ct() {
}
}
func signalTerminals() {
signalByName("kitty", syscall.SIGUSR1)
signalByName("ghostty", syscall.SIGUSR2)
signalByName(".kitty-wrapped", syscall.SIGUSR1)
signalByName(".ghostty-wrappe", syscall.SIGUSR2)
func signalTerminals(opts *Options) {
if !opts.ShouldSkipTemplate("kitty") && appExists(opts.AppChecker, []string{"kitty"}, nil) {
signalByName("kitty", syscall.SIGUSR1)
signalByName(".kitty-wrapped", syscall.SIGUSR1)
}
if !opts.ShouldSkipTemplate("ghostty") && appExists(opts.AppChecker, []string{"ghostty"}, nil) {
signalByName("ghostty", syscall.SIGUSR2)
signalByName(".ghostty-wrappe", syscall.SIGUSR2)
}
}
func signalByName(name string, sig syscall.Signal) {
@@ -802,6 +909,8 @@ func CheckTemplates(checker utils.AppChecker) []TemplateCheck {
detected = true
case tmpl.Kind == TemplateKindVSCode:
detected = checkVSCodeExtension(homeDir)
case tmpl.Kind == TemplateKindEmacs:
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks) && utils.EmacsConfigDir() != ""
default:
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks)
}

View File

@@ -15,6 +15,9 @@ const (
notifyDest = "org.freedesktop.Notifications"
notifyPath = "/org/freedesktop/Notifications"
notifyInterface = "org.freedesktop.Notifications"
maxSummaryLen = 29
maxBodyLen = 80
)
type Notification struct {
@@ -39,6 +42,13 @@ func Send(n Notification) error {
n.Timeout = 5000
}
if len(n.Summary) > maxSummaryLen {
n.Summary = n.Summary[:maxSummaryLen-3] + "..."
}
if len(n.Body) > maxBodyLen {
n.Body = n.Body[:maxBodyLen-3] + "..."
}
var actions []string
if n.FilePath != "" {
actions = []string{

View File

@@ -21,6 +21,7 @@ const (
CompositorNiri
CompositorDWL
CompositorScroll
CompositorMiracle
)
var detectedCompositor Compositor = -1
@@ -34,6 +35,7 @@ func DetectCompositor() Compositor {
niriSocket := os.Getenv("NIRI_SOCKET")
swaySocket := os.Getenv("SWAYSOCK")
scrollSocket := os.Getenv("SCROLLSOCK")
miracleSocket := os.Getenv("MIRACLESOCK")
switch {
case niriSocket != "":
@@ -46,7 +48,11 @@ func DetectCompositor() Compositor {
detectedCompositor = CompositorScroll
return detectedCompositor
}
case miracleSocket != "":
if _, err := os.Stat(miracleSocket); err == nil {
detectedCompositor = CompositorMiracle
return detectedCompositor
}
case swaySocket != "":
if _, err := os.Stat(swaySocket); err == nil {
detectedCompositor = CompositorSway
@@ -260,6 +266,25 @@ func getScrollFocusedMonitor() string {
return ""
}
func getMiracleFocusedMonitor() string {
output, err := exec.Command("miraclemsg", "-t", "get_workspaces").Output()
if err != nil {
return ""
}
var workspaces []swayWorkspace
if err := json.Unmarshal(output, &workspaces); err != nil {
return ""
}
for _, ws := range workspaces {
if ws.Focused {
return ws.Output
}
}
return ""
}
type niriWorkspace struct {
Output string `json:"output"`
IsFocused bool `json:"is_focused"`
@@ -407,6 +432,8 @@ func GetFocusedMonitor() string {
return getSwayFocusedMonitor()
case CompositorScroll:
return getScrollFocusedMonitor()
case CompositorMiracle:
return getMiracleFocusedMonitor()
case CompositorNiri:
return getNiriFocusedMonitor()
case CompositorDWL:

View File

@@ -108,7 +108,7 @@ func NewRegionSelector(s *Screenshoter) *RegionSelector {
screenshoter: s,
outputs: make(map[uint32]*WaylandOutput),
preCapture: make(map[*WaylandOutput]*PreCapture),
showCapturedCursor: true,
showCapturedCursor: s.config.Cursor == CursorOn,
}
}

View File

@@ -453,10 +453,7 @@ func (s *Screenshoter) blitBuffer(dst, src *ShmBuffer, dstX, dstY int, yInverted
}
func (s *Screenshoter) captureWholeOutput(output *WaylandOutput) (*CaptureResult, error) {
cursor := int32(0)
if s.config.IncludeCursor {
cursor = 1
}
cursor := int32(s.config.Cursor)
frame, err := s.screencopy.CaptureOutput(cursor, output.wlOutput)
if err != nil {
@@ -624,10 +621,7 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio
}
}
cursor := int32(0)
if s.config.IncludeCursor {
cursor = 1
}
cursor := int32(s.config.Cursor)
frame, err := s.screencopy.CaptureOutputRegion(cursor, output.wlOutput, localX, localY, w, h)
if err != nil {

View File

@@ -19,6 +19,13 @@ const (
FormatPPM
)
type CursorMode int
const (
CursorOff CursorMode = iota
CursorOn
)
type Region struct {
X int32 `json:"x"`
Y int32 `json:"y"`
@@ -42,29 +49,29 @@ type Output struct {
}
type Config struct {
Mode Mode
OutputName string
IncludeCursor bool
Format Format
Quality int
OutputDir string
Filename string
Clipboard bool
SaveFile bool
Notify bool
Stdout bool
Mode Mode
OutputName string
Cursor CursorMode
Format Format
Quality int
OutputDir string
Filename string
Clipboard bool
SaveFile bool
Notify bool
Stdout bool
}
func DefaultConfig() Config {
return Config{
Mode: ModeRegion,
IncludeCursor: false,
Format: FormatPNG,
Quality: 90,
OutputDir: "",
Filename: "",
Clipboard: true,
SaveFile: true,
Notify: true,
Mode: ModeRegion,
Cursor: CursorOff,
Format: FormatPNG,
Quality: 90,
OutputDir: "",
Filename: "",
Clipboard: true,
SaveFile: true,
Notify: true,
}
}

View File

@@ -96,6 +96,12 @@ func (m *Manager) Rescan() {
}
}
if m.sysfsReady && m.sysfsBackend != nil {
if err := m.sysfsBackend.Rescan(); err != nil {
log.Debugf("Sysfs rescan failed: %v", err)
}
}
m.updateState()
}

View File

@@ -101,6 +101,10 @@ func shouldSuppressDevice(name string) bool {
return false
}
func (b *SysfsBackend) Rescan() error {
return b.scanDevices()
}
func (b *SysfsBackend) GetDevices() ([]Device, error) {
devices := make([]Device, 0)

View File

@@ -52,11 +52,31 @@ func (m *Manager) initializeScreensaver() error {
return nil
}
screensaverIface := introspect.Interface{
Name: dbusScreensaverInterface,
Methods: []introspect.Method{
{
Name: "Inhibit",
Args: []introspect.Arg{
{Name: "application_name", Type: "s", Direction: "in"},
{Name: "reason_for_inhibit", Type: "s", Direction: "in"},
{Name: "cookie", Type: "u", Direction: "out"},
},
},
{
Name: "UnInhibit",
Args: []introspect.Arg{
{Name: "cookie", Type: "u", Direction: "in"},
},
},
},
}
introNode := &introspect.Node{
Name: dbusScreensaverPath,
Interfaces: []introspect.Interface{
introspect.IntrospectData,
{Name: dbusScreensaverInterface},
screensaverIface,
},
}
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode), dbusScreensaverPath, "org.freedesktop.DBus.Introspectable"); err != nil {
@@ -67,7 +87,7 @@ func (m *Manager) initializeScreensaver() error {
Name: dbusScreensaverPath2,
Interfaces: []introspect.Interface{
introspect.IntrospectData,
{Name: dbusScreensaverInterface},
screensaverIface,
},
}
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode2), dbusScreensaverPath2, "org.freedesktop.DBus.Introspectable"); err != nil {

View File

@@ -32,8 +32,10 @@ type SecretAgent struct {
backend *NetworkManagerBackend
}
type nmVariantMap map[string]dbus.Variant
type nmSettingMap map[string]nmVariantMap
type (
nmVariantMap map[string]dbus.Variant
nmSettingMap map[string]nmVariantMap
)
const introspectXML = `
<node>
@@ -122,7 +124,7 @@ func (a *SecretAgent) GetSecrets(
connType, displayName, vpnSvc := readConnTypeAndName(conn)
ssid := readSSID(conn)
fields := fieldsNeeded(settingName, hints)
fields := fieldsNeeded(settingName, hints, conn)
vpnPasswordFlags := readVPNPasswordFlags(conn, settingName)
log.Infof("[SecretAgent] connType=%s, name=%s, vpnSvc=%s, fields=%v, flags=%d, vpnPasswordFlags=%d", connType, displayName, vpnSvc, fields, flags, vpnPasswordFlags)
@@ -218,8 +220,16 @@ func (a *SecretAgent) GetSecrets(
out[settingName] = nmVariantMap{}
return out, nil
} else if passwordFlags&NM_SETTING_SECRET_FLAG_AGENT_OWNED != 0 {
log.Warnf("[SecretAgent] Secrets are agent-owned but we don't store secrets (flags=%d) - returning NoSecrets error", passwordFlags)
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
switch settingName {
case "802-11-wireless-security":
fields = []string{"psk"}
case "802-1x":
fields = infer8021xFields(conn)
default:
log.Warnf("[SecretAgent] Agent-owned secrets for unhandled setting %s (flags=%d)", settingName, passwordFlags)
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
}
log.Infof("[SecretAgent] Agent-owned secrets, inferred fields: %v", fields)
} else {
log.Infof("[SecretAgent] No secrets needed, using system stored secrets (flags=%d)", passwordFlags)
out := nmSettingMap{}
@@ -300,6 +310,63 @@ func (a *SecretAgent) GetSecrets(
return out, nil
}
a.backend.cachedVPNCredsMu.Unlock()
a.backend.cachedGPSamlMu.Lock()
cachedGPSaml := a.backend.cachedGPSamlCookie
if cachedGPSaml != nil && cachedGPSaml.ConnectionUUID == connUuid {
a.backend.cachedGPSamlMu.Unlock()
log.Infof("[SecretAgent] Using cached GlobalProtect SAML cookie for %s", connUuid)
return buildGPSamlSecretsResponse(settingName, cachedGPSaml.Cookie, cachedGPSaml.Host, cachedGPSaml.Fingerprint), nil
}
a.backend.cachedGPSamlMu.Unlock()
if len(fields) == 1 && fields[0] == "gp-saml" {
gateway := ""
protocol := ""
if vpnSettings, ok := conn["vpn"]; ok {
if dataVariant, ok := vpnSettings["data"]; ok {
if dataMap, ok := dataVariant.Value().(map[string]string); ok {
if gw, ok := dataMap["gateway"]; ok {
gateway = gw
}
if proto, ok := dataMap["protocol"]; ok && proto != "" {
protocol = proto
}
}
}
}
if protocol != "gp" {
return nil, dbus.MakeFailedError(fmt.Errorf("gp-saml auth only supported for GlobalProtect (protocol=gp), got: %s", protocol))
}
log.Infof("[SecretAgent] Starting GlobalProtect SAML authentication for gateway=%s", gateway)
samlCtx, samlCancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer samlCancel()
authResult, err := a.backend.runGlobalProtectSAMLAuth(samlCtx, gateway, protocol)
if err != nil {
log.Warnf("[SecretAgent] GlobalProtect SAML authentication failed: %v", err)
return nil, dbus.MakeFailedError(fmt.Errorf("GlobalProtect SAML authentication failed: %w", err))
}
log.Infof("[SecretAgent] GlobalProtect SAML authentication successful, returning cookie to NetworkManager")
a.backend.cachedGPSamlMu.Lock()
a.backend.cachedGPSamlCookie = &cachedGPSamlCookie{
ConnectionUUID: connUuid,
Cookie: authResult.Cookie,
Host: authResult.Host,
User: authResult.User,
Fingerprint: authResult.Fingerprint,
}
a.backend.cachedGPSamlMu.Unlock()
return buildGPSamlSecretsResponse(settingName, authResult.Cookie, authResult.Host, authResult.Fingerprint), nil
}
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
@@ -418,8 +485,19 @@ func (a *SecretAgent) GetSecrets(
log.Infof("[SecretAgent] Cached PKCS11 PIN for potential re-request")
}
case "802-1x":
out[settingName] = sec
log.Infof("[SecretAgent] Returning 802-1x enterprise secrets with %d fields", len(sec))
secretsOnly := nmVariantMap{}
for k, v := range reply.Secrets {
switch k {
case "password", "private-key-password", "phase2-private-key-password", "pin":
secretsOnly[k] = dbus.MakeVariant(v)
}
}
out[settingName] = secretsOnly
if identity, ok := reply.Secrets["identity"]; ok && identity != "" {
a.save8021xIdentity(path, identity)
}
log.Infof("[SecretAgent] Returning 802-1x enterprise secrets with %d fields", len(secretsOnly))
default:
out[settingName] = sec
}
@@ -434,63 +512,6 @@ func (a *SecretAgent) GetSecrets(
}
a.backend.pendingVPNSaveMu.Unlock()
log.Infof("[SecretAgent] Queued credentials persist for after connection succeeds")
} else if reply.Save && settingName != "vpn" {
// Non-VPN save logic
go func() {
log.Infof("[SecretAgent] Persisting secrets with Update2: path=%s, setting=%s", path, settingName)
connObj := a.conn.Object("org.freedesktop.NetworkManager", path)
var existingSettings map[string]map[string]dbus.Variant
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0).Store(&existingSettings); err != nil {
log.Warnf("[SecretAgent] GetSettings failed: %v", err)
return
}
settings := make(map[string]map[string]dbus.Variant)
if connSection, ok := existingSettings["connection"]; ok {
settings["connection"] = connSection
}
switch settingName {
case "802-11-wireless-security":
wifiSec, ok := existingSettings["802-11-wireless-security"]
if !ok {
wifiSec = make(map[string]dbus.Variant)
}
wifiSec["psk-flags"] = dbus.MakeVariant(uint32(0))
if psk, ok := reply.Secrets["psk"]; ok {
wifiSec["psk"] = dbus.MakeVariant(psk)
log.Infof("[SecretAgent] Updated WiFi settings: psk-flags=0")
}
settings["802-11-wireless-security"] = wifiSec
case "802-1x":
dot1x, ok := existingSettings["802-1x"]
if !ok {
dot1x = make(map[string]dbus.Variant)
}
dot1x["password-flags"] = dbus.MakeVariant(uint32(0))
if password, ok := reply.Secrets["password"]; ok {
dot1x["password"] = dbus.MakeVariant(password)
log.Infof("[SecretAgent] Updated 802.1x settings: password-flags=0")
}
settings["802-1x"] = dot1x
}
// Call Update2 with correct signature:
// Update2(IN settings, IN flags, IN args) -> OUT result
// flags: 0x1 = to-disk
var result map[string]dbus.Variant
err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.Update2", 0,
settings, uint32(0x1), map[string]dbus.Variant{}).Store(&result)
if err != nil {
log.Warnf("[SecretAgent] Update2(to-disk) failed: %v", err)
} else {
log.Infof("[SecretAgent] Successfully persisted secrets to disk for %s", settingName)
}
}()
}
return out, nil
@@ -523,6 +544,35 @@ func (a *SecretAgent) Introspect() (string, *dbus.Error) {
return introspectXML, nil
}
func (a *SecretAgent) save8021xIdentity(path dbus.ObjectPath, identity string) {
connObj := a.conn.Object("org.freedesktop.NetworkManager", path)
var existing map[string]map[string]dbus.Variant
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0).Store(&existing); err != nil {
log.Warnf("[SecretAgent] Failed to get settings for identity save: %v", err)
return
}
settings := make(map[string]map[string]dbus.Variant)
if connSection, ok := existing["connection"]; ok {
settings["connection"] = connSection
}
dot1x, ok := existing["802-1x"]
if !ok {
dot1x = make(map[string]dbus.Variant)
}
dot1x["identity"] = dbus.MakeVariant(identity)
settings["802-1x"] = dot1x
var result map[string]dbus.Variant
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.Update2", 0,
settings, uint32(0x1), map[string]dbus.Variant{}).Store(&result); err != nil {
log.Warnf("[SecretAgent] Failed to save 802.1x identity: %v", err)
return
}
log.Infof("[SecretAgent] Saved 802.1x identity to connection profile")
}
func readSSID(conn map[string]nmVariantMap) string {
if w, ok := conn["802-11-wireless"]; ok {
if v, ok := w["ssid"]; ok {
@@ -564,12 +614,15 @@ func readConnTypeAndName(conn map[string]nmVariantMap) (string, string, string)
return connType, name, svc
}
func fieldsNeeded(setting string, hints []string) []string {
func fieldsNeeded(setting string, hints []string, conn map[string]nmVariantMap) []string {
switch setting {
case "802-11-wireless-security":
return []string{"psk"}
case "802-1x":
return []string{"identity", "password"}
if len(hints) > 0 {
return hints
}
return infer8021xFields(conn)
case "vpn":
return hints
default:
@@ -577,6 +630,41 @@ func fieldsNeeded(setting string, hints []string) []string {
}
}
func infer8021xFields(conn map[string]nmVariantMap) []string {
dot1x, ok := conn["802-1x"]
if !ok {
return []string{"identity", "password"}
}
var fields []string
if v, ok := dot1x["identity"]; ok {
if id, ok := v.Value().(string); ok && id != "" {
// identity already stored, don't ask again
} else {
fields = append(fields, "identity")
}
} else {
fields = append(fields, "identity")
}
var eapMethods []string
if v, ok := dot1x["eap"]; ok {
if methods, ok := v.Value().([]string); ok {
eapMethods = methods
}
}
switch {
case len(eapMethods) > 0 && eapMethods[0] == "tls":
fields = append(fields, "private-key-password")
default:
fields = append(fields, "password")
}
return fields
}
func buildFieldsInfo(setting string, fields []string, vpnService string) []FieldInfo {
result := make([]FieldInfo, 0, len(fields))
for _, f := range fields {
@@ -630,12 +718,25 @@ func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string {
switch {
case strings.Contains(vpnService, "openconnect"):
protocol := dataMap["protocol"]
authType := dataMap["authtype"]
userCert := dataMap["usercert"]
if authType == "cert" && strings.HasPrefix(userCert, "pkcs11:") {
username := dataMap["username"]
if authType == "cert" && strings.HasPrefix(dataMap["usercert"], "pkcs11:") {
return []string{"key_pass"}
}
if dataMap["username"] == "" {
if needsExternalBrowserAuth(protocol, authType, username, dataMap) {
switch protocol {
case "gp":
log.Infof("[SecretAgent] GlobalProtect SAML auth detected")
return []string{"gp-saml"}
default:
log.Infof("[SecretAgent] External browser auth detected for protocol '%s' but only GlobalProtect (gp) SAML is currently supported, falling back to credentials", protocol)
}
}
if username == "" {
fields = []string{"username", "password"}
}
case strings.Contains(vpnService, "openvpn"):
@@ -654,8 +755,31 @@ func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string {
return fields
}
func needsExternalBrowserAuth(protocol, authType, username string, data map[string]string) bool {
if method, ok := data["saml-auth-method"]; ok {
if method == "REDIRECT" || method == "POST" {
return true
}
}
if authType != "" && authType != "password" && authType != "cert" {
return true
}
switch protocol {
case "gp":
if authType == "" && username == "" {
return true
}
}
return false
}
func vpnFieldMeta(field, vpnService string) (label string, isSecret bool) {
switch field {
case "gp-saml":
return "GlobalProtect SAML/SSO", false
case "key_pass":
return "PIN", true
case "password":
@@ -756,3 +880,18 @@ func reasonFromFlags(flags uint32) string {
}
return "required"
}
func buildGPSamlSecretsResponse(settingName, cookie, host, fingerprint string) nmSettingMap {
out := nmSettingMap{}
vpnSec := nmVariantMap{}
secrets := map[string]string{
"cookie": cookie,
"gateway": host,
"gwcert": fingerprint,
}
vpnSec["secrets"] = dbus.MakeVariant(secrets)
out[settingName] = vpnSec
return out
}

View File

@@ -0,0 +1,355 @@
package network
import (
"testing"
"github.com/godbus/dbus/v5"
"github.com/stretchr/testify/assert"
)
func TestNeedsExternalBrowserAuth(t *testing.T) {
tests := []struct {
name string
protocol string
authType string
username string
data map[string]string
expected bool
}{
{
name: "GP with saml-auth-method REDIRECT",
protocol: "gp",
authType: "password",
username: "user",
data: map[string]string{"saml-auth-method": "REDIRECT"},
expected: true,
},
{
name: "GP with saml-auth-method POST",
protocol: "gp",
authType: "password",
username: "user",
data: map[string]string{"saml-auth-method": "POST"},
expected: true,
},
{
name: "GP with no authtype and no username",
protocol: "gp",
authType: "",
username: "",
data: map[string]string{},
expected: true,
},
{
name: "GP with username and password authtype",
protocol: "gp",
authType: "password",
username: "john",
data: map[string]string{},
expected: false,
},
{
name: "GP with username but no authtype",
protocol: "gp",
authType: "",
username: "john",
data: map[string]string{},
expected: false,
},
{
name: "GP with authtype but no username - should detect SAML",
protocol: "gp",
authType: "",
username: "",
data: map[string]string{},
expected: true,
},
{
name: "pulse with SAML",
protocol: "pulse",
authType: "",
username: "",
data: map[string]string{"saml-auth-method": "REDIRECT"},
expected: true,
},
{
name: "fortinet with non-password authtype",
protocol: "fortinet",
authType: "saml",
username: "",
data: map[string]string{},
expected: true,
},
{
name: "anyconnect with cert",
protocol: "anyconnect",
authType: "cert",
username: "",
data: map[string]string{},
expected: false,
},
{
name: "anyconnect with password",
protocol: "anyconnect",
authType: "password",
username: "user",
data: map[string]string{},
expected: false,
},
{
name: "empty protocol",
protocol: "",
authType: "",
username: "",
data: map[string]string{},
expected: false,
},
{
name: "GP with cert authtype",
protocol: "gp",
authType: "cert",
username: "",
data: map[string]string{},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := needsExternalBrowserAuth(tt.protocol, tt.authType, tt.username, tt.data)
assert.Equal(t, tt.expected, result)
})
}
}
func TestBuildGPSamlSecretsResponse(t *testing.T) {
tests := []struct {
name string
settingName string
cookie string
host string
fingerprint string
}{
{
name: "all fields populated",
settingName: "vpn",
cookie: "authcookie=abc123&portal=GATE",
host: "vpn.example.com",
fingerprint: "pin-sha256:ABCD1234",
},
{
name: "empty fingerprint",
settingName: "vpn",
cookie: "authcookie=xyz",
host: "10.0.0.1",
fingerprint: "",
},
{
name: "complex cookie with special chars",
settingName: "vpn",
cookie: "authcookie=077058d3bc81&portal=PANGP_GW_01-N&user=john.doe@example.com&domain=Default&preferred-ip=192.168.1.100",
host: "connect.seclore.com",
fingerprint: "pin-sha256:xp3scfzy3rOgQEXnfPiYKrUk7D66a8b8O+gEXaMPleE=",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := buildGPSamlSecretsResponse(tt.settingName, tt.cookie, tt.host, tt.fingerprint)
assert.NotNil(t, result)
assert.Contains(t, result, tt.settingName)
vpnSec := result[tt.settingName]
assert.NotNil(t, vpnSec)
secretsVariant, ok := vpnSec["secrets"]
assert.True(t, ok, "secrets key should exist")
secrets, ok := secretsVariant.Value().(map[string]string)
assert.True(t, ok, "secrets should be map[string]string")
assert.Equal(t, tt.cookie, secrets["cookie"])
assert.Equal(t, tt.host, secrets["gateway"])
assert.Equal(t, tt.fingerprint, secrets["gwcert"])
})
}
}
func TestVpnFieldMeta_GPSaml(t *testing.T) {
label, isSecret := vpnFieldMeta("gp-saml", "org.freedesktop.NetworkManager.openconnect")
assert.Equal(t, "GlobalProtect SAML/SSO", label)
assert.False(t, isSecret, "gp-saml should not be marked as secret")
}
func TestVpnFieldMeta_StandardFields(t *testing.T) {
tests := []struct {
field string
vpnService string
expectedLabel string
expectedSecret bool
}{
{
field: "username",
vpnService: "org.freedesktop.NetworkManager.openconnect",
expectedLabel: "Username",
expectedSecret: false,
},
{
field: "password",
vpnService: "org.freedesktop.NetworkManager.openconnect",
expectedLabel: "Password",
expectedSecret: true,
},
{
field: "key_pass",
vpnService: "org.freedesktop.NetworkManager.openconnect",
expectedLabel: "PIN",
expectedSecret: true,
},
}
for _, tt := range tests {
t.Run(tt.field, func(t *testing.T) {
label, isSecret := vpnFieldMeta(tt.field, tt.vpnService)
assert.Equal(t, tt.expectedLabel, label)
assert.Equal(t, tt.expectedSecret, isSecret)
})
}
}
func TestInferVPNFields_GPSaml(t *testing.T) {
tests := []struct {
name string
vpnService string
dataMap map[string]string
expectedLen int
shouldHave []string
}{
{
name: "GP with no authtype and no username - should require SAML",
vpnService: "org.freedesktop.NetworkManager.openconnect",
dataMap: map[string]string{
"protocol": "gp",
"gateway": "vpn.example.com",
},
expectedLen: 1,
shouldHave: []string{"gp-saml"},
},
{
name: "GP with saml-auth-method REDIRECT",
vpnService: "org.freedesktop.NetworkManager.openconnect",
dataMap: map[string]string{
"protocol": "gp",
"gateway": "vpn.example.com",
"saml-auth-method": "REDIRECT",
"username": "john",
},
expectedLen: 1,
shouldHave: []string{"gp-saml"},
},
{
name: "GP with saml-auth-method POST",
vpnService: "org.freedesktop.NetworkManager.openconnect",
dataMap: map[string]string{
"protocol": "gp",
"gateway": "vpn.example.com",
"saml-auth-method": "POST",
},
expectedLen: 1,
shouldHave: []string{"gp-saml"},
},
{
name: "GP with username and password authtype - should use credentials",
vpnService: "org.freedesktop.NetworkManager.openconnect",
dataMap: map[string]string{
"protocol": "gp",
"gateway": "vpn.example.com",
"authtype": "password",
"username": "john",
},
expectedLen: 1,
shouldHave: []string{"password"},
},
{
name: "GP with username but no authtype - password only",
vpnService: "org.freedesktop.NetworkManager.openconnect",
dataMap: map[string]string{
"protocol": "gp",
"gateway": "vpn.example.com",
"username": "john",
},
expectedLen: 1,
shouldHave: []string{"password"},
},
{
name: "GP with PKCS11 cert",
vpnService: "org.freedesktop.NetworkManager.openconnect",
dataMap: map[string]string{
"protocol": "gp",
"gateway": "vpn.example.com",
"authtype": "cert",
"usercert": "pkcs11:model=PKCS%2315%20emulated;manufacturer=piv_II",
},
expectedLen: 1,
shouldHave: []string{"key_pass"},
},
{
name: "non-GP protocol (anyconnect)",
vpnService: "org.freedesktop.NetworkManager.openconnect",
dataMap: map[string]string{
"protocol": "anyconnect",
"gateway": "vpn.example.com",
},
expectedLen: 2,
shouldHave: []string{"username", "password"},
},
{
name: "OpenVPN with username",
vpnService: "org.freedesktop.NetworkManager.openvpn",
dataMap: map[string]string{
"connection-type": "password",
"username": "john",
},
expectedLen: 1,
shouldHave: []string{"password"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Convert dataMap to nmVariantMap
vpnSettings := make(nmVariantMap)
vpnSettings["data"] = dbus.MakeVariant(tt.dataMap)
vpnSettings["service-type"] = dbus.MakeVariant(tt.vpnService)
conn := make(map[string]nmVariantMap)
conn["vpn"] = vpnSettings
fields := inferVPNFields(conn, tt.vpnService)
assert.Len(t, fields, tt.expectedLen, "unexpected number of fields")
if len(tt.shouldHave) > 0 {
for _, expected := range tt.shouldHave {
assert.Contains(t, fields, expected, "should contain field: %s", expected)
}
}
})
}
}
func TestNmVariantMap(t *testing.T) {
// Test that nmVariantMap and nmSettingMap work correctly
settingMap := make(nmSettingMap)
variantMap := make(nmVariantMap)
variantMap["test-key"] = dbus.MakeVariant("test-value")
settingMap["test-setting"] = variantMap
assert.Contains(t, settingMap, "test-setting")
assert.Contains(t, settingMap["test-setting"], "test-key")
value := settingMap["test-setting"]["test-key"].Value()
assert.Equal(t, "test-value", value)
}

View File

@@ -69,12 +69,14 @@ type NetworkManagerBackend struct {
lastFailedTime int64
failedMutex sync.RWMutex
pendingVPNSave *pendingVPNCredentials
pendingVPNSaveMu sync.Mutex
cachedVPNCreds *cachedVPNCredentials
cachedVPNCredsMu sync.Mutex
cachedPKCS11PIN *cachedPKCS11PIN
cachedPKCS11Mu sync.Mutex
pendingVPNSave *pendingVPNCredentials
pendingVPNSaveMu sync.Mutex
cachedVPNCreds *cachedVPNCredentials
cachedVPNCredsMu sync.Mutex
cachedPKCS11PIN *cachedPKCS11PIN
cachedPKCS11Mu sync.Mutex
cachedGPSamlCookie *cachedGPSamlCookie
cachedGPSamlMu sync.Mutex
onStateChange func()
}
@@ -97,6 +99,14 @@ type cachedPKCS11PIN struct {
PIN string
}
type cachedGPSamlCookie struct {
ConnectionUUID string
Cookie string
Host string
User string
Fingerprint string
}
func NewNetworkManagerBackend(nmConn ...gonetworkmanager.NetworkManager) (*NetworkManagerBackend, error) {
var nm gonetworkmanager.NetworkManager
var err error

View File

@@ -0,0 +1,203 @@
package network
import (
"bufio"
"context"
"fmt"
"os/exec"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
)
type gpSamlAuthResult struct {
Cookie string
Host string
User string
Fingerprint string
}
// runGlobalProtectSAMLAuth handles GlobalProtect SAML/SSO authentication using gp-saml-gui.
// Only supports protocol=gp. Other protocols need their own implementations.
func (b *NetworkManagerBackend) runGlobalProtectSAMLAuth(ctx context.Context, gateway, protocol string) (*gpSamlAuthResult, error) {
if gateway == "" {
return nil, fmt.Errorf("GP SAML auth: gateway is empty")
}
if protocol != "gp" {
return nil, fmt.Errorf("only GlobalProtect (protocol=gp) SAML is supported, got: %s", protocol)
}
log.Infof("[GP-SAML] Starting GlobalProtect SAML authentication with gp-saml-gui for gateway=%s", gateway)
gpSamlPath, err := exec.LookPath("gp-saml-gui")
if err != nil {
return nil, fmt.Errorf("GlobalProtect SAML requires gp-saml-gui (install: pip install gp-saml-gui): %w", err)
}
args := []string{
"--gateway",
"--allow-insecure-crypto",
gateway,
}
cmd := exec.CommandContext(ctx, gpSamlPath, args...)
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, fmt.Errorf("GP SAML auth: failed to create stdout pipe: %w", err)
}
stderr, err := cmd.StderrPipe()
if err != nil {
return nil, fmt.Errorf("GP SAML auth: failed to create stderr pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("GP SAML auth: failed to start gp-saml-gui: %w", err)
}
go func() {
scanner := bufio.NewScanner(stderr)
for scanner.Scan() {
log.Debugf("[GP-SAML] gp-saml-gui: %s", scanner.Text())
}
}()
result := &gpSamlAuthResult{Host: gateway}
var allOutput []string
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
line := scanner.Text()
allOutput = append(allOutput, line)
log.Infof("[GP-SAML] stdout: %s", line)
switch {
case strings.HasPrefix(line, "COOKIE="):
result.Cookie = unshellQuote(strings.TrimPrefix(line, "COOKIE="))
case strings.HasPrefix(line, "HOST="):
result.Host = unshellQuote(strings.TrimPrefix(line, "HOST="))
case strings.HasPrefix(line, "USER="):
result.User = unshellQuote(strings.TrimPrefix(line, "USER="))
case strings.HasPrefix(line, "FINGERPRINT="):
result.Fingerprint = unshellQuote(strings.TrimPrefix(line, "FINGERPRINT="))
default:
parseGPSamlFromCommandLine(line, result)
}
}
if err := cmd.Wait(); err != nil {
if ctx.Err() != nil {
return nil, fmt.Errorf("GP SAML auth timed out or was cancelled: %w", ctx.Err())
}
if result.Cookie == "" {
return nil, fmt.Errorf("GP SAML auth failed: %w (output: %s)", err, strings.Join(allOutput, "\n"))
}
log.Warnf("[GP-SAML] gp-saml-gui exited with error but cookie was captured: %v", err)
}
if result.Cookie == "" {
return nil, fmt.Errorf("GP SAML auth: no cookie in gp-saml-gui output")
}
log.Infof("[GP-SAML] Got prelogin-cookie from gp-saml-gui, converting to openconnect cookie via --authenticate")
// Convert prelogin-cookie to full openconnect cookie format
ocResult, err := convertGPPreloginCookie(ctx, gateway, result.Cookie, result.User)
if err != nil {
return nil, fmt.Errorf("GP SAML auth: failed to convert prelogin-cookie: %w", err)
}
result.Cookie = ocResult.Cookie
result.Host = ocResult.Host
result.Fingerprint = ocResult.Fingerprint
log.Infof("[GP-SAML] Authentication successful: user=%s, host=%s, cookie_len=%d, has_fingerprint=%v",
result.User, result.Host, len(result.Cookie), result.Fingerprint != "")
return result, nil
}
func convertGPPreloginCookie(ctx context.Context, gateway, preloginCookie, user string) (*gpSamlAuthResult, error) {
ocPath, err := exec.LookPath("openconnect")
if err != nil {
return nil, fmt.Errorf("openconnect not found: %w", err)
}
args := []string{
"--protocol=gp",
"--usergroup=gateway:prelogin-cookie",
"--user=" + user,
"--passwd-on-stdin",
"--allow-insecure-crypto",
"--authenticate",
gateway,
}
cmd := exec.CommandContext(ctx, ocPath, args...)
cmd.Stdin = strings.NewReader(preloginCookie)
output, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("openconnect --authenticate failed: %w\noutput: %s", err, string(output))
}
result := &gpSamlAuthResult{}
for _, line := range strings.Split(string(output), "\n") {
line = strings.TrimSpace(line)
switch {
case strings.HasPrefix(line, "COOKIE="):
result.Cookie = unshellQuote(strings.TrimPrefix(line, "COOKIE="))
case strings.HasPrefix(line, "HOST="):
result.Host = unshellQuote(strings.TrimPrefix(line, "HOST="))
case strings.HasPrefix(line, "FINGERPRINT="):
result.Fingerprint = unshellQuote(strings.TrimPrefix(line, "FINGERPRINT="))
case strings.HasPrefix(line, "CONNECT_URL="):
connectURL := unshellQuote(strings.TrimPrefix(line, "CONNECT_URL="))
if connectURL != "" && result.Host == "" {
result.Host = connectURL
}
}
}
if result.Cookie == "" {
return nil, fmt.Errorf("no COOKIE in openconnect --authenticate output: %s", string(output))
}
log.Infof("[GP-SAML] openconnect --authenticate: cookie_len=%d, host=%s, fingerprint=%s",
len(result.Cookie), result.Host, result.Fingerprint)
return result, nil
}
func unshellQuote(s string) string {
if len(s) >= 2 {
if (s[0] == '\'' && s[len(s)-1] == '\'') ||
(s[0] == '"' && s[len(s)-1] == '"') {
return s[1 : len(s)-1]
}
}
return s
}
func parseGPSamlFromCommandLine(line string, result *gpSamlAuthResult) {
if !strings.Contains(line, "openconnect") {
return
}
for _, part := range strings.Fields(line) {
switch {
case strings.HasPrefix(part, "--cookie="):
if result.Cookie == "" {
result.Cookie = strings.TrimPrefix(part, "--cookie=")
}
case strings.HasPrefix(part, "--servercert="):
if result.Fingerprint == "" {
result.Fingerprint = strings.TrimPrefix(part, "--servercert=")
}
case strings.HasPrefix(part, "--user="):
if result.User == "" {
result.User = strings.TrimPrefix(part, "--user=")
}
}
}
}

View File

@@ -0,0 +1,169 @@
package network
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestUnshellQuote(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "single quoted",
input: "'hello world'",
expected: "hello world",
},
{
name: "double quoted",
input: `"hello world"`,
expected: "hello world",
},
{
name: "unquoted",
input: "hello",
expected: "hello",
},
{
name: "empty single quotes",
input: "''",
expected: "",
},
{
name: "empty double quotes",
input: `""`,
expected: "",
},
{
name: "single quote only",
input: "'",
expected: "'",
},
{
name: "mismatched quotes",
input: "'hello\"",
expected: "'hello\"",
},
{
name: "with special chars",
input: "'cookie=abc123&user=john'",
expected: "cookie=abc123&user=john",
},
{
name: "complex cookie",
input: `'authcookie=077058d3bc81&portal=PANGP_GW_01-N&user=john.doe@example.com&domain=Default&preferred-ip=192.168.1.100'`,
expected: "authcookie=077058d3bc81&portal=PANGP_GW_01-N&user=john.doe@example.com&domain=Default&preferred-ip=192.168.1.100",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := unshellQuote(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestParseGPSamlFromCommandLine(t *testing.T) {
tests := []struct {
name string
line string
initialResult *gpSamlAuthResult
expectedCookie string
expectedUser string
expectedFP string
}{
{
name: "full openconnect command",
line: "openconnect --protocol=gp --cookie=AUTH123 --servercert=pin-sha256:ABC --user=john",
initialResult: &gpSamlAuthResult{},
expectedCookie: "AUTH123",
expectedUser: "john",
expectedFP: "pin-sha256:ABC",
},
{
name: "with equals signs in cookie",
line: "openconnect --cookie=authcookie=xyz123&portal=GATE --user=jane",
initialResult: &gpSamlAuthResult{},
expectedCookie: "authcookie=xyz123&portal=GATE",
expectedUser: "jane",
expectedFP: "",
},
{
name: "non-openconnect line",
line: "some other output",
initialResult: &gpSamlAuthResult{},
expectedCookie: "",
expectedUser: "",
expectedFP: "",
},
{
name: "preserves existing values",
line: "openconnect --user=newuser",
initialResult: &gpSamlAuthResult{Cookie: "existing", Fingerprint: "existing-fp"},
expectedCookie: "existing",
expectedUser: "newuser",
expectedFP: "existing-fp",
},
{
name: "only updates empty fields",
line: "openconnect --cookie=NEW --user=NEW",
initialResult: &gpSamlAuthResult{Cookie: "OLD"},
expectedCookie: "OLD",
expectedUser: "NEW",
expectedFP: "",
},
{
name: "real gp-saml-gui output",
line: "openconnect --protocol=gp --user=john.doe@example.com --os=linux-64 --usergroup=gateway:prelogin-cookie --passwd-on-stdin",
initialResult: &gpSamlAuthResult{},
expectedCookie: "",
expectedUser: "john.doe@example.com",
expectedFP: "",
},
{
name: "with server cert flag",
line: "openconnect --servercert=pin-sha256:xp3scfzy3rOgQEXnfPiYKrUk7D66a8b8O+gEXaMPleE= vpn.example.com",
initialResult: &gpSamlAuthResult{},
expectedCookie: "",
expectedUser: "",
expectedFP: "pin-sha256:xp3scfzy3rOgQEXnfPiYKrUk7D66a8b8O+gEXaMPleE=",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.initialResult
parseGPSamlFromCommandLine(tt.line, result)
assert.Equal(t, tt.expectedCookie, result.Cookie, "cookie mismatch")
assert.Equal(t, tt.expectedUser, result.User, "user mismatch")
assert.Equal(t, tt.expectedFP, result.Fingerprint, "fingerprint mismatch")
})
}
}
func TestParseGPSamlFromCommandLine_MultipleLines(t *testing.T) {
// Simulate gp-saml-gui output with command line suggestion
lines := []string{
"",
"SAML REDIRECT",
"Got SAML Login URL",
"POST to ACS endpoint...",
"Got 'prelogin-cookie': 'FAKE_cookie_12345'",
"openconnect --protocol=gp --user=john.doe@example.com --usergroup=gateway:prelogin-cookie --passwd-on-stdin vpn.example.com",
"",
}
result := &gpSamlAuthResult{}
for _, line := range lines {
parseGPSamlFromCommandLine(line, result)
}
assert.Equal(t, "john.doe@example.com", result.User)
assert.Empty(t, result.Cookie, "cookie should not be parsed from command line")
assert.Empty(t, result.Fingerprint)
}

View File

@@ -212,32 +212,28 @@ func (b *NetworkManagerBackend) updateWiFiState() error {
}
}
var forgetSSID string
b.stateMutex.Lock()
defer b.stateMutex.Unlock()
wasConnecting = b.state.IsConnecting
connectingSSID = b.state.ConnectingSSID
if wasConnecting && connectingSSID != "" {
if connected && ssid == connectingSSID {
switch {
case connected && ssid == connectingSSID:
log.Infof("[updateWiFiState] Connection successful: %s", ssid)
b.state.IsConnecting = false
b.state.ConnectingSSID = ""
b.state.LastError = ""
} else if failed || (disconnected && !connected) {
case failed || (disconnected && !connected):
log.Warnf("[updateWiFiState] Connection failed: SSID=%s, state=%d", connectingSSID, state)
b.state.IsConnecting = false
b.state.ConnectingSSID = ""
b.state.LastError = reasonCode
// If user cancelled, delete the connection profile that was just created
if reasonCode == errdefs.ErrUserCanceled {
log.Infof("[updateWiFiState] User cancelled authentication, removing connection profile for %s", connectingSSID)
b.stateMutex.Unlock()
if err := b.ForgetWiFiNetwork(connectingSSID); err != nil {
log.Warnf("[updateWiFiState] Failed to remove cancelled connection: %v", err)
}
b.stateMutex.Lock()
forgetSSID = connectingSSID
}
b.failedMutex.Lock()
@@ -254,6 +250,15 @@ func (b *NetworkManagerBackend) updateWiFiState() error {
b.state.WiFiBSSID = bssid
b.state.WiFiSignal = signal
b.stateMutex.Unlock()
if forgetSSID != "" {
log.Infof("[updateWiFiState] User cancelled authentication, removing connection profile for %s", forgetSSID)
if err := b.ForgetWiFiNetwork(forgetSSID); err != nil {
log.Warnf("[updateWiFiState] Failed to remove cancelled connection: %v", err)
}
}
return nil
}

View File

@@ -304,6 +304,51 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
if err := b.handleOpenVPNUsernameAuth(targetConn, connName, targetUUID, vpnServiceType); err != nil {
return err
}
case "gp_saml":
gateway := vpnData["gateway"]
protocol := vpnData["protocol"]
if protocol != "gp" {
return fmt.Errorf("GlobalProtect SAML authentication only supported for protocol=gp, got: %s", protocol)
}
log.Infof("[ConnectVPN] GlobalProtect SAML/SSO authentication required for %s (gateway=%s)", connName, gateway)
samlCtx, samlCancel := context.WithTimeout(context.Background(), 5*time.Minute)
authResult, err := b.runGlobalProtectSAMLAuth(samlCtx, gateway, protocol)
samlCancel()
if err != nil {
errMsg := err.Error()
switch {
case strings.Contains(errMsg, "not installed"):
return fmt.Errorf("gp-saml-gui is not installed (required for GlobalProtect SAML/SSO VPN)")
case strings.Contains(errMsg, "timed out") || strings.Contains(errMsg, "cancelled"):
return fmt.Errorf("GlobalProtect SAML authentication timed out — please try again")
case strings.Contains(errMsg, "no cookie"):
return fmt.Errorf("GlobalProtect SAML login did not complete — browser was closed before authentication finished")
case strings.Contains(errMsg, "convert prelogin-cookie"):
return fmt.Errorf("GlobalProtect VPN authentication succeeded but cookie exchange failed: %w", err)
default:
return fmt.Errorf("GlobalProtect SAML authentication failed: %w", err)
}
}
b.cachedGPSamlMu.Lock()
b.cachedGPSamlCookie = &cachedGPSamlCookie{
ConnectionUUID: targetUUID,
Cookie: authResult.Cookie,
Host: authResult.Host,
User: authResult.User,
Fingerprint: authResult.Fingerprint,
}
b.cachedGPSamlMu.Unlock()
if err := targetConn.ClearSecrets(); err != nil {
log.Warnf("[ConnectVPN] ClearSecrets failed (non-fatal): %v", err)
} else {
log.Infof("[ConnectVPN] Cleared stale stored secrets for %s", connName)
}
log.Infof("[ConnectVPN] GlobalProtect SAML cookie cached for %s, proceeding with activation", connName)
}
b.stateMutex.Lock()
@@ -339,6 +384,16 @@ func detectVPNAuthAction(serviceType string, data map[string]string) string {
}
switch {
case strings.Contains(serviceType, "openconnect"):
protocol := data["protocol"]
if needsExternalBrowserAuth(protocol, data["authtype"], data["username"], data) {
switch protocol {
case "gp":
return "gp_saml"
default:
log.Infof("[VPN] External browser auth detected for protocol '%s' but only GlobalProtect (gp) is currently supported", protocol)
}
}
case strings.Contains(serviceType, "openvpn"):
connType := data["connection-type"]
username := data["username"]
@@ -412,16 +467,6 @@ func (b *NetworkManagerBackend) handleOpenVPNUsernameAuth(targetConn gonetworkma
}
data["username"] = username
if reply.Save && password != "" {
data["password-flags"] = "0"
secs := make(map[string]string)
secs["password"] = password
vpn["secrets"] = dbus.MakeVariant(secs)
log.Infof("[ConnectVPN] Saving username and password to vpn.data")
} else {
log.Infof("[ConnectVPN] Saving username to vpn.data (password will be prompted)")
}
vpn["data"] = dbus.MakeVariant(data)
settings["vpn"] = vpn
@@ -432,7 +477,7 @@ func (b *NetworkManagerBackend) handleOpenVPNUsernameAuth(targetConn gonetworkma
}
log.Infof("[ConnectVPN] Username saved to connection")
if password != "" && !reply.Save {
if password != "" {
b.cachedVPNCredsMu.Lock()
b.cachedVPNCreds = &cachedVPNCredentials{
ConnectionUUID: targetUUID,
@@ -614,11 +659,7 @@ func (b *NetworkManagerBackend) ClearVPNCredentials(uuidOrName string) error {
dataMap["password-flags"] = "1"
vpnSettings["data"] = dataMap
}
vpnSettings["password-flags"] = uint32(1)
}
settings["vpn-secrets"] = make(map[string]any)
}
if err := conn.Update(settings); err != nil {
@@ -684,10 +725,13 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
b.state.LastError = ""
b.stateMutex.Unlock()
// Clear cached PKCS11 PIN on success
// Clear cached PKCS11 PIN and SAML cookie on success
b.cachedPKCS11Mu.Lock()
b.cachedPKCS11PIN = nil
b.cachedPKCS11Mu.Unlock()
b.cachedGPSamlMu.Lock()
b.cachedGPSamlCookie = nil
b.cachedGPSamlMu.Unlock()
b.pendingVPNSaveMu.Lock()
pending := b.pendingVPNSave
@@ -706,10 +750,13 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
b.state.LastError = "VPN connection failed"
b.stateMutex.Unlock()
// Clear cached PKCS11 PIN on failure
// Clear cached PKCS11 PIN and SAML cookie on failure
b.cachedPKCS11Mu.Lock()
b.cachedPKCS11PIN = nil
b.cachedPKCS11Mu.Unlock()
b.cachedGPSamlMu.Lock()
b.cachedGPSamlCookie = nil
b.cachedGPSamlMu.Unlock()
return
}
}
@@ -723,10 +770,13 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
b.state.LastError = "VPN connection failed"
b.stateMutex.Unlock()
// Clear cached PKCS11 PIN
// Clear cached PKCS11 PIN and SAML cookie
b.cachedPKCS11Mu.Lock()
b.cachedPKCS11PIN = nil
b.cachedPKCS11Mu.Unlock()
b.cachedGPSamlMu.Lock()
b.cachedGPSamlCookie = nil
b.cachedGPSamlMu.Unlock()
}
}

View File

@@ -92,21 +92,13 @@ func HandleListInstalled(conn net.Conn, req models.Request) {
return
}
registry, err := themes.NewRegistry()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to create registry: %v", err))
return
}
allThemes, err := registry.List()
if err != nil {
models.RespondError(conn, req.ID, fmt.Sprintf("failed to list themes: %v", err))
return
}
themeMap := make(map[string]themes.Theme)
for _, t := range allThemes {
themeMap[t.ID] = t
if registry, err := themes.NewRegistry(); err == nil {
if allThemes, err := registry.List(); err == nil {
for _, t := range allThemes {
themeMap[t.ID] = t
}
}
}
result := make([]ThemeInfo, 0, len(installedIDs))

View File

@@ -1,6 +1,8 @@
package utils
import (
"slices"
"github.com/godbus/dbus/v5"
)
@@ -18,3 +20,18 @@ func IsDBusServiceAvailable(busName string) bool {
}
return owned
}
func IsDBusServiceActivatable(busName string) bool {
conn, err := dbus.ConnectSystemBus()
if err != nil {
return false
}
defer conn.Close()
obj := conn.Object("org.freedesktop.DBus", "/org/freedesktop/DBus")
var activatable []string
if err := obj.Call("org.freedesktop.DBus.ListActivatableNames", 0).Store(&activatable); err != nil {
return false
}
return slices.Contains(activatable, busName)
}

View File

@@ -0,0 +1,37 @@
package utils
import (
"os"
"strings"
)
func HasGroup(groupName string) bool {
return HasGroupIn(groupName, "/etc/group")
}
func HasGroupIn(groupName, path string) bool {
data, err := os.ReadFile(path)
if err != nil {
return false
}
return HasGroupData(groupName, string(data))
}
func HasGroupData(groupName, data string) bool {
prefix := groupName + ":"
for line := range strings.SplitSeq(data, "\n") {
if strings.HasPrefix(line, prefix) {
return true
}
}
return false
}
func FindGroupData(data string, candidates ...string) (string, bool) {
for _, candidate := range candidates {
if HasGroupData(candidate, data) {
return candidate, true
}
}
return "", false
}

View File

@@ -0,0 +1,142 @@
package utils
import "testing"
const testGroupData = `root:x:0:brltty,root
sys:x:3:bin,testuser
mem:x:8:
ftp:x:11:
mail:x:12:
log:x:19:
smmsp:x:25:
proc:x:26:
games:x:50:
lock:x:54:
network:x:90:
floppy:x:94:
scanner:x:96:
power:x:98:
nobody:x:65534:
adm:x:999:daemon
wheel:x:998:testuser
utmp:x:997:
audio:x:996:brltty
disk:x:995:
input:x:994:brltty,testuser,greeter
kmem:x:993:
kvm:x:992:libvirt-qemu,qemu,testuser
lp:x:991:cups,testuser
optical:x:990:
render:x:989:
sgx:x:988:
storage:x:987:
tty:x:5:brltty
uucp:x:986:brltty
video:x:985:cosmic-greeter,greeter,testuser
users:x:984:
groups:x:983:
systemd-journal:x:982:
rfkill:x:981:
bin:x:1:daemon
daemon:x:2:bin
http:x:33:
dbus:x:81:
systemd-coredump:x:980:
systemd-network:x:979:
systemd-oom:x:978:
systemd-journal-remote:x:977:
systemd-resolve:x:976:
systemd-timesync:x:975:
tss:x:974:
uuidd:x:973:
alpm:x:972:
polkitd:x:102:
testuser:x:1000:
avahi:x:971:
git:x:970:
nvidia-persistenced:x:143:
i2c:x:969:testuser
seat:x:968:
rtkit:x:133:
brlapi:x:967:brltty
gdm:x:120:
brltty:x:966:
colord:x:965:
flatpak:x:964:
geoclue:x:963:testuser
gnome-remote-desktop:x:962:
saned:x:961:
usbmux:x:140:
cosmic-greeter:x:960:
greeter:x:959:testuser
openvpn:x:958:
nm-openvpn:x:957:
named:x:40:
_talkd:x:956:
keyd:x:955:
cups:x:209:testuser
docker:x:954:testuser
mysql:x:953:
radicale:x:952:
onepassword:x:1001:
nixbld:x:951:nixbld01,nixbld02,nixbld03,nixbld04,nixbld05,nixbld06,nixbld07,nixbld08,nixbld09,nixbld10
virtlogin:x:940:
libvirt:x:939:testuser
libvirt-qemu:x:938:
qemu:x:937:
dnsmasq:x:936:
clock:x:935:
dms-greeter:x:1002:greeter,testuser
pcscd:x:934:
test:x:1003:
empower:x:933:
`
func TestHasGroupData(t *testing.T) {
tests := []struct {
group string
want bool
}{
{"greeter", true},
{"root", true},
{"docker", true},
{"cosmic-greeter", true},
{"dms-greeter", true},
{"nonexistent", false},
{"greet", false},
}
for _, tt := range tests {
if got := HasGroupData(tt.group, testGroupData); got != tt.want {
t.Errorf("HasGroupData(%q) = %v, want %v", tt.group, got, tt.want)
}
}
}
func TestFindGroupData(t *testing.T) {
tests := []struct {
name string
candidates []string
wantGroup string
wantFound bool
}{
{"first match wins", []string{"greeter", "greetd", "_greeter"}, "greeter", true},
{"fallback to second", []string{"greetd", "greeter"}, "greeter", true},
{"none found", []string{"_greetd", "greetd"}, "", false},
{"single match", []string{"docker"}, "docker", true},
}
for _, tt := range tests {
got, found := FindGroupData(testGroupData, tt.candidates...)
if got != tt.wantGroup || found != tt.wantFound {
t.Errorf("%s: FindGroupData(%v) = (%q, %v), want (%q, %v)",
tt.name, tt.candidates, got, found, tt.wantGroup, tt.wantFound)
}
}
}
func TestHasGroupDataEmpty(t *testing.T) {
if HasGroupData("greeter", "") {
t.Error("expected false for empty data")
}
}

View File

@@ -38,6 +38,22 @@ func XDGConfigHome() string {
return filepath.Join(home, ".config")
}
func EmacsConfigDir() string {
home, _ := os.UserHomeDir()
emacsD := filepath.Join(home, ".emacs.d")
if info, err := os.Stat(emacsD); err == nil && info.IsDir() {
return emacsD
}
xdgEmacs := filepath.Join(XDGConfigHome(), "emacs")
if info, err := os.Stat(xdgEmacs); err == nil && info.IsDir() {
return xdgEmacs
}
return ""
}
func ExpandPath(path string) (string, error) {
expanded := os.ExpandEnv(path)
expanded = filepath.Clean(expanded)

View File

@@ -71,6 +71,9 @@ in
"hyprland"
"sway"
"labwc"
"mango"
"scroll"
"miracle"
];
description = "Compositor to run greeter in";
};

View File

@@ -50,5 +50,6 @@ in
services.power-profiles-daemon.enable = lib.mkDefault true;
services.accounts-daemon.enable = lib.mkDefault true;
security.polkit.enable = lib.mkDefault true;
};
}

View File

@@ -48,6 +48,7 @@
sonnet
qtmultimedia
qtimageformats
kimageformats
];
in
{
@@ -79,7 +80,7 @@
inherit version;
pname = "dms-shell";
src = ./core;
vendorHash = "sha256-vsfCgpilOHzJbTaJjJfMK/cSvtyFYJsPDjY4m3iuoFg=";
vendorHash = "sha256-cVUJXgzYMRSM0od1xzDVkMTdxHu3OIQX2bQ8AJbGQ1Q=";
subPackages = [ "cmd/dms" ];

View File

@@ -9,6 +9,20 @@ Singleton {
property var currentOSDsByScreen: ({})
Connections {
target: Quickshell
function onScreensChanged() {
const activeNames = {};
for (let i = 0; i < Quickshell.screens.length; i++)
activeNames[Quickshell.screens[i].name] = true;
for (const screenName in osdManager.currentOSDsByScreen) {
if (activeNames[screenName])
continue;
osdManager.currentOSDsByScreen[screenName] = null;
}
}
}
function showOSD(osd) {
if (!osd || !osd.screen)
return;

View File

@@ -80,7 +80,10 @@ Singleton {
return Quickshell.iconPath(moddedId, true);
}
return desktopEntry && desktopEntry.icon ? Quickshell.iconPath(desktopEntry.icon, true) : "";
if (desktopEntry && desktopEntry.icon) {
return Quickshell.iconPath(desktopEntry.icon, true);
}
return Quickshell.iconPath(appId, true);
}
function getAppName(appId: string, desktopEntry: var): string {

View File

@@ -58,6 +58,7 @@ Singleton {
property string wallpaperPathDark: ""
property var monitorWallpapersLight: ({})
property var monitorWallpapersDark: ({})
property var monitorWallpaperFillModes: ({})
property string wallpaperTransition: "fade"
readonly property var availableWallpaperTransitions: ["none", "fade", "wipe", "disc", "stripes", "iris bloom", "pixelate", "portal"]
property var includedTransitions: availableWallpaperTransitions.filter(t => t !== "none")
@@ -122,6 +123,8 @@ Singleton {
property string vpnLastConnected: ""
property var deviceMaxVolumes: ({})
property var hiddenOutputDeviceNames: []
property var hiddenInputDeviceNames: []
Component.onCompleted: {
if (!isGreeterMode) {
@@ -1068,6 +1071,20 @@ Singleton {
saveSettings();
}
function setHiddenOutputDeviceNames(deviceNames) {
if (!Array.isArray(deviceNames))
return;
hiddenOutputDeviceNames = deviceNames;
saveSettings();
}
function setHiddenInputDeviceNames(deviceNames) {
if (!Array.isArray(deviceNames))
return;
hiddenInputDeviceNames = deviceNames;
saveSettings();
}
function getDeviceMaxVolume(nodeName) {
if (!nodeName)
return 100;
@@ -1094,11 +1111,7 @@ Singleton {
wallpaperPath = isLightMode ? wallpaperPathLight : wallpaperPathDark;
}
function getMonitorWallpaper(screenName) {
if (!perMonitorWallpaper) {
return wallpaperPath;
}
function _findMonitorValue(map, screenName) {
var screen = null;
var screens = Quickshell.screens;
for (var i = 0; i < screens.length; i++) {
@@ -1108,52 +1121,72 @@ Singleton {
}
}
if (!screen) {
return monitorWallpapers[screenName] || wallpaperPath;
if (!screen)
return map[screenName];
if (map[screen.name] !== undefined)
return map[screen.name];
if (screen.model && map[screen.model] !== undefined)
return map[screen.model];
if (typeof SettingsData !== "undefined") {
var displayName = SettingsData.getScreenDisplayName(screen);
if (displayName && map[displayName] !== undefined)
return map[displayName];
}
return undefined;
}
function getMonitorWallpaper(screenName) {
if (!perMonitorWallpaper)
return wallpaperPath;
var value = _findMonitorValue(monitorWallpapers, screenName);
return value !== undefined ? value : wallpaperPath;
}
function getMonitorWallpaperFillMode(screenName) {
var globalFillMode = (typeof SettingsData !== "undefined") ? SettingsData.wallpaperFillMode : "Fill";
if (!perMonitorWallpaper)
return globalFillMode;
var value = _findMonitorValue(monitorWallpaperFillModes, screenName);
return value !== undefined ? value : globalFillMode;
}
function setMonitorWallpaperFillMode(screenName, mode) {
var screen = null;
var screens = Quickshell.screens;
for (var i = 0; i < screens.length; i++) {
if (screens[i].name === screenName) {
screen = screens[i];
break;
}
}
if (monitorWallpapers[screen.name]) {
return monitorWallpapers[screen.name];
}
if (screen.model && monitorWallpapers[screen.model]) {
return monitorWallpapers[screen.model];
if (!screen)
return;
var identifier = typeof SettingsData !== "undefined" ? SettingsData.getScreenDisplayName(screen) : screen.name;
var newModes = {};
for (var key in monitorWallpaperFillModes) {
var isThisScreen = key === screen.name || (screen.model && key === screen.model);
if (!isThisScreen)
newModes[key] = monitorWallpaperFillModes[key];
}
return wallpaperPath;
newModes[identifier] = mode;
monitorWallpaperFillModes = newModes;
saveSettings();
}
function getMonitorCyclingSettings(screenName) {
var screen = null;
var screens = Quickshell.screens;
for (var i = 0; i < screens.length; i++) {
if (screens[i].name === screenName) {
screen = screens[i];
break;
}
}
if (!screen) {
return monitorCyclingSettings[screenName] || {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
};
}
if (monitorCyclingSettings[screen.name]) {
return monitorCyclingSettings[screen.name];
}
if (screen.model && monitorCyclingSettings[screen.model]) {
return monitorCyclingSettings[screen.model];
}
return {
var defaults = {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
};
var value = _findMonitorValue(monitorCyclingSettings, screenName);
return value !== undefined ? value : defaults;
}
FileView {

View File

@@ -60,6 +60,7 @@ Singleton {
property bool _hasLoaded: false
property bool _isReadOnly: false
property bool _hasUnsavedChanges: false
property bool _selfWrite: false
property var _loadedSettingsSnapshot: null
property var pluginSettings: ({})
property var builtInPluginSettings: ({})
@@ -79,6 +80,8 @@ Singleton {
saveSettings();
}
property bool clipboardEnterToPaste: false
property var launcherPluginVisibility: ({})
function getPluginAllowWithoutTrigger(pluginId) {
@@ -312,6 +315,7 @@ Singleton {
property int dankLauncherV2BorderThickness: 2
property string dankLauncherV2BorderColor: "primary"
property bool dankLauncherV2ShowFooter: true
property bool dankLauncherV2UnloadOnClose: false
property string _legacyWeatherLocation: "New York, NY"
property string _legacyWeatherCoordinates: "40.7128,-74.0060"
@@ -469,6 +473,8 @@ Singleton {
property bool dockShowOverflowBadge: true
property bool notificationOverlayEnabled: false
property bool notificationPopupShadowEnabled: true
property bool notificationPopupPrivacyMode: false
property int overviewRows: 2
property int overviewColumns: 5
property real overviewScale: 0.16
@@ -498,12 +504,15 @@ Singleton {
property int notificationTimeoutCritical: 0
property bool notificationCompactMode: false
property int notificationPopupPosition: SettingsData.Position.Top
property int notificationAnimationSpeed: SettingsData.AnimationSpeed.Short
property int notificationCustomAnimationDuration: 400
property bool notificationHistoryEnabled: true
property int notificationHistoryMaxCount: 50
property int notificationHistoryMaxAgeDays: 7
property bool notificationHistorySaveLow: true
property bool notificationHistorySaveNormal: true
property bool notificationHistorySaveCritical: true
property var notificationRules: []
property bool osdAlwaysShowValue: false
property int osdPosition: SettingsData.Position.BottomCenter
@@ -1006,6 +1015,42 @@ Singleton {
function applyStoredIconTheme() {
updateGtkIconTheme();
updateQtIconTheme();
updateCosmicIconTheme();
}
function updateCosmicIconTheme() {
let cosmicThemeName = (iconTheme === "System Default") ? systemDefaultIconTheme : iconTheme;
if (!cosmicThemeName || cosmicThemeName === "System Default") {
const detectScript = `if command -v gsettings >/dev/null 2>&1; then
gsettings get org.gnome.desktop.interface icon-theme 2>/dev/null | sed "s/'//g"
elif command -v dconf >/dev/null 2>&1; then
dconf read /org/gnome/desktop/interface/icon-theme 2>/dev/null | sed "s/'//g"
fi`;
Proc.runCommand("detectCosmicIconTheme", ["sh", "-c", detectScript], (output, exitCode) => {
if (exitCode !== 0)
return;
const detected = (output || "").trim();
if (!detected || detected === "System Default")
return;
const detectedEscaped = detected.replace(/'/g, "'\\''");
const writeScript = `mkdir -p ${_configDir}/cosmic/com.system76.CosmicTk/v1
printf '"%s"\\n' '${detectedEscaped}' > ${_configDir}/cosmic/com.system76.CosmicTk/v1/icon_theme 2>/dev/null || true`;
Quickshell.execDetached(["sh", "-lc", writeScript]);
});
return;
}
const cosmicThemeNameEscaped = cosmicThemeName.replace(/'/g, "'\\''");
const script = `mkdir -p ${_configDir}/cosmic/com.system76.CosmicTk/v1
printf '"%s"\\n' '${cosmicThemeNameEscaped}' > ${_configDir}/cosmic/com.system76.CosmicTk/v1/icon_theme 2>/dev/null || true`;
Quickshell.execDetached(["sh", "-lc", script]);
}
function updateCosmicThemeMode(isLightMode) {
const isDark = isLightMode ? "false" : "true";
const script = `mkdir -p ${_configDir}/cosmic/com.system76.CosmicTheme.Mode/v1
printf '%s\\n' ${isDark} > ${_configDir}/cosmic/com.system76.CosmicTheme.Mode/v1/is_dark 2>/dev/null || true`;
Quickshell.execDetached(["sh", "-lc", script]);
}
function updateGtkIconTheme() {
@@ -1199,6 +1244,7 @@ Singleton {
function saveSettings() {
if (_loading || _parseError || !_hasLoaded)
return;
_selfWrite = true;
settingsFile.setText(JSON.stringify(Store.toJson(root), null, 2));
if (_isReadOnly)
_checkSettingsWritable();
@@ -1812,6 +1858,7 @@ Singleton {
iconTheme = themeName;
updateGtkIconTheme();
updateQtIconTheme();
updateCosmicIconTheme();
saveSettings();
if (typeof Theme !== "undefined" && Theme.currentTheme === Theme.dynamic)
Theme.generateSystemThemesFromCurrentTheme();
@@ -2134,6 +2181,143 @@ Singleton {
saveSettings();
}
property bool _pendingExpandNotificationRules: false
property int _pendingNotificationRuleIndex: -1
function addNotificationRule() {
var rules = JSON.parse(JSON.stringify(notificationRules || []));
rules.push({
enabled: true,
field: "appName",
pattern: "",
matchType: "contains",
action: "default",
urgency: "default"
});
notificationRules = rules;
saveSettings();
}
function addNotificationRuleForNotification(appName, desktopEntry) {
var rules = JSON.parse(JSON.stringify(notificationRules || []));
var pattern = (desktopEntry && desktopEntry !== "") ? desktopEntry : (appName || "");
var field = (desktopEntry && desktopEntry !== "") ? "desktopEntry" : "appName";
var rule = {
enabled: true,
field: pattern ? field : "appName",
pattern: pattern || "",
matchType: pattern ? "exact" : "contains",
action: "default",
urgency: "default"
};
rules.push(rule);
notificationRules = rules;
saveSettings();
var index = rules.length - 1;
_pendingExpandNotificationRules = true;
_pendingNotificationRuleIndex = index;
return index;
}
function addMuteRuleForApp(appName, desktopEntry) {
var rules = JSON.parse(JSON.stringify(notificationRules || []));
var pattern = (desktopEntry && desktopEntry !== "") ? desktopEntry : (appName || "");
var field = (desktopEntry && desktopEntry !== "") ? "desktopEntry" : "appName";
if (pattern === "")
return;
rules.push({
enabled: true,
field: field,
pattern: pattern,
matchType: "exact",
action: "mute",
urgency: "default"
});
notificationRules = rules;
saveSettings();
}
function isAppMuted(appName, desktopEntry) {
const rules = notificationRules || [];
const pat = (desktopEntry && desktopEntry !== "" ? desktopEntry : appName || "").toString().toLowerCase();
if (!pat)
return false;
for (let i = 0; i < rules.length; i++) {
const r = rules[i];
if ((r.action || "").toString().toLowerCase() !== "mute" || r.enabled === false)
continue;
const field = (r.field || "appName").toString().toLowerCase();
const rulePat = (r.pattern || "").toString().toLowerCase();
if (!rulePat)
continue;
const useDesktop = field === "desktopentry";
const matches = (useDesktop && desktopEntry) ? (desktopEntry.toString().toLowerCase() === rulePat) : (appName && appName.toString().toLowerCase() === rulePat);
if (matches)
return true;
if (rulePat === pat)
return true;
}
return false;
}
function removeMuteRuleForApp(appName, desktopEntry) {
var rules = JSON.parse(JSON.stringify(notificationRules || []));
const app = (appName || "").toString().toLowerCase();
const desktop = (desktopEntry || "").toString().toLowerCase();
if (!app && !desktop)
return;
for (let i = rules.length - 1; i >= 0; i--) {
const r = rules[i];
if ((r.action || "").toString().toLowerCase() !== "mute")
continue;
const rulePat = (r.pattern || "").toString().toLowerCase();
if (!rulePat)
continue;
if (rulePat === app || rulePat === desktop) {
rules.splice(i, 1);
notificationRules = rules;
saveSettings();
return;
}
}
}
function updateNotificationRule(index, ruleData) {
var rules = JSON.parse(JSON.stringify(notificationRules || []));
if (index < 0 || index >= rules.length)
return;
var existing = rules[index] || {};
rules[index] = Object.assign({}, existing, ruleData || {});
notificationRules = rules;
saveSettings();
}
function updateNotificationRuleField(index, key, value) {
if (key === undefined || key === null || key === "")
return;
var patch = {};
patch[key] = value;
updateNotificationRule(index, patch);
}
function removeNotificationRule(index) {
var rules = JSON.parse(JSON.stringify(notificationRules || []));
if (index < 0 || index >= rules.length)
return;
rules.splice(index, 1);
notificationRules = rules;
saveSettings();
}
function getDefaultNotificationRules() {
return Spec.SPEC.notificationRules.def;
}
function resetNotificationRules() {
notificationRules = JSON.parse(JSON.stringify(Spec.SPEC.notificationRules.def));
saveSettings();
}
function getDefaultAppIdSubstitutions() {
return Spec.SPEC.appIdSubstitutions.def;
}
@@ -2159,19 +2343,40 @@ Singleton {
Theme.reloadCustomThemeVariant();
}
function getRegistryThemeMultiVariant(themeId, defaults) {
function getRegistryThemeMultiVariant(themeId, defaults, mode) {
var stored = registryThemeVariants[themeId];
if (stored && typeof stored === "object")
return stored;
return defaults || {};
if (!stored || typeof stored !== "object")
return defaults || {};
if ((stored.dark && typeof stored.dark === "object") || (stored.light && typeof stored.light === "object")) {
if (!mode)
return stored.dark || stored.light || defaults || {};
var modeData = stored[mode];
if (modeData && typeof modeData === "object")
return modeData;
return defaults || {};
}
return stored;
}
function setRegistryThemeMultiVariant(themeId, flavor, accent) {
function setRegistryThemeMultiVariant(themeId, flavor, accent, mode) {
var variants = JSON.parse(JSON.stringify(registryThemeVariants));
variants[themeId] = {
var existing = variants[themeId];
var perMode = {};
if (existing && typeof existing === "object") {
if ((existing.dark && typeof existing.dark === "object") || (existing.light && typeof existing.light === "object")) {
perMode = existing;
} else if (typeof existing.flavor === "string") {
perMode.dark = {
flavor: existing.flavor,
accent: existing.accent || ""
};
}
}
perMode[mode || "dark"] = {
flavor: flavor,
accent: accent
};
variants[themeId] = perMode;
registryThemeVariants = variants;
saveSettings();
if (typeof Theme !== "undefined")
@@ -2371,6 +2576,13 @@ Singleton {
property alias settingsFile: settingsFile
Timer {
id: settingsFileReloadDebounce
interval: 50
onTriggered: settingsFile.reload()
repeat: false
}
FileView {
id: settingsFile
@@ -2378,7 +2590,14 @@ Singleton {
blockLoading: true
blockWrites: true
atomicWrites: true
watchChanges: !isGreeterMode
watchChanges: true
onFileChanged: {
if (_selfWrite) {
_selfWrite = false;
return;
}
settingsFileReloadDebounce.restart();
}
onLoaded: {
if (isGreeterMode)
return;

View File

@@ -45,11 +45,12 @@ Singleton {
if (typeof SessionData === "undefined")
return "";
var monitors = SessionData.monitorWallpapers;
if (SessionData.perMonitorWallpaper) {
var screens = Quickshell.screens;
if (screens.length > 0) {
var firstMonitorWallpaper = SessionData.getMonitorWallpaper(screens[0].name);
return firstMonitorWallpaper || SessionData.wallpaperPath;
var s = screens[0];
return monitors[s.name] || (s.model ? monitors[s.model] : "") || SessionData.wallpaperPath;
}
}
@@ -59,6 +60,7 @@ Singleton {
if (typeof SessionData === "undefined")
return "";
var monitors = SessionData.monitorWallpapers;
if (SessionData.perMonitorWallpaper) {
var screens = Quickshell.screens;
if (screens.length > 0) {
@@ -72,12 +74,20 @@ Singleton {
}
}
if (!targetMonitorExists) {
if (!targetMonitorExists)
targetMonitor = screens[0].name;
var s = null;
for (var j = 0; j < screens.length; j++) {
if (screens[j].name === targetMonitor) {
s = screens[j];
break;
}
}
var targetMonitorWallpaper = SessionData.getMonitorWallpaper(targetMonitor);
return targetMonitorWallpaper || SessionData.wallpaperPath;
if (s)
return monitors[s.name] || (s.model ? monitors[s.model] : "") || SessionData.wallpaperPath;
return monitors[targetMonitor] || SessionData.wallpaperPath;
}
}
@@ -178,6 +188,8 @@ Singleton {
if (typeof SettingsData !== "undefined" && SettingsData.currentThemeName) {
switchTheme(SettingsData.currentThemeName, false, false);
const currentIsLight = (typeof SessionData !== "undefined") ? SessionData.isLightMode : false;
SettingsData.updateCosmicThemeMode(currentIsLight);
}
if (typeof SessionData !== "undefined" && SessionData.themeModeAutoEnabled) {
@@ -766,6 +778,53 @@ Singleton {
};
}
readonly property int notificationAnimationBaseDuration: {
if (typeof SettingsData === "undefined")
return 200;
if (SettingsData.notificationAnimationSpeed === SettingsData.AnimationSpeed.None)
return 0;
if (SettingsData.notificationAnimationSpeed === SettingsData.AnimationSpeed.Custom)
return SettingsData.notificationCustomAnimationDuration;
const presetMap = [0, 200, 400, 600];
return presetMap[SettingsData.notificationAnimationSpeed] ?? 200;
}
readonly property int notificationEnterDuration: {
const base = notificationAnimationBaseDuration;
return base === 0 ? 0 : Math.round(base * 0.875);
}
readonly property int notificationExitDuration: {
const base = notificationAnimationBaseDuration;
return base === 0 ? 0 : Math.round(base * 0.75);
}
readonly property int notificationExpandDuration: {
const base = notificationAnimationBaseDuration;
return base === 0 ? 0 : Math.round(base * 1.0);
}
readonly property int notificationCollapseDuration: {
const base = notificationAnimationBaseDuration;
return base === 0 ? 0 : Math.round(base * 0.85);
}
readonly property real notificationIconSizeNormal: 56
readonly property real notificationIconSizeCompact: 48
readonly property real notificationExpandedIconSizeNormal: 48
readonly property real notificationExpandedIconSizeCompact: 40
readonly property real notificationActionMinWidth: 48
readonly property real notificationButtonCornerRadius: cornerRadius / 2
readonly property real notificationHoverRevealMargin: spacingXL
readonly property real notificationContentSpacing: spacingXS
readonly property real notificationCardPadding: spacingM
readonly property real notificationCardPaddingCompact: spacingS
readonly property real stateLayerHover: 0.08
readonly property real stateLayerFocus: 0.12
readonly property real stateLayerPressed: 0.12
readonly property real stateLayerDrag: 0.16
readonly property int popoutAnimationDuration: {
if (typeof SettingsData === "undefined")
return 150;
@@ -906,6 +965,9 @@ Singleton {
if (!matugenAvailable) {
PortalService.setLightMode(light);
}
if (typeof SettingsData !== "undefined") {
SettingsData.updateCosmicThemeMode(light);
}
generateSystemThemesFromCurrentTheme();
}
}
@@ -961,7 +1023,7 @@ Singleton {
if (themeData.variants.type === "multi" && themeData.variants.flavors && themeData.variants.accents) {
const defaults = themeData.variants.defaults || {};
const modeDefaults = defaults[colorMode] || defaults.dark || {};
const stored = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults) : modeDefaults;
const stored = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults, colorMode) : modeDefaults;
var flavorId = stored.flavor || modeDefaults.flavor || "";
const accentId = stored.accent || modeDefaults.accent || "";
var flavor = findVariant(themeData.variants.flavors, flavorId);
@@ -1355,8 +1417,8 @@ Singleton {
const defaults = customThemeRawData.variants.defaults || {};
const darkDefaults = defaults.dark || {};
const lightDefaults = defaults.light || defaults.dark || {};
const storedDark = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults) : darkDefaults;
const storedLight = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults) : lightDefaults;
const storedDark = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults, "dark") : darkDefaults;
const storedLight = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults;
const darkFlavorId = storedDark.flavor || darkDefaults.flavor || "";
const lightFlavorId = storedLight.flavor || lightDefaults.flavor || "";
const accentId = storedDark.accent || darkDefaults.accent || "";

View File

@@ -32,8 +32,15 @@ function markdownToHtml(text) {
return `\x00INLINECODE${inlineIndex++}\x00`;
});
// Now process everything else
// Escape HTML entities (but not in code blocks)
// Extract plain URLs before escaping so & in query strings is preserved
const urls = [];
let urlIndex = 0;
html = html.replace(/(^|[\s])((?:https?|file):\/\/[^\s]+)/gm, (match, prefix, url) => {
urls.push(url);
return prefix + `\x00URL${urlIndex++}\x00`;
});
// Escape HTML entities (but not in code blocks or URLs)
html = html.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
@@ -64,8 +71,12 @@ function markdownToHtml(text) {
return '<ul>' + match + '</ul>';
});
// Detect plain URLs and wrap them in anchor tags (but not inside existing <a> or markdown links)
html = html.replace(/(^|[^"'>])((https?|file):\/\/[^\s<]+)/g, '$1<a href="$2">$2</a>');
// Restore extracted URLs as anchor tags (preserves raw & in href)
html = html.replace(/\x00URL(\d+)\x00/g, (_, index) => {
const url = urls[parseInt(index)];
const display = url.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
return `<a href="${url}">${display}</a>`;
});
// Restore code blocks and inline code BEFORE line break processing
html = html.replace(/\x00CODEBLOCK(\d+)\x00/g, (match, index) => {

View File

@@ -12,6 +12,7 @@ var SPEC = {
wallpaperPathDark: { def: "" },
monitorWallpapersLight: { def: {} },
monitorWallpapersDark: { def: {} },
monitorWallpaperFillModes: { def: {} },
wallpaperTransition: { def: "fade" },
includedTransitions: { def: ["fade", "wipe", "disc", "stripes", "iris bloom", "pixelate", "portal"] },
@@ -74,7 +75,9 @@ var SPEC = {
vpnLastConnected: { def: "" },
deviceMaxVolumes: { def: {} }
deviceMaxVolumes: { def: {} },
hiddenOutputDeviceNames: { def: [] },
hiddenInputDeviceNames: { def: [] }
};
function getValidKeys() {

View File

@@ -173,6 +173,7 @@ var SPEC = {
dankLauncherV2BorderThickness: { def: 2 },
dankLauncherV2BorderColor: { def: "primary" },
dankLauncherV2ShowFooter: { def: true },
dankLauncherV2UnloadOnClose: { def: false },
useAutoLocation: { def: false },
weatherEnabled: { def: true },
@@ -296,6 +297,8 @@ var SPEC = {
dockShowOverflowBadge: { def: true },
notificationOverlayEnabled: { def: false },
notificationPopupShadowEnabled: { def: true },
notificationPopupPrivacyMode: { def: false },
overviewRows: { def: 2, persist: false },
overviewColumns: { def: 5, persist: false },
overviewScale: { def: 0.16, persist: false },
@@ -324,12 +327,15 @@ var SPEC = {
notificationTimeoutCritical: { def: 0 },
notificationCompactMode: { def: false },
notificationPopupPosition: { def: 0 },
notificationAnimationSpeed: { def: 1 },
notificationCustomAnimationDuration: { def: 400 },
notificationHistoryEnabled: { def: true },
notificationHistoryMaxCount: { def: 50 },
notificationHistoryMaxAgeDays: { def: 7 },
notificationHistorySaveLow: { def: true },
notificationHistorySaveNormal: { def: true },
notificationHistorySaveCritical: { def: true },
notificationRules: { def: [] },
osdAlwaysShowValue: { def: false },
osdPosition: { def: 5 },
@@ -468,6 +474,8 @@ var SPEC = {
desktopWidgetGroups: { def: [] },
builtInPluginSettings: { def: {} },
clipboardEnterToPaste: { def: false },
launcherPluginVisibility: { def: {} },
launcherPluginOrder: { def: [] }
};

View File

@@ -251,13 +251,20 @@ Item {
active: false
asynchronous: false
Component.onCompleted: {
PopoutService.dankDashPopoutLoader = dankDashPopoutLoader;
}
onLoaded: {
if (item) {
PopoutService.dankDashPopout = item;
PopoutService._onDankDashPopoutLoaded();
}
}
sourceComponent: Component {
DankDashPopout {
id: dankDashPopout
Component.onCompleted: {
PopoutService.dankDashPopout = dankDashPopout;
}
}
}
}

View File

@@ -197,7 +197,7 @@ Item {
if (CompositorService.isNiri && NiriService.currentOutput) {
return NiriService.currentOutput;
}
if ((CompositorService.isSway || CompositorService.isScroll) && I3.workspaces?.values) {
if ((CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) && I3.workspaces?.values) {
const focusedWs = I3.workspaces.values.find(ws => ws.focused === true);
return focusedWs?.monitor?.name || "";
}

View File

@@ -65,7 +65,7 @@ Column {
StyledText {
id: codenameText
anchors.centerIn: parent
text: "Spicy Miso"
text: "Saffron Bloom"
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.primary
@@ -74,7 +74,7 @@ Column {
}
StyledText {
text: "Desktop widgets, theme registry, native clipboard & more"
text: "New launcher, enhanced plugin system, KDE Connect, & more"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
}
@@ -108,67 +108,76 @@ Column {
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "widgets"
title: "Desktop Widgets"
description: "Widgets on your desktop"
onClicked: PopoutService.openSettingsWithTab("desktop_widgets")
iconName: "space_dashboard"
title: "Dank Launcher V2"
description: "New capabilities & plugins"
onClicked: PopoutService.openDankLauncherV2()
}
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "palette"
title: "Theme Registry"
description: "Community themes"
onClicked: PopoutService.openSettingsWithTab("theme")
iconName: "smartphone"
title: "Phone Connect"
description: "KDE Connect & Valent"
onClicked: Qt.openUrlExternally("https://github.com/AvengeMedia/dms-plugins/tree/master/DankKDEConnect")
}
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "content_paste"
title: "Native Clipboard"
description: "Zero-dependency history"
onClicked: PopoutService.openSettingsWithTab("clipboard")
iconName: "monitor_heart"
title: "System Monitor"
description: "Redesigned process list"
onClicked: PopoutService.showProcessListModal()
}
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "display_settings"
title: "Monitor Config"
description: "Full display setup"
onClicked: PopoutService.openSettingsWithTab("display_config")
iconName: "window"
title: "Window Rules"
description: "niri window rule manager"
visible: CompositorService.isNiri
onClicked: PopoutService.openSettingsWithTab("window_rules")
}
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "notifications_active"
title: "Notifications"
description: "History & gestures"
title: "Enhanced Notifications"
description: "Configurable rules & styling"
visible: !CompositorService.isNiri
onClicked: PopoutService.openSettingsWithTab("notifications")
}
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "healing"
title: "DMS Doctor"
description: "Diagnose issues"
onClicked: FirstLaunchService.showDoctor()
iconName: "dock_to_bottom"
title: "Dock Enhancements"
description: "Bar dock widget & more"
onClicked: PopoutService.openSettingsWithTab("dock")
}
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "keyboard"
title: "Keybinds Editor"
description: "niri, Hyprland, & MangoWC"
visible: KeybindsService.available
onClicked: PopoutService.openSettingsWithTab("keybinds")
iconName: "volume_up"
title: "Audio Aliases"
description: "Custom device names"
onClicked: PopoutService.openSettingsWithTab("audio")
}
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "search"
title: "Settings Search"
description: "Find settings fast"
onClicked: PopoutService.openSettings()
iconName: "extension"
title: "Enhanced Plugin System"
description: "Enables new types of plugins"
onClicked: PopoutService.openSettingsWithTab("plugins")
}
ChangelogFeatureCard {
width: (parent.width - Theme.spacingS) / 2
iconName: "light_mode"
title: "Auto Light/Dark"
description: "Automatic mode switching"
onClicked: PopoutService.openSettingsWithTab("theme")
}
}
}
@@ -221,26 +230,21 @@ Column {
ChangelogUpgradeNote {
width: parent.width
text: "Ghostty theme path changed to ~/.config/ghostty/themes/danktheme"
text: "Spotlight replaced by Dank Launcher V2 — check settings for new options"
}
ChangelogUpgradeNote {
width: parent.width
text: "VS Code theme reinstall required"
}
ChangelogUpgradeNote {
width: parent.width
text: "Clipboard history migration available from cliphist"
text: "Plugin API updated — third-party plugins may need updates"
}
}
}
StyledText {
text: "See full release notes for migration steps"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
}
// StyledText {
// text: "See full release notes for migration steps"
// font.pixelSize: Theme.fontSizeSmall
// color: Theme.surfaceVariantText
// width: parent.width
// }
}
}

View File

@@ -129,7 +129,7 @@ FloatingWindow {
iconName: "open_in_new"
backgroundColor: Theme.surfaceContainerHighest
textColor: Theme.surfaceText
onClicked: Qt.openUrlExternally("https://danklinux.com/blog/v1-2-release")
onClicked: Qt.openUrlExternally("https://danklinux.com/blog/v1-4-release")
}
DankButton {

View File

@@ -268,6 +268,7 @@ Item {
sourceComponent: ClipboardKeyboardHints {
wtypeAvailable: modal.wtypeAvailable
enterToPaste: SettingsData.clipboardEnterToPaste
}
}
}

View File

@@ -1,4 +1,5 @@
import QtQuick
import qs.Common
import qs.Services
QtObject {
@@ -133,7 +134,11 @@ QtObject {
case Qt.Key_Return:
case Qt.Key_Enter:
if (ClipboardService.keyboardNavigationActive) {
modal.pasteSelected();
if (SettingsData.clipboardEnterToPaste) {
copySelected();
} else {
modal.pasteSelected();
}
event.accepted = true;
}
return;
@@ -144,7 +149,11 @@ QtObject {
switch (event.key) {
case Qt.Key_Return:
case Qt.Key_Enter:
copySelected();
if (SettingsData.clipboardEnterToPaste) {
modal.pasteSelected();
} else {
copySelected();
}
event.accepted = true;
return;
case Qt.Key_Delete:

View File

@@ -6,7 +6,12 @@ Rectangle {
id: keyboardHints
property bool wtypeAvailable: false
readonly property string hintsText: wtypeAvailable ? I18n.tr("Shift+Enter: Paste • Shift+Del: Clear All • Esc: Close") : I18n.tr("Shift+Del: Clear All • Esc: Close")
property bool enterToPaste: false
readonly property string hintsText: {
if (!wtypeAvailable)
return I18n.tr("Shift+Del: Clear All • Esc: Close");
return enterToPaste ? I18n.tr("Shift+Enter: Copy • Shift+Del: Clear All • Esc: Close", "Keyboard hints when enter-to-paste is enabled") : I18n.tr("Shift+Enter: Paste • Shift+Del: Clear All • Esc: Close");
}
height: ClipboardConstants.keyboardHintsHeight
radius: Theme.cornerRadius
@@ -21,7 +26,7 @@ Rectangle {
spacing: 2
StyledText {
text: "↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help"
text: keyboardHints.enterToPaste ? I18n.tr("↑/↓: Navigate • Enter: Paste • Del: Delete • F10: Help", "Keyboard hints when enter-to-paste is enabled") : "↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter

View File

@@ -3,7 +3,6 @@ import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
@@ -378,11 +377,11 @@ Item {
}
}
DankRectangle {
Rectangle {
anchors.fill: parent
color: root.backgroundColor
borderColor: root.borderColor
borderWidth: root.borderWidth
border.color: root.borderColor
border.width: root.borderWidth
radius: root.cornerRadius
}

View File

@@ -42,6 +42,15 @@ Item {
signal viewModeChanged(string sectionId, string mode)
signal searchQueryRequested(string query)
onActiveChanged: {
if (!active) {
sections = [];
flatModel = [];
selectedItem = null;
_clearModeCache();
}
}
Connections {
target: SettingsData
function onSortAppsAlphabeticallyChanged() {

View File

@@ -4,7 +4,6 @@ import Quickshell.Wayland
import Quickshell.Hyprland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
@@ -14,10 +13,14 @@ Item {
property bool spotlightOpen: false
property bool keyboardActive: false
property bool contentVisible: false
property alias spotlightContent: launcherContent
property var spotlightContent: launcherContentLoader.item
property bool openedFromOverview: false
property bool isClosing: false
property bool _windowEnabled: true
property bool _pendingInitialize: false
property string _pendingQuery: ""
property string _pendingMode: ""
readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
readonly property var effectiveScreen: launcherWindow.screen
@@ -76,7 +79,22 @@ Item {
signal dialogClosed
function _ensureContentLoadedAndInitialize(query, mode) {
_pendingQuery = query || "";
_pendingMode = mode || "";
_pendingInitialize = true;
contentVisible = true;
launcherContentLoader.active = true;
if (spotlightContent) {
_initializeAndShow(_pendingQuery, _pendingMode);
_pendingInitialize = false;
}
}
function _initializeAndShow(query, mode) {
if (!spotlightContent)
return;
contentVisible = true;
spotlightContent.searchField.forceActiveFocus();
@@ -122,7 +140,7 @@ Item {
if (useHyprlandFocusGrab)
focusGrab.active = true;
_initializeAndShow("");
_ensureContentLoadedAndInitialize("", "");
}
function showWithQuery(query) {
@@ -140,7 +158,7 @@ Item {
if (useHyprlandFocusGrab)
focusGrab.active = true;
_initializeAndShow(query);
_ensureContentLoadedAndInitialize(query, "");
}
function hide() {
@@ -177,7 +195,7 @@ Item {
if (useHyprlandFocusGrab)
focusGrab.active = true;
_initializeAndShow("", mode);
_ensureContentLoadedAndInitialize("", mode);
}
function toggleWithMode(mode) {
@@ -202,6 +220,8 @@ Item {
repeat: false
onTriggered: {
isClosing = false;
if (root.unloadContentOnClose)
launcherContentLoader.active = false;
dialogClosed();
}
}
@@ -264,7 +284,7 @@ Item {
PanelWindow {
id: launcherWindow
visible: root._windowEnabled
visible: root._windowEnabled && (!root.unloadContentOnClose || spotlightOpen || isClosing)
color: "transparent"
exclusionMode: ExclusionMode.Ignore
@@ -357,11 +377,11 @@ Item {
}
}
DankRectangle {
Rectangle {
anchors.fill: parent
color: root.backgroundColor
borderColor: root.borderColor
borderWidth: root.borderWidth
border.color: root.borderColor
border.width: root.borderWidth
radius: root.cornerRadius
}
@@ -374,10 +394,22 @@ Item {
anchors.fill: parent
focus: keyboardActive
LauncherContent {
id: launcherContent
Loader {
id: launcherContentLoader
anchors.fill: parent
parentModal: root
active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
asynchronous: false
sourceComponent: LauncherContent {
focus: true
parentModal: root
}
onLoaded: {
if (root._pendingInitialize) {
root._initializeAndShow(root._pendingQuery, root._pendingMode);
root._pendingInitialize = false;
}
}
}
Keys.onEscapePressed: event => {

View File

@@ -112,7 +112,7 @@ function transformBuiltInLauncherItem(item, pluginId, openLabel) {
_hName: "",
_hSub: "",
_hRich: false,
_preScored: undefined
_preScored: item._preScored
};
}
@@ -186,7 +186,7 @@ function transformPluginItem(item, pluginId, selectLabel) {
_hName: "",
_hSub: "",
_hRich: false,
_preScored: undefined
_preScored: item._preScored
};
}

View File

@@ -288,7 +288,7 @@ FocusScope {
Rectangle {
anchors.fill: parent
anchors.topMargin: -Theme.cornerRadius
color: Theme.surfaceContainerHigh
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
radius: Theme.cornerRadius
}

View File

@@ -81,6 +81,12 @@ function calculateNextIndex(flatModel, selectedFlatIndex, sectionId, viewMode, g
return bounds.start + newPosInSection;
}
var currentRow = Math.floor(posInSection / cols);
var lastRow = Math.floor((bounds.count - 1) / cols);
if (currentRow < lastRow) {
return bounds.start + bounds.count - 1;
}
var nextSection = findNextNonHeaderIndex(flatModel, bounds.end + 1);
return nextSection !== -1 ? nextSection : selectedFlatIndex;
}

View File

@@ -132,7 +132,16 @@ Item {
var rowIndex = _flatIndexToRowMap[index];
if (rowIndex === undefined)
return;
mainListView.positionViewAtIndex(rowIndex, ListView.Contain);
if (stickyHeader.visible && rowIndex < _cumulativeHeights.length) {
var rowY = _cumulativeHeights[rowIndex];
var scrollY = mainListView.contentY - mainListView.originY;
if (rowY < scrollY + stickyHeader.height) {
mainListView.contentY = Math.max(mainListView.originY, rowY - stickyHeader.height + mainListView.originY);
}
}
}
function getSelectedItemPosition() {

View File

@@ -152,7 +152,7 @@ function scoreItems(items, query, getFrecencyFn) {
var item = items[i]
var itemScore
if (item._preScored !== undefined) {
if (query && item._preScored !== undefined) {
itemScore = item._preScored
} else {
var frecencyData = getFrecencyFn ? getFrecencyFn(item) : null

View File

@@ -64,7 +64,7 @@ Rectangle {
if (!path)
return false;
var ext = path.split('.').pop().toLowerCase();
return ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp"].indexOf(ext) >= 0;
return ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "jxl", "avif", "heif", "exr"].indexOf(ext) >= 0;
}
DankRipple {

View File

@@ -280,6 +280,7 @@ FocusScope {
showDirsFirst: true
showDotAndDotDot: false
showHidden: root.showHiddenFiles
caseSensitive: false
nameFilters: fileExtensions
showFiles: true
showDirs: true

View File

@@ -31,7 +31,7 @@ StyledRect {
function determineFileType(fileName) {
const ext = getFileExtension(fileName);
const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico"];
const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico", "jxl", "avif", "heif", "exr"];
if (imageExts.includes(ext)) {
return "image";
}
@@ -119,7 +119,7 @@ StyledRect {
id: gridPreviewImage
anchors.fill: parent
anchors.margins: 2
property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga"]
property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga", ".jxl", ".avif", ".heif", ".exr"]
property int weExtIndex: 0
property string imagePath: {
if (weMode && delegateRoot.fileIsDir)

View File

@@ -30,7 +30,7 @@ StyledRect {
function determineFileType(fileName) {
const ext = getFileExtension(fileName);
const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico"];
const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico", "jxl", "avif", "heif", "exr"];
if (imageExts.includes(ext)) {
return "image";
}

View File

@@ -1,4 +1,6 @@
import QtQml
import QtQuick
import QtQuick.Layouts
import Quickshell.Hyprland
import qs.Common
import qs.Modals.Common
@@ -18,7 +20,11 @@ DankModal {
modalHeight: _maxH
onBackgroundClicked: close()
onOpened: {
Qt.callLater(() => modalFocusScope.forceActiveFocus());
Qt.callLater(() => {
modalFocusScope.forceActiveFocus();
if (contentLoader.item?.searchField)
contentLoader.item.searchField.forceActiveFocus();
});
if (!Object.keys(KeybindsService.cheatsheet).length && KeybindsService.cheatsheetAvailable)
KeybindsService.loadCheatsheet();
}
@@ -63,17 +69,39 @@ DankModal {
content: Component {
Item {
anchors.fill: parent
property alias searchField: searchField
Column {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
StyledText {
text: KeybindsService.cheatsheet.title || "Keybinds"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.primary
RowLayout {
width: parent.width
StyledText {
Layout.alignment: Qt.AlignLeft
text: KeybindsService.cheatsheet.title || "Keybinds"
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.primary
}
DankTextField {
id: searchField
Layout.alignment: Qt.AlignRight
leftIconName: "search"
onTextEdited: searchDebounce.restart()
}
}
Timer {
id: searchDebounce
interval: 50
repeat: false
onTriggered: {
mainFlickable.categories = mainFlickable.generateCategories(searchField.text);
}
}
DankFlickable {
@@ -87,17 +115,26 @@ DankModal {
Component.onCompleted: root.activeFlickable = mainFlickable
property var rawBinds: KeybindsService.cheatsheet.binds || {}
property var categories: {
function generateCategories(query) {
const lowerQuery = query ? query.toLowerCase().trim() : "";
const processed = {};
for (const cat in rawBinds) {
const binds = rawBinds[cat];
const catLower = cat.toLowerCase();
const subcats = {};
let hasSubcats = false;
for (let i = 0; i < binds.length; i++) {
const bind = binds[i];
const keyLower = bind.key.toLowerCase();
const descLower = bind.desc.toLowerCase();
const actionLower = bind.action.toLowerCase();
if (!(lowerQuery.length === 0 || keyLower.includes(lowerQuery) || descLower.includes(lowerQuery) || catLower.includes(lowerQuery) || actionLower.includes(lowerQuery)))
continue;
if (bind.hideOnOverlay)
continue;
if (bind.subcat) {
hasSubcats = true;
if (!subcats[bind.subcat])
@@ -119,9 +156,11 @@ DankModal {
subcatKeys: Object.keys(subcats)
};
}
return processed;
}
property var categoryKeys: Object.keys(categories)
property var categories: generateCategories("");
function estimateCategoryHeight(catName) {
const catData = categories[catName];
@@ -136,6 +175,8 @@ DankModal {
return 40 + bindCount * 28;
}
property var categoryKeys: Object.keys(categories);
function distributeCategories(cols) {
const columns = [];
const heights = [];

View File

@@ -70,8 +70,8 @@ DankModal {
NotificationService.dismissAllPopups();
}
modalWidth: 500
modalHeight: 700
modalWidth: Math.min(500, screenWidth - 48)
modalHeight: Math.min(700, screenHeight * 0.85)
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: false
onBackgroundClicked: hide()

View File

@@ -14,6 +14,7 @@ FloatingWindow {
property int currentTab: 0
property string searchText: ""
property string expandedPid: ""
property string processFilter: "all"
property bool shouldHaveFocus: visible
property alias shouldBeVisible: processListModal.visible
@@ -82,9 +83,9 @@ FloatingWindow {
objectName: "processListModal"
title: I18n.tr("System Monitor", "sysmon window title")
minimumSize: Qt.size(750, 550)
implicitWidth: 1000
implicitHeight: 720
minimumSize: Qt.size(Math.min(Math.round(Theme.fontSizeMedium * 48), Screen.width), Math.min(Math.round(Theme.fontSizeMedium * 34), Screen.height))
implicitWidth: Math.round(Theme.fontSizeMedium * 71)
implicitHeight: Math.round(Theme.fontSizeMedium * 51)
color: Theme.surfaceContainer
visible: false
@@ -98,6 +99,8 @@ FloatingWindow {
closingModal();
searchText = "";
expandedPid = "";
processFilter = "all";
processFilterGroup.currentIndex = 0;
if (processesTabLoader.item)
processesTabLoader.item.reset();
DgopService.removeRef(["cpu", "memory", "network", "disk", "system"]);
@@ -233,7 +236,7 @@ FloatingWindow {
Item {
Layout.fillWidth: true
Layout.preferredHeight: 48
Layout.preferredHeight: Math.round(Theme.fontSizeMedium * 3.4)
MouseArea {
anchors.fill: parent
@@ -290,10 +293,10 @@ FloatingWindow {
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: 52
Layout.preferredHeight: Math.round(Theme.fontSizeMedium * 3.7)
Layout.leftMargin: Theme.spacingL
Layout.rightMargin: Theme.spacingL
spacing: Theme.spacingL
spacing: Theme.spacingM
Row {
spacing: 2
@@ -319,14 +322,15 @@ FloatingWindow {
]
Rectangle {
width: 120
height: 44
width: tabRowContent.implicitWidth + Theme.spacingM * 2
height: Math.round(Theme.fontSizeMedium * 3.1)
radius: Theme.cornerRadius
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
border.color: currentTab === index ? Theme.primary : "transparent"
border.width: currentTab === index ? 1 : 0
Row {
id: tabRowContent
anchors.centerIn: parent
spacing: Theme.spacingXS
@@ -368,10 +372,40 @@ FloatingWindow {
Layout.fillWidth: true
}
DankButtonGroup {
id: processFilterGroup
model: [I18n.tr("All"), I18n.tr("User"), I18n.tr("System")]
currentIndex: 0
checkEnabled: false
buttonHeight: Math.round(Theme.fontSizeSmall * 2.6)
minButtonWidth: 0
buttonPadding: Theme.spacingS
textSize: Theme.fontSizeSmall
visible: currentTab === 0
onSelectionChanged: (index, selected) => {
if (!selected)
return;
currentIndex = index;
switch (index) {
case 0:
processListModal.processFilter = "all";
return;
case 1:
processListModal.processFilter = "user";
return;
case 2:
processListModal.processFilter = "system";
return;
}
}
}
DankTextField {
id: searchField
Layout.preferredWidth: 250
Layout.preferredHeight: 40
Layout.fillWidth: true
Layout.maximumWidth: Math.round(Theme.fontSizeMedium * 18)
Layout.minimumWidth: Theme.fontSizeMedium * 4
Layout.preferredHeight: Math.round(Theme.fontSizeMedium * 2.8)
placeholderText: I18n.tr("Search processes...", "process search placeholder")
leftIconName: "search"
showClearButton: true
@@ -403,6 +437,7 @@ FloatingWindow {
sourceComponent: ProcessesView {
searchText: processListModal.searchText
expandedPid: processListModal.expandedPid
processFilter: processListModal.processFilter
contextMenu: processContextMenu
onExpandedPidChanged: processListModal.expandedPid = expandedPid
}
@@ -438,7 +473,7 @@ FloatingWindow {
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 32
Layout.preferredHeight: Math.round(Theme.fontSizeSmall * 2.7)
Layout.leftMargin: Theme.spacingL
Layout.rightMargin: Theme.spacingL
Layout.bottomMargin: Theme.spacingM

View File

@@ -128,7 +128,7 @@ FloatingWindow {
browserIcon: "person"
browserType: "profile"
showHiddenFiles: true
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"]
onFileSelected: path => {
PortalService.setProfileImage(path);
close();
@@ -152,7 +152,7 @@ FloatingWindow {
browserIcon: "wallpaper"
browserType: "wallpaper"
showHiddenFiles: true
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"]
onFileSelected: path => {
SessionData.setWallpaper(path);
close();

View File

@@ -17,8 +17,9 @@ Rectangle {
property var parentModal: null
signal tabChangeRequested(int tabIndex)
property var expandedCategories: ({})
property var autoExpandedCategories: ({})
property string _expandedIds: ","
property string _collapsedIds: ","
property string _autoExpandedIds: ","
property bool searchActive: searchField.text.length > 0
property int searchSelectedIndex: 0
property int keyboardHighlightIndex: -1
@@ -208,7 +209,7 @@ Rectangle {
"children": [
{
"id": "display_config",
"text": I18n.tr("Configuration") + " (Beta)",
"text": I18n.tr("Configuration"),
"icon": "display_settings",
"tabIndex": 24
},
@@ -340,24 +341,46 @@ Rectangle {
return true;
}
function toggleCategory(categoryId) {
var newExpanded = Object.assign({}, expandedCategories);
newExpanded[categoryId] = !isCategoryExpanded(categoryId);
expandedCategories = newExpanded;
function _setExpanded(id, expanded) {
var marker = "," + id + ",";
if (expanded) {
if (_expandedIds.indexOf(marker) < 0)
_expandedIds = _expandedIds + id + ",";
_collapsedIds = _collapsedIds.replace(marker, ",");
} else {
_expandedIds = _expandedIds.replace(marker, ",");
if (_collapsedIds.indexOf(marker) < 0)
_collapsedIds = _collapsedIds + id + ",";
}
}
var newAutoExpanded = Object.assign({}, autoExpandedCategories);
delete newAutoExpanded[categoryId];
autoExpandedCategories = newAutoExpanded;
function _setAutoExpanded(id, value) {
var marker = "," + id + ",";
if (value) {
if (_autoExpandedIds.indexOf(marker) < 0)
_autoExpandedIds = _autoExpandedIds + id + ",";
} else {
_autoExpandedIds = _autoExpandedIds.replace(marker, ",");
}
}
function _isAutoExpanded(id) {
return _autoExpandedIds.indexOf("," + id + ",") >= 0;
}
function toggleCategory(categoryId) {
_setExpanded(categoryId, !isCategoryExpanded(categoryId));
_setAutoExpanded(categoryId, false);
}
function isCategoryExpanded(categoryId) {
if (expandedCategories[categoryId] !== undefined) {
return expandedCategories[categoryId];
}
var category = categoryStructure.find(cat => cat.id === categoryId);
if (category && category.collapsedByDefault) {
if (_collapsedIds.indexOf("," + categoryId + ",") >= 0)
return false;
if (_expandedIds.indexOf("," + categoryId + ",") >= 0)
return true;
var category = categoryStructure.find(cat => cat.id === categoryId);
if (category && category.collapsedByDefault)
return false;
}
return true;
}
@@ -387,13 +410,8 @@ Rectangle {
return;
if (!isCategoryExpanded(parent.id)) {
var newExpanded = Object.assign({}, expandedCategories);
newExpanded[parent.id] = true;
expandedCategories = newExpanded;
var newAutoExpanded = Object.assign({}, autoExpandedCategories);
newAutoExpanded[parent.id] = true;
autoExpandedCategories = newAutoExpanded;
_setExpanded(parent.id, true);
_setAutoExpanded(parent.id, true);
}
}
@@ -401,14 +419,9 @@ Rectangle {
var oldParent = findParentCategory(oldTabIndex);
var newParent = findParentCategory(newTabIndex);
if (oldParent && oldParent !== newParent && autoExpandedCategories[oldParent.id]) {
var newExpanded = Object.assign({}, expandedCategories);
newExpanded[oldParent.id] = false;
expandedCategories = newExpanded;
var newAutoExpanded = Object.assign({}, autoExpandedCategories);
delete newAutoExpanded[oldParent.id];
autoExpandedCategories = newAutoExpanded;
if (oldParent && oldParent !== newParent && _isAutoExpanded(oldParent.id)) {
_setExpanded(oldParent.id, false);
_setAutoExpanded(oldParent.id, false);
}
}

View File

@@ -305,6 +305,8 @@ FloatingWindow {
color: Theme.primary
topPadding: Theme.spacingM
bottomPadding: Theme.spacingXS
width: parent.width
horizontalAlignment: Text.AlignLeft
}
component CheckboxRow: Row {
@@ -371,6 +373,9 @@ FloatingWindow {
anchors.fill: parent
focus: true
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
Keys.onEscapePressed: event => {
hide();
event.accepted = true;
@@ -401,12 +406,16 @@ FloatingWindow {
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
width: parent.width
horizontalAlignment: Text.AlignLeft
}
StyledText {
text: I18n.tr("Configure match criteria and actions")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceTextMedium
width: parent.width
horizontalAlignment: Text.AlignLeft
}
}
}
@@ -550,6 +559,8 @@ FloatingWindow {
text: I18n.tr("Output")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
horizontalAlignment: Text.AlignLeft
}
InputField {
@@ -575,6 +586,8 @@ FloatingWindow {
text: I18n.tr("Workspace")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
horizontalAlignment: Text.AlignLeft
}
InputField {
@@ -606,6 +619,8 @@ FloatingWindow {
text: I18n.tr("Column Width")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
horizontalAlignment: Text.AlignLeft
}
InputField {
@@ -631,6 +646,8 @@ FloatingWindow {
text: I18n.tr("Window Height")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
horizontalAlignment: Text.AlignLeft
}
InputField {
@@ -710,6 +727,8 @@ FloatingWindow {
text: I18n.tr("Block Out From")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
horizontalAlignment: Text.AlignLeft
}
DankDropdown {
@@ -730,6 +749,8 @@ FloatingWindow {
text: I18n.tr("Column Display")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
horizontalAlignment: Text.AlignLeft
}
DankDropdown {
@@ -802,6 +823,8 @@ FloatingWindow {
text: I18n.tr("Min W")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
horizontalAlignment: Text.AlignLeft
}
InputField {
@@ -827,6 +850,8 @@ FloatingWindow {
text: I18n.tr("Max W")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
horizontalAlignment: Text.AlignLeft
}
InputField {
@@ -852,6 +877,8 @@ FloatingWindow {
text: I18n.tr("Min H")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
horizontalAlignment: Text.AlignLeft
}
InputField {
@@ -877,6 +904,8 @@ FloatingWindow {
text: I18n.tr("Max H")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
horizontalAlignment: Text.AlignLeft
}
InputField {
@@ -960,6 +989,8 @@ FloatingWindow {
text: I18n.tr("Size")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
horizontalAlignment: Text.AlignLeft
}
InputField {
@@ -985,6 +1016,8 @@ FloatingWindow {
text: I18n.tr("Move")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
horizontalAlignment: Text.AlignLeft
}
InputField {
@@ -1016,6 +1049,8 @@ FloatingWindow {
text: I18n.tr("Monitor")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
horizontalAlignment: Text.AlignLeft
}
InputField {
@@ -1041,6 +1076,8 @@ FloatingWindow {
text: I18n.tr("Workspace")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
horizontalAlignment: Text.AlignLeft
}
InputField {

View File

@@ -54,9 +54,6 @@ DankPopout {
property alias launcherContent: launcherContent
color: "transparent"
radius: Theme.cornerRadius
antialiasing: true
smooth: true
QtObject {
id: modalAdapter
@@ -68,35 +65,6 @@ DankPopout {
}
}
Repeater {
model: [
{
"margin": -3,
"color": Qt.rgba(0, 0, 0, 0.05),
"z": -3
},
{
"margin": -2,
"color": Qt.rgba(0, 0, 0, 0.08),
"z": -2
},
{
"margin": 0,
"color": Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12),
"z": -1
}
]
Rectangle {
anchors.fill: parent
anchors.margins: modelData.margin
color: "transparent"
radius: parent.radius + Math.abs(modelData.margin)
border.color: modelData.color
border.width: 0
z: modelData.z
}
}
FocusScope {
anchors.fill: parent
focus: true

View File

@@ -189,7 +189,7 @@ Variants {
smooth: true
cache: true
sourceSize: Qt.size(root.textureWidth, root.textureHeight)
fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SettingsData.wallpaperFillMode)
fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SessionData.getMonitorWallpaperFillMode(modelData.name))
}
Image {
@@ -201,7 +201,7 @@ Variants {
smooth: true
cache: true
sourceSize: Qt.size(root.textureWidth, root.textureHeight)
fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SettingsData.wallpaperFillMode)
fillMode: root.getFillMode(SessionData.isGreeterMode ? GreetdSettings.wallpaperFillMode : SessionData.getMonitorWallpaperFillMode(modelData.name))
onStatusChanged: {
if (status !== Image.Ready)

View File

@@ -75,6 +75,7 @@ Item {
readonly property color dimColor: Theme.surfaceVariantText
property string currentGpuPciIdRef: ""
property var activeModuleRefs: []
property var cpuHistory: []
property var memHistory: []
@@ -140,12 +141,13 @@ Item {
modules.push("disk", "diskmounts");
if (showTopProcesses)
modules.push("processes");
activeModuleRefs = modules;
DgopService.addRef(modules);
updateGpuRef();
}
Component.onDestruction: {
DgopService.removeRef();
DgopService.removeRef(activeModuleRefs);
if (currentGpuPciIdRef)
DgopService.removeGpuPciId(currentGpuPciIdRef);
}
@@ -153,8 +155,13 @@ Item {
onShowGpuTempChanged: updateGpuRef()
onSelectedGpuPciIdChanged: updateGpuRef()
onShowTopProcessesChanged: {
if (showTopProcesses)
if (showTopProcesses) {
activeModuleRefs = activeModuleRefs.concat(["processes"]);
DgopService.addRef(["processes"]);
} else {
DgopService.removeRef(["processes"]);
activeModuleRefs = activeModuleRefs.filter(m => m !== "processes");
}
}
function updateGpuRef() {

View File

@@ -945,22 +945,31 @@ Column {
}
}
Component.onCompleted: {
Qt.callLater(() => {
const pluginComponent = PluginService.pluginWidgetComponents[pluginId];
if (pluginComponent) {
const instance = pluginComponent.createObject(null, {
"pluginId": pluginId,
"pluginService": PluginService,
"visible": false,
"width": 0,
"height": 0
});
if (instance) {
pluginInstance = instance;
}
function tryCreatePluginInstance() {
const pluginComponent = PluginService.pluginWidgetComponents[pluginId];
if (!pluginComponent)
return false;
try {
const instance = pluginComponent.createObject(null, {
"pluginId": pluginId,
"pluginService": PluginService,
"visible": false,
"width": 0,
"height": 0
});
if (instance) {
pluginInstance = instance;
return true;
}
});
} catch (e) {
console.warn("DragDropGrid: stale plugin component for", pluginId, "- reloading");
PluginService.reloadPlugin(pluginId);
}
return false;
}
Component.onCompleted: {
Qt.callLater(() => tryCreatePluginInstance());
}
Connections {
@@ -970,6 +979,11 @@ Column {
pluginInstance.loadPluginData();
}
}
function onPluginLoaded(loadedPluginId) {
if (loadedPluginId !== pluginId || pluginInstance)
return;
Qt.callLater(() => tryCreatePluginInstance());
}
}
Component.onDestruction: {

View File

@@ -13,8 +13,8 @@ Row {
property Item popoutContent: null
signal addWidget(string widgetId)
signal resetToDefault()
signal clearAll()
signal resetToDefault
signal clearAll
height: 48
spacing: Theme.spacingS
@@ -28,7 +28,7 @@ Row {
y: parent ? Math.round((parent.height - height) / 2) : 0
width: 400
height: 300
modal: true
modal: false
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
@@ -133,7 +133,7 @@ Row {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.addWidget(modelData.id)
root.addWidget(modelData.id);
}
}
}

View File

@@ -12,6 +12,7 @@ DankPopout {
id: root
layerNamespace: "dms:control-center"
fullHeightSurface: true
property string expandedSection: ""
property var triggerScreen: null
@@ -115,11 +116,6 @@ DankPopout {
property alias bluetoothCodecSelector: bluetoothCodecSelector
color: "transparent"
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 0
antialiasing: true
smooth: true
Rectangle {
anchors.fill: parent

View File

@@ -13,15 +13,12 @@ Rectangle {
LayoutMirroring.childrenInherit: true
implicitHeight: {
if (height > 0) {
if (height > 0)
return height;
}
if (NetworkService.wifiToggling) {
if (NetworkService.wifiToggling)
return headerRow.height + wifiToggleContent.height + Theme.spacingM;
}
if (NetworkService.wifiEnabled) {
if (NetworkService.wifiEnabled)
return headerRow.height + wifiContent.height + Theme.spacingM;
}
return headerRow.height + wifiOffContent.height + Theme.spacingM;
}
radius: Theme.cornerRadius
@@ -40,34 +37,40 @@ Rectangle {
property bool hasEthernetAvailable: (NetworkService.ethernetDevices?.length ?? 0) > 0
property bool hasWifiAvailable: (NetworkService.wifiDevices?.length ?? 0) > 0
property bool hasBothConnectionTypes: hasEthernetAvailable && hasWifiAvailable
property int maxPinnedNetworks: 3
function normalizePinList(value) {
if (Array.isArray(value))
return value.filter(v => v);
if (typeof value === "string" && value.length > 0)
return [value];
return [];
}
function getPinnedNetworks() {
const pins = SettingsData.wifiNetworkPins || {};
return normalizePinList(pins["preferredWifi"]);
}
property int currentPreferenceIndex: {
if (DMSService.apiVersion < 5) {
if (DMSService.apiVersion < 5)
return 1;
}
if (NetworkService.backend !== "networkmanager" || DMSService.apiVersion <= 10) {
if (NetworkService.backend !== "networkmanager" || DMSService.apiVersion <= 10)
return 1;
}
if (!hasEthernetAvailable) {
if (!hasEthernetAvailable)
return 1;
}
if (!hasWifiAvailable) {
if (!hasWifiAvailable)
return 0;
}
const pref = NetworkService.userPreference;
const status = NetworkService.networkStatus;
if (pref === "ethernet") {
switch (pref) {
case "ethernet":
return 0;
}
if (pref === "wifi") {
case "wifi":
return 1;
default:
return NetworkService.networkStatus === "ethernet" ? 0 : 1;
}
return status === "ethernet" ? 0 : 1;
}
Row {
@@ -78,7 +81,7 @@ Rectangle {
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingS
height: 40
height: Math.max(headerLeft.implicitHeight, rightControls.implicitHeight) + Theme.spacingS * 2
StyledText {
id: headerLeft
@@ -162,9 +165,10 @@ Rectangle {
anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM
visible: currentPreferenceIndex === 1 && NetworkService.wifiToggling
height: visible ? 80 : 0
height: visible ? wifiToggleColumn.implicitHeight + Theme.spacingM * 2 : 0
Column {
id: wifiToggleColumn
anchors.centerIn: parent
spacing: Theme.spacingM
@@ -201,9 +205,10 @@ Rectangle {
anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM
visible: currentPreferenceIndex === 1 && !NetworkService.wifiEnabled && !NetworkService.wifiToggling
height: visible ? 120 : 0
height: visible ? wifiOffColumn.implicitHeight + Theme.spacingM * 2 : 0
Column {
id: wifiOffColumn
anchors.centerIn: parent
spacing: Theme.spacingL
width: parent.width
@@ -226,14 +231,15 @@ Rectangle {
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
width: 120
height: 36
radius: 18
width: enableWifiLabel.implicitWidth + Theme.spacingL * 2
height: enableWifiLabel.implicitHeight + Theme.spacingM * 2
radius: height / 2
color: enableWifiButton.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
border.width: 0
border.color: Theme.primary
StyledText {
id: enableWifiLabel
anchors.centerIn: parent
text: I18n.tr("Enable WiFi")
color: Theme.primary
@@ -252,6 +258,25 @@ Rectangle {
}
}
ScriptModel {
id: wiredConnectionsModel
objectProp: "uuid"
values: {
const networks = NetworkService.wiredConnections;
if (!networks)
return [];
let sorted = [...networks];
sorted.sort((a, b) => {
if (a.isActive && !b.isActive)
return -1;
if (!a.isActive && b.isActive)
return 1;
return a.id.localeCompare(b.id);
});
return sorted;
}
}
DankFlickable {
id: wiredContent
anchors.top: headerRow.bottom
@@ -270,34 +295,25 @@ Rectangle {
spacing: Theme.spacingS
Repeater {
model: ScriptModel {
values: {
const currentUuid = NetworkService.ethernetConnectionUuid;
const networks = NetworkService.wiredConnections;
let sorted = [...networks];
sorted.sort((a, b) => {
if (a.isActive && !b.isActive)
return -1;
if (!a.isActive && b.isActive)
return 1;
return a.id.localeCompare(b.id);
});
return sorted;
}
}
model: wiredConnectionsModel
delegate: Rectangle {
id: wiredDelegate
required property var modelData
required property int index
readonly property bool isActive: modelData.isActive
readonly property string configName: modelData.id || I18n.tr("Unknown Config")
width: parent.width
height: 50
height: wiredContentRow.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: wiredNetworkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
border.color: Theme.primary
border.width: 0
Row {
id: wiredContentRow
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
@@ -306,7 +322,7 @@ Rectangle {
DankIcon {
name: "lan"
size: Theme.iconSize - 4
color: modelData.isActive ? Theme.primary : Theme.surfaceText
color: wiredDelegate.isActive ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
@@ -315,10 +331,10 @@ Rectangle {
width: 200
StyledText {
text: modelData.id || I18n.tr("Unknown Config")
text: wiredDelegate.configName
font.pixelSize: Theme.fontSizeMedium
color: modelData.isActive ? Theme.primary : Theme.surfaceText
font.weight: modelData.isActive ? Font.Medium : Font.Normal
color: wiredDelegate.isActive ? Theme.primary : Theme.surfaceText
font.weight: wiredDelegate.isActive ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
}
@@ -335,12 +351,12 @@ Rectangle {
onClicked: {
if (wiredNetworkContextMenu.visible) {
wiredNetworkContextMenu.close();
} else {
wiredNetworkContextMenu.currentID = modelData.id;
wiredNetworkContextMenu.currentUUID = modelData.uuid;
wiredNetworkContextMenu.currentConnected = modelData.isActive;
wiredNetworkContextMenu.popup(wiredOptionsButton, -wiredNetworkContextMenu.width + wiredOptionsButton.width, wiredOptionsButton.height + Theme.spacingXS);
return;
}
wiredNetworkContextMenu.currentID = modelData.id;
wiredNetworkContextMenu.currentUUID = modelData.uuid;
wiredNetworkContextMenu.currentConnected = wiredDelegate.isActive;
wiredNetworkContextMenu.popup(wiredOptionsButton, -wiredNetworkContextMenu.width + wiredOptionsButton.width, wiredOptionsButton.height + Theme.spacingXS);
}
}
@@ -357,9 +373,8 @@ Rectangle {
cursorShape: Qt.PointingHandCursor
onPressed: mouse => wiredRipple.trigger(mouse.x, mouse.y)
onClicked: function (event) {
if (modelData.uuid !== NetworkService.ethernetConnectionUuid) {
if (modelData.uuid !== NetworkService.ethernetConnectionUuid)
NetworkService.connectToSpecificWiredConfig(modelData.uuid);
}
event.accepted = true;
}
}
@@ -403,9 +418,8 @@ Rectangle {
}
onTriggered: {
if (!wiredNetworkContextMenu.currentConnected) {
if (!wiredNetworkContextMenu.currentConnected)
NetworkService.connectToSpecificWiredConfig(wiredNetworkContextMenu.currentUUID);
}
}
}
@@ -451,13 +465,46 @@ Rectangle {
}
onTriggered: {
let networkData = NetworkService.getWiredNetworkInfo(wiredNetworkContextMenu.currentUUID);
networkWiredInfoModal.showNetworkInfo(wiredNetworkContextMenu.currentID, networkData);
const networkData = NetworkService.getWiredNetworkInfo(wiredNetworkContextMenu.currentUUID);
networkWiredInfoModalLoader.active = true;
networkWiredInfoModalLoader.item.showNetworkInfo(wiredNetworkContextMenu.currentID, networkData);
}
}
}
DankFlickable {
ScriptModel {
id: wifiNetworksModel
objectProp: "ssid"
values: wifiContent.menuOpen ? wifiContent.frozenNetworks : wifiContent.sortedNetworks
}
Item {
id: wifiScanningOverlay
anchors.top: headerRow.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM
visible: currentPreferenceIndex === 1 && NetworkService.wifiEnabled && !NetworkService.wifiToggling && NetworkService.wifiInterface && (NetworkService.wifiNetworks?.length ?? 0) < 1 && NetworkService.isScanning
DankIcon {
anchors.centerIn: parent
name: "refresh"
size: 48
color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.3)
RotationAnimation on rotation {
running: wifiScanningOverlay.visible
loops: Animation.Infinite
from: 0
to: 360
duration: 1000
}
}
}
DankListView {
id: wifiContent
anchors.top: headerRow.bottom
anchors.left: parent.left
@@ -465,31 +512,17 @@ Rectangle {
anchors.bottom: parent.bottom
anchors.margins: Theme.spacingM
anchors.topMargin: Theme.spacingM
visible: currentPreferenceIndex === 1 && NetworkService.wifiEnabled && !NetworkService.wifiToggling
contentHeight: wifiColumn.height
visible: currentPreferenceIndex === 1 && NetworkService.wifiEnabled && !NetworkService.wifiToggling && !wifiScanningOverlay.visible
clip: true
property int maxPinnedNetworks: 3
function normalizePinList(value) {
if (Array.isArray(value))
return value.filter(v => v);
if (typeof value === "string" && value.length > 0)
return [value];
return [];
}
function getPinnedNetworks() {
const pins = SettingsData.wifiNetworkPins || {};
return normalizePinList(pins["preferredWifi"]);
}
spacing: Theme.spacingS
model: wifiNetworksModel
property var frozenNetworks: []
property bool menuOpen: false
property var sortedNetworks: {
const ssid = NetworkService.currentWifiSSID;
const networks = NetworkService.wifiNetworks;
const pinnedList = getPinnedNetworks();
const pinnedList = root.getPinnedNetworks();
let sorted = [...networks];
sorted.sort((a, b) => {
@@ -519,229 +552,188 @@ Rectangle {
frozenNetworks = sortedNetworks;
}
Column {
id: wifiColumn
width: parent.width
spacing: Theme.spacingS
delegate: Rectangle {
id: wifiDelegate
required property var modelData
required property int index
Item {
width: parent.width
height: 200
visible: NetworkService.wifiInterface && NetworkService.wifiNetworks?.length < 1 && !NetworkService.wifiToggling && NetworkService.isScanning
readonly property bool isConnected: modelData.ssid === NetworkService.currentWifiSSID
readonly property bool isPinned: root.getPinnedNetworks().includes(modelData.ssid)
readonly property string networkName: modelData.ssid || I18n.tr("Unknown Network")
readonly property int signalStrength: modelData.signal || 0
width: wifiContent.width
height: wifiContentRow.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: networkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
border.color: wifiDelegate.isConnected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 0
Row {
id: wifiContentRow
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
anchors.centerIn: parent
name: "refresh"
size: 48
color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.3)
name: {
if (wifiDelegate.signalStrength >= 50)
return "wifi";
if (wifiDelegate.signalStrength >= 25)
return "wifi_2_bar";
return "wifi_1_bar";
}
size: Theme.iconSize - 4
color: wifiDelegate.isConnected ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
RotationAnimation on rotation {
running: NetworkService.isScanning
loops: Animation.Infinite
from: 0
to: 360
duration: 1000
Column {
anchors.verticalCenter: parent.verticalCenter
width: 200
StyledText {
text: wifiDelegate.networkName
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: wifiDelegate.isConnected ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
}
Row {
spacing: Theme.spacingXS
StyledText {
text: wifiDelegate.isConnected ? I18n.tr("Connected") + " \u2022" : (modelData.secured ? I18n.tr("Secured") + " \u2022" : I18n.tr("Open") + " \u2022")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: modelData.saved ? I18n.tr("Saved") : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
visible: text.length > 0
}
StyledText {
text: (modelData.saved ? "\u2022 " : "") + wifiDelegate.signalStrength + "%"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
}
Repeater {
model: ScriptModel {
values: wifiContent.menuOpen ? wifiContent.frozenNetworks : wifiContent.sortedNetworks
DankActionButton {
id: optionsButton
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
iconName: "more_horiz"
buttonSize: 28
onClicked: {
if (networkContextMenu.visible) {
networkContextMenu.close();
return;
}
wifiContent.menuOpen = true;
networkContextMenu.currentSSID = modelData.ssid;
networkContextMenu.currentSecured = modelData.secured;
networkContextMenu.currentConnected = wifiDelegate.isConnected;
networkContextMenu.currentSaved = modelData.saved;
networkContextMenu.currentSignal = modelData.signal;
networkContextMenu.currentAutoconnect = modelData.autoconnect || false;
networkContextMenu.popup(optionsButton, -networkContextMenu.width + optionsButton.width, optionsButton.height + Theme.spacingXS);
}
}
Rectangle {
anchors.right: parent.right
anchors.rightMargin: optionsButton.width + Theme.spacingM + Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
width: pinWifiRow.width + Theme.spacingS * 2
height: pinWifiRow.implicitHeight + Theme.spacingXS * 2
radius: height / 2
color: wifiDelegate.isPinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05)
Row {
id: pinWifiRow
anchors.centerIn: parent
spacing: 4
DankIcon {
name: "push_pin"
size: 16
color: wifiDelegate.isPinned ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: wifiDelegate.isPinned ? I18n.tr("Pinned") : I18n.tr("Pin")
font.pixelSize: Theme.fontSizeSmall
color: wifiDelegate.isPinned ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
delegate: Rectangle {
required property var modelData
required property int index
DankRipple {
id: pinRipple
cornerRadius: parent.radius
}
width: parent.width
height: 50
radius: Theme.cornerRadius
color: networkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
border.color: modelData.ssid === NetworkService.currentWifiSSID ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
border.width: 0
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {}));
let pinnedList = root.normalizePinList(pins["preferredWifi"]);
const pinIndex = pinnedList.indexOf(modelData.ssid);
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: {
let strength = modelData.signal || 0;
if (strength >= 50)
return "wifi";
if (strength >= 25)
return "wifi_2_bar";
return "wifi_1_bar";
}
size: Theme.iconSize - 4
color: modelData.ssid === NetworkService.currentWifiSSID ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
if (pinIndex !== -1) {
pinnedList.splice(pinIndex, 1);
} else {
pinnedList.unshift(modelData.ssid);
if (pinnedList.length > root.maxPinnedNetworks)
pinnedList = pinnedList.slice(0, root.maxPinnedNetworks);
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: 200
if (pinnedList.length > 0)
pins["preferredWifi"] = pinnedList;
else
delete pins["preferredWifi"];
StyledText {
text: modelData.ssid || I18n.tr("Unknown Network")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData.ssid === NetworkService.currentWifiSSID ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
}
Row {
spacing: Theme.spacingXS
StyledText {
text: modelData.ssid === NetworkService.currentWifiSSID ? I18n.tr("Connected") + " •" : (modelData.secured ? I18n.tr("Secured") + " •" : I18n.tr("Open") + " •")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: modelData.saved ? I18n.tr("Saved") : ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.primary
visible: text.length > 0
}
StyledText {
text: (modelData.saved ? "• " : "") + modelData.signal + "%"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
SettingsData.set("wifiNetworkPins", pins);
}
}
}
DankActionButton {
id: optionsButton
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
iconName: "more_horiz"
buttonSize: 28
onClicked: {
if (networkContextMenu.visible) {
networkContextMenu.close();
} else {
wifiContent.menuOpen = true;
networkContextMenu.currentSSID = modelData.ssid;
networkContextMenu.currentSecured = modelData.secured;
networkContextMenu.currentConnected = modelData.ssid === NetworkService.currentWifiSSID;
networkContextMenu.currentSaved = modelData.saved;
networkContextMenu.currentSignal = modelData.signal;
networkContextMenu.currentAutoconnect = modelData.autoconnect || false;
networkContextMenu.popup(optionsButton, -networkContextMenu.width + optionsButton.width, optionsButton.height + Theme.spacingXS);
}
}
DankRipple {
id: wifiRipple
cornerRadius: parent.radius
}
MouseArea {
id: networkMouseArea
anchors.fill: parent
anchors.rightMargin: optionsButton.width + Theme.spacingM + Theme.spacingS + pinWifiRow.width + Theme.spacingS * 4
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: mouse => wifiRipple.trigger(mouse.x, mouse.y)
onClicked: function (event) {
if (wifiDelegate.isConnected) {
event.accepted = true;
return;
}
Rectangle {
anchors.right: parent.right
anchors.rightMargin: optionsButton.width + Theme.spacingM + Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
width: pinWifiRow.width + Theme.spacingS * 2
height: 28
radius: height / 2
color: {
const isThisNetworkPinned = wifiContent.getPinnedNetworks().includes(modelData.ssid);
return isThisNetworkPinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05);
}
Row {
id: pinWifiRow
anchors.centerIn: parent
spacing: 4
DankIcon {
name: "push_pin"
size: 16
color: {
const isThisNetworkPinned = wifiContent.getPinnedNetworks().includes(modelData.ssid);
return isThisNetworkPinned ? Theme.primary : Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: {
const isThisNetworkPinned = wifiContent.getPinnedNetworks().includes(modelData.ssid);
return isThisNetworkPinned ? I18n.tr("Pinned") : I18n.tr("Pin");
}
font.pixelSize: Theme.fontSizeSmall
color: {
const isThisNetworkPinned = wifiContent.getPinnedNetworks().includes(modelData.ssid);
return isThisNetworkPinned ? Theme.primary : Theme.surfaceText;
}
anchors.verticalCenter: parent.verticalCenter
}
}
DankRipple {
id: pinRipple
cornerRadius: parent.radius
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onPressed: mouse => pinRipple.trigger(mouse.x, mouse.y)
onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {}));
let pinnedList = wifiContent.normalizePinList(pins["preferredWifi"]);
const pinIndex = pinnedList.indexOf(modelData.ssid);
if (pinIndex !== -1) {
pinnedList.splice(pinIndex, 1);
} else {
pinnedList.unshift(modelData.ssid);
if (pinnedList.length > wifiContent.maxPinnedNetworks)
pinnedList = pinnedList.slice(0, wifiContent.maxPinnedNetworks);
}
if (pinnedList.length > 0)
pins["preferredWifi"] = pinnedList;
else
delete pins["preferredWifi"];
SettingsData.set("wifiNetworkPins", pins);
}
}
}
DankRipple {
id: wifiRipple
cornerRadius: parent.radius
}
MouseArea {
id: networkMouseArea
anchors.fill: parent
anchors.rightMargin: optionsButton.width + Theme.spacingM + Theme.spacingS + pinWifiRow.width + Theme.spacingS * 4
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: mouse => wifiRipple.trigger(mouse.x, mouse.y)
onClicked: function (event) {
if (modelData.ssid !== NetworkService.currentWifiSSID) {
if (modelData.secured && !modelData.saved) {
if (DMSService.apiVersion >= 7) {
NetworkService.connectToWifi(modelData.ssid);
} else {
PopoutService.showWifiPasswordModal(modelData.ssid);
}
} else {
NetworkService.connectToWifi(modelData.ssid);
}
}
event.accepted = true;
}
if (modelData.secured && !modelData.saved && DMSService.apiVersion < 7) {
PopoutService.showWifiPasswordModal(modelData.ssid);
} else {
NetworkService.connectToWifi(modelData.ssid);
}
event.accepted = true;
}
}
}
@@ -759,6 +751,8 @@ Rectangle {
property int currentSignal: 0
property bool currentAutoconnect: false
readonly property bool showSavedOptions: currentSaved || currentConnected
onClosed: {
wifiContent.menuOpen = false;
}
@@ -790,17 +784,13 @@ Rectangle {
onTriggered: {
if (networkContextMenu.currentConnected) {
NetworkService.disconnectWifi();
} else {
if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved) {
if (DMSService.apiVersion >= 7) {
NetworkService.connectToWifi(networkContextMenu.currentSSID);
} else {
PopoutService.showWifiPasswordModal(networkContextMenu.currentSSID);
}
} else {
NetworkService.connectToWifi(networkContextMenu.currentSSID);
}
return;
}
if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved && DMSService.apiVersion < 7) {
PopoutService.showWifiPasswordModal(networkContextMenu.currentSSID);
return;
}
NetworkService.connectToWifi(networkContextMenu.currentSSID);
}
}
@@ -822,15 +812,16 @@ Rectangle {
}
onTriggered: {
let networkData = NetworkService.getNetworkInfo(networkContextMenu.currentSSID);
networkInfoModal.showNetworkInfo(networkContextMenu.currentSSID, networkData);
const networkData = NetworkService.getNetworkInfo(networkContextMenu.currentSSID);
networkInfoModalLoader.active = true;
networkInfoModalLoader.item.showNetworkInfo(networkContextMenu.currentSSID, networkData);
}
}
MenuItem {
text: networkContextMenu.currentAutoconnect ? I18n.tr("Disable Autoconnect") : I18n.tr("Enable Autoconnect")
height: (networkContextMenu.currentSaved || networkContextMenu.currentConnected) && DMSService.apiVersion > 13 ? 32 : 0
visible: (networkContextMenu.currentSaved || networkContextMenu.currentConnected) && DMSService.apiVersion > 13
height: networkContextMenu.showSavedOptions && DMSService.apiVersion > 13 ? 32 : 0
visible: networkContextMenu.showSavedOptions && DMSService.apiVersion > 13
contentItem: StyledText {
text: parent.text
@@ -852,8 +843,8 @@ Rectangle {
MenuItem {
text: I18n.tr("Forget Network")
height: networkContextMenu.currentSaved || networkContextMenu.currentConnected ? 32 : 0
visible: networkContextMenu.currentSaved || networkContextMenu.currentConnected
height: networkContextMenu.showSavedOptions ? 32 : 0
visible: networkContextMenu.showSavedOptions
contentItem: StyledText {
text: parent.text
@@ -874,11 +865,15 @@ Rectangle {
}
}
NetworkInfoModal {
id: networkInfoModal
Loader {
id: networkInfoModalLoader
active: false
sourceComponent: NetworkInfoModal {}
}
NetworkWiredInfoModal {
id: networkWiredInfoModal
Loader {
id: networkWiredInfoModalLoader
active: false
sourceComponent: NetworkWiredInfoModal {}
}
}

View File

@@ -216,14 +216,18 @@ QtObject {
}
const pluginComponent = PluginService.pluginWidgetComponents[plugin.id];
if (!pluginComponent || typeof pluginComponent.createObject !== 'function') {
if (!pluginComponent)
continue;
}
const tempInstance = pluginComponent.createObject(null);
if (!tempInstance) {
let tempInstance;
try {
tempInstance = pluginComponent.createObject(null);
} catch (e) {
PluginService.reloadPlugin(plugin.id);
continue;
}
if (!tempInstance)
continue;
const hasCCWidget = tempInstance.ccWidgetIcon && tempInstance.ccWidgetIcon.length > 0;
tempInstance.destroy();

View File

@@ -96,7 +96,7 @@ Item {
focusedScreenName = Hyprland.focusedWorkspace.monitor.name;
} else if (CompositorService.isNiri && NiriService.currentOutput) {
focusedScreenName = NiriService.currentOutput;
} else if (CompositorService.isSway || CompositorService.isScroll) {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
focusedScreenName = focusedWs?.monitor?.name || "";
} else if (CompositorService.isDwl && DwlService.activeOutput) {
@@ -125,7 +125,7 @@ Item {
focusedScreenName = Hyprland.focusedWorkspace.monitor.name;
} else if (CompositorService.isNiri && NiriService.currentOutput) {
focusedScreenName = NiriService.currentOutput;
} else if (CompositorService.isSway || CompositorService.isScroll) {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
focusedScreenName = focusedWs?.monitor?.name || "";
} else if (CompositorService.isDwl && DwlService.activeOutput) {

View File

@@ -103,7 +103,7 @@ Item {
}, (_, i) => i);
}
return DwlService.getVisibleTags(barWindow.screenName);
} else if (CompositorService.isSway || CompositorService.isScroll) {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const workspaces = I3.workspaces?.values || [];
if (workspaces.length === 0)
return [
@@ -145,7 +145,7 @@ Item {
return 0;
const activeTags = DwlService.getActiveTags(barWindow.screenName);
return activeTags.length > 0 ? activeTags[0] : 0;
} else if (CompositorService.isSway || CompositorService.isScroll) {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
if (!barWindow.screenName || SettingsData.workspaceFollowFocus) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
return focusedWs ? focusedWs.num : 1;
@@ -194,7 +194,7 @@ Item {
if (nextIndex !== validIndex) {
DwlService.switchToTag(barWindow.screenName, realWorkspaces[nextIndex]);
}
} else if (CompositorService.isSway || CompositorService.isScroll) {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const currentWs = getCurrentWorkspace();
const currentIndex = realWorkspaces.findIndex(ws => ws.num === currentWs);
const validIndex = currentIndex === -1 ? 0 : currentIndex;

View File

@@ -48,11 +48,6 @@ DankPopout {
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
color: "transparent"
radius: Theme.cornerRadius
border.color: Theme.outlineMedium
border.width: 0
antialiasing: true
smooth: true
focus: true
Component.onCompleted: {
if (root.shouldBeVisible) {
@@ -78,35 +73,6 @@ DankPopout {
target: root
}
Rectangle {
anchors.fill: parent
anchors.margins: -3
color: "transparent"
radius: parent.radius + 3
border.color: Qt.rgba(0, 0, 0, 0.05)
border.width: 0
z: -3
}
Rectangle {
anchors.fill: parent
anchors.margins: -2
color: "transparent"
radius: parent.radius + 2
border.color: Theme.shadowMedium
border.width: 0
z: -2
}
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: Theme.outlineStrong
border.width: 0
radius: parent.radius
z: -1
}
Column {
id: contentColumn

View File

@@ -111,11 +111,6 @@ DankPopout {
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
color: "transparent"
radius: Theme.cornerRadius
border.color: Theme.outlineMedium
border.width: 0
antialiasing: true
smooth: true
focus: true
Component.onCompleted: {
@@ -142,15 +137,6 @@ DankPopout {
}
}
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: Theme.outlineStrong
border.width: 0
radius: parent.radius
z: -1
}
Column {
id: contentColumn

View File

@@ -37,9 +37,6 @@ DankPopout {
implicitHeight: contentColumn.height + Theme.spacingL * 2
color: "transparent"
radius: Theme.cornerRadius
border.color: Theme.outlineMedium
border.width: 0
focus: true
Keys.onPressed: event => {

View File

@@ -400,6 +400,7 @@ BasePill {
Component.onCompleted: updateModel()
visible: dockItems.length > 0
readonly property real iconCellSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground) + 6
content: Component {
Item {
@@ -460,7 +461,7 @@ BasePill {
readonly property bool isOverflowToggle: modelData.type === "overflow-toggle"
readonly property bool isInOverflow: modelData.isInOverflow === true
readonly property real visualSize: isSeparator ? 8 : ((widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? 24 : (24 + Theme.spacingXS + 120))
readonly property real visualSize: isSeparator ? 8 : ((widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? root.iconCellSize : (root.iconCellSize + Theme.spacingXS + 120))
readonly property real visualWidth: root.isVerticalOrientation ? root.barThickness : visualSize
readonly property real visualHeight: root.isVerticalOrientation ? visualSize : root.barThickness
@@ -620,8 +621,8 @@ BasePill {
Rectangle {
id: visualContent
width: root.isVerticalOrientation ? 24 : delegateItem.visualSize
height: root.isVerticalOrientation ? delegateItem.visualSize : 24
width: root.isVerticalOrientation ? root.iconCellSize : delegateItem.visualSize
height: root.isVerticalOrientation ? delegateItem.visualSize : root.iconCellSize
anchors.centerIn: parent
radius: Theme.cornerRadius
color: {
@@ -937,18 +938,15 @@ BasePill {
const globalPos = delegateItem.mapToGlobal(0, delegateItem.height / 2);
const screenX = root.parentScreen ? root.parentScreen.x : 0;
const screenY = root.parentScreen ? root.parentScreen.y : 0;
const barThickness = root.effectiveBarThickness;
const spacing = barConfig?.spacing ?? 4;
const isLeft = root.axis?.edge === "left";
const tooltipOffset = barThickness + spacing + Theme.spacingM;
const tooltipX = isLeft ? tooltipOffset : (root.parentScreen.width - tooltipOffset);
const screenRelativeY = globalPos.y - screenY + root.barY;
const tooltipX = isLeft ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (root.parentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS);
const screenRelativeY = globalPos.y - screenY + root.minTooltipY;
tooltipLoader.item.show(appItem.tooltipText, screenX + tooltipX, screenRelativeY, root.parentScreen, isLeft, !isLeft);
} else {
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height);
const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height;
const isBottom = root.axis?.edge === "bottom";
const tooltipY = isBottom ? (screenHeight - Theme.barHeight - (barConfig?.spacing ?? 4) - Theme.spacingXS - 35) : (Theme.barHeight + (barConfig?.spacing ?? 4) + Theme.spacingXS);
const tooltipY = isBottom ? (screenHeight - root.barThickness - root.barSpacing - Theme.spacingXS - 35) : (root.barThickness + root.barSpacing + Theme.spacingXS);
tooltipLoader.item.show(appItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false);
}
}
@@ -972,21 +970,27 @@ BasePill {
if (contextMenuLoader.item) {
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height / 2);
const screenX = root.parentScreen ? root.parentScreen.x : 0;
const screenY = root.parentScreen ? root.parentScreen.y : 0;
const isBarVertical = root.axis?.isVertical ?? false;
const barEdge = root.axis?.edge ?? "top";
let x = globalPos.x;
let y = globalPos.y;
let x = globalPos.x - screenX;
let y = globalPos.y - screenY;
if (barEdge === "bottom") {
y = (root.parentScreen ? root.parentScreen.height : Screen.height) - root.effectiveBarThickness;
} else if (barEdge === "top") {
y = root.effectiveBarThickness;
} else if (barEdge === "left") {
x = root.effectiveBarThickness;
} else if (barEdge === "right") {
x = (root.parentScreen ? root.parentScreen.width : Screen.width) - root.effectiveBarThickness;
switch (barEdge) {
case "bottom":
y = (root.parentScreen ? root.parentScreen.height : Screen.height) - root.barThickness - root.barSpacing;
break;
case "top":
y = root.barThickness + root.barSpacing;
break;
case "left":
x = root.barThickness + root.barSpacing;
break;
case "right":
x = (root.parentScreen ? root.parentScreen.width : Screen.width) - root.barThickness - root.barSpacing;
break;
}
const shouldHidePin = modelData.appId === "org.quickshell";

View File

@@ -55,7 +55,7 @@ BasePill {
}
IconImage {
visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isLabwc)
visible: SettingsData.launcherLogoMode === "compositor" && (CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isLabwc)
anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.noBackground)
height: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.noBackground)
@@ -72,6 +72,8 @@ BasePill {
return "file://" + Theme.shellDir + "/assets/sway.svg";
} else if (CompositorService.isScroll) {
return "file://" + Theme.shellDir + "/assets/sway.svg";
} else if (CompositorService.isMiracle) {
return "file://" + Theme.shellDir + "/assets/miraclewm.svg";
} else if (CompositorService.isLabwc) {
return "file://" + Theme.shellDir + "/assets/labwc.png";
}

View File

@@ -135,12 +135,27 @@ BasePill {
}
}
readonly property int windowCount: _groupByApp ? (groupedWindows?.length || 0) : (sortedToplevels?.length || 0)
readonly property real iconCellSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground) + 6
readonly property string focusedAppId: {
const toplevels = CompositorService.sortedToplevels;
if (!toplevels)
return "";
let result = "";
for (let i = 0; i < toplevels.length; i++) {
if (toplevels[i].activated)
result = toplevels[i].appId || "";
}
return result;
}
visible: windowCount > 0
property real scrollAccumulator: 0
property real touchpadThreshold: 500
onWheel: function (wheelEvent) {
wheelEvent.accepted = true;
const deltaY = wheelEvent.angleDelta.y;
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
@@ -226,7 +241,7 @@ BasePill {
property bool isGrouped: root._groupByApp
property var groupData: isGrouped ? modelData : null
property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData
property bool isFocused: toplevelData ? toplevelData.activated : false
property bool isFocused: isGrouped ? (root.focusedAppId === appId) : (toplevelData ? toplevelData.activated : false)
property string appId: isGrouped ? modelData.appId : (modelData.appId || "")
property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)"
property var toplevelObject: toplevelData
@@ -242,7 +257,7 @@ BasePill {
}
return appName + (windowTitle ? " • " + windowTitle : "");
}
readonly property real visualWidth: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? 24 : (24 + Theme.spacingXS + 120)
readonly property real visualWidth: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? root.iconCellSize : (root.iconCellSize + Theme.spacingXS + 120)
width: visualWidth
height: root.barThickness
@@ -250,7 +265,7 @@ BasePill {
Rectangle {
id: visualContent
width: delegateItem.visualWidth
height: 24
height: root.iconCellSize
anchors.centerIn: parent
radius: Theme.cornerRadius
color: {
@@ -433,7 +448,7 @@ BasePill {
const screenX = root.parentScreen ? root.parentScreen.x : 0;
const screenY = root.parentScreen ? root.parentScreen.y : 0;
const relativeY = globalPos.y - screenY;
const tooltipX = root.axis?.edge === "left" ? (Theme.barHeight + (barConfig?.spacing ?? 4) + Theme.spacingXS) : (root.parentScreen.width - Theme.barHeight - (barConfig?.spacing ?? 4) - Theme.spacingXS);
const tooltipX = root.axis?.edge === "left" ? (root.barThickness + root.barSpacing + Theme.spacingXS) : (root.parentScreen.width - root.barThickness - root.barSpacing - Theme.spacingXS);
const isLeft = root.axis?.edge === "left";
const adjustedY = relativeY + root.minTooltipY;
const finalX = screenX + tooltipX;
@@ -442,7 +457,7 @@ BasePill {
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height);
const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height;
const isBottom = root.axis?.edge === "bottom";
const tooltipY = isBottom ? (screenHeight - Theme.barHeight - (barConfig?.spacing ?? 4) - Theme.spacingXS - 35) : (Theme.barHeight + (barConfig?.spacing ?? 4) + Theme.spacingXS);
const tooltipY = isBottom ? (screenHeight - root.barThickness - root.barSpacing - Theme.spacingXS - 35) : (root.barThickness + root.barSpacing + Theme.spacingXS);
tooltipLoader.item.show(delegateItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false);
}
}
@@ -481,7 +496,7 @@ BasePill {
property bool isGrouped: root._groupByApp
property var groupData: isGrouped ? modelData : null
property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData
property bool isFocused: toplevelData ? toplevelData.activated : false
property bool isFocused: isGrouped ? (root.focusedAppId === appId) : (toplevelData ? toplevelData.activated : false)
property string appId: isGrouped ? modelData.appId : (modelData.appId || "")
property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)"
property var toplevelObject: toplevelData
@@ -497,15 +512,15 @@ BasePill {
}
return appName + (windowTitle ? " • " + windowTitle : "");
}
readonly property real visualWidth: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? 24 : (24 + Theme.spacingXS + 120)
readonly property real visualWidth: (widgetData?.runningAppsCompactMode !== undefined ? widgetData.runningAppsCompactMode : SettingsData.runningAppsCompactMode) ? root.iconCellSize : (root.iconCellSize + Theme.spacingXS + 120)
width: root.barThickness
height: 24
height: root.iconCellSize
Rectangle {
id: visualContent
width: delegateItem.visualWidth
height: 24
height: root.iconCellSize
anchors.centerIn: parent
radius: Theme.cornerRadius
color: {

View File

@@ -99,6 +99,7 @@ BasePill {
property bool suppressShiftAnimation: false
readonly property bool hasHiddenItems: allTrayItems.length > mainBarItems.length
visible: allTrayItems.length > 0
readonly property real trayItemSize: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.noBackground) + 6
readonly property real minTooltipY: {
if (!parentScreen || !isVerticalOrientation) {
@@ -172,7 +173,7 @@ BasePill {
return "";
}
width: 24
width: root.trayItemSize
height: root.barThickness
z: dragHandler.dragging ? 100 : 0
@@ -183,7 +184,7 @@ BasePill {
return 0;
const dragIdx = root.draggedIndex;
const dropIdx = root.dropTargetIndex;
const shiftAmount = 24;
const shiftAmount = root.trayItemSize;
if (dropIdx < 0)
return 0;
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
@@ -222,8 +223,8 @@ BasePill {
Rectangle {
id: visualContent
width: 24
height: 24
width: root.trayItemSize
height: root.trayItemSize
anchors.centerIn: parent
radius: Theme.cornerRadius
color: trayItemArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
@@ -328,7 +329,7 @@ BasePill {
const axisOffset = mouse.x - dragHandler.dragStartPos.x;
dragHandler.dragAxisOffset = axisOffset;
const itemSize = 24;
const itemSize = root.trayItemSize;
const slotOffset = Math.round(axisOffset / itemSize);
const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
if (newTargetIndex !== root.dropTargetIndex) {
@@ -351,14 +352,14 @@ BasePill {
}
Item {
width: 24
width: root.trayItemSize
height: root.barThickness
visible: root.hasHiddenItems
Rectangle {
id: caretButton
width: 24
height: 24
width: root.trayItemSize
height: root.trayItemSize
anchors.centerIn: parent
radius: Theme.cornerRadius
color: caretArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
@@ -430,7 +431,7 @@ BasePill {
}
width: root.barThickness
height: 24
height: root.trayItemSize
z: dragHandler.dragging ? 100 : 0
property real shiftOffset: {
@@ -440,7 +441,7 @@ BasePill {
return 0;
const dragIdx = root.draggedIndex;
const dropIdx = root.dropTargetIndex;
const shiftAmount = 24;
const shiftAmount = root.trayItemSize;
if (dropIdx < 0)
return 0;
if (dragIdx < dropIdx && index > dragIdx && index <= dropIdx)
@@ -479,8 +480,8 @@ BasePill {
Rectangle {
id: visualContent
width: 24
height: 24
width: root.trayItemSize
height: root.trayItemSize
anchors.centerIn: parent
radius: Theme.cornerRadius
color: trayItemArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
@@ -585,7 +586,7 @@ BasePill {
const axisOffset = mouse.y - dragHandler.dragStartPos.y;
dragHandler.dragAxisOffset = axisOffset;
const itemSize = 24;
const itemSize = root.trayItemSize;
const slotOffset = Math.round(axisOffset / itemSize);
const newTargetIndex = Math.max(0, Math.min(root.mainBarItems.length - 1, index + slotOffset));
if (newTargetIndex !== root.dropTargetIndex) {
@@ -609,13 +610,13 @@ BasePill {
Item {
width: root.barThickness
height: 24
height: root.trayItemSize
visible: root.hasHiddenItems
Rectangle {
id: caretButtonVert
width: 24
height: 24
width: root.trayItemSize
height: root.trayItemSize
anchors.centerIn: parent
radius: Theme.cornerRadius
color: caretAreaVert.containsMouse ? Theme.widgetBaseHoverColor : "transparent"
@@ -835,7 +836,7 @@ BasePill {
readonly property real rawWidth: {
const itemCount = root.hiddenBarItems.length;
const cols = Math.min(5, itemCount);
const itemSize = 28;
const itemSize = root.trayItemSize + 4;
const spacing = 2;
return cols * itemSize + (cols - 1) * spacing + Theme.spacingS * 2;
}
@@ -843,7 +844,7 @@ BasePill {
const itemCount = root.hiddenBarItems.length;
const cols = Math.min(5, itemCount);
const rows = Math.ceil(itemCount / cols);
const itemSize = 28;
const itemSize = root.trayItemSize + 4;
const spacing = 2;
return rows * itemSize + (rows - 1) * spacing + Theme.spacingS * 2;
}
@@ -982,8 +983,8 @@ BasePill {
return "";
}
width: 28
height: 28
width: root.trayItemSize + 4
height: root.trayItemSize + 4
radius: Theme.cornerRadius
color: itemArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.withAlpha(Theme.surfaceContainer, 0)

View File

@@ -10,6 +10,45 @@ BasePill {
property bool isActive: false
readonly property bool hasUpdates: SystemUpdateService.updateCount > 0
readonly property bool isChecking: SystemUpdateService.isChecking
readonly property bool shouldHide: SettingsData.updaterHideWidget && !hasUpdates && !isChecking && !SystemUpdateService.hasError
opacity: shouldHide ? 0 : 1
states: [
State {
name: "hidden_horizontal"
when: root.shouldHide && !isVerticalOrientation
PropertyChanges {
target: root
width: 0
}
},
State {
name: "hidden_vertical"
when: root.shouldHide && isVerticalOrientation
PropertyChanges {
target: root
height: 0
}
}
]
transitions: [
Transition {
NumberAnimation {
properties: "width,height"
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
]
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Ref {
service: SystemUpdateService

View File

@@ -37,6 +37,7 @@ Item {
return DwlService.activeOutput || root.screenName;
case "sway":
case "scroll":
case "miracle":
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
return focusedWs?.monitor?.name || root.screenName;
default:
@@ -44,7 +45,7 @@ Item {
}
}
readonly property bool useExtWorkspace: DMSService.forceExtWorkspace || (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isDwl && !CompositorService.isSway && !CompositorService.isScroll && ExtWorkspaceService.extWorkspaceAvailable)
readonly property bool useExtWorkspace: DMSService.forceExtWorkspace || (!CompositorService.isNiri && !CompositorService.isHyprland && !CompositorService.isDwl && !CompositorService.isSway && !CompositorService.isScroll && !CompositorService.isMiracle && ExtWorkspaceService.extWorkspaceAvailable)
Connections {
target: DesktopEntries
@@ -67,6 +68,7 @@ Item {
return activeTags.length > 0 ? activeTags[0] : -1;
case "sway":
case "scroll":
case "miracle":
return getSwayActiveWorkspace();
default:
return 1;
@@ -97,6 +99,7 @@ Item {
break;
case "sway":
case "scroll":
case "miracle":
baseList = getSwayWorkspaces();
break;
default:
@@ -114,12 +117,23 @@ Item {
}
];
function mapWorkspace(ws) {
return {
"num": ws.number,
"name": ws.name,
"focused": ws.focused,
"active": ws.active,
"urgent": ws.urgent,
"monitor": ws.monitor
};
}
if (!root.screenName || SettingsData.workspaceFollowFocus) {
return workspaces.slice().sort((a, b) => a.num - b.num);
return workspaces.slice().sort((a, b) => a.num - b.num).map(mapWorkspace);
}
const monitorWorkspaces = workspaces.filter(ws => ws.monitor?.name === root.screenName);
return monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.num - b.num) : [
return monitorWorkspaces.length > 0 ? monitorWorkspaces.sort((a, b) => a.num - b.num).map(mapWorkspace) : [
{
"num": 1
}
@@ -222,7 +236,7 @@ Item {
return [];
}
targetWorkspaceId = ws.tag;
} else if (CompositorService.isSway || CompositorService.isScroll) {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
targetWorkspaceId = ws.num !== undefined ? ws.num : ws;
} else {
return [];
@@ -234,7 +248,7 @@ Item {
let isActiveWs = false;
if (CompositorService.isNiri) {
isActiveWs = NiriService.allWorkspaces.some(ws => ws.id === targetWorkspaceId && ws.is_active);
} else if (CompositorService.isSway || CompositorService.isScroll) {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
isActiveWs = focusedWs ? (focusedWs.num === targetWorkspaceId) : false;
} else if (CompositorService.isDwl) {
@@ -255,7 +269,7 @@ Item {
let winWs = null;
if (CompositorService.isNiri) {
winWs = w.workspace_id;
} else if (CompositorService.isSway || CompositorService.isScroll) {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
winWs = w.workspace?.num;
} else {
const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []);
@@ -322,7 +336,7 @@ Item {
placeholder = {
"tag": -1
};
} else if (CompositorService.isSway || CompositorService.isScroll) {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
placeholder = {
"num": -1
};
@@ -516,7 +530,7 @@ Item {
return ws && ws.id !== -1;
if (CompositorService.isDwl)
return ws && ws.tag !== -1;
if (CompositorService.isSway || CompositorService.isScroll)
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return ws && ws.num !== -1;
return ws !== -1;
});
@@ -588,7 +602,7 @@ Item {
}
DwlService.switchToTag(root.screenName, realWorkspaces[nextIndex].tag);
} else if (CompositorService.isSway || CompositorService.isScroll) {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const realWorkspaces = getRealWorkspaces();
if (realWorkspaces.length < 2) {
return;
@@ -617,7 +631,7 @@ Item {
return modelData?.id || "";
if (CompositorService.isDwl)
return (modelData?.tag !== undefined) ? (modelData.tag + 1) : "";
if (CompositorService.isSway || CompositorService.isScroll)
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return modelData?.num || "";
return modelData - 1;
}
@@ -632,7 +646,7 @@ Item {
isPlaceholder = modelData?.id === -1;
} else if (CompositorService.isDwl) {
isPlaceholder = modelData?.tag === -1;
} else if (CompositorService.isSway || CompositorService.isScroll) {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
isPlaceholder = modelData?.num === -1;
} else {
isPlaceholder = modelData === -1;
@@ -665,7 +679,7 @@ Item {
return getWorkspaceIndexFallback(modelData, index);
}
readonly property bool hasNativeWorkspaceSupport: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll
readonly property bool hasNativeWorkspaceSupport: CompositorService.isNiri || CompositorService.isHyprland || CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle
readonly property bool hasWorkspaces: getRealWorkspaces().length > 0
readonly property bool shouldShow: hasNativeWorkspaceSupport || (useExtWorkspace && hasWorkspaces)
@@ -865,7 +879,7 @@ Item {
return !!(modelData && modelData.id === root.currentWorkspace);
if (CompositorService.isDwl)
return !!(modelData && root.dwlActiveTags.includes(modelData.tag));
if (CompositorService.isSway || CompositorService.isScroll)
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return !!(modelData && modelData.num === root.currentWorkspace);
return modelData === root.currentWorkspace;
}
@@ -889,7 +903,7 @@ Item {
return !!(modelData && modelData.id === -1);
if (CompositorService.isDwl)
return !!(modelData && modelData.tag === -1);
if (CompositorService.isSway || CompositorService.isScroll)
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return !!(modelData && modelData.num === -1);
return modelData === -1;
}
@@ -906,12 +920,17 @@ Item {
return loadedIsUrgent;
if (CompositorService.isDwl)
return modelData?.state === 2;
if (CompositorService.isSway || CompositorService.isScroll)
if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return loadedIsUrgent;
return false;
}
property var loadedIconData: null
property bool loadedHasIcon: false
readonly property var loadedIconData: {
if (isPlaceholder) return null;
const name = modelData?.name;
if (!name) return null;
return SettingsData.getWorkspaceNameIcon(name);
}
readonly property bool loadedHasIcon: loadedIconData !== null
property var loadedIcons: []
readonly property int stableIconCount: {
@@ -927,7 +946,7 @@ Item {
targetWorkspaceId = modelData?.id;
} else if (CompositorService.isDwl) {
targetWorkspaceId = modelData?.tag;
} else if (CompositorService.isSway || CompositorService.isScroll) {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
targetWorkspaceId = modelData?.num;
}
if (targetWorkspaceId === undefined || targetWorkspaceId === null)
@@ -946,7 +965,7 @@ Item {
let winWs = null;
if (CompositorService.isNiri) {
winWs = w.workspace_id;
} else if (CompositorService.isSway || CompositorService.isScroll) {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
winWs = w.workspace?.num;
} else if (CompositorService.isHyprland) {
const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []);
@@ -971,9 +990,9 @@ Item {
readonly property real baseWidth: root.isVertical ? (SettingsData.showWorkspaceApps ? Math.max(widgetHeight * 0.7, root.appIconSize + Theme.spacingXS * 2) : widgetHeight * 0.5) : (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7)
readonly property real baseHeight: root.isVertical ? (isActive ? root.widgetHeight * 1.05 : root.widgetHeight * 0.7) : (SettingsData.showWorkspaceApps ? Math.max(widgetHeight * 0.7, root.appIconSize + Theme.spacingXS * 2) : widgetHeight * 0.5)
readonly property bool hasWorkspaceName: SettingsData.showWorkspaceName && modelData?.name && modelData.name !== ""
readonly property bool workspaceNamesEnabled: SettingsData.showWorkspaceName && CompositorService.isNiri
readonly property real contentImplicitWidth: (hasWorkspaceName || loadedHasIcon) ? (appIconsLoader.item?.contentWidth ?? 0) : 0
readonly property real contentImplicitHeight: (workspaceNamesEnabled || loadedHasIcon) ? (appIconsLoader.item?.contentHeight ?? 0) : 0
readonly property bool workspaceNamesEnabled: SettingsData.showWorkspaceName && (CompositorService.isNiri || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
readonly property real contentImplicitWidth: hasWorkspaceName ? (appIconsLoader.item?.contentWidth ?? 0) : 0
readonly property real contentImplicitHeight: workspaceNamesEnabled ? (appIconsLoader.item?.contentHeight ?? 0) : 0
readonly property real iconsExtraWidth: {
if (!root.isVertical && SettingsData.showWorkspaceApps && stableIconCount > 0) {
@@ -1123,9 +1142,7 @@ Item {
return;
if (!dragHandler.dragging) {
const distance = root.isVertical
? Math.abs(mouse.y - dragHandler.dragStartPos.y)
: Math.abs(mouse.x - dragHandler.dragStartPos.x);
const distance = root.isVertical ? Math.abs(mouse.y - dragHandler.dragStartPos.y) : Math.abs(mouse.x - dragHandler.dragStartPos.x);
if (distance > 5) {
dragHandler.dragging = true;
root.dragSourceIndex = index;
@@ -1136,9 +1153,7 @@ Item {
if (!dragHandler.dragging)
return;
const rawAxisOffset = root.isVertical
? (mouse.y - dragHandler.dragStartPos.y)
: (mouse.x - dragHandler.dragStartPos.x);
const rawAxisOffset = root.isVertical ? (mouse.y - dragHandler.dragStartPos.y) : (mouse.x - dragHandler.dragStartPos.x);
const itemSize = (root.isVertical ? delegateRoot.height : delegateRoot.width) + Theme.spacingS;
const maxOffsetPositive = (root.workspaceList.length - 1 - index) * itemSize;
@@ -1189,7 +1204,7 @@ Item {
Hyprland.dispatch(`workspace ${modelData.id}`);
} else if (CompositorService.isDwl && modelData?.tag !== undefined) {
DwlService.switchToTag(root.screenName, modelData.tag);
} else if ((CompositorService.isSway || CompositorService.isScroll) && modelData?.num) {
} else if ((CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) && modelData?.num) {
try {
I3.dispatch(`workspace number ${modelData.num}`);
} catch (_) {}
@@ -1212,8 +1227,6 @@ Item {
onTriggered: {
if (isPlaceholder) {
delegateRoot.loadedWorkspaceData = null;
delegateRoot.loadedIconData = null;
delegateRoot.loadedHasIcon = false;
delegateRoot.loadedIcons = [];
delegateRoot.loadedIsUrgent = false;
return;
@@ -1228,7 +1241,7 @@ Item {
wsData = modelData;
} else if (CompositorService.isDwl) {
wsData = modelData;
} else if (CompositorService.isSway || CompositorService.isScroll) {
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
wsData = modelData;
}
delegateRoot.loadedWorkspaceData = wsData;
@@ -1239,15 +1252,8 @@ Item {
delegateRoot.loadedIsUrgent = wsData?.urgent ?? false;
}
var icData = null;
if (wsData?.name) {
icData = SettingsData.getWorkspaceNameIcon(wsData.name);
}
delegateRoot.loadedIconData = icData;
delegateRoot.loadedHasIcon = icData !== null;
if (SettingsData.showWorkspaceApps) {
if (CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll) {
if (CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
delegateRoot.loadedIcons = root.getWorkspaceIcons(modelData);
} else if (CompositorService.isNiri) {
delegateRoot.loadedIcons = root.getWorkspaceIcons(isPlaceholder ? null : modelData);
@@ -1410,7 +1416,7 @@ Item {
Item {
visible: loadedHasIcon && loadedIconData?.type === "icon"
width: wsIcon.width + (isActive && loadedIcons.length > 0 ? 4 : 0)
width: wsIcon.width
height: root.appIconSize
DankIcon {
@@ -1425,7 +1431,7 @@ Item {
Item {
visible: loadedHasIcon && loadedIconData?.type === "text"
width: wsText.implicitWidth + (isActive && loadedIcons.length > 0 ? 4 : 0)
width: wsText.implicitWidth
height: root.appIconSize
StyledText {
@@ -1439,14 +1445,14 @@ Item {
}
Item {
visible: (SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName) && !loadedHasIcon
width: wsIndexText.implicitWidth + (isActive && loadedIcons.length > 0 ? 4 : 0)
visible: ((SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName) && !loadedHasIcon) || (loadedHasIcon && SettingsData.showWorkspaceName && hasWorkspaceName)
width: wsIndexText.implicitWidth
height: root.appIconSize
StyledText {
id: wsIndexText
anchors.verticalCenter: parent.verticalCenter
text: root.getWorkspaceIndex(modelData, index)
text: loadedHasIcon ? (modelData?.name ?? "") : root.getWorkspaceIndex(modelData, index)
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
@@ -1564,9 +1570,9 @@ Item {
}
StyledText {
visible: (SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName) && !loadedHasIcon
visible: ((SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName) && !loadedHasIcon) || (loadedHasIcon && SettingsData.showWorkspaceName && hasWorkspaceName)
anchors.horizontalCenter: parent.horizontalCenter
text: root.getWorkspaceIndex(modelData, index)
text: loadedHasIcon ? (root.isVertical ? (modelData?.name ?? "").charAt(0) : (modelData?.name ?? "")) : root.getWorkspaceIndex(modelData, index)
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
@@ -1660,55 +1666,6 @@ Item {
}
}
// Loader for Custom Name Icon
Loader {
id: customIconLoader
anchors.fill: parent
active: !isPlaceholder && loadedHasIcon && loadedIconData.type === "icon" && !SettingsData.showWorkspaceApps
sourceComponent: Item {
DankIcon {
anchors.centerIn: parent
name: loadedIconData ? loadedIconData.value : "" // NULL CHECK
size: Theme.fontSizeSmall
color: isActive ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : Theme.surfaceTextMedium
weight: isActive && !isPlaceholder ? 500 : 400
}
}
}
// Loader for Custom Name Text
Loader {
id: customTextLoader
anchors.fill: parent
active: !isPlaceholder && loadedHasIcon && loadedIconData.type === "text" && !SettingsData.showWorkspaceApps
sourceComponent: Item {
StyledText {
anchors.centerIn: parent
text: loadedIconData ? loadedIconData.value : "" // NULL CHECK
color: isActive ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : Theme.surfaceTextMedium
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
}
}
}
// Loader for Workspace Index
Loader {
id: indexLoader
anchors.fill: parent
active: (SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName) && !loadedHasIcon && !SettingsData.showWorkspaceApps
sourceComponent: Item {
StyledText {
anchors.centerIn: parent
text: {
return root.getWorkspaceIndex(modelData, index);
}
color: (isActive || isUrgent) ? Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) : isPlaceholder ? Theme.surfaceTextAlpha : Theme.surfaceTextMedium
font.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
}
}
}
}
Component.onCompleted: updateAllData()
@@ -1760,7 +1717,7 @@ Item {
}
Connections {
target: I3.workspaces
enabled: (CompositorService.isSway || CompositorService.isScroll)
enabled: (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
function onValuesChanged() {
delegateRoot.updateAllData();
}

View File

@@ -170,7 +170,6 @@ DankPopout {
implicitHeight: contentColumn.height + Theme.spacingM * 2
color: "transparent"
radius: Theme.cornerRadius
focus: true
Component.onCompleted: {

View File

@@ -74,6 +74,8 @@ Card {
return "on Sway";
if (CompositorService.isScroll)
return "on Scroll";
if (CompositorService.isMiracle)
return "on Miracle WM";
return "";
}
font.pixelSize: Theme.fontSizeSmall

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