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

Compare commits

...

96 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
194 changed files with 15253 additions and 5006 deletions

View File

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

View File

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

View File

@@ -191,6 +191,11 @@ jobs:
git fetch origin --force tag ${TAG} git fetch origin --force tag ${TAG}
git checkout ${TAG} git checkout ${TAG}
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: ./core/go.mod
- name: Download core artifacts - name: Download core artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
@@ -229,6 +234,7 @@ jobs:
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems - **`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-amd64.gz`** - Installer binary for x86_64 systems
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 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 - **`dms-qml.tar.gz`** - QML source code only
### Checksums ### Checksums
@@ -387,6 +393,19 @@ jobs:
rm -rf _temp_full rm -rf _temp_full
done 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 - name: Create GitHub Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
</div> </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 ## Repository Structure
@@ -105,7 +105,7 @@ Extend functionality with the [plugin registry](https://plugins.danklinux.com).
## Supported Compositors ## 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) [Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors)

View File

@@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/golangci/golangci-lint - repo: https://github.com/golangci/golangci-lint
rev: v2.6.2 rev: v2.9.0
hooks: hooks:
- id: golangci-lint-fmt - id: golangci-lint-fmt
require_serial: true 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"} 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 { func detectNetworkBackend(stackResult *network.DetectResult) string {
switch stackResult.Backend { switch stackResult.Backend {
case network.BackendNetworkManager: case network.BackendNetworkManager:
@@ -689,7 +792,21 @@ func checkOptionalDependencies() []checkResult {
logindStatus, logindMsg := getOptionalDBusStatus("org.freedesktop.login1") logindStatus, logindMsg := getOptionalDBusStatus("org.freedesktop.login1")
results = append(results, checkResult{catOptionalFeatures, "logind", logindStatus, logindMsg, "Session management", optionalFeaturesURL}) 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, checkI2CAvailability())
results = append(results, checkImageFormatPlugins()...)
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"} terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 { 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) 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 { if greeterGroupExists {
currentUser, err := user.Current() currentUser, err := user.Current()
if err != nil { if err != nil {
@@ -182,25 +183,27 @@ func syncGreeter() error {
return fmt.Errorf("failed to check groups: %w", err) return fmt.Errorf("failed to check groups: %w", err)
} }
inGreeterGroup := strings.Contains(string(groupsOutput), "greeter") inGreeterGroup := strings.Contains(string(groupsOutput), greeterGroup)
if !inGreeterGroup { if !inGreeterGroup {
fmt.Println("\n⚠ Warning: You are not in the greeter group.") fmt.Printf("\n⚠ Warning: You are not in the %s group.\n", greeterGroup)
fmt.Print("Would you like to add your user to the greeter group? (y/N): ") fmt.Printf("Would you like to add your user to the %s group? (Y/n): ", greeterGroup)
var response string var response string
fmt.Scanln(&response) fmt.Scanln(&response)
response = strings.ToLower(strings.TrimSpace(response)) response = strings.ToLower(strings.TrimSpace(response))
if response == "y" || response == "yes" { if response != "n" && response != "no" {
fmt.Println("\nAdding user to greeter group...") fmt.Printf("\nAdding user to %s group...\n", greeterGroup)
addUserCmd := exec.Command("sudo", "usermod", "-aG", "greeter", currentUser.Username) addUserCmd := exec.Command("sudo", "usermod", "-aG", greeterGroup, currentUser.Username)
addUserCmd.Stdout = os.Stdout addUserCmd.Stdout = os.Stdout
addUserCmd.Stderr = os.Stderr addUserCmd.Stderr = os.Stderr
if err := addUserCmd.Run(); err != nil { 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") 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 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) { func disableDisplayManager(dmName string) (bool, error) {
state, err := getSystemdServiceState(dmName) state, err := getSystemdServiceState(dmName)
if err != nil { if err != nil {
@@ -389,7 +377,7 @@ func ensureGraphicalTarget() error {
func handleConflictingDisplayManagers() error { func handleConflictingDisplayManagers() error {
fmt.Println("\n=== Checking for Conflicting Display Managers ===") 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 disabledAny := false
var errors []string var errors []string

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,11 @@
module github.com/AvengeMedia/DankMaterialShell/core module github.com/AvengeMedia/DankMaterialShell/core
go 1.24.6 go 1.25.0
require ( require (
github.com/Wifx/gonetworkmanager/v2 v2.2.0 github.com/Wifx/gonetworkmanager/v2 v2.2.0
github.com/alecthomas/chroma/v2 v2.23.1 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/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/log v0.4.2 github.com/charmbracelet/log v0.4.2
@@ -19,44 +19,43 @@ require (
github.com/yuin/goldmark v1.7.16 github.com/yuin/goldmark v1.7.16
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.etcd.io/bbolt v1.4.3 go.etcd.io/bbolt v1.4.3
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a
golang.org/x/image v0.35.0 golang.org/x/image v0.36.0
) )
require ( require (
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/clipperhouse/displaywidth v0.8.0 // indirect github.com/clipperhouse/displaywidth v0.10.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/clipperhouse/uax29/v2 v2.4.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect github.com/cloudflare/circl v1.6.3 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg/v2 v2.0.2 // 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/go-logfmt/logfmt v0.6.1 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // 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/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/pjbgf/sha1cd v0.5.0 // indirect github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect github.com/stretchr/objx v0.5.3 // indirect
golang.org/x/crypto v0.47.0 // indirect golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.49.0 // indirect golang.org/x/net v0.50.0 // indirect
) )
require ( require (
github.com/atotto/clipboard v0.1.4 // indirect github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // 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/harmonica v0.2.0 // indirect
github.com/charmbracelet/x/ansi v0.11.4 // indirect github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // 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/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 github.com/lucasb-eyer/go-colorful v1.3.0
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
@@ -70,7 +69,11 @@ require (
github.com/spf13/afero v1.15.0 github.com/spf13/afero v1.15.0
github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.40.0 golang.org/x/sys v0.41.0
golang.org/x/text v0.33.0 golang.org/x/text v0.34.0
gopkg.in/yaml.v3 v3.0.1 // indirect 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/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 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 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 v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= 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 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= 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.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk= 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 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= 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 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= 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 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw= 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.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.4/go.mod h1:/5AZ+UfWExW3int5H5ugnsG/PWjNcSQcwYsHBlPFQN4= github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4= github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA= 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 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= 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.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
github.com/clipperhouse/displaywidth v0.8.0/go.mod h1:UpOXiIKep+TohQYwvAAM/VDU8v3Z5rnWTxiwueR0XvQ= github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
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/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= 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/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 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/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 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs= 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-20260209124918-37866f83c2d3 h1:UU7oARtwQ5g85aFiCSwIUA6PBmAshYj0sytl/5CCBgs=
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/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 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-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-20260216160506-e6a3f881772f h1:TBkCJv9YwPOuXq1OG0r01bcxRrvs15Hp/DtZuPt4H6s=
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/go.mod h1:B88nWzfnhTlIikoJ4d84Nc9noKS5mJoA7SgDdkt0aPU=
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE= 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/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 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/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 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 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.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= 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 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 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/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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 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 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= 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 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I= golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk= golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= 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 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= 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 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-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 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") { if strings.HasPrefix(trimmed, "exec-once = systemctl --user start") {
startupSectionFound = true startupSectionFound = true
result = append(result, "exec-once = dms run") 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 = ELECTRON_OZONE_PLATFORM_HINT,auto")
result = append(result, "env = QT_QPA_PLATFORMTHEME,gtk3") result = append(result, "env = QT_QPA_PLATFORMTHEME,gtk3")
result = append(result, "env = QT_QPA_PLATFORMTHEME_QT6,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") { if strings.Contains(line, "STARTUP APPS") {
insertLines := []string{ insertLines := []string{
"exec-once = dms run", "exec-once = dms run",
"env = QT_QPA_PLATFORM,wayland", "env = QT_QPA_PLATFORM,wayland;xcb",
"env = ELECTRON_OZONE_PLATFORM_HINT,auto", "env = ELECTRON_OZONE_PLATFORM_HINT,auto",
"env = QT_QPA_PLATFORMTHEME,gtk3", "env = QT_QPA_PLATFORMTHEME,gtk3",
"env = QT_QPA_PLATFORMTHEME_QT6,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 { func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalCommand string) string {
envVars := fmt.Sprintf(`environment { envVars := fmt.Sprintf(`environment {
XDG_CURRENT_DESKTOP "niri" XDG_CURRENT_DESKTOP "niri"
QT_QPA_PLATFORM "wayland" QT_QPA_PLATFORM "wayland;xcb"
ELECTRON_OZONE_PLATFORM_HINT "auto" ELECTRON_OZONE_PLATFORM_HINT "auto"
QT_QPA_PLATFORMTHEME "gtk3" QT_QPA_PLATFORMTHEME "gtk3"
QT_QPA_PLATFORMTHEME_QT6 "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)$ # windowrule = float on, match:class ^(org.quickshell)$
layerrule = no_anim on, match:namespace ^(quickshell)$ layerrule = no_anim on, match:namespace ^(quickshell)$
layerrule = no_anim on, match:namespace ^dms:.*
source = ./dms/colors.conf source = ./dms/colors.conf
source = ./dms/outputs.conf source = ./dms/outputs.conf

View File

@@ -430,7 +430,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
} }
// Add repository // Add repository
repoLine := fmt.Sprintf("deb [signed-by=%s, arch=%s] %s/ /", keyringPath, runtime.GOARCH, baseURL) repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, runtime.GOARCH, baseURL)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages, Phase: PhaseSystemPackages,

View File

@@ -22,6 +22,21 @@ func DetectDMSPath() (string, error) {
return config.LocateDMSConfig() 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 // DetectCompositors checks which compositors are installed
func DetectCompositors() []string { func DetectCompositors() []string {
var compositors []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) 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) return fmt.Errorf("failed to set cache directory owner: %w", err)
} }
if err := runSudoCmd(sudoPassword, "chmod", "755", cacheDir); err != nil { if err := runSudoCmd(sudoPassword, "chmod", "755", cacheDir); err != nil {
return fmt.Errorf("failed to set cache directory permissions: %w", err) 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 return nil
} }
@@ -234,6 +252,8 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
{filepath.Join(homeDir, ".local", "share"), ".local/share directory"}, {filepath.Join(homeDir, ".local", "share"), ".local/share directory"},
} }
owner := DetectGreeterGroup()
logFunc("\nSetting up parent directory ACLs for greeter user access...") logFunc("\nSetting up parent directory ACLs for greeter user access...")
for _, dir := range parentDirs { 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) // 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("⚠ 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 continue
} }
@@ -271,17 +291,19 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
return fmt.Errorf("failed to determine current user") return fmt.Errorf("failed to determine current user")
} }
group := DetectGreeterGroup()
// Check if user is already in greeter group // Check if user is already in greeter group
groupsCmd := exec.Command("groups", currentUser) groupsCmd := exec.Command("groups", currentUser)
groupsOutput, err := groupsCmd.Output() groupsOutput, err := groupsCmd.Output()
if err == nil && strings.Contains(string(groupsOutput), "greeter") { if err == nil && strings.Contains(string(groupsOutput), group) {
logFunc(fmt.Sprintf("✓ %s is already in greeter group", currentUser)) logFunc(fmt.Sprintf("✓ %s is already in %s group", currentUser, group))
} else { } else {
// Add current user to greeter group for file access permissions // Add current user to greeter group for file access permissions
if err := runSudoCmd(sudoPassword, "usermod", "-aG", "greeter", currentUser); err != nil { if err := runSudoCmd(sudoPassword, "usermod", "-aG", group, currentUser); err != nil {
return fmt.Errorf("failed to add %s to greeter group: %w", currentUser, err) 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 { 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)) logFunc(fmt.Sprintf("⚠ Warning: Failed to set group for %s: %v", dir.desc, err))
continue continue
} }
@@ -436,10 +458,11 @@ func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
} }
greeterDir := "/etc/greetd/niri" greeterDir := "/etc/greetd/niri"
greeterGroup := DetectGreeterGroup()
if err := runSudoCmd(sudoPassword, "mkdir", "-p", greeterDir); err != nil { if err := runSudoCmd(sudoPassword, "mkdir", "-p", greeterDir); err != nil {
return fmt.Errorf("failed to create greetd niri directory: %w", err) 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) return fmt.Errorf("failed to set greetd niri directory ownership: %w", err)
} }
if err := runSudoCmd(sudoPassword, "chmod", "755", greeterDir); err != nil { 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 { if err := backupFileIfExists(sudoPassword, dmsPath, ".backup"); err != nil {
return fmt.Errorf("failed to backup %s: %w", dmsPath, err) 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) 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 { if err := backupFileIfExists(sudoPassword, mainPath, ".backup"); err != nil {
return fmt.Errorf("failed to backup %s: %w", mainPath, err) 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) 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)) logFunc(fmt.Sprintf("✓ Backed up existing config to %s", backupPath))
} }
greeterUser := DetectGreeterGroup()
var configContent string var configContent string
if data, err := os.ReadFile(configPath); err == nil { if data, err := os.ReadFile(configPath); err == nil {
configContent = string(data) configContent = string(data)
} else { } else {
configContent = `[terminal] configContent = fmt.Sprintf(`[terminal]
vt = 1 vt = 1
[default_session] [default_session]
user = "greeter" user = "%s"
` `, greeterUser)
} }
lines := strings.Split(configContent, "\n") lines := strings.Split(configContent, "\n")
@@ -755,7 +780,7 @@ user = "greeter"
trimmed := strings.TrimSpace(line) trimmed := strings.TrimSpace(line)
if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") { if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") {
if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") { if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") {
newLines = append(newLines, `user = "greeter"`) newLines = append(newLines, fmt.Sprintf(`user = "%s"`, greeterUser))
} else { } else {
newLines = append(newLines, line) newLines = append(newLines, line)
} }
@@ -807,7 +832,7 @@ user = "greeter"
return fmt.Errorf("failed to move config to /etc/greetd: %w", err) 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 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 ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
@@ -18,14 +19,21 @@ func NewSwayProvider(configPath string) *SwayProvider {
_, scrollEnvSet := os.LookupEnv("SCROLLSOCK") _, scrollEnvSet := os.LookupEnv("SCROLLSOCK")
if configPath == "" { if configPath == "" {
configDir, err := os.UserConfigDir()
if err != nil {
configDir = ""
}
if scrollEnvSet { if scrollEnvSet {
configPath = "$HOME/.config/scroll" if configDir != "" {
configPath = filepath.Join(configDir, "scroll")
}
isScroll = true isScroll = true
} else { } else {
configPath = "$HOME/.config/sway" if configDir != "" {
configPath = filepath.Join(configDir, "sway")
}
} }
} else { } else {
// Determine isScroll based on the provided config path
isScroll = strings.Contains(configPath, "scroll") isScroll = strings.Contains(configPath, "scroll")
} }
@@ -36,16 +44,16 @@ func NewSwayProvider(configPath string) *SwayProvider {
} }
func (s *SwayProvider) Name() string { func (s *SwayProvider) Name() string {
if s != nil && s.isScroll {
return "scroll"
}
if s == nil { if s == nil {
_, ok := os.LookupEnv("SCROLLSOCK") if os.Getenv("SCROLLSOCK") != "" {
if ok {
return "scroll" return "scroll"
} }
return "sway"
} }
if s.isScroll {
return "scroll"
}
return "sway" return "sway"
} }

View File

@@ -15,8 +15,13 @@ func TestSwayProviderName(t *testing.T) {
func TestSwayProviderDefaultPath(t *testing.T) { func TestSwayProviderDefaultPath(t *testing.T) {
provider := NewSwayProvider("") provider := NewSwayProvider("")
if provider.configPath != "$HOME/.config/sway" { configDir, err := os.UserConfigDir()
t.Errorf("configPath = %q, want %q", provider.configPath, "$HOME/.config/sway") 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 TemplateKindTerminal
TemplateKindGTK TemplateKindGTK
TemplateKindVSCode TemplateKindVSCode
TemplateKindEmacs
) )
type TemplateDef struct { type TemplateDef struct {
@@ -65,7 +66,7 @@ var templateRegistry = []TemplateDef{
{ID: "dgop", Commands: []string{"dgop"}, ConfigFile: "dgop.toml"}, {ID: "dgop", Commands: []string{"dgop"}, ConfigFile: "dgop.toml"},
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true}, {ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
{ID: "vscode", Kind: TemplateKindVSCode}, {ID: "vscode", Kind: TemplateKindVSCode},
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml"}, {ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs},
} }
func (c *ColorMode) GTKTheme() string { func (c *ColorMode) GTKTheme() string {
@@ -78,8 +79,10 @@ func (c *ColorMode) GTKTheme() string {
} }
var ( var (
matugenVersionOnce sync.Once matugenVersionMu sync.Mutex
matugenVersionOK bool
matugenSupportsCOE bool matugenSupportsCOE bool
matugenIsV4 bool
) )
type Options struct { type Options struct {
@@ -268,7 +271,7 @@ func buildOnce(opts *Options) error {
refreshQt6ct() refreshQt6ct()
} }
signalTerminals() signalTerminals(opts)
return nil return nil
} }
@@ -333,6 +336,10 @@ output_path = '%s'
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions"), opts.ShellDir) appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions"), opts.ShellDir)
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/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) 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: default:
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile) 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, "'CONFIG_DIR/", "'"+utils.XDGConfigHome()+"/")
result = strings.ReplaceAll(result, "'DATA_DIR/", "'"+utils.XDGDataHome()+"/") result = strings.ReplaceAll(result, "'DATA_DIR/", "'"+utils.XDGDataHome()+"/")
result = strings.ReplaceAll(result, "'CACHE_DIR/", "'"+utils.XDGCacheHome()+"/") result = strings.ReplaceAll(result, "'CACHE_DIR/", "'"+utils.XDGCacheHome()+"/")
if emacsDir := utils.EmacsConfigDir(); emacsDir != "" {
result = strings.ReplaceAll(result, "'EMACS_DIR/", "'"+emacsDir+"/")
}
return result return result
} }
@@ -510,67 +520,160 @@ func extractTOMLSection(content, startMarker, endMarker string) string {
return content[startIdx : startIdx+endIdx] return content[startIdx : startIdx+endIdx]
} }
func checkMatugenVersion() { type matugenFlags struct {
matugenVersionOnce.Do(func() { supportsCOE bool
cmd := exec.Command("matugen", "--version") isV4 bool
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)
}
})
} }
func runMatugen(args []string) error { func detectMatugenVersion() (matugenFlags, error) {
checkMatugenVersion() matugenVersionMu.Lock()
defer matugenVersionMu.Unlock()
if matugenSupportsCOE { if matugenVersionOK {
args = append([]string{"--continue-on-error"}, args...) 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 := exec.Command("matugen", args...)
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr 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) { func runMatugenDryRun(opts *Options) (string, error) {
var args []string flags, err := detectMatugenVersion()
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()
if err != nil { if err != nil {
return "", err 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 return strings.ReplaceAll(string(output), "\n", ""), nil
} }
@@ -692,11 +795,15 @@ func refreshQt6ct() {
} }
} }
func signalTerminals() { func signalTerminals(opts *Options) {
signalByName("kitty", syscall.SIGUSR1) if !opts.ShouldSkipTemplate("kitty") && appExists(opts.AppChecker, []string{"kitty"}, nil) {
signalByName("ghostty", syscall.SIGUSR2) signalByName("kitty", syscall.SIGUSR1)
signalByName(".kitty-wrapped", syscall.SIGUSR1) signalByName(".kitty-wrapped", syscall.SIGUSR1)
signalByName(".ghostty-wrappe", syscall.SIGUSR2) }
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) { func signalByName(name string, sig syscall.Signal) {
@@ -802,6 +909,8 @@ func CheckTemplates(checker utils.AppChecker) []TemplateCheck {
detected = true detected = true
case tmpl.Kind == TemplateKindVSCode: case tmpl.Kind == TemplateKindVSCode:
detected = checkVSCodeExtension(homeDir) detected = checkVSCodeExtension(homeDir)
case tmpl.Kind == TemplateKindEmacs:
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks) && utils.EmacsConfigDir() != ""
default: default:
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks) detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks)
} }

View File

@@ -15,6 +15,9 @@ const (
notifyDest = "org.freedesktop.Notifications" notifyDest = "org.freedesktop.Notifications"
notifyPath = "/org/freedesktop/Notifications" notifyPath = "/org/freedesktop/Notifications"
notifyInterface = "org.freedesktop.Notifications" notifyInterface = "org.freedesktop.Notifications"
maxSummaryLen = 29
maxBodyLen = 80
) )
type Notification struct { type Notification struct {
@@ -39,6 +42,13 @@ func Send(n Notification) error {
n.Timeout = 5000 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 var actions []string
if n.FilePath != "" { if n.FilePath != "" {
actions = []string{ actions = []string{

View File

@@ -21,6 +21,7 @@ const (
CompositorNiri CompositorNiri
CompositorDWL CompositorDWL
CompositorScroll CompositorScroll
CompositorMiracle
) )
var detectedCompositor Compositor = -1 var detectedCompositor Compositor = -1
@@ -34,6 +35,7 @@ func DetectCompositor() Compositor {
niriSocket := os.Getenv("NIRI_SOCKET") niriSocket := os.Getenv("NIRI_SOCKET")
swaySocket := os.Getenv("SWAYSOCK") swaySocket := os.Getenv("SWAYSOCK")
scrollSocket := os.Getenv("SCROLLSOCK") scrollSocket := os.Getenv("SCROLLSOCK")
miracleSocket := os.Getenv("MIRACLESOCK")
switch { switch {
case niriSocket != "": case niriSocket != "":
@@ -46,7 +48,11 @@ func DetectCompositor() Compositor {
detectedCompositor = CompositorScroll detectedCompositor = CompositorScroll
return detectedCompositor return detectedCompositor
} }
case miracleSocket != "":
if _, err := os.Stat(miracleSocket); err == nil {
detectedCompositor = CompositorMiracle
return detectedCompositor
}
case swaySocket != "": case swaySocket != "":
if _, err := os.Stat(swaySocket); err == nil { if _, err := os.Stat(swaySocket); err == nil {
detectedCompositor = CompositorSway detectedCompositor = CompositorSway
@@ -260,6 +266,25 @@ func getScrollFocusedMonitor() string {
return "" 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 { type niriWorkspace struct {
Output string `json:"output"` Output string `json:"output"`
IsFocused bool `json:"is_focused"` IsFocused bool `json:"is_focused"`
@@ -407,6 +432,8 @@ func GetFocusedMonitor() string {
return getSwayFocusedMonitor() return getSwayFocusedMonitor()
case CompositorScroll: case CompositorScroll:
return getScrollFocusedMonitor() return getScrollFocusedMonitor()
case CompositorMiracle:
return getMiracleFocusedMonitor()
case CompositorNiri: case CompositorNiri:
return getNiriFocusedMonitor() return getNiriFocusedMonitor()
case CompositorDWL: case CompositorDWL:

View File

@@ -108,7 +108,7 @@ func NewRegionSelector(s *Screenshoter) *RegionSelector {
screenshoter: s, screenshoter: s,
outputs: make(map[uint32]*WaylandOutput), outputs: make(map[uint32]*WaylandOutput),
preCapture: make(map[*WaylandOutput]*PreCapture), 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) { func (s *Screenshoter) captureWholeOutput(output *WaylandOutput) (*CaptureResult, error) {
cursor := int32(0) cursor := int32(s.config.Cursor)
if s.config.IncludeCursor {
cursor = 1
}
frame, err := s.screencopy.CaptureOutput(cursor, output.wlOutput) frame, err := s.screencopy.CaptureOutput(cursor, output.wlOutput)
if err != nil { if err != nil {
@@ -624,10 +621,7 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio
} }
} }
cursor := int32(0) cursor := int32(s.config.Cursor)
if s.config.IncludeCursor {
cursor = 1
}
frame, err := s.screencopy.CaptureOutputRegion(cursor, output.wlOutput, localX, localY, w, h) frame, err := s.screencopy.CaptureOutputRegion(cursor, output.wlOutput, localX, localY, w, h)
if err != nil { if err != nil {

View File

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

View File

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

View File

@@ -52,11 +52,31 @@ func (m *Manager) initializeScreensaver() error {
return nil 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{ introNode := &introspect.Node{
Name: dbusScreensaverPath, Name: dbusScreensaverPath,
Interfaces: []introspect.Interface{ Interfaces: []introspect.Interface{
introspect.IntrospectData, introspect.IntrospectData,
{Name: dbusScreensaverInterface}, screensaverIface,
}, },
} }
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode), dbusScreensaverPath, "org.freedesktop.DBus.Introspectable"); err != nil { 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, Name: dbusScreensaverPath2,
Interfaces: []introspect.Interface{ Interfaces: []introspect.Interface{
introspect.IntrospectData, introspect.IntrospectData,
{Name: dbusScreensaverInterface}, screensaverIface,
}, },
} }
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode2), dbusScreensaverPath2, "org.freedesktop.DBus.Introspectable"); err != nil { 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 backend *NetworkManagerBackend
} }
type nmVariantMap map[string]dbus.Variant type (
type nmSettingMap map[string]nmVariantMap nmVariantMap map[string]dbus.Variant
nmSettingMap map[string]nmVariantMap
)
const introspectXML = ` const introspectXML = `
<node> <node>
@@ -122,7 +124,7 @@ func (a *SecretAgent) GetSecrets(
connType, displayName, vpnSvc := readConnTypeAndName(conn) connType, displayName, vpnSvc := readConnTypeAndName(conn)
ssid := readSSID(conn) ssid := readSSID(conn)
fields := fieldsNeeded(settingName, hints) fields := fieldsNeeded(settingName, hints, conn)
vpnPasswordFlags := readVPNPasswordFlags(conn, settingName) 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) 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{} out[settingName] = nmVariantMap{}
return out, nil return out, nil
} else if passwordFlags&NM_SETTING_SECRET_FLAG_AGENT_OWNED != 0 { } 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) switch settingName {
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil) 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 { } else {
log.Infof("[SecretAgent] No secrets needed, using system stored secrets (flags=%d)", passwordFlags) log.Infof("[SecretAgent] No secrets needed, using system stored secrets (flags=%d)", passwordFlags)
out := nmSettingMap{} out := nmSettingMap{}
@@ -300,6 +310,63 @@ func (a *SecretAgent) GetSecrets(
return out, nil return out, nil
} }
a.backend.cachedVPNCredsMu.Unlock() 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) 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") log.Infof("[SecretAgent] Cached PKCS11 PIN for potential re-request")
} }
case "802-1x": case "802-1x":
out[settingName] = sec secretsOnly := nmVariantMap{}
log.Infof("[SecretAgent] Returning 802-1x enterprise secrets with %d fields", len(sec)) 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: default:
out[settingName] = sec out[settingName] = sec
} }
@@ -434,63 +512,6 @@ func (a *SecretAgent) GetSecrets(
} }
a.backend.pendingVPNSaveMu.Unlock() a.backend.pendingVPNSaveMu.Unlock()
log.Infof("[SecretAgent] Queued credentials persist for after connection succeeds") 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 return out, nil
@@ -523,6 +544,35 @@ func (a *SecretAgent) Introspect() (string, *dbus.Error) {
return introspectXML, nil 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 { func readSSID(conn map[string]nmVariantMap) string {
if w, ok := conn["802-11-wireless"]; ok { if w, ok := conn["802-11-wireless"]; ok {
if v, ok := w["ssid"]; ok { if v, ok := w["ssid"]; ok {
@@ -564,12 +614,15 @@ func readConnTypeAndName(conn map[string]nmVariantMap) (string, string, string)
return connType, name, svc return connType, name, svc
} }
func fieldsNeeded(setting string, hints []string) []string { func fieldsNeeded(setting string, hints []string, conn map[string]nmVariantMap) []string {
switch setting { switch setting {
case "802-11-wireless-security": case "802-11-wireless-security":
return []string{"psk"} return []string{"psk"}
case "802-1x": case "802-1x":
return []string{"identity", "password"} if len(hints) > 0 {
return hints
}
return infer8021xFields(conn)
case "vpn": case "vpn":
return hints return hints
default: 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 { func buildFieldsInfo(setting string, fields []string, vpnService string) []FieldInfo {
result := make([]FieldInfo, 0, len(fields)) result := make([]FieldInfo, 0, len(fields))
for _, f := range fields { for _, f := range fields {
@@ -630,12 +718,25 @@ func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string {
switch { switch {
case strings.Contains(vpnService, "openconnect"): case strings.Contains(vpnService, "openconnect"):
protocol := dataMap["protocol"]
authType := dataMap["authtype"] authType := dataMap["authtype"]
userCert := dataMap["usercert"] username := dataMap["username"]
if authType == "cert" && strings.HasPrefix(userCert, "pkcs11:") {
if authType == "cert" && strings.HasPrefix(dataMap["usercert"], "pkcs11:") {
return []string{"key_pass"} 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"} fields = []string{"username", "password"}
} }
case strings.Contains(vpnService, "openvpn"): case strings.Contains(vpnService, "openvpn"):
@@ -654,8 +755,31 @@ func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string {
return fields 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) { func vpnFieldMeta(field, vpnService string) (label string, isSecret bool) {
switch field { switch field {
case "gp-saml":
return "GlobalProtect SAML/SSO", false
case "key_pass": case "key_pass":
return "PIN", true return "PIN", true
case "password": case "password":
@@ -756,3 +880,18 @@ func reasonFromFlags(flags uint32) string {
} }
return "required" 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 lastFailedTime int64
failedMutex sync.RWMutex failedMutex sync.RWMutex
pendingVPNSave *pendingVPNCredentials pendingVPNSave *pendingVPNCredentials
pendingVPNSaveMu sync.Mutex pendingVPNSaveMu sync.Mutex
cachedVPNCreds *cachedVPNCredentials cachedVPNCreds *cachedVPNCredentials
cachedVPNCredsMu sync.Mutex cachedVPNCredsMu sync.Mutex
cachedPKCS11PIN *cachedPKCS11PIN cachedPKCS11PIN *cachedPKCS11PIN
cachedPKCS11Mu sync.Mutex cachedPKCS11Mu sync.Mutex
cachedGPSamlCookie *cachedGPSamlCookie
cachedGPSamlMu sync.Mutex
onStateChange func() onStateChange func()
} }
@@ -97,6 +99,14 @@ type cachedPKCS11PIN struct {
PIN string PIN string
} }
type cachedGPSamlCookie struct {
ConnectionUUID string
Cookie string
Host string
User string
Fingerprint string
}
func NewNetworkManagerBackend(nmConn ...gonetworkmanager.NetworkManager) (*NetworkManagerBackend, error) { func NewNetworkManagerBackend(nmConn ...gonetworkmanager.NetworkManager) (*NetworkManagerBackend, error) {
var nm gonetworkmanager.NetworkManager var nm gonetworkmanager.NetworkManager
var err error 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() b.stateMutex.Lock()
defer b.stateMutex.Unlock()
wasConnecting = b.state.IsConnecting wasConnecting = b.state.IsConnecting
connectingSSID = b.state.ConnectingSSID connectingSSID = b.state.ConnectingSSID
if wasConnecting && connectingSSID != "" { if wasConnecting && connectingSSID != "" {
if connected && ssid == connectingSSID { switch {
case connected && ssid == connectingSSID:
log.Infof("[updateWiFiState] Connection successful: %s", ssid) log.Infof("[updateWiFiState] Connection successful: %s", ssid)
b.state.IsConnecting = false b.state.IsConnecting = false
b.state.ConnectingSSID = "" b.state.ConnectingSSID = ""
b.state.LastError = "" b.state.LastError = ""
} else if failed || (disconnected && !connected) { case failed || (disconnected && !connected):
log.Warnf("[updateWiFiState] Connection failed: SSID=%s, state=%d", connectingSSID, state) log.Warnf("[updateWiFiState] Connection failed: SSID=%s, state=%d", connectingSSID, state)
b.state.IsConnecting = false b.state.IsConnecting = false
b.state.ConnectingSSID = "" b.state.ConnectingSSID = ""
b.state.LastError = reasonCode b.state.LastError = reasonCode
// If user cancelled, delete the connection profile that was just created
if reasonCode == errdefs.ErrUserCanceled { if reasonCode == errdefs.ErrUserCanceled {
log.Infof("[updateWiFiState] User cancelled authentication, removing connection profile for %s", connectingSSID) forgetSSID = connectingSSID
b.stateMutex.Unlock()
if err := b.ForgetWiFiNetwork(connectingSSID); err != nil {
log.Warnf("[updateWiFiState] Failed to remove cancelled connection: %v", err)
}
b.stateMutex.Lock()
} }
b.failedMutex.Lock() b.failedMutex.Lock()
@@ -254,6 +250,15 @@ func (b *NetworkManagerBackend) updateWiFiState() error {
b.state.WiFiBSSID = bssid b.state.WiFiBSSID = bssid
b.state.WiFiSignal = signal 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 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 { if err := b.handleOpenVPNUsernameAuth(targetConn, connName, targetUUID, vpnServiceType); err != nil {
return err 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() b.stateMutex.Lock()
@@ -339,6 +384,16 @@ func detectVPNAuthAction(serviceType string, data map[string]string) string {
} }
switch { 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"): case strings.Contains(serviceType, "openvpn"):
connType := data["connection-type"] connType := data["connection-type"]
username := data["username"] username := data["username"]
@@ -412,16 +467,6 @@ func (b *NetworkManagerBackend) handleOpenVPNUsernameAuth(targetConn gonetworkma
} }
data["username"] = username 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) vpn["data"] = dbus.MakeVariant(data)
settings["vpn"] = vpn settings["vpn"] = vpn
@@ -432,7 +477,7 @@ func (b *NetworkManagerBackend) handleOpenVPNUsernameAuth(targetConn gonetworkma
} }
log.Infof("[ConnectVPN] Username saved to connection") log.Infof("[ConnectVPN] Username saved to connection")
if password != "" && !reply.Save { if password != "" {
b.cachedVPNCredsMu.Lock() b.cachedVPNCredsMu.Lock()
b.cachedVPNCreds = &cachedVPNCredentials{ b.cachedVPNCreds = &cachedVPNCredentials{
ConnectionUUID: targetUUID, ConnectionUUID: targetUUID,
@@ -614,11 +659,7 @@ func (b *NetworkManagerBackend) ClearVPNCredentials(uuidOrName string) error {
dataMap["password-flags"] = "1" dataMap["password-flags"] = "1"
vpnSettings["data"] = dataMap vpnSettings["data"] = dataMap
} }
vpnSettings["password-flags"] = uint32(1)
} }
settings["vpn-secrets"] = make(map[string]any)
} }
if err := conn.Update(settings); err != nil { if err := conn.Update(settings); err != nil {
@@ -684,10 +725,13 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
b.state.LastError = "" b.state.LastError = ""
b.stateMutex.Unlock() b.stateMutex.Unlock()
// Clear cached PKCS11 PIN on success // Clear cached PKCS11 PIN and SAML cookie on success
b.cachedPKCS11Mu.Lock() b.cachedPKCS11Mu.Lock()
b.cachedPKCS11PIN = nil b.cachedPKCS11PIN = nil
b.cachedPKCS11Mu.Unlock() b.cachedPKCS11Mu.Unlock()
b.cachedGPSamlMu.Lock()
b.cachedGPSamlCookie = nil
b.cachedGPSamlMu.Unlock()
b.pendingVPNSaveMu.Lock() b.pendingVPNSaveMu.Lock()
pending := b.pendingVPNSave pending := b.pendingVPNSave
@@ -706,10 +750,13 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
b.state.LastError = "VPN connection failed" b.state.LastError = "VPN connection failed"
b.stateMutex.Unlock() b.stateMutex.Unlock()
// Clear cached PKCS11 PIN on failure // Clear cached PKCS11 PIN and SAML cookie on failure
b.cachedPKCS11Mu.Lock() b.cachedPKCS11Mu.Lock()
b.cachedPKCS11PIN = nil b.cachedPKCS11PIN = nil
b.cachedPKCS11Mu.Unlock() b.cachedPKCS11Mu.Unlock()
b.cachedGPSamlMu.Lock()
b.cachedGPSamlCookie = nil
b.cachedGPSamlMu.Unlock()
return return
} }
} }
@@ -723,10 +770,13 @@ func (b *NetworkManagerBackend) updateVPNConnectionState() {
b.state.LastError = "VPN connection failed" b.state.LastError = "VPN connection failed"
b.stateMutex.Unlock() b.stateMutex.Unlock()
// Clear cached PKCS11 PIN // Clear cached PKCS11 PIN and SAML cookie
b.cachedPKCS11Mu.Lock() b.cachedPKCS11Mu.Lock()
b.cachedPKCS11PIN = nil b.cachedPKCS11PIN = nil
b.cachedPKCS11Mu.Unlock() 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 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) themeMap := make(map[string]themes.Theme)
for _, t := range allThemes { if registry, err := themes.NewRegistry(); err == nil {
themeMap[t.ID] = t if allThemes, err := registry.List(); err == nil {
for _, t := range allThemes {
themeMap[t.ID] = t
}
}
} }
result := make([]ThemeInfo, 0, len(installedIDs)) result := make([]ThemeInfo, 0, len(installedIDs))

View File

@@ -1,6 +1,8 @@
package utils package utils
import ( import (
"slices"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
) )
@@ -18,3 +20,18 @@ func IsDBusServiceAvailable(busName string) bool {
} }
return owned 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") 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) { func ExpandPath(path string) (string, error) {
expanded := os.ExpandEnv(path) expanded := os.ExpandEnv(path)
expanded = filepath.Clean(expanded) expanded = filepath.Clean(expanded)

View File

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

View File

@@ -50,5 +50,6 @@ in
services.power-profiles-daemon.enable = lib.mkDefault true; services.power-profiles-daemon.enable = lib.mkDefault true;
services.accounts-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 sonnet
qtmultimedia qtmultimedia
qtimageformats qtimageformats
kimageformats
]; ];
in in
{ {
@@ -79,7 +80,7 @@
inherit version; inherit version;
pname = "dms-shell"; pname = "dms-shell";
src = ./core; src = ./core;
vendorHash = "sha256-vsfCgpilOHzJbTaJjJfMK/cSvtyFYJsPDjY4m3iuoFg="; vendorHash = "sha256-cVUJXgzYMRSM0od1xzDVkMTdxHu3OIQX2bQ8AJbGQ1Q=";
subPackages = [ "cmd/dms" ]; subPackages = [ "cmd/dms" ];

View File

@@ -9,6 +9,20 @@ Singleton {
property var currentOSDsByScreen: ({}) 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) { function showOSD(osd) {
if (!osd || !osd.screen) if (!osd || !osd.screen)
return; return;

View File

@@ -80,7 +80,10 @@ Singleton {
return Quickshell.iconPath(moddedId, true); 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 { function getAppName(appId: string, desktopEntry: var): string {

View File

@@ -123,6 +123,8 @@ Singleton {
property string vpnLastConnected: "" property string vpnLastConnected: ""
property var deviceMaxVolumes: ({}) property var deviceMaxVolumes: ({})
property var hiddenOutputDeviceNames: []
property var hiddenInputDeviceNames: []
Component.onCompleted: { Component.onCompleted: {
if (!isGreeterMode) { if (!isGreeterMode) {
@@ -1069,6 +1071,20 @@ Singleton {
saveSettings(); 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) { function getDeviceMaxVolume(nodeName) {
if (!nodeName) if (!nodeName)
return 100; return 100;

View File

@@ -60,6 +60,7 @@ Singleton {
property bool _hasLoaded: false property bool _hasLoaded: false
property bool _isReadOnly: false property bool _isReadOnly: false
property bool _hasUnsavedChanges: false property bool _hasUnsavedChanges: false
property bool _selfWrite: false
property var _loadedSettingsSnapshot: null property var _loadedSettingsSnapshot: null
property var pluginSettings: ({}) property var pluginSettings: ({})
property var builtInPluginSettings: ({}) property var builtInPluginSettings: ({})
@@ -79,6 +80,8 @@ Singleton {
saveSettings(); saveSettings();
} }
property bool clipboardEnterToPaste: false
property var launcherPluginVisibility: ({}) property var launcherPluginVisibility: ({})
function getPluginAllowWithoutTrigger(pluginId) { function getPluginAllowWithoutTrigger(pluginId) {
@@ -312,6 +315,7 @@ Singleton {
property int dankLauncherV2BorderThickness: 2 property int dankLauncherV2BorderThickness: 2
property string dankLauncherV2BorderColor: "primary" property string dankLauncherV2BorderColor: "primary"
property bool dankLauncherV2ShowFooter: true property bool dankLauncherV2ShowFooter: true
property bool dankLauncherV2UnloadOnClose: false
property string _legacyWeatherLocation: "New York, NY" property string _legacyWeatherLocation: "New York, NY"
property string _legacyWeatherCoordinates: "40.7128,-74.0060" property string _legacyWeatherCoordinates: "40.7128,-74.0060"
@@ -469,6 +473,8 @@ Singleton {
property bool dockShowOverflowBadge: true property bool dockShowOverflowBadge: true
property bool notificationOverlayEnabled: false property bool notificationOverlayEnabled: false
property bool notificationPopupShadowEnabled: true
property bool notificationPopupPrivacyMode: false
property int overviewRows: 2 property int overviewRows: 2
property int overviewColumns: 5 property int overviewColumns: 5
property real overviewScale: 0.16 property real overviewScale: 0.16
@@ -498,12 +504,15 @@ Singleton {
property int notificationTimeoutCritical: 0 property int notificationTimeoutCritical: 0
property bool notificationCompactMode: false property bool notificationCompactMode: false
property int notificationPopupPosition: SettingsData.Position.Top property int notificationPopupPosition: SettingsData.Position.Top
property int notificationAnimationSpeed: SettingsData.AnimationSpeed.Short
property int notificationCustomAnimationDuration: 400
property bool notificationHistoryEnabled: true property bool notificationHistoryEnabled: true
property int notificationHistoryMaxCount: 50 property int notificationHistoryMaxCount: 50
property int notificationHistoryMaxAgeDays: 7 property int notificationHistoryMaxAgeDays: 7
property bool notificationHistorySaveLow: true property bool notificationHistorySaveLow: true
property bool notificationHistorySaveNormal: true property bool notificationHistorySaveNormal: true
property bool notificationHistorySaveCritical: true property bool notificationHistorySaveCritical: true
property var notificationRules: []
property bool osdAlwaysShowValue: false property bool osdAlwaysShowValue: false
property int osdPosition: SettingsData.Position.BottomCenter property int osdPosition: SettingsData.Position.BottomCenter
@@ -1006,6 +1015,42 @@ Singleton {
function applyStoredIconTheme() { function applyStoredIconTheme() {
updateGtkIconTheme(); updateGtkIconTheme();
updateQtIconTheme(); 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() { function updateGtkIconTheme() {
@@ -1199,6 +1244,7 @@ Singleton {
function saveSettings() { function saveSettings() {
if (_loading || _parseError || !_hasLoaded) if (_loading || _parseError || !_hasLoaded)
return; return;
_selfWrite = true;
settingsFile.setText(JSON.stringify(Store.toJson(root), null, 2)); settingsFile.setText(JSON.stringify(Store.toJson(root), null, 2));
if (_isReadOnly) if (_isReadOnly)
_checkSettingsWritable(); _checkSettingsWritable();
@@ -1812,6 +1858,7 @@ Singleton {
iconTheme = themeName; iconTheme = themeName;
updateGtkIconTheme(); updateGtkIconTheme();
updateQtIconTheme(); updateQtIconTheme();
updateCosmicIconTheme();
saveSettings(); saveSettings();
if (typeof Theme !== "undefined" && Theme.currentTheme === Theme.dynamic) if (typeof Theme !== "undefined" && Theme.currentTheme === Theme.dynamic)
Theme.generateSystemThemesFromCurrentTheme(); Theme.generateSystemThemesFromCurrentTheme();
@@ -2134,6 +2181,143 @@ Singleton {
saveSettings(); 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() { function getDefaultAppIdSubstitutions() {
return Spec.SPEC.appIdSubstitutions.def; return Spec.SPEC.appIdSubstitutions.def;
} }
@@ -2159,19 +2343,40 @@ Singleton {
Theme.reloadCustomThemeVariant(); Theme.reloadCustomThemeVariant();
} }
function getRegistryThemeMultiVariant(themeId, defaults) { function getRegistryThemeMultiVariant(themeId, defaults, mode) {
var stored = registryThemeVariants[themeId]; var stored = registryThemeVariants[themeId];
if (stored && typeof stored === "object") if (!stored || typeof stored !== "object")
return stored; return defaults || {};
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)); 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, flavor: flavor,
accent: accent accent: accent
}; };
variants[themeId] = perMode;
registryThemeVariants = variants; registryThemeVariants = variants;
saveSettings(); saveSettings();
if (typeof Theme !== "undefined") if (typeof Theme !== "undefined")
@@ -2371,6 +2576,13 @@ Singleton {
property alias settingsFile: settingsFile property alias settingsFile: settingsFile
Timer {
id: settingsFileReloadDebounce
interval: 50
onTriggered: settingsFile.reload()
repeat: false
}
FileView { FileView {
id: settingsFile id: settingsFile
@@ -2378,7 +2590,14 @@ Singleton {
blockLoading: true blockLoading: true
blockWrites: true blockWrites: true
atomicWrites: true atomicWrites: true
watchChanges: !isGreeterMode watchChanges: true
onFileChanged: {
if (_selfWrite) {
_selfWrite = false;
return;
}
settingsFileReloadDebounce.restart();
}
onLoaded: { onLoaded: {
if (isGreeterMode) if (isGreeterMode)
return; return;

View File

@@ -188,6 +188,8 @@ Singleton {
if (typeof SettingsData !== "undefined" && SettingsData.currentThemeName) { if (typeof SettingsData !== "undefined" && SettingsData.currentThemeName) {
switchTheme(SettingsData.currentThemeName, false, false); switchTheme(SettingsData.currentThemeName, false, false);
const currentIsLight = (typeof SessionData !== "undefined") ? SessionData.isLightMode : false;
SettingsData.updateCosmicThemeMode(currentIsLight);
} }
if (typeof SessionData !== "undefined" && SessionData.themeModeAutoEnabled) { if (typeof SessionData !== "undefined" && SessionData.themeModeAutoEnabled) {
@@ -776,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: { readonly property int popoutAnimationDuration: {
if (typeof SettingsData === "undefined") if (typeof SettingsData === "undefined")
return 150; return 150;
@@ -916,6 +965,9 @@ Singleton {
if (!matugenAvailable) { if (!matugenAvailable) {
PortalService.setLightMode(light); PortalService.setLightMode(light);
} }
if (typeof SettingsData !== "undefined") {
SettingsData.updateCosmicThemeMode(light);
}
generateSystemThemesFromCurrentTheme(); generateSystemThemesFromCurrentTheme();
} }
} }
@@ -971,7 +1023,7 @@ Singleton {
if (themeData.variants.type === "multi" && themeData.variants.flavors && themeData.variants.accents) { if (themeData.variants.type === "multi" && themeData.variants.flavors && themeData.variants.accents) {
const defaults = themeData.variants.defaults || {}; const defaults = themeData.variants.defaults || {};
const modeDefaults = defaults[colorMode] || defaults.dark || {}; const modeDefaults = defaults[colorMode] || defaults.dark || {};
const stored = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults) : modeDefaults; const stored = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, modeDefaults, colorMode) : modeDefaults;
var flavorId = stored.flavor || modeDefaults.flavor || ""; var flavorId = stored.flavor || modeDefaults.flavor || "";
const accentId = stored.accent || modeDefaults.accent || ""; const accentId = stored.accent || modeDefaults.accent || "";
var flavor = findVariant(themeData.variants.flavors, flavorId); var flavor = findVariant(themeData.variants.flavors, flavorId);
@@ -1365,8 +1417,8 @@ Singleton {
const defaults = customThemeRawData.variants.defaults || {}; const defaults = customThemeRawData.variants.defaults || {};
const darkDefaults = defaults.dark || {}; const darkDefaults = defaults.dark || {};
const lightDefaults = defaults.light || defaults.dark || {}; const lightDefaults = defaults.light || defaults.dark || {};
const storedDark = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults) : darkDefaults; const storedDark = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, darkDefaults, "dark") : darkDefaults;
const storedLight = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults) : lightDefaults; const storedLight = typeof SettingsData !== "undefined" ? SettingsData.getRegistryThemeMultiVariant(themeId, lightDefaults, "light") : lightDefaults;
const darkFlavorId = storedDark.flavor || darkDefaults.flavor || ""; const darkFlavorId = storedDark.flavor || darkDefaults.flavor || "";
const lightFlavorId = storedLight.flavor || lightDefaults.flavor || ""; const lightFlavorId = storedLight.flavor || lightDefaults.flavor || "";
const accentId = storedDark.accent || darkDefaults.accent || ""; const accentId = storedDark.accent || darkDefaults.accent || "";

View File

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

View File

@@ -75,7 +75,9 @@ var SPEC = {
vpnLastConnected: { def: "" }, vpnLastConnected: { def: "" },
deviceMaxVolumes: { def: {} } deviceMaxVolumes: { def: {} },
hiddenOutputDeviceNames: { def: [] },
hiddenInputDeviceNames: { def: [] }
}; };
function getValidKeys() { function getValidKeys() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,12 @@ Rectangle {
id: keyboardHints id: keyboardHints
property bool wtypeAvailable: false 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 height: ClipboardConstants.keyboardHintsHeight
radius: Theme.cornerRadius radius: Theme.cornerRadius
@@ -21,7 +26,7 @@ Rectangle {
spacing: 2 spacing: 2
StyledText { 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 font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -81,6 +81,12 @@ function calculateNextIndex(flatModel, selectedFlatIndex, sectionId, viewMode, g
return bounds.start + newPosInSection; 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); var nextSection = findNextNonHeaderIndex(flatModel, bounds.end + 1);
return nextSection !== -1 ? nextSection : selectedFlatIndex; return nextSection !== -1 ? nextSection : selectedFlatIndex;
} }

View File

@@ -132,7 +132,16 @@ Item {
var rowIndex = _flatIndexToRowMap[index]; var rowIndex = _flatIndexToRowMap[index];
if (rowIndex === undefined) if (rowIndex === undefined)
return; return;
mainListView.positionViewAtIndex(rowIndex, ListView.Contain); 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() { function getSelectedItemPosition() {

View File

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

View File

@@ -64,7 +64,7 @@ Rectangle {
if (!path) if (!path)
return false; return false;
var ext = path.split('.').pop().toLowerCase(); 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 { DankRipple {

View File

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

View File

@@ -31,7 +31,7 @@ StyledRect {
function determineFileType(fileName) { function determineFileType(fileName) {
const ext = getFileExtension(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)) { if (imageExts.includes(ext)) {
return "image"; return "image";
} }
@@ -119,7 +119,7 @@ StyledRect {
id: gridPreviewImage id: gridPreviewImage
anchors.fill: parent anchors.fill: parent
anchors.margins: 2 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 int weExtIndex: 0
property string imagePath: { property string imagePath: {
if (weMode && delegateRoot.fileIsDir) if (weMode && delegateRoot.fileIsDir)

View File

@@ -30,7 +30,7 @@ StyledRect {
function determineFileType(fileName) { function determineFileType(fileName) {
const ext = getFileExtension(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)) { if (imageExts.includes(ext)) {
return "image"; return "image";
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -209,7 +209,7 @@ Rectangle {
"children": [ "children": [
{ {
"id": "display_config", "id": "display_config",
"text": I18n.tr("Configuration") + " (Beta)", "text": I18n.tr("Configuration"),
"icon": "display_settings", "icon": "display_settings",
"tabIndex": 24 "tabIndex": 24
}, },

View File

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

View File

@@ -54,9 +54,6 @@ DankPopout {
property alias launcherContent: launcherContent property alias launcherContent: launcherContent
color: "transparent" color: "transparent"
radius: Theme.cornerRadius
antialiasing: true
smooth: true
QtObject { QtObject {
id: modalAdapter 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 { FocusScope {
anchors.fill: parent anchors.fill: parent
focus: true focus: true

View File

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

View File

@@ -945,22 +945,31 @@ Column {
} }
} }
Component.onCompleted: { function tryCreatePluginInstance() {
Qt.callLater(() => { const pluginComponent = PluginService.pluginWidgetComponents[pluginId];
const pluginComponent = PluginService.pluginWidgetComponents[pluginId]; if (!pluginComponent)
if (pluginComponent) { return false;
const instance = pluginComponent.createObject(null, { try {
"pluginId": pluginId, const instance = pluginComponent.createObject(null, {
"pluginService": PluginService, "pluginId": pluginId,
"visible": false, "pluginService": PluginService,
"width": 0, "visible": false,
"height": 0 "width": 0,
}); "height": 0
if (instance) { });
pluginInstance = instance; 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 { Connections {
@@ -970,6 +979,11 @@ Column {
pluginInstance.loadPluginData(); pluginInstance.loadPluginData();
} }
} }
function onPluginLoaded(loadedPluginId) {
if (loadedPluginId !== pluginId || pluginInstance)
return;
Qt.callLater(() => tryCreatePluginInstance());
}
} }
Component.onDestruction: { Component.onDestruction: {

View File

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

View File

@@ -12,6 +12,7 @@ DankPopout {
id: root id: root
layerNamespace: "dms:control-center" layerNamespace: "dms:control-center"
fullHeightSurface: true
property string expandedSection: "" property string expandedSection: ""
property var triggerScreen: null property var triggerScreen: null
@@ -115,11 +116,6 @@ DankPopout {
property alias bluetoothCodecSelector: bluetoothCodecSelector property alias bluetoothCodecSelector: bluetoothCodecSelector
color: "transparent" 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 { Rectangle {
anchors.fill: parent anchors.fill: parent

View File

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

View File

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

View File

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

View File

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

View File

@@ -48,11 +48,6 @@ DankPopout {
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2 implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
color: "transparent" color: "transparent"
radius: Theme.cornerRadius
border.color: Theme.outlineMedium
border.width: 0
antialiasing: true
smooth: true
focus: true focus: true
Component.onCompleted: { Component.onCompleted: {
if (root.shouldBeVisible) { if (root.shouldBeVisible) {
@@ -78,35 +73,6 @@ DankPopout {
target: root 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 { Column {
id: contentColumn id: contentColumn

View File

@@ -111,11 +111,6 @@ DankPopout {
implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2 implicitHeight: contentColumn.implicitHeight + Theme.spacingL * 2
color: "transparent" color: "transparent"
radius: Theme.cornerRadius
border.color: Theme.outlineMedium
border.width: 0
antialiasing: true
smooth: true
focus: true focus: true
Component.onCompleted: { 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 { Column {
id: contentColumn id: contentColumn

View File

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

View File

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

View File

@@ -55,7 +55,7 @@ BasePill {
} }
IconImage { 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 anchors.centerIn: parent
width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.noBackground) width: Theme.barIconSize(root.barThickness, SettingsData.launcherLogoSizeOffset, root.barConfig?.noBackground)
height: 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"; return "file://" + Theme.shellDir + "/assets/sway.svg";
} else if (CompositorService.isScroll) { } else if (CompositorService.isScroll) {
return "file://" + Theme.shellDir + "/assets/sway.svg"; return "file://" + Theme.shellDir + "/assets/sway.svg";
} else if (CompositorService.isMiracle) {
return "file://" + Theme.shellDir + "/assets/miraclewm.svg";
} else if (CompositorService.isLabwc) { } else if (CompositorService.isLabwc) {
return "file://" + Theme.shellDir + "/assets/labwc.png"; 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 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 visible: windowCount > 0
property real scrollAccumulator: 0 property real scrollAccumulator: 0
property real touchpadThreshold: 500 property real touchpadThreshold: 500
onWheel: function (wheelEvent) { onWheel: function (wheelEvent) {
wheelEvent.accepted = true;
const deltaY = wheelEvent.angleDelta.y; const deltaY = wheelEvent.angleDelta.y;
const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0; const isMouseWheel = Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
@@ -226,7 +241,7 @@ BasePill {
property bool isGrouped: root._groupByApp property bool isGrouped: root._groupByApp
property var groupData: isGrouped ? modelData : null property var groupData: isGrouped ? modelData : null
property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData 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 appId: isGrouped ? modelData.appId : (modelData.appId || "")
property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)" property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)"
property var toplevelObject: toplevelData property var toplevelObject: toplevelData
@@ -242,7 +257,7 @@ BasePill {
} }
return appName + (windowTitle ? " • " + windowTitle : ""); 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 width: visualWidth
height: root.barThickness height: root.barThickness
@@ -250,7 +265,7 @@ BasePill {
Rectangle { Rectangle {
id: visualContent id: visualContent
width: delegateItem.visualWidth width: delegateItem.visualWidth
height: 24 height: root.iconCellSize
anchors.centerIn: parent anchors.centerIn: parent
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
@@ -433,7 +448,7 @@ BasePill {
const screenX = root.parentScreen ? root.parentScreen.x : 0; const screenX = root.parentScreen ? root.parentScreen.x : 0;
const screenY = root.parentScreen ? root.parentScreen.y : 0; const screenY = root.parentScreen ? root.parentScreen.y : 0;
const relativeY = globalPos.y - screenY; 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 isLeft = root.axis?.edge === "left";
const adjustedY = relativeY + root.minTooltipY; const adjustedY = relativeY + root.minTooltipY;
const finalX = screenX + tooltipX; const finalX = screenX + tooltipX;
@@ -442,7 +457,7 @@ BasePill {
const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height); const globalPos = delegateItem.mapToGlobal(delegateItem.width / 2, delegateItem.height);
const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height; const screenHeight = root.parentScreen ? root.parentScreen.height : Screen.height;
const isBottom = root.axis?.edge === "bottom"; 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); tooltipLoader.item.show(delegateItem.tooltipText, globalPos.x, tooltipY, root.parentScreen, false, false);
} }
} }
@@ -481,7 +496,7 @@ BasePill {
property bool isGrouped: root._groupByApp property bool isGrouped: root._groupByApp
property var groupData: isGrouped ? modelData : null property var groupData: isGrouped ? modelData : null
property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData 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 appId: isGrouped ? modelData.appId : (modelData.appId || "")
property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)" property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)"
property var toplevelObject: toplevelData property var toplevelObject: toplevelData
@@ -497,15 +512,15 @@ BasePill {
} }
return appName + (windowTitle ? " • " + windowTitle : ""); 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 width: root.barThickness
height: 24 height: root.iconCellSize
Rectangle { Rectangle {
id: visualContent id: visualContent
width: delegateItem.visualWidth width: delegateItem.visualWidth
height: 24 height: root.iconCellSize
anchors.centerIn: parent anchors.centerIn: parent
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {

View File

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

View File

@@ -10,6 +10,45 @@ BasePill {
property bool isActive: false property bool isActive: false
readonly property bool hasUpdates: SystemUpdateService.updateCount > 0 readonly property bool hasUpdates: SystemUpdateService.updateCount > 0
readonly property bool isChecking: SystemUpdateService.isChecking 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 { Ref {
service: SystemUpdateService service: SystemUpdateService

View File

@@ -37,6 +37,7 @@ Item {
return DwlService.activeOutput || root.screenName; return DwlService.activeOutput || root.screenName;
case "sway": case "sway":
case "scroll": case "scroll":
case "miracle":
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true); const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
return focusedWs?.monitor?.name || root.screenName; return focusedWs?.monitor?.name || root.screenName;
default: 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 { Connections {
target: DesktopEntries target: DesktopEntries
@@ -67,6 +68,7 @@ Item {
return activeTags.length > 0 ? activeTags[0] : -1; return activeTags.length > 0 ? activeTags[0] : -1;
case "sway": case "sway":
case "scroll": case "scroll":
case "miracle":
return getSwayActiveWorkspace(); return getSwayActiveWorkspace();
default: default:
return 1; return 1;
@@ -97,6 +99,7 @@ Item {
break; break;
case "sway": case "sway":
case "scroll": case "scroll":
case "miracle":
baseList = getSwayWorkspaces(); baseList = getSwayWorkspaces();
break; break;
default: 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) { 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); 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 "num": 1
} }
@@ -222,7 +236,7 @@ Item {
return []; return [];
} }
targetWorkspaceId = ws.tag; targetWorkspaceId = ws.tag;
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
targetWorkspaceId = ws.num !== undefined ? ws.num : ws; targetWorkspaceId = ws.num !== undefined ? ws.num : ws;
} else { } else {
return []; return [];
@@ -234,7 +248,7 @@ Item {
let isActiveWs = false; let isActiveWs = false;
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
isActiveWs = NiriService.allWorkspaces.some(ws => ws.id === targetWorkspaceId && ws.is_active); 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); const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
isActiveWs = focusedWs ? (focusedWs.num === targetWorkspaceId) : false; isActiveWs = focusedWs ? (focusedWs.num === targetWorkspaceId) : false;
} else if (CompositorService.isDwl) { } else if (CompositorService.isDwl) {
@@ -255,7 +269,7 @@ Item {
let winWs = null; let winWs = null;
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
winWs = w.workspace_id; winWs = w.workspace_id;
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
winWs = w.workspace?.num; winWs = w.workspace?.num;
} else { } else {
const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []); const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []);
@@ -322,7 +336,7 @@ Item {
placeholder = { placeholder = {
"tag": -1 "tag": -1
}; };
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
placeholder = { placeholder = {
"num": -1 "num": -1
}; };
@@ -516,7 +530,7 @@ Item {
return ws && ws.id !== -1; return ws && ws.id !== -1;
if (CompositorService.isDwl) if (CompositorService.isDwl)
return ws && ws.tag !== -1; return ws && ws.tag !== -1;
if (CompositorService.isSway || CompositorService.isScroll) if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return ws && ws.num !== -1; return ws && ws.num !== -1;
return ws !== -1; return ws !== -1;
}); });
@@ -588,7 +602,7 @@ Item {
} }
DwlService.switchToTag(root.screenName, realWorkspaces[nextIndex].tag); DwlService.switchToTag(root.screenName, realWorkspaces[nextIndex].tag);
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
const realWorkspaces = getRealWorkspaces(); const realWorkspaces = getRealWorkspaces();
if (realWorkspaces.length < 2) { if (realWorkspaces.length < 2) {
return; return;
@@ -617,7 +631,7 @@ Item {
return modelData?.id || ""; return modelData?.id || "";
if (CompositorService.isDwl) if (CompositorService.isDwl)
return (modelData?.tag !== undefined) ? (modelData.tag + 1) : ""; return (modelData?.tag !== undefined) ? (modelData.tag + 1) : "";
if (CompositorService.isSway || CompositorService.isScroll) if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return modelData?.num || ""; return modelData?.num || "";
return modelData - 1; return modelData - 1;
} }
@@ -632,7 +646,7 @@ Item {
isPlaceholder = modelData?.id === -1; isPlaceholder = modelData?.id === -1;
} else if (CompositorService.isDwl) { } else if (CompositorService.isDwl) {
isPlaceholder = modelData?.tag === -1; isPlaceholder = modelData?.tag === -1;
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
isPlaceholder = modelData?.num === -1; isPlaceholder = modelData?.num === -1;
} else { } else {
isPlaceholder = modelData === -1; isPlaceholder = modelData === -1;
@@ -665,7 +679,7 @@ Item {
return getWorkspaceIndexFallback(modelData, index); 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 hasWorkspaces: getRealWorkspaces().length > 0
readonly property bool shouldShow: hasNativeWorkspaceSupport || (useExtWorkspace && hasWorkspaces) readonly property bool shouldShow: hasNativeWorkspaceSupport || (useExtWorkspace && hasWorkspaces)
@@ -865,7 +879,7 @@ Item {
return !!(modelData && modelData.id === root.currentWorkspace); return !!(modelData && modelData.id === root.currentWorkspace);
if (CompositorService.isDwl) if (CompositorService.isDwl)
return !!(modelData && root.dwlActiveTags.includes(modelData.tag)); 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 && modelData.num === root.currentWorkspace);
return modelData === root.currentWorkspace; return modelData === root.currentWorkspace;
} }
@@ -889,7 +903,7 @@ Item {
return !!(modelData && modelData.id === -1); return !!(modelData && modelData.id === -1);
if (CompositorService.isDwl) if (CompositorService.isDwl)
return !!(modelData && modelData.tag === -1); return !!(modelData && modelData.tag === -1);
if (CompositorService.isSway || CompositorService.isScroll) if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return !!(modelData && modelData.num === -1); return !!(modelData && modelData.num === -1);
return modelData === -1; return modelData === -1;
} }
@@ -906,12 +920,17 @@ Item {
return loadedIsUrgent; return loadedIsUrgent;
if (CompositorService.isDwl) if (CompositorService.isDwl)
return modelData?.state === 2; return modelData?.state === 2;
if (CompositorService.isSway || CompositorService.isScroll) if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
return loadedIsUrgent; return loadedIsUrgent;
return false; return false;
} }
property var loadedIconData: null readonly property var loadedIconData: {
property bool loadedHasIcon: false 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: [] property var loadedIcons: []
readonly property int stableIconCount: { readonly property int stableIconCount: {
@@ -927,7 +946,7 @@ Item {
targetWorkspaceId = modelData?.id; targetWorkspaceId = modelData?.id;
} else if (CompositorService.isDwl) { } else if (CompositorService.isDwl) {
targetWorkspaceId = modelData?.tag; targetWorkspaceId = modelData?.tag;
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
targetWorkspaceId = modelData?.num; targetWorkspaceId = modelData?.num;
} }
if (targetWorkspaceId === undefined || targetWorkspaceId === null) if (targetWorkspaceId === undefined || targetWorkspaceId === null)
@@ -946,7 +965,7 @@ Item {
let winWs = null; let winWs = null;
if (CompositorService.isNiri) { if (CompositorService.isNiri) {
winWs = w.workspace_id; winWs = w.workspace_id;
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
winWs = w.workspace?.num; winWs = w.workspace?.num;
} else if (CompositorService.isHyprland) { } else if (CompositorService.isHyprland) {
const hyprlandToplevels = Array.from(Hyprland.toplevels?.values || []); 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 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 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 hasWorkspaceName: SettingsData.showWorkspaceName && modelData?.name && modelData.name !== ""
readonly property bool workspaceNamesEnabled: SettingsData.showWorkspaceName && CompositorService.isNiri readonly property bool workspaceNamesEnabled: SettingsData.showWorkspaceName && (CompositorService.isNiri || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
readonly property real contentImplicitWidth: (hasWorkspaceName || loadedHasIcon) ? (appIconsLoader.item?.contentWidth ?? 0) : 0 readonly property real contentImplicitWidth: hasWorkspaceName ? (appIconsLoader.item?.contentWidth ?? 0) : 0
readonly property real contentImplicitHeight: (workspaceNamesEnabled || loadedHasIcon) ? (appIconsLoader.item?.contentHeight ?? 0) : 0 readonly property real contentImplicitHeight: workspaceNamesEnabled ? (appIconsLoader.item?.contentHeight ?? 0) : 0
readonly property real iconsExtraWidth: { readonly property real iconsExtraWidth: {
if (!root.isVertical && SettingsData.showWorkspaceApps && stableIconCount > 0) { if (!root.isVertical && SettingsData.showWorkspaceApps && stableIconCount > 0) {
@@ -1123,9 +1142,7 @@ Item {
return; return;
if (!dragHandler.dragging) { if (!dragHandler.dragging) {
const distance = root.isVertical const distance = root.isVertical ? Math.abs(mouse.y - dragHandler.dragStartPos.y) : Math.abs(mouse.x - dragHandler.dragStartPos.x);
? Math.abs(mouse.y - dragHandler.dragStartPos.y)
: Math.abs(mouse.x - dragHandler.dragStartPos.x);
if (distance > 5) { if (distance > 5) {
dragHandler.dragging = true; dragHandler.dragging = true;
root.dragSourceIndex = index; root.dragSourceIndex = index;
@@ -1136,9 +1153,7 @@ Item {
if (!dragHandler.dragging) if (!dragHandler.dragging)
return; return;
const rawAxisOffset = root.isVertical const rawAxisOffset = root.isVertical ? (mouse.y - dragHandler.dragStartPos.y) : (mouse.x - dragHandler.dragStartPos.x);
? (mouse.y - dragHandler.dragStartPos.y)
: (mouse.x - dragHandler.dragStartPos.x);
const itemSize = (root.isVertical ? delegateRoot.height : delegateRoot.width) + Theme.spacingS; const itemSize = (root.isVertical ? delegateRoot.height : delegateRoot.width) + Theme.spacingS;
const maxOffsetPositive = (root.workspaceList.length - 1 - index) * itemSize; const maxOffsetPositive = (root.workspaceList.length - 1 - index) * itemSize;
@@ -1189,7 +1204,7 @@ Item {
Hyprland.dispatch(`workspace ${modelData.id}`); Hyprland.dispatch(`workspace ${modelData.id}`);
} else if (CompositorService.isDwl && modelData?.tag !== undefined) { } else if (CompositorService.isDwl && modelData?.tag !== undefined) {
DwlService.switchToTag(root.screenName, modelData.tag); 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 { try {
I3.dispatch(`workspace number ${modelData.num}`); I3.dispatch(`workspace number ${modelData.num}`);
} catch (_) {} } catch (_) {}
@@ -1212,8 +1227,6 @@ Item {
onTriggered: { onTriggered: {
if (isPlaceholder) { if (isPlaceholder) {
delegateRoot.loadedWorkspaceData = null; delegateRoot.loadedWorkspaceData = null;
delegateRoot.loadedIconData = null;
delegateRoot.loadedHasIcon = false;
delegateRoot.loadedIcons = []; delegateRoot.loadedIcons = [];
delegateRoot.loadedIsUrgent = false; delegateRoot.loadedIsUrgent = false;
return; return;
@@ -1228,7 +1241,7 @@ Item {
wsData = modelData; wsData = modelData;
} else if (CompositorService.isDwl) { } else if (CompositorService.isDwl) {
wsData = modelData; wsData = modelData;
} else if (CompositorService.isSway || CompositorService.isScroll) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
wsData = modelData; wsData = modelData;
} }
delegateRoot.loadedWorkspaceData = wsData; delegateRoot.loadedWorkspaceData = wsData;
@@ -1239,15 +1252,8 @@ Item {
delegateRoot.loadedIsUrgent = wsData?.urgent ?? false; 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 (SettingsData.showWorkspaceApps) {
if (CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll) { if (CompositorService.isDwl || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
delegateRoot.loadedIcons = root.getWorkspaceIcons(modelData); delegateRoot.loadedIcons = root.getWorkspaceIcons(modelData);
} else if (CompositorService.isNiri) { } else if (CompositorService.isNiri) {
delegateRoot.loadedIcons = root.getWorkspaceIcons(isPlaceholder ? null : modelData); delegateRoot.loadedIcons = root.getWorkspaceIcons(isPlaceholder ? null : modelData);
@@ -1410,7 +1416,7 @@ Item {
Item { Item {
visible: loadedHasIcon && loadedIconData?.type === "icon" visible: loadedHasIcon && loadedIconData?.type === "icon"
width: wsIcon.width + (isActive && loadedIcons.length > 0 ? 4 : 0) width: wsIcon.width
height: root.appIconSize height: root.appIconSize
DankIcon { DankIcon {
@@ -1425,7 +1431,7 @@ Item {
Item { Item {
visible: loadedHasIcon && loadedIconData?.type === "text" visible: loadedHasIcon && loadedIconData?.type === "text"
width: wsText.implicitWidth + (isActive && loadedIcons.length > 0 ? 4 : 0) width: wsText.implicitWidth
height: root.appIconSize height: root.appIconSize
StyledText { StyledText {
@@ -1439,14 +1445,14 @@ Item {
} }
Item { Item {
visible: (SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName) && !loadedHasIcon visible: ((SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName) && !loadedHasIcon) || (loadedHasIcon && SettingsData.showWorkspaceName && hasWorkspaceName)
width: wsIndexText.implicitWidth + (isActive && loadedIcons.length > 0 ? 4 : 0) width: wsIndexText.implicitWidth
height: root.appIconSize height: root.appIconSize
StyledText { StyledText {
id: wsIndexText id: wsIndexText
anchors.verticalCenter: parent.verticalCenter 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 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.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal
@@ -1564,9 +1570,9 @@ Item {
} }
StyledText { StyledText {
visible: (SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName) && !loadedHasIcon visible: ((SettingsData.showWorkspaceIndex || SettingsData.showWorkspaceName) && !loadedHasIcon) || (loadedHasIcon && SettingsData.showWorkspaceName && hasWorkspaceName)
anchors.horizontalCenter: parent.horizontalCenter 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 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.pixelSize: Theme.barTextSize(barThickness, barConfig?.fontScale)
font.weight: (isActive && !isPlaceholder) ? Font.DemiBold : Font.Normal 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() Component.onCompleted: updateAllData()
@@ -1760,7 +1717,7 @@ Item {
} }
Connections { Connections {
target: I3.workspaces target: I3.workspaces
enabled: (CompositorService.isSway || CompositorService.isScroll) enabled: (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle)
function onValuesChanged() { function onValuesChanged() {
delegateRoot.updateAllData(); delegateRoot.updateAllData();
} }

View File

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

View File

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

View File

@@ -306,7 +306,8 @@ Item {
showDirsFirst: false showDirsFirst: false
showDotAndDotDot: false showDotAndDotDot: false
showHidden: false showHidden: false
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] caseSensitive: false
nameFilters: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"]
showFiles: true showFiles: true
showDirs: false showDirs: false
sortField: FolderListModel.Name sortField: FolderListModel.Name
@@ -320,7 +321,7 @@ Item {
browserIcon: "folder_open" browserIcon: "folder_open"
browserType: "wallpaper" browserType: "wallpaper"
showHiddenFiles: false showHiddenFiles: false
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif", "*.exr"]
parentPopout: root.parentPopout parentPopout: root.parentPopout
onFileSelected: path => { onFileSelected: path => {

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