1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-12 23:32:50 -04:00

Compare commits

..

86 Commits

Author SHA1 Message Date
bbedward f0be36062e dankbar: guard against nil screen names 2026-03-16 11:32:59 -04:00
bbedward 9578d6daf9 popout: fix focusing of password prompts when popout is open
undesired effect of closing the popout but its probably the best
solution
2026-03-16 11:30:31 -04:00
bbedward cc6766135d focused app: fallback to app name if no title in compact mode
fixes #2005
2026-03-16 11:25:50 -04:00
bbedward 28c9bb0925 cc: fix invalid number displays on percentages
fixes #2010
2026-03-16 11:18:42 -04:00
Ron Harel 7826d827dd feat: add configurable control center group ordering (#2006)
* Add grouped element reordering to control center setting popup.

Reorganize the control center widget menu into grouped rows and add drag handles for reordering.
Introduce controlCenterGroups to drive the grouped popup layout, along with dynamic content width calculation.
Disable dependent options when their parent icon is turned off, and refine DankToggle disabled colors to better distinguish checked and unchecked states.

* Apply Control Center group order to live widget rendering.

Apply persisted `controlCenterGroupOrder` to the actual control center button rendering path instead of only using it in the settings UI.
This refactors `ControlCenterButton.qml` to derive a normalized effective group order, build a small render model from that order, and use model-driven rendering for both vertical and horizontal layouts.

Highlights:
- add a default control center group order and normalize saved order data
- ignore unknown ids, deduplicate duplicates, and append missing known groups
- add shared group visibility helpers and derive a render model from them
- render both vertical and horizontal indicators from the effective order
- preserve existing icon, color, percent text, and visibility behavior
- keep the fallback settings icon outside persisted ordering
- reconnect cached interaction refs for audio, mic, and brightness to the real rendered group containers so wheel and right-click behavior still work
- clear and refresh interaction refs on orientation, visibility, and delegate lifecycle changes
- tighten horizontal composite group sizing by measuring actual rendered content, fixing extra spacing around the audio indicator

Also updates the settings widgets UI to persist and restore control center group ordering consistently with the live control center rendering.
2026-03-16 11:11:26 -04:00
Michael Erdely 7f392acc54 Implement ability to cycle through launcher modes (#2003)
Use Ctrl+Left/Right and Ctrl+H/L to move back and forward through the
modes of the launcher
2026-03-16 11:08:07 -04:00
Michael Erdely 190fd662ad Implement more intuitive keybinds for Launcher (#2002)
With programs like rofi, pressing the tab key advances to the next item
in the list. This change makes the Launcher behave in the same way,
moving the action cycling to Ctrl+Tab (and Ctrl+Shift+Tab for reverse.
2026-03-16 11:07:25 -04:00
Triệu Kha e18587c471 feat(calendar): add show week number option (#1990)
* increase DankDashPopout width to accommodate week number column

* add getWeekNumber function

* add week number column

* add showWeekNumber SettingsData

* add showWeekNumber SettingsSpec

* make dash popout width changes reponsively to showWeekNumber option

* complete and cleanup

* fix typo

* fix typo
2026-03-16 11:06:21 -04:00
Walid Salah ddb079b62d Add terminal multiplexer launcher (#1687)
* Add tmux

* Add mux modal

* Restore the settings config version

* Revert typo

* Use DankModal for InputModal

* Simplify terminal flags

* use showWithOptions for inputModals instead

* Fix translation

* use Quickshell.env("TERMINAL") to choose terminal

* Fix typo

* Hide muxModal after creating new session

* Add mux check, moved exclusion to service, And use ScriptModel

* Revert unrelated change

* Add blank line
2026-03-16 11:05:16 -04:00
purian23 e7c8d208e2 copr(fedora): Update Go Toolchain for compatibility 2026-03-15 23:26:45 -04:00
zion 0e2162cf29 fix(nix/greeter): skip invalid customThemeFile in preStart (#1997)
* fix(nix/greeter): skip invalid customThemeFile in preStart

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

Update distro/nix/greeter.nix

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

* nix/greeter: update customTheme verification

---------

Co-authored-by: Lucas <43530291+LuckShiba@users.noreply.github.com>
Co-authored-by: LuckShiba <luckshiba@protonmail.com>
2026-03-15 04:21:19 -03:00
NikSne 4cf9b0adc7 feat(nix/niri): add new includes for dms 1.4 (#1998) 2026-03-15 04:09:00 -03:00
purian23 1661d32641 (greeter): Trial fix for 30s auth delay & wireplumber state dir 2026-03-15 02:54:23 -04:00
Ron Harel aa59187403 Add Color Picker to DMS launcher. (#1999) 2026-03-14 23:31:21 -04:00
bbedward bb08e1233a matugen: bump default queue timeout to 90s 2026-03-13 15:40:55 -04:00
bbedward 5343e97ab2 core/server: initialize geolocation async on startup 2026-03-13 15:06:11 -04:00
purian23 edc544df7a dms(policy): Restore dms greeter sync in immutable distros 2026-03-13 14:27:15 -04:00
bbedward a880edd9fb core: restore core go version to 1.26.0 2026-03-13 13:40:11 -04:00
Jonas Bloch 7e1d808d70 New neovim theme engine (#1985)
* feat(matugen)!: rework completely neovim's theme engine

* fix: link to neovim theme plugin

* fix: expect AvengeMedia/base46 instead of Silzinc/base46
2026-03-13 13:37:16 -04:00
bbedward ce93f22669 chore: Makefile shouldnt build when installing 2026-03-13 13:36:44 -04:00
bbedward a58037b968 fix: missing import in Hyprland service 2026-03-13 13:25:20 -04:00
bbedward ccf0b60935 core: add toolchain directive to go.mod 2026-03-13 13:05:31 -04:00
bbedward aad7011b1c ci: fix hardcoded branch in vendor workflow 2026-03-13 12:22:27 -04:00
bbedward 3bde7ef4d3 nix: update flake 2026-03-13 12:13:58 -04:00
bbedward 04555dbfa7 nix: fix go regex matching 2026-03-13 12:03:42 -04:00
bbedward 3b494aa591 nix: dynamically resolve go version in flake 2026-03-13 11:58:08 -04:00
bbedward 365387c3cd ci: reveal errors in nix vendor hash update 2026-03-13 11:53:17 -04:00
Nek bb74a0ca4d fix(wallpaper): preserve per-monitor cycling when changing interval (#1981)
(#1816)
2026-03-13 11:46:02 -04:00
nick-linux8 9cf2ef84b7 Added Better Handling In Event Dispatcher Function (#1980) 2026-03-13 11:43:24 -04:00
bbedward 46aaf5ff77 fix(udev): avoid event loop termination
core: bump go to 1.26
2026-03-13 11:42:46 -04:00
Nek c544bda5df fix(matugen): detect Zed Linux binary aliases (#1982) 2026-03-13 11:29:51 -04:00
purian23 e86227f05f fix(greeter): add wireplumber state directory & update U2F env variables 2026-03-12 22:35:26 -04:00
bbedward 53da60e4ca settings: allow custom json to render all theme options 2026-03-12 17:58:42 -04:00
purian23 727d9c6c22 greeter(auth): Enhance fingerprint/U2F auth support w/Quickshell PAM
- Split auth capability state by lock screen and greeter
- Share detection between settings UI and lock runtime
- Broaden greeter PAM include detection across supported distros
2026-03-12 15:06:07 -04:00
purian23 908e1f600e dankinstall(distros): Enhance DMS minimal install logic
-Updated for Debian, Ubuntu, Fedora, and OpenSUSE
- New shared minimal installation logic to streamline package handling across distros
2026-03-12 14:55:02 -04:00
purian23 270d800df2 greeter(distros): Move comps to Suggests on Debian/OpenSUSE 2026-03-12 14:42:44 -04:00
bbedward d445d182ea fix(settings): fix animation speed binding in notifications tab
fixes #1974
2026-03-12 11:43:33 -04:00
Adarsh219 476256c9e7 fix(matugen): use single quotes for zed template paths (#1972) 2026-03-12 08:54:18 -04:00
Triệu Kha 06ea7373f7 parity(danktoggle): follow m3 disabled state color specs (#1973) 2026-03-12 08:54:00 -04:00
bbedward e78ba77def fix(idle): ensure timeouts can never be 0 2026-03-11 18:55:13 -04:00
purian23 7113afe9e2 fix(settings): Improve error handling for plugin settings loading 2026-03-11 18:03:31 -04:00
purian23 1a2b6524e6 (processes): Add environment flag checks for fprintd and U2F availability 2026-03-11 17:57:30 -04:00
purian23 95c4aa9e4c fix(greeter): Dup crash handlers 2026-03-11 17:13:46 -04:00
purian23 9f2518c9e1 (settings): Enhance authentication checks in Greeter & LockScreen tabs 2026-03-11 16:58:15 -04:00
purian23 76c50a654a fix(qmllint): Update distro detection logic for qmllint 2026-03-11 16:22:37 -04:00
purian23 ded2c38551 fix(greeter): Allow empty password submits to reach PAM 2026-03-11 16:13:26 -04:00
Triệu Kha 772094eacd feat(dropdown): have selected item at dropdown beginning on launch (#1968)
* fix(appdrawer): launcher launched via appdrawer doesnt respect size
setting

* feat(dropdown): have selected item at dropdown beginning on launch
2026-03-11 13:46:44 -04:00
Evgeny Zemtsov bddc2f6295 display: support generic wlr-output-management-unstable-v1 (#1840)
The display config UI only applied changes for compositors with a
config-file backend (niri, hyprland, dwl).  For any other compositor
that supports wlr-output-management-unstable-v1 the "Apply Changes"
button was silently a no-op.

Add WlrOutputService.applyOutputsConfig() as a high-level apply that
mirrors the generateOutputsConfig() pattern of the existing services
but applies directly via the protocol instead of writing a config file.
Route the default case in backendWriteOutputsConfig() to it.

This enables using dms-shell as a wayland compositor for emacs wayland
manager (ewm).
2026-03-11 13:28:14 -04:00
bbedward 25dce2961b fix(launcher): select first file search result by default
fixes #1967
2026-03-11 12:47:05 -04:00
nick-linux8 653cfbe6e0 Issue:(Settings)Switched Neovim Mutagen Theme To Default False (#1964)
* Issue:(Settings)Switched Neovim Mutagen Theme To Default False

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

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-03-11 12:43:56 -04:00
bbedward c539311083 settings: update search index 2026-03-11 12:07:56 -04:00
bbedward 60118c5d5b fix: dsearch references 2026-03-11 12:07:44 -04:00
Vladimir c6b9b36566 feat: highlight active workspace app tiles (#1957)
* feat: highlight active workspace app tiles

* feat: add workspace active-app highlight toggle
2026-03-11 10:57:05 -04:00
Triệu Kha fd5b1b7c00 fix(appdrawer): launcher launched via appdrawer doesnt respect size (#1960)
setting
2026-03-11 10:55:54 -04:00
bbedward ebc77b62c8 plugins: fix list delegates 2026-03-11 09:57:54 -04:00
purian23 2ce888581f obs: Reduce retry timing 2026-03-10 23:00:01 -04:00
purian23 0e901b6404 distros: Update working copy 2026-03-10 22:12:07 -04:00
purian23 688b9076e7 workflows: Update node versioning 2026-03-10 21:30:19 -04:00
purian23 c6ec7579b6 distro: Update OBS workflows 2026-03-10 21:15:43 -04:00
bbedward 9417edac8d fix(popout): anchor cc and notification center to top and bottom 2026-03-10 15:56:40 -04:00
bbedward 6185cc79d7 tooling: make qmllint auto-resolution smarter 2026-03-10 15:41:22 -04:00
bbedward 4ecdba94c2 fix(lint): unused imports removed 2026-03-10 15:32:22 -04:00
Vladimir a11640d840 build: run qmllint through Quickshell tooling VFS (#1958) 2026-03-10 15:32:16 -04:00
purian23 177a4c4095 (greeter): PAM auth improvements and defaults update 2026-03-10 15:02:26 -04:00
lpv 63df19ab78 dock: restore Hyprland special workspace windows on click (#1924)
* dock: restore Hyprland special workspace windows on click

* settings: add dock special workspace restore key to spec
2026-03-10 12:55:36 -04:00
Adarsh219 54e0eb5979 feat: Add Zed editor theming support (#1954)
* feat: Add Zed editor theming support

* fix formatting and switch to CONFIG_DIR
2026-03-10 12:03:01 -04:00
bbedward 185284d422 fix(lock): restore login config fallback 2026-03-10 11:33:44 -04:00
bbedward ce240405d9 system tray: fix shadow consistency
fixes #1946
2026-03-10 11:10:18 -04:00
Marcin Jahn 58b700ed0d fix(shell): cover edge cases of compact focused app widget (#1918)
Fixes two cases:

- some apps (e.g., Zen browser use the "—" character at the end of
  webpage name)
- in compact mode, when app has only appName, and not window name, we
  should display the appName to avoid empty title.
2026-03-10 10:49:28 -04:00
Vladimir d436fa4920 fix(quickshell): stabilize control center numeric widths (#1943) 2026-03-10 10:48:13 -04:00
Augusto César Dias d58486193e feature(notification): show notification only on current focused display (#1923) 2026-03-10 10:46:04 -04:00
bbedward e9404eb9b6 i18n: add russian 2026-03-10 10:43:46 -04:00
purian23 0fef4d515e dankinstall: Update Arch/Quickshell installation 2026-03-09 18:10:55 -04:00
CaptainSpof 86f9cf4376 fix(wallpaper): follow symlinks when scanning wallpaper directory (#1947) 2026-03-09 08:53:22 -04:00
purian23 acf63c57e8 fix(Greeter): Multi-distro reliability updates
- Merge duplicate niri input/output KDL nodes instead of appending. Allows more overrides
- Guard AppArmor install/uninstall behind IsAppArmorEnabled() check
2026-03-08 22:28:32 -04:00
purian23 baa956c3a1 fix(Greeter): Don't stop greeter immediately upon uninstallation 2026-03-07 22:23:21 -05:00
purian23 bb2081a936 feat(Greeter): Add install/uninstall/activate cli commands & new UI opts
- AppArmor profile management
- Introduced `dms greeter uninstall` command to remove DMS greeter configuration and restore previous display manager.
- Implemented AppArmor profile installation and uninstallation for enhanced security.
2026-03-07 20:44:19 -05:00
purian23 c984b0b9ae fix(Clipboard) remove unused copyServe logic 2026-03-07 20:42:54 -05:00
micko 754bf8fa3c update deprecated syntax (#1928) 2026-03-06 21:13:03 -06:00
purian23 7840294517 fix(Clipboard): Epic RAM Growth
- Closes #1920
2026-03-06 22:12:24 -05:00
Connor Welsh caaee88654 fix(Calendar): add missing qs.Common import (#1926)
fixes calendar events getting dropped
2026-03-06 14:19:43 -05:00
Augusto César Dias e872ddc1e7 feature(vpn): add toggle to enable/disable auto connecting (#1925)
* feature(vpn): add toggle to enable/disable auto connecting

* refresh status after updating
2026-03-06 14:19:31 -05:00
purian23 1eca9b4c2c feat: Implement immutable DMS command policy
- Added pre-run checks for greeter and setup commands to enforce policy restrictions
- Created cli-policy.default.json to define blocked commands and user messages for immutable environments.
2026-03-05 23:08:27 -05:00
purian23 fe5bd42e25 greeter: New Greeter Settings UI & Sync fixes
- Add PAM Auth via GUI
- Added new sync flags
- Refactored cache directory management & many others
- Fix for wireplumber permissions
- Fix for polkit auth w/icon
- Add pam_fprintd timeout=5 to prevent 30s auth blocks when using password
2026-03-05 23:04:59 -05:00
purian23 32d16d0673 refactor(greeter): Update auth flows and add configurable opts
- Finally fix debug info logs before dms greeter loads
- prevent greeter/lockscreen auth stalls with timeout recovery and unlock-state sync
2026-03-04 14:17:56 -05:00
Lucas 27c26d35ab flake: allow extra QT packages in dms-shell package (#1903) 2026-03-03 21:47:45 -05:00
160 changed files with 32720 additions and 8246 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
private-key: ${{ secrets.APP_PRIVATE_KEY }} private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ steps.app_token.outputs.token }} token: ${{ steps.app_token.outputs.token }}
+2 -2
View File
@@ -26,7 +26,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Install flatpak - name: Install flatpak
run: sudo apt update && sudo apt install -y flatpak run: sudo apt update && sudo apt install -y flatpak
@@ -38,7 +38,7 @@ jobs:
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version-file: ./core/go.mod go-version-file: ./core/go.mod
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
+2 -2
View File
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Install flatpak - name: Install flatpak
run: sudo apt update && sudo apt install -y flatpak run: sudo apt update && sudo apt install -y flatpak
@@ -21,7 +21,7 @@ jobs:
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version-file: core/go.mod go-version-file: core/go.mod
+8 -8
View File
@@ -32,13 +32,13 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
ref: ${{ inputs.tag }} ref: ${{ inputs.tag }}
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version-file: ./core/go.mod go-version-file: ./core/go.mod
@@ -106,7 +106,7 @@ jobs:
- name: Upload artifacts (${{ matrix.arch }}) - name: Upload artifacts (${{ matrix.arch }})
if: matrix.arch == 'arm64' if: matrix.arch == 'arm64'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: core-assets-${{ matrix.arch }} name: core-assets-${{ matrix.arch }}
path: | path: |
@@ -120,7 +120,7 @@ jobs:
- name: Upload artifacts with completions - name: Upload artifacts with completions
if: matrix.arch == 'amd64' if: matrix.arch == 'amd64'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: core-assets-${{ matrix.arch }} name: core-assets-${{ matrix.arch }}
path: | path: |
@@ -147,7 +147,7 @@ jobs:
# private-key: ${{ secrets.APP_PRIVATE_KEY }} # private-key: ${{ secrets.APP_PRIVATE_KEY }}
# - name: Checkout # - name: Checkout
# uses: actions/checkout@v4 # uses: actions/checkout@v6
# with: # with:
# token: ${{ steps.app_token.outputs.token }} # token: ${{ steps.app_token.outputs.token }}
# fetch-depth: 0 # fetch-depth: 0
@@ -181,7 +181,7 @@ jobs:
TAG: ${{ inputs.tag }} TAG: ${{ inputs.tag }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
ref: ${{ inputs.tag }} ref: ${{ inputs.tag }}
fetch-depth: 0 fetch-depth: 0
@@ -192,12 +192,12 @@ jobs:
git checkout ${TAG} git checkout ${TAG}
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version-file: ./core/go.mod go-version-file: ./core/go.mod
- name: Download core artifacts - name: Download core artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v5
with: with:
pattern: core-assets-* pattern: core-assets-*
merge-multiple: true merge-multiple: true
+2 -2
View File
@@ -46,7 +46,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: Determine version - name: Determine version
id: version id: version
@@ -134,7 +134,7 @@ jobs:
rpm -qpi "$SRPM" rpm -qpi "$SRPM"
- name: Upload SRPM artifact - name: Upload SRPM artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v5
with: with:
name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }} name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }}
path: ${{ steps.build.outputs.srpm_path }} path: ${{ steps.build.outputs.srpm_path }}
+6 -3
View File
@@ -32,7 +32,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -195,10 +195,13 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Wait before OBS upload
run: sleep 3
- name: Determine packages to update - name: Determine packages to update
id: packages id: packages
run: | run: |
@@ -344,7 +347,7 @@ jobs:
done done
- name: Install Go - name: Install Go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version-file: ./core/go.mod go-version-file: ./core/go.mod
+3 -3
View File
@@ -31,7 +31,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -157,12 +157,12 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version-file: ./core/go.mod go-version-file: ./core/go.mod
cache: false cache: false
+4 -4
View File
@@ -24,7 +24,7 @@ jobs:
private-key: ${{ secrets.APP_PRIVATE_KEY }} private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ steps.app_token.outputs.token }} token: ${{ steps.app_token.outputs.token }}
@@ -40,7 +40,7 @@ jobs:
echo "Build succeeded, no hash update needed" echo "Build succeeded, no hash update needed"
exit 0 exit 0
fi fi
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1) new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1 || true)
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; } [ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix) current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; } [ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
@@ -59,8 +59,8 @@ jobs:
git config user.email "dms-ci[bot]@users.noreply.github.com" git config user.email "dms-ci[bot]@users.noreply.github.com"
git add flake.nix git add flake.nix
git commit -m "nix: update vendorHash for go.mod changes" || exit 0 git commit -m "nix: update vendorHash for go.mod changes" || exit 0
git pull --rebase origin master git pull --rebase origin ${{ github.ref_name }}
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:${{ github.ref_name }}
else else
echo "No changes to flake.nix" echo "No changes to flake.nix"
fi fi
+3 -1
View File
@@ -86,7 +86,9 @@ touch .qmlls.ini
4. Restart dms to generate the `.qmlls.ini` file 4. Restart dms to generate the `.qmlls.ini` file
5. Make your changes, test, and open a pull request. 5. Run `make lint-qml` from the repo root to lint QML entrypoints (requires the `.qmlls.ini` generated above). The script needs the **Qt 6** `qmllint`; it checks `qmllint6`, Fedora's `qmllint-qt6`, `/usr/lib/qt6/bin/qmllint`, then `qmllint` in `PATH`. If your Qt 6 binary lives elsewhere, set `QMLLINT=/path/to/qmllint`.
6. Make your changes, test, and open a pull request.
### I18n/Localization ### I18n/Localization
+6 -2
View File
@@ -18,7 +18,7 @@ SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
ASSETS_DIR=assets ASSETS_DIR=assets
APPLICATIONS_DIR=$(DATA_DIR)/applications APPLICATIONS_DIR=$(DATA_DIR)/applications
.PHONY: all build clean install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help .PHONY: all build clean lint-qml install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
all: build all: build
@@ -32,6 +32,9 @@ clean:
@$(MAKE) -C $(CORE_DIR) clean @$(MAKE) -C $(CORE_DIR) clean
@echo "Clean complete" @echo "Clean complete"
lint-qml:
@./quickshell/scripts/qmllint-entrypoints.sh
# Installation targets # Installation targets
install-bin: install-bin:
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..." @echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
@@ -76,7 +79,7 @@ install-desktop:
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true @update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
@echo "Desktop entry installed" @echo "Desktop entry installed"
install: build install-bin install-shell install-completions install-systemd install-icon install-desktop install: install-bin install-shell install-completions install-systemd install-icon install-desktop
@echo "" @echo ""
@echo "Installation complete!" @echo "Installation complete!"
@echo "" @echo ""
@@ -130,6 +133,7 @@ help:
@echo " all (default) - Build the DMS binary" @echo " all (default) - Build the DMS binary"
@echo " build - Same as 'all'" @echo " build - Same as 'all'"
@echo " clean - Clean build artifacts" @echo " clean - Clean build artifacts"
@echo " lint-qml - Run qmllint on shell entrypoints using the Quickshell tooling VFS"
@echo "" @echo ""
@echo "Install:" @echo "Install:"
@echo " install - Build and install everything (requires sudo)" @echo " install - Build and install everything (requires sudo)"
+20 -7
View File
@@ -1,13 +1,26 @@
repos: repos:
- repo: https://github.com/golangci/golangci-lint
rev: v2.10.1
hooks:
- id: golangci-lint-fmt
require_serial: true
- id: golangci-lint-full
- id: golangci-lint-config-verify
- repo: local - repo: local
hooks: hooks:
- id: golangci-lint-fmt
name: golangci-lint-fmt
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 fmt
language: system
require_serial: true
types: [go]
pass_filenames: false
- id: golangci-lint-full
name: golangci-lint-full
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 run --fix
language: system
require_serial: true
types: [go]
pass_filenames: false
- id: golangci-lint-config-verify
name: golangci-lint-config-verify
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 config verify
language: system
files: \.golangci\.(?:yml|yaml|toml|json)
pass_filenames: false
- id: go-test - id: go-test
name: go test name: go test
entry: go test ./... entry: go test ./...
+3 -3
View File
@@ -63,19 +63,19 @@ endif
build-all: build dankinstall build-all: build dankinstall
install: build install:
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..." @echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME) @install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
@echo "Installation complete" @echo "Installation complete"
install-all: build-all install-all:
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..." @echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME) @install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..." @echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL) @install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
@echo "Installation complete" @echo "Installation complete"
install-dankinstall: dankinstall install-dankinstall:
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..." @echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL) @install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
@echo "Installation complete" @echo "Installation complete"
@@ -0,0 +1,10 @@
{
"policy_version": 1,
"blocked_commands": [
"greeter install",
"greeter enable",
"greeter uninstall",
"setup"
],
"message": "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes."
}
+11 -1
View File
@@ -222,16 +222,19 @@ func init() {
func runClipCopy(cmd *cobra.Command, args []string) { func runClipCopy(cmd *cobra.Command, args []string) {
var data []byte var data []byte
copyFromStdin := false
switch { switch {
case len(args) > 0: case len(args) > 0:
data = []byte(args[0]) data = []byte(args[0])
default: case clipCopyDownload || clipCopyType == "__multi__":
var err error var err error
data, err = io.ReadAll(os.Stdin) data, err = io.ReadAll(os.Stdin)
if err != nil { if err != nil {
log.Fatalf("read stdin: %v", err) log.Fatalf("read stdin: %v", err)
} }
default:
copyFromStdin = true
} }
if clipCopyDownload { if clipCopyDownload {
@@ -257,6 +260,13 @@ func runClipCopy(cmd *cobra.Command, args []string) {
return return
} }
if copyFromStdin {
if err := clipboard.CopyReader(os.Stdin, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
log.Fatalf("copy: %v", err)
}
return
}
if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil { if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
log.Fatalf("copy: %v", err) log.Fatalf("copy: %v", err)
} }
+5 -5
View File
@@ -1079,14 +1079,14 @@ func formatResultsPlain(results []checkResult) string {
if currentCategory != -1 { if currentCategory != -1 {
sb.WriteString("\n") sb.WriteString("\n")
} }
sb.WriteString(fmt.Sprintf("**%s**\n", r.category.String())) fmt.Fprintf(&sb, "**%s**\n", r.category.String())
currentCategory = r.category currentCategory = r.category
} }
sb.WriteString(fmt.Sprintf("- [%s] %s: %s\n", r.status, r.name, r.message)) fmt.Fprintf(&sb, "- [%s] %s: %s\n", r.status, r.name, r.message)
if doctorVerbose && r.details != "" { if doctorVerbose && r.details != "" {
sb.WriteString(fmt.Sprintf(" - %s\n", r.details)) fmt.Fprintf(&sb, " - %s\n", r.details)
} }
} }
@@ -1096,8 +1096,8 @@ func formatResultsPlain(results []checkResult) string {
} }
sb.WriteString("\n---\n") sb.WriteString("\n---\n")
sb.WriteString(fmt.Sprintf("**Summary:** %d error(s), %d warning(s), %d ok\n", fmt.Fprintf(&sb, "**Summary:** %d error(s), %d warning(s), %d ok\n",
ds.ErrorCount(), ds.WarningCount(), ds.OKCount())) ds.ErrorCount(), ds.WarningCount(), ds.OKCount())
return sb.String() return sb.String()
} }
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -60,7 +60,7 @@ func init() {
} }
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion") matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
matugenQueueCmd.Flags().Duration("timeout", 30*time.Second, "Timeout for waiting") matugenQueueCmd.Flags().Duration("timeout", 90*time.Second, "Timeout for waiting")
} }
func buildMatugenOptions(cmd *cobra.Command) matugen.Options { func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
+4 -3
View File
@@ -16,9 +16,10 @@ import (
) )
var setupCmd = &cobra.Command{ var setupCmd = &cobra.Command{
Use: "setup", Use: "setup",
Short: "Deploy DMS configurations", Short: "Deploy DMS configurations",
Long: "Deploy compositor and terminal configurations with interactive prompts", Long: "Deploy compositor and terminal configurations with interactive prompts",
PersistentPreRunE: requireMutableSystemCommand,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if err := runSetup(); err != nil { if err := runSetup(); err != nil {
log.Fatalf("Error during setup: %v", err) log.Fatalf("Error during setup: %v", err)
+271
View File
@@ -0,0 +1,271 @@
package main
import (
"bufio"
_ "embed"
"encoding/json"
"fmt"
"os"
"strings"
"sync"
"github.com/spf13/cobra"
)
const (
cliPolicyPackagedPath = "/usr/share/dms/cli-policy.json"
cliPolicyAdminPath = "/etc/dms/cli-policy.json"
)
var (
immutablePolicyOnce sync.Once
immutablePolicy immutableCommandPolicy
immutablePolicyErr error
)
//go:embed assets/cli-policy.default.json
var defaultCLIPolicyJSON []byte
type immutableCommandPolicy struct {
ImmutableSystem bool
ImmutableReason string
BlockedCommands []string
Message string
}
type cliPolicyFile struct {
PolicyVersion int `json:"policy_version"`
ImmutableSystem *bool `json:"immutable_system"`
BlockedCommands *[]string `json:"blocked_commands"`
Message *string `json:"message"`
}
func normalizeCommandSpec(raw string) string {
normalized := strings.ToLower(strings.TrimSpace(raw))
normalized = strings.TrimPrefix(normalized, "dms ")
return strings.Join(strings.Fields(normalized), " ")
}
func normalizeBlockedCommands(raw []string) []string {
normalized := make([]string, 0, len(raw))
seen := make(map[string]bool)
for _, cmd := range raw {
spec := normalizeCommandSpec(cmd)
if spec == "" || seen[spec] {
continue
}
seen[spec] = true
normalized = append(normalized, spec)
}
return normalized
}
func commandBlockedByPolicy(commandPath string, blocked []string) bool {
normalizedPath := normalizeCommandSpec(commandPath)
if normalizedPath == "" {
return false
}
for _, entry := range blocked {
spec := normalizeCommandSpec(entry)
if spec == "" {
continue
}
if normalizedPath == spec || strings.HasPrefix(normalizedPath, spec+" ") {
return true
}
}
return false
}
func loadPolicyFile(path string) (*cliPolicyFile, error) {
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("failed to read %s: %w", path, err)
}
var policy cliPolicyFile
if err := json.Unmarshal(data, &policy); err != nil {
return nil, fmt.Errorf("failed to parse %s: %w", path, err)
}
return &policy, nil
}
func mergePolicyFile(base *immutableCommandPolicy, path string) error {
policyFile, err := loadPolicyFile(path)
if err != nil {
return err
}
if policyFile == nil {
return nil
}
if policyFile.ImmutableSystem != nil {
base.ImmutableSystem = *policyFile.ImmutableSystem
}
if policyFile.BlockedCommands != nil {
base.BlockedCommands = normalizeBlockedCommands(*policyFile.BlockedCommands)
}
if policyFile.Message != nil {
msg := strings.TrimSpace(*policyFile.Message)
if msg != "" {
base.Message = msg
}
}
return nil
}
func readOSReleaseMap(path string) map[string]string {
values := make(map[string]string)
file, err := os.Open(path)
if err != nil {
return values
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.ToUpper(strings.TrimSpace(parts[0]))
value := strings.Trim(strings.TrimSpace(parts[1]), "\"")
values[key] = strings.ToLower(value)
}
return values
}
func hasAnyToken(text string, tokens ...string) bool {
if text == "" {
return false
}
for _, token := range tokens {
if strings.Contains(text, token) {
return true
}
}
return false
}
func detectImmutableSystem() (bool, string) {
if _, err := os.Stat("/run/ostree-booted"); err == nil {
return true, "/run/ostree-booted is present"
}
osRelease := readOSReleaseMap("/etc/os-release")
if len(osRelease) == 0 {
return false, ""
}
id := osRelease["ID"]
idLike := osRelease["ID_LIKE"]
variantID := osRelease["VARIANT_ID"]
name := osRelease["NAME"]
prettyName := osRelease["PRETTY_NAME"]
immutableIDs := map[string]bool{
"bluefin": true,
"bazzite": true,
"silverblue": true,
"kinoite": true,
"sericea": true,
"onyx": true,
"aurora": true,
"fedora-iot": true,
"fedora-coreos": true,
}
if immutableIDs[id] {
return true, "os-release ID=" + id
}
markers := []string{"silverblue", "kinoite", "sericea", "onyx", "bazzite", "bluefin", "aurora", "ostree", "atomic"}
if hasAnyToken(variantID, markers...) {
return true, "os-release VARIANT_ID=" + variantID
}
if hasAnyToken(idLike, "ostree", "rpm-ostree") {
return true, "os-release ID_LIKE=" + idLike
}
if hasAnyToken(name, markers...) || hasAnyToken(prettyName, markers...) {
return true, "os-release identifies an atomic/ostree variant"
}
return false, ""
}
func getImmutablePolicy() (*immutableCommandPolicy, error) {
immutablePolicyOnce.Do(func() {
detectedImmutable, reason := detectImmutableSystem()
immutablePolicy = immutableCommandPolicy{
ImmutableSystem: detectedImmutable,
ImmutableReason: reason,
BlockedCommands: []string{"greeter install", "greeter enable", "setup"},
Message: "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes.",
}
var defaultPolicy cliPolicyFile
if err := json.Unmarshal(defaultCLIPolicyJSON, &defaultPolicy); err != nil {
immutablePolicyErr = fmt.Errorf("failed to parse embedded default CLI policy: %w", err)
return
}
if defaultPolicy.BlockedCommands != nil {
immutablePolicy.BlockedCommands = normalizeBlockedCommands(*defaultPolicy.BlockedCommands)
}
if defaultPolicy.Message != nil {
msg := strings.TrimSpace(*defaultPolicy.Message)
if msg != "" {
immutablePolicy.Message = msg
}
}
if err := mergePolicyFile(&immutablePolicy, cliPolicyPackagedPath); err != nil {
immutablePolicyErr = err
return
}
if err := mergePolicyFile(&immutablePolicy, cliPolicyAdminPath); err != nil {
immutablePolicyErr = err
return
}
})
if immutablePolicyErr != nil {
return nil, immutablePolicyErr
}
return &immutablePolicy, nil
}
func requireMutableSystemCommand(cmd *cobra.Command, _ []string) error {
policy, err := getImmutablePolicy()
if err != nil {
return err
}
if !policy.ImmutableSystem {
return nil
}
commandPath := normalizeCommandSpec(cmd.CommandPath())
if !commandBlockedByPolicy(commandPath, policy.BlockedCommands) {
return nil
}
reason := ""
if policy.ImmutableReason != "" {
reason = "Detected immutable system: " + policy.ImmutableReason + "\n"
}
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath)
}
+1 -10
View File
@@ -16,19 +16,10 @@ func init() {
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)") runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().MarkHidden("daemon-child") runCmd.Flags().MarkHidden("daemon-child")
// Add subcommands to greeter greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
// Add subcommands to setup
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
// Add subcommands to update
updateCmd.AddCommand(updateCheckCmd) updateCmd.AddCommand(updateCheckCmd)
// Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
// Add common commands to root
rootCmd.AddCommand(getCommonCommands()...) rootCmd.AddCommand(getCommonCommands()...)
rootCmd.AddCommand(updateCmd) rootCmd.AddCommand(updateCmd)
+1 -10
View File
@@ -11,29 +11,20 @@ import (
var Version = "dev" var Version = "dev"
func init() { func init() {
// Add flags
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode") runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process") runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)") runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().MarkHidden("daemon-child") runCmd.Flags().MarkHidden("daemon-child")
// Add subcommands to greeter greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
// Add subcommands to setup
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
// Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
// Add common commands to root
rootCmd.AddCommand(getCommonCommands()...) rootCmd.AddCommand(getCommonCommands()...)
rootCmd.SetHelpTemplate(getHelpTemplate()) rootCmd.SetHelpTemplate(getHelpTemplate())
} }
func main() { func main() {
// Block root
if os.Geteuid() == 0 { if os.Geteuid() == 0 {
log.Fatal("This program should not be run as root. Exiting.") log.Fatal("This program should not be run as root. Exiting.")
} }
-8
View File
@@ -7,14 +7,6 @@ import (
"strings" "strings"
) )
func findCommandPath(cmd string) (string, error) {
path, err := exec.LookPath(cmd)
if err != nil {
return "", fmt.Errorf("command '%s' not found in PATH", cmd)
}
return path, nil
}
func isArchPackageInstalled(packageName string) bool { func isArchPackageInstalled(packageName string) bool {
cmd := exec.Command("pacman", "-Q", packageName) cmd := exec.Command("pacman", "-Q", packageName)
err := cmd.Run() err := cmd.Run()
+3 -1
View File
@@ -1,6 +1,8 @@
module github.com/AvengeMedia/DankMaterialShell/core module github.com/AvengeMedia/DankMaterialShell/core
go 1.25.0 go 1.26.0
toolchain go1.26.1
require ( require (
github.com/Wifx/gonetworkmanager/v2 v2.2.0 github.com/Wifx/gonetworkmanager/v2 v2.2.0
+92 -8
View File
@@ -1,10 +1,12 @@
package clipboard package clipboard
import ( import (
"bytes"
"fmt" "fmt"
"io" "io"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"syscall" "syscall"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
@@ -12,17 +14,37 @@ import (
) )
func Copy(data []byte, mimeType string) error { func Copy(data []byte, mimeType string) error {
return CopyOpts(data, mimeType, false, false) return CopyReader(bytes.NewReader(data), mimeType, false, false)
} }
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error { func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
if foreground {
return copyServeWithWriter(func(writer io.Writer) error {
total := 0
for total < len(data) {
n, err := writer.Write(data[total:])
total += n
if err != nil {
return err
}
}
if total != len(data) {
return io.ErrShortWrite
}
return nil
}, mimeType, pasteOnce)
}
return CopyReader(bytes.NewReader(data), mimeType, foreground, pasteOnce)
}
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
if !foreground { if !foreground {
return copyFork(data, mimeType, pasteOnce) return copyFork(data, mimeType, pasteOnce)
} }
return copyServe(data, mimeType, pasteOnce) return copyServeReader(data, mimeType, pasteOnce)
} }
func copyFork(data []byte, mimeType string, pasteOnce bool) error { func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
args := []string{os.Args[0], "cl", "copy", "--foreground"} args := []string{os.Args[0], "cl", "copy", "--foreground"}
if pasteOnce { if pasteOnce {
args = append(args, "--paste-once") args = append(args, "--paste-once")
@@ -30,11 +52,15 @@ func copyFork(data []byte, mimeType string, pasteOnce bool) error {
args = append(args, "--type", mimeType) args = append(args, "--type", mimeType)
cmd := exec.Command(args[0], args[1:]...) cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = nil
cmd.Stdout = nil cmd.Stdout = nil
cmd.Stderr = nil cmd.Stderr = nil
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
if stdinSource, ok := data.(*os.File); ok {
cmd.Stdin = stdinSource
return cmd.Start()
}
stdin, err := cmd.StdinPipe() stdin, err := cmd.StdinPipe()
if err != nil { if err != nil {
return fmt.Errorf("stdin pipe: %w", err) return fmt.Errorf("stdin pipe: %w", err)
@@ -44,16 +70,66 @@ func copyFork(data []byte, mimeType string, pasteOnce bool) error {
return fmt.Errorf("start: %w", err) return fmt.Errorf("start: %w", err)
} }
if _, err := stdin.Write(data); err != nil { if _, err := io.Copy(stdin, data); err != nil {
stdin.Close() stdin.Close()
return fmt.Errorf("write stdin: %w", err) return fmt.Errorf("write stdin: %w", err)
} }
stdin.Close() if err := stdin.Close(); err != nil {
return fmt.Errorf("close stdin: %w", err)
}
return nil return nil
} }
func copyServe(data []byte, mimeType string, pasteOnce bool) error { func copyServeReader(data io.Reader, mimeType string, pasteOnce bool) error {
cachedData, err := createClipboardCacheFile()
if err != nil {
return fmt.Errorf("create clipboard cache file: %w", err)
}
defer os.Remove(cachedData.Name())
if _, err := io.Copy(cachedData, data); err != nil {
return fmt.Errorf("cache clipboard data: %w", err)
}
if err := cachedData.Close(); err != nil {
return fmt.Errorf("close temp cache file: %w", err)
}
return copyServeWithWriter(func(writer io.Writer) error {
cachedFile, err := os.Open(cachedData.Name())
if err != nil {
return fmt.Errorf("open temp cache file: %w", err)
}
defer cachedFile.Close()
if _, err := io.Copy(writer, cachedFile); err != nil {
return fmt.Errorf("write clipboard data: %w", err)
}
return nil
}, mimeType, pasteOnce)
}
func createClipboardCacheFile() (*os.File, error) {
preferredDirs := []string{}
if cacheDir, err := os.UserCacheDir(); err == nil {
preferredDirs = append(preferredDirs, filepath.Join(cacheDir, "dms", "clipboard"))
}
preferredDirs = append(preferredDirs, "/var/tmp/dms/clipboard")
for _, dir := range preferredDirs {
if err := os.MkdirAll(dir, 0o700); err != nil {
continue
}
cachedData, err := os.CreateTemp(dir, "dms-clipboard-*")
if err == nil {
return cachedData, nil
}
}
return os.CreateTemp("", "dms-clipboard-*")
}
func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOnce bool) error {
display, err := wlclient.Connect("") display, err := wlclient.Connect("")
if err != nil { if err != nil {
return fmt.Errorf("wayland connect: %w", err) return fmt.Errorf("wayland connect: %w", err)
@@ -139,12 +215,18 @@ func copyServe(data []byte, mimeType string, pasteOnce bool) error {
cancelled := make(chan struct{}) cancelled := make(chan struct{})
pasted := make(chan struct{}, 1) pasted := make(chan struct{}, 1)
sendErr := make(chan error, 1)
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) { source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
defer syscall.Close(e.Fd) defer syscall.Close(e.Fd)
file := os.NewFile(uintptr(e.Fd), "pipe") file := os.NewFile(uintptr(e.Fd), "pipe")
defer file.Close() defer file.Close()
file.Write(data) if err := writeTo(file); err != nil {
select {
case sendErr <- err:
default:
}
}
select { select {
case pasted <- struct{}{}: case pasted <- struct{}{}:
default: default:
@@ -165,6 +247,8 @@ func copyServe(data []byte, mimeType string, pasteOnce bool) error {
select { select {
case <-cancelled: case <-cancelled:
return nil return nil
case err := <-sendErr:
return err
case <-pasted: case <-pasted:
if pasteOnce { if pasteOnce {
return nil return nil
+18 -39
View File
@@ -440,29 +440,10 @@ func (a *ArchDistribution) installAURPackages(ctx context.Context, packages []st
a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", "))) a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", ")))
hasNiri := false hasNiri := false
hasQuickshell := false
for _, pkg := range packages { for _, pkg := range packages {
if pkg == "niri-git" { if pkg == "niri-git" {
hasNiri = true hasNiri = true
} }
if pkg == "quickshell" || pkg == "quickshell-git" {
hasQuickshell = true
}
}
// If quickshell is in the list, always reinstall google-breakpad first
if hasQuickshell {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.63,
Step: "Reinstalling google-breakpad for quickshell...",
IsComplete: false,
CommandInfo: "Reinstalling prerequisite AUR package for quickshell",
}
if err := a.installSingleAURPackage(ctx, "google-breakpad", sudoPassword, progressChan, 0.63, 0.65); err != nil {
return fmt.Errorf("failed to reinstall google-breakpad prerequisite for quickshell: %w", err)
}
} }
// If niri is in the list, install makepkg-git-lfs-proto first if not already installed // If niri is in the list, install makepkg-git-lfs-proto first if not already installed
@@ -616,10 +597,16 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err) return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err)
} }
// Skip dependency installation for dms-shell-git and dms-shell-bin srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
// since we manually manage those dependencies if pkg == "dms-shell-bin" {
if pkg != "dms-shell-git" && pkg != "dms-shell-bin" { progressChan <- InstallProgressMsg{
// Pre-install dependencies from .SRCINFO Phase: PhaseAURPackages,
Progress: startProgress + 0.35*(endProgress-startProgress),
Step: fmt.Sprintf("Skipping dependency installation for %s (manually managed)...", pkg),
IsComplete: false,
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
}
} else {
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages, Phase: PhaseAURPackages,
Progress: startProgress + 0.3*(endProgress-startProgress), Progress: startProgress + 0.3*(endProgress-startProgress),
@@ -628,19 +615,19 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
CommandInfo: "Installing package dependencies and makedepends", CommandInfo: "Installing package dependencies and makedepends",
} }
// Install dependencies and makedepends explicitly // Install dependencies from .SRCINFO
srcinfoPath = filepath.Join(packageDir, ".SRCINFO") depFilter := ""
if pkg == "dms-shell-git" {
depFilter = ` | sed -E 's/[[:space:]]*(quickshell|dgop)[[:space:]]*/ /g' | tr -s ' '`
}
depsCmd := exec.CommandContext(ctx, "bash", "-c", depsCmd := exec.CommandContext(ctx, "bash", "-c",
fmt.Sprintf(` fmt.Sprintf(`
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' | sed 's/[[:space:]]*$//') deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' %s | sed 's/[[:space:]]*$//')
if [[ "%s" == *"quickshell"* ]]; then
deps=$(echo "$deps" | sed 's/google-breakpad//g' | sed 's/ / /g' | sed 's/^ *//g' | sed 's/ *$//g')
fi
if [ ! -z "$deps" ] && [ "$deps" != " " ]; then if [ ! -z "$deps" ] && [ "$deps" != " " ]; then
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
fi fi
`, srcinfoPath, pkg, sudoPassword)) `, srcinfoPath, depFilter, sudoPassword))
if err := a.runWithProgress(depsCmd, progressChan, PhaseAURPackages, startProgress+0.3*(endProgress-startProgress), startProgress+0.35*(endProgress-startProgress)); err != nil { if err := a.runWithProgress(depsCmd, progressChan, PhaseAURPackages, startProgress+0.3*(endProgress-startProgress), startProgress+0.35*(endProgress-startProgress)); err != nil {
return fmt.Errorf("FAILED to install runtime dependencies for %s: %w", pkg, err) return fmt.Errorf("FAILED to install runtime dependencies for %s: %w", pkg, err)
@@ -657,14 +644,6 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
if err := a.runWithProgress(makedepsCmd, progressChan, PhaseAURPackages, startProgress+0.35*(endProgress-startProgress), startProgress+0.4*(endProgress-startProgress)); err != nil { if err := a.runWithProgress(makedepsCmd, progressChan, PhaseAURPackages, startProgress+0.35*(endProgress-startProgress), startProgress+0.4*(endProgress-startProgress)); err != nil {
return fmt.Errorf("FAILED to install make dependencies for %s: %w", pkg, err) return fmt.Errorf("FAILED to install make dependencies for %s: %w", pkg, err)
} }
} else {
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.35*(endProgress-startProgress),
Step: fmt.Sprintf("Skipping dependency installation for %s (manually managed)...", pkg),
IsComplete: false,
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
}
} }
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
@@ -677,7 +656,7 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm") buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm")
buildCmd.Dir = packageDir buildCmd.Dir = packageDir
buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar") // Disable compression for speed buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar")
if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil { if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil {
return fmt.Errorf("failed to build %s: %w", pkg, err) return fmt.Errorf("failed to build %s: %w", pkg, err)
+66 -16
View File
@@ -92,9 +92,25 @@ func (d *DebianDistribution) detectDMSGreeter() deps.Dependency {
} }
func (d *DebianDistribution) packageInstalled(pkg string) bool { func (d *DebianDistribution) packageInstalled(pkg string) bool {
cmd := exec.Command("dpkg", "-l", pkg) return debianPackageInstalledPrecisely(pkg)
err := cmd.Run() }
return err == nil
func debianPackageInstalledPrecisely(pkg string) bool {
cmd := exec.Command("dpkg-query", "-W", "-f=${db:Status-Status}", pkg)
output, err := cmd.Output()
if err != nil {
return false
}
return strings.TrimSpace(string(output)) == "installed"
}
func containsString(values []string, target string) bool {
for _, value := range values {
if value == target {
return true
}
}
return false
} }
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
@@ -194,12 +210,12 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
Step: "Installing development dependencies...", Step: "Installing development dependencies...",
IsComplete: false, IsComplete: false,
NeedsSudo: true, NeedsSudo: true,
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev", CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
LogOutput: "Installing additional development tools", LogOutput: "Installing additional development tools",
} }
devToolsCmd := ExecSudoCommand(ctx, sudoPassword, devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev") "DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil { if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
return fmt.Errorf("failed to install development tools: %w", err) return fmt.Errorf("failed to install development tools: %w", err)
} }
@@ -379,6 +395,14 @@ func (d *DebianDistribution) extractPackageNames(packages []PackageMapping) []st
return names return names
} }
func (d *DebianDistribution) aptInstallArgs(packages []string, minimal bool) []string {
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
if minimal {
args = append(args, "--no-install-recommends")
}
return append(args, packages...)
}
func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
enabledRepos := make(map[string]bool) enabledRepos := make(map[string]bool)
@@ -482,20 +506,46 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", "))) d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"} groups := orderedMinimalInstallGroups(packages)
args = append(args, packages...) totalGroups := len(groups)
progressChan <- InstallProgressMsg{ groupIndex := 0
Phase: PhaseSystemPackages, installGroup := func(groupPackages []string, minimal bool) error {
Progress: 0.40, if len(groupPackages) == 0 {
Step: "Installing system packages...", return nil
IsComplete: false, }
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), groupIndex++
startProgress := 0.40
endProgress := 0.60
if totalGroups > 1 {
if groupIndex == 1 {
endProgress = 0.50
} else {
startProgress = 0.50
}
}
args := d.aptInstallArgs(groupPackages, minimal)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: startProgress,
Step: "Installing system packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, startProgress, endProgress)
} }
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) for _, group := range groups {
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60) if err := installGroup(group.packages, group.minimal); err != nil {
return err
}
}
return nil
} }
func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
+51 -41
View File
@@ -484,28 +484,7 @@ func (f *FedoraDistribution) installDNFPackages(ctx context.Context, packages []
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", "))) f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
args := []string{"dnf", "install", "-y"} return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60)
for _, pkg := range packages {
if pkg == "niri" || pkg == "niri-git" {
args = append(args, "--setopt=install_weak_deps=False")
break
}
}
args = append(args, packages...)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.40,
Step: "Installing system packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return f.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
} }
func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
@@ -515,26 +494,57 @@ func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages [
f.log(fmt.Sprintf("Installing COPR packages: %s", strings.Join(packages, ", "))) f.log(fmt.Sprintf("Installing COPR packages: %s", strings.Join(packages, ", ")))
args := []string{"dnf", "install", "-y"} return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseAURPackages, "Installing COPR packages...", 0.70, 0.85)
}
for _, pkg := range packages { func (f *FedoraDistribution) dnfInstallArgs(packages []string, minimal bool) []string {
if pkg == "niri" || pkg == "niri-git" { args := []string{"dnf", "install", "-y"}
args = append(args, "--setopt=install_weak_deps=False") if minimal {
break args = append(args, "--setopt=install_weak_deps=False")
}
return append(args, packages...)
}
func (f *FedoraDistribution) installDNFGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
groups := orderedMinimalInstallGroups(packages)
totalGroups := len(groups)
groupIndex := 0
installGroup := func(groupPackages []string, minimal bool) error {
if len(groupPackages) == 0 {
return nil
}
groupIndex++
groupStart := startProgress
groupEnd := endProgress
if totalGroups > 1 {
midpoint := startProgress + ((endProgress - startProgress) / 2)
if groupIndex == 1 {
groupEnd = midpoint
} else {
groupStart = midpoint
}
}
args := f.dnfInstallArgs(groupPackages, minimal)
progressChan <- InstallProgressMsg{
Phase: phase,
Progress: groupStart,
Step: step,
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return f.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
}
for _, group := range groups {
if err := installGroup(group.packages, group.minimal); err != nil {
return err
} }
} }
return nil
args = append(args, packages...)
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: 0.70,
Step: "Installing COPR packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return f.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
} }
+44
View File
@@ -0,0 +1,44 @@
package distros
type minimalInstallGroup struct {
packages []string
minimal bool
}
func shouldPreferMinimalInstall(pkg string) bool {
switch pkg {
case "niri", "niri-git":
return true
default:
return false
}
}
func splitMinimalInstallPackages(packages []string) (normal []string, minimal []string) {
for _, pkg := range packages {
if shouldPreferMinimalInstall(pkg) {
minimal = append(minimal, pkg)
continue
}
normal = append(normal, pkg)
}
return normal, minimal
}
func orderedMinimalInstallGroups(packages []string) []minimalInstallGroup {
normal, minimal := splitMinimalInstallPackages(packages)
groups := make([]minimalInstallGroup, 0, 2)
if len(minimal) > 0 {
groups = append(groups, minimalInstallGroup{
packages: minimal,
minimal: true,
})
}
if len(normal) > 0 {
groups = append(groups, minimalInstallGroup{
packages: normal,
minimal: false,
})
}
return groups
}
+163 -43
View File
@@ -29,6 +29,8 @@ type OpenSUSEDistribution struct {
config DistroConfig config DistroConfig
} }
const openSUSENiriWaylandServerPackage = "libwayland-server0"
func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution { func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution {
base := NewBaseDistribution(logChan) base := NewBaseDistribution(logChan)
return &OpenSUSEDistribution{ return &OpenSUSEDistribution{
@@ -199,35 +201,7 @@ func (o *OpenSUSEDistribution) detectAccountsService() deps.Dependency {
} }
func (o *OpenSUSEDistribution) getPrerequisites() []string { func (o *OpenSUSEDistribution) getPrerequisites() []string {
return []string{ return []string{}
"make",
"unzip",
"gcc",
"gcc-c++",
"cmake",
"ninja",
"pkgconf-pkg-config",
"git",
"qt6-base-devel",
"qt6-declarative-devel",
"qt6-declarative-private-devel",
"qt6-shadertools",
"qt6-shadertools-devel",
"qt6-wayland-devel",
"qt6-waylandclient-private-devel",
"spirv-tools-devel",
"cli11-devel",
"wayland-protocols-devel",
"libgbm-devel",
"libdrm-devel",
"pipewire-devel",
"jemalloc-devel",
"wayland-utils",
"Mesa-libGLESv3-devel",
"pam-devel",
"glib2-devel",
"polkit-devel",
}
} }
func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
@@ -297,6 +271,10 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
LogOutput: "Starting prerequisite check...", LogOutput: "Starting prerequisite check...",
} }
if err := o.disableInstallMediaRepos(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to disable install media repositories: %w", err)
}
if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil { if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install prerequisites: %w", err) return fmt.Errorf("failed to install prerequisites: %w", err)
} }
@@ -327,7 +305,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
NeedsSudo: true, NeedsSudo: true,
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")), LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
} }
if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil { if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60); err != nil {
return fmt.Errorf("failed to install zypper packages: %w", err) return fmt.Errorf("failed to install zypper packages: %w", err)
} }
} }
@@ -342,7 +320,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
IsComplete: false, IsComplete: false,
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")), LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
} }
if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan); err != nil { if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan, PhaseAURPackages, "Installing OBS packages...", 0.70, 0.85); err != nil {
return fmt.Errorf("failed to install OBS packages: %w", err) return fmt.Errorf("failed to install OBS packages: %w", err)
} }
} }
@@ -432,9 +410,32 @@ func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency
} }
} }
systemPkgs = o.appendMissingSystemPackages(systemPkgs, openSUSENiriRuntimePackages(wm, disabledFlags))
return systemPkgs, obsPkgs, manualPkgs, variantMap return systemPkgs, obsPkgs, manualPkgs, variantMap
} }
func openSUSENiriRuntimePackages(wm deps.WindowManager, disabledFlags map[string]bool) []string {
if wm != deps.WindowManagerNiri || disabledFlags["niri"] {
return nil
}
return []string{openSUSENiriWaylandServerPackage}
}
func (o *OpenSUSEDistribution) appendMissingSystemPackages(systemPkgs []string, extraPkgs []string) []string {
for _, pkg := range extraPkgs {
if containsString(systemPkgs, pkg) || o.packageInstalled(pkg) {
continue
}
o.log(fmt.Sprintf("Adding openSUSE runtime package: %s", pkg))
systemPkgs = append(systemPkgs, pkg)
}
return systemPkgs
}
func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string { func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string {
names := make([]string, len(packages)) names := make([]string, len(packages))
for i, pkg := range packages { for i, pkg := range packages {
@@ -514,27 +515,146 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
return nil return nil
} }
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func isOpenSUSEInstallMediaURI(uri string) bool {
normalizedURI := strings.ToLower(strings.TrimSpace(uri))
return strings.HasPrefix(normalizedURI, "cd:/") ||
strings.HasPrefix(normalizedURI, "dvd:/") ||
strings.HasPrefix(normalizedURI, "hd:/") ||
strings.HasPrefix(normalizedURI, "iso:/")
}
func parseZypperInstallMediaAliases(output string) []string {
var aliases []string
for _, line := range strings.Split(output, "\n") {
line = strings.TrimSpace(line)
if line == "" || !strings.Contains(line, "|") {
continue
}
parts := strings.Split(line, "|")
if len(parts) < 7 {
continue
}
for i := range parts {
parts[i] = strings.TrimSpace(parts[i])
}
alias := parts[1]
enabled := strings.ToLower(parts[3])
uri := parts[len(parts)-1]
if alias == "" || strings.EqualFold(alias, "alias") {
continue
}
if enabled != "" && enabled != "yes" {
continue
}
if !isOpenSUSEInstallMediaURI(uri) {
continue
}
aliases = append(aliases, alias)
}
return aliases
}
func (o *OpenSUSEDistribution) disableInstallMediaRepos(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
listCmd := exec.CommandContext(ctx, "zypper", "repos", "-u")
output, err := listCmd.CombinedOutput()
if err != nil {
o.log(fmt.Sprintf("Warning: failed to list zypper repositories: %s", strings.TrimSpace(string(output))))
return fmt.Errorf("failed to list zypper repositories: %w", err)
}
aliases := parseZypperInstallMediaAliases(string(output))
if len(aliases) == 0 {
return nil
}
o.log(fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")))
progressChan <- InstallProgressMsg{
Phase: PhasePrerequisites,
Progress: 0.055,
Step: "Disabling install media repositories...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo zypper modifyrepo -d %s", strings.Join(aliases, " ")),
LogOutput: fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")),
}
for _, alias := range aliases {
cmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("zypper modifyrepo -d '%s'", escapeSingleQuotes(alias)))
repoOutput, err := cmd.CombinedOutput()
if err != nil {
o.log(fmt.Sprintf("Failed to disable install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
return fmt.Errorf("failed to disable install media repo %s: %w", alias, err)
}
o.log(fmt.Sprintf("Disabled install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
}
return nil
}
func (o *OpenSUSEDistribution) zypperInstallArgs(packages []string, minimal bool) []string {
args := []string{"zypper", "install", "-y"}
if minimal {
args = append(args, "--no-recommends")
}
return append(args, packages...)
}
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
if len(packages) == 0 { if len(packages) == 0 {
return nil return nil
} }
o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", "))) o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", ")))
args := []string{"zypper", "install", "-y"} groups := orderedMinimalInstallGroups(packages)
args = append(args, packages...) totalGroups := len(groups)
progressChan <- InstallProgressMsg{ groupIndex := 0
Phase: PhaseSystemPackages, installGroup := func(groupPackages []string, minimal bool) error {
Progress: 0.40, if len(groupPackages) == 0 {
Step: "Installing system packages...", return nil
IsComplete: false, }
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), groupIndex++
groupStart := startProgress
groupEnd := endProgress
if totalGroups > 1 {
midpoint := startProgress + ((endProgress - startProgress) / 2)
if groupIndex == 1 {
groupEnd = midpoint
} else {
groupStart = midpoint
}
}
args := o.zypperInstallArgs(groupPackages, minimal)
progressChan <- InstallProgressMsg{
Phase: phase,
Progress: groupStart,
Step: step,
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return o.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
} }
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) for _, group := range groups {
return o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60) if err := installGroup(group.packages, group.minimal); err != nil {
return err
}
}
return nil
} }
func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
+51 -29
View File
@@ -100,9 +100,7 @@ func (u *UbuntuDistribution) detectDMSGreeter() deps.Dependency {
} }
func (u *UbuntuDistribution) packageInstalled(pkg string) bool { func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
cmd := exec.Command("dpkg", "-l", pkg) return debianPackageInstalledPrecisely(pkg)
err := cmd.Run()
return err == nil
} }
func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
@@ -454,21 +452,7 @@ func (u *UbuntuDistribution) installAPTPackages(ctx context.Context, packages []
} }
u.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", "))) u.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
return u.installAPTGroups(ctx, packages, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60)
args := []string{"apt-get", "install", "-y"}
args = append(args, packages...)
progressChan <- InstallProgressMsg{
Phase: PhaseSystemPackages,
Progress: 0.40,
Step: "Installing system packages...",
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
} }
func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
@@ -477,21 +461,59 @@ func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []
} }
u.log(fmt.Sprintf("Installing PPA packages: %s", strings.Join(packages, ", "))) u.log(fmt.Sprintf("Installing PPA packages: %s", strings.Join(packages, ", ")))
return u.installAPTGroups(ctx, packages, sudoPassword, progressChan, PhaseAURPackages, "Installing PPA packages...", 0.70, 0.85)
}
args := []string{"apt-get", "install", "-y"} func (u *UbuntuDistribution) aptInstallArgs(packages []string, minimal bool) []string {
args = append(args, packages...) args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
if minimal {
args = append(args, "--no-install-recommends")
}
return append(args, packages...)
}
progressChan <- InstallProgressMsg{ func (u *UbuntuDistribution) installAPTGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
Phase: PhaseAURPackages, groups := orderedMinimalInstallGroups(packages)
Progress: 0.70, totalGroups := len(groups)
Step: "Installing PPA packages...",
IsComplete: false, groupIndex := 0
NeedsSudo: true, installGroup := func(groupPackages []string, minimal bool) error {
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")), if len(groupPackages) == 0 {
return nil
}
groupIndex++
groupStart := startProgress
groupEnd := endProgress
if totalGroups > 1 {
midpoint := startProgress + ((endProgress - startProgress) / 2)
if groupIndex == 1 {
groupEnd = midpoint
} else {
groupStart = midpoint
}
}
args := u.aptInstallArgs(groupPackages, minimal)
progressChan <- InstallProgressMsg{
Phase: phase,
Progress: groupStart,
Step: step,
IsComplete: false,
NeedsSudo: true,
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
}
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
return u.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
} }
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " ")) for _, group := range groups {
return u.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85) if err := installGroup(group.packages, group.minimal); err != nil {
return err
}
}
return nil
} }
func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error { func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
@@ -0,0 +1,91 @@
# AppArmor profile for dms-greeter
#
# Managed by DMS — regenerated on every `dms greeter install` / `dms greeter sync`.
# Manual edits will be overwritten on next sync.
#
# Mode: complain (denials are logged, nothing is blocked)
# To switch to enforce after validating with `aa-logprof`:
# sudo aa-enforce /etc/apparmor.d/usr.bin.dms-greeter
#
#include <tunables/global>
profile dms-greeter /usr/bin/dms-greeter flags=(complain) {
#include <abstractions/base>
#include <abstractions/bash>
# The launcher script itself
/usr/bin/dms-greeter r,
# Cache directory — created by dms greeter sync/enable with greeter:greeter ownership
/var/cache/dms-greeter/ rw,
/var/cache/dms-greeter/** rwlk,
# DMS config — packaged path
/usr/share/quickshell/dms-greeter/ r,
/usr/share/quickshell/dms-greeter/** r,
/usr/share/quickshell/ r,
/usr/share/quickshell/** r,
# DMS config — system and user overrides
/etc/dms/ r,
/etc/dms/** r,
/usr/share/dms/ r,
/usr/share/dms/** r,
/home/*/.config/quickshell/ r,
/home/*/.config/quickshell/** r,
/root/.config/quickshell/ r,
/root/.config/quickshell/** r,
# greetd / PAM — read-only for session setup
/etc/greetd/ r,
/etc/greetd/** r,
/etc/pam.d/ r,
/etc/pam.d/** r,
/usr/lib/pam.d/ r,
/usr/lib/pam.d/** r,
# Compositor binaries — run unconfined so each compositor uses its own profile
/usr/bin/niri Ux,
/usr/bin/hyprland Ux,
/usr/bin/Hyprland Ux,
/usr/bin/sway Ux,
/usr/bin/labwc Ux,
/usr/bin/scroll Ux,
/usr/bin/miracle-wm Ux,
/usr/bin/mango Ux,
# Quickshell — run unconfined (has its own compositor profile on some distros)
/usr/bin/qs Ux,
/usr/bin/quickshell Ux,
# Wayland / XDG runtime (pipewire, wireplumber, wayland socket)
/run/user/[0-9]*/ rw,
/run/user/[0-9]*/** rw,
# DRM / GPU devices (required for Wayland compositor startup)
/dev/dri/ r,
/dev/dri/* rw,
/dev/udmabuf rw,
# Input devices
/dev/input/ r,
/dev/input/* r,
# Systemd journal / logging
/run/systemd/journal/socket rw,
/dev/log rw,
# Shell helper binaries invoked by the launcher script
/usr/bin/env ix,
/usr/bin/mkdir ix,
/usr/bin/cat ix,
/usr/bin/grep ix,
/usr/bin/dirname ix,
/usr/bin/basename ix,
/usr/bin/command ix,
/bin/env ix,
/bin/mkdir ix,
# Signal management (compositor lifecycle)
signal (send, receive) set=("term", "int", "hup", "kill"),
}
File diff suppressed because it is too large Load Diff
+1
View File
@@ -71,6 +71,7 @@ var templateRegistry = []TemplateDef{
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true}, {ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
{ID: "vscode", Kind: TemplateKindVSCode}, {ID: "vscode", Kind: TemplateKindVSCode},
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs}, {ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs},
{ID: "zed", Commands: []string{"zed", "zeditor", "zedit"}, ConfigFile: "zed.toml"},
} }
func (c *ColorMode) GTKTheme() string { func (c *ColorMode) GTKTheme() string {
+50 -10
View File
@@ -6,12 +6,20 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"syscall"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/pilebones/go-udev/netlink" "github.com/pilebones/go-udev/netlink"
) )
const (
udevRecvBufSize = 8 * 1024 * 1024
udevMaxRetries = 5
udevBaseDelay = 2 * time.Second
udevMaxDelay = 60 * time.Second
)
type UdevMonitor struct { type UdevMonitor struct {
stop chan struct{} stop chan struct{}
rescanMutex sync.Mutex rescanMutex sync.Mutex
@@ -29,13 +37,6 @@ func NewUdevMonitor(manager *Manager) *UdevMonitor {
} }
func (m *UdevMonitor) run(manager *Manager) { func (m *UdevMonitor) run(manager *Manager) {
conn := &netlink.UEventConn{}
if err := conn.Connect(netlink.UdevEvent); err != nil {
log.Errorf("Failed to connect to udev netlink: %v", err)
return
}
defer conn.Close()
matcher := &netlink.RuleDefinitions{ matcher := &netlink.RuleDefinitions{
Rules: []netlink.RuleDefinition{ Rules: []netlink.RuleDefinition{
{Env: map[string]string{"SUBSYSTEM": "backlight"}}, {Env: map[string]string{"SUBSYSTEM": "backlight"}},
@@ -48,6 +49,46 @@ func (m *UdevMonitor) run(manager *Manager) {
return return
} }
failures := 0
for {
if err := m.monitorLoop(manager, matcher); err != nil {
log.Errorf("Udev monitor error: %v", err)
}
select {
case <-m.stop:
return
default:
}
failures++
if failures > udevMaxRetries {
log.Errorf("Udev monitor exceeded %d retries, giving up", udevMaxRetries)
return
}
delay := min(udevBaseDelay*time.Duration(1<<(failures-1)), udevMaxDelay)
log.Infof("Udev monitor reconnecting in %v (attempt %d/%d)", delay, failures, udevMaxRetries)
select {
case <-m.stop:
return
case <-time.After(delay):
}
}
}
func (m *UdevMonitor) monitorLoop(manager *Manager, matcher *netlink.RuleDefinitions) error {
conn := &netlink.UEventConn{}
if err := conn.Connect(netlink.UdevEvent); err != nil {
return err
}
defer conn.Close()
if err := syscall.SetsockoptInt(conn.Fd, syscall.SOL_SOCKET, syscall.SO_RCVBUF, udevRecvBufSize); err != nil {
log.Warnf("Failed to set udev socket receive buffer: %v", err)
}
events := make(chan netlink.UEvent) events := make(chan netlink.UEvent)
errs := make(chan error) errs := make(chan error)
conn.Monitor(events, errs, matcher) conn.Monitor(events, errs, matcher)
@@ -57,10 +98,9 @@ func (m *UdevMonitor) run(manager *Manager) {
for { for {
select { select {
case <-m.stop: case <-m.stop:
return return nil
case err := <-errs: case err := <-errs:
log.Errorf("Udev monitor error: %v", err) return err
return
case event := <-events: case event := <-events:
m.handleEvent(manager, event) m.handleEvent(manager, event)
} }
+9 -2
View File
@@ -1,6 +1,7 @@
package network package network
import ( import (
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -28,7 +29,13 @@ func TestDetectResult_HasNetworkdField(t *testing.T) {
func TestDetectNetworkStack_Integration(t *testing.T) { func TestDetectNetworkStack_Integration(t *testing.T) {
result, err := DetectNetworkStack() result, err := DetectNetworkStack()
if err != nil && strings.Contains(err.Error(), "connect system bus") {
t.Skipf("system D-Bus unavailable: %v", err)
}
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, result) if assert.NotNil(t, result) {
assert.NotEmpty(t, result.ChosenReason) assert.NotEmpty(t, result.ChosenReason)
}
} }
+40 -27
View File
@@ -73,6 +73,7 @@ var dbusManager *serverDbus.Manager
var wlContext *wlcontext.SharedContext var wlContext *wlcontext.SharedContext
var themeModeManager *thememode.Manager var themeModeManager *thememode.Manager
var locationManager *location.Manager var locationManager *location.Manager
var geoClientInstance geolocation.Client
const dbusClientID = "dms-dbus-client" const dbusClientID = "dms-dbus-client"
@@ -191,7 +192,7 @@ func InitializeFreedeskManager() error {
return nil return nil
} }
func InitializeWaylandManager(geoClient geolocation.Client) error { func InitializeWaylandManager() error {
log.Info("Attempting to initialize Wayland gamma control...") log.Info("Attempting to initialize Wayland gamma control...")
if wlContext == nil { if wlContext == nil {
@@ -204,7 +205,7 @@ func InitializeWaylandManager(geoClient geolocation.Client) error {
} }
config := wayland.DefaultConfig() config := wayland.DefaultConfig()
manager, err := wayland.NewManager(wlContext.Display(), geoClient, config) manager, err := wayland.NewManager(wlContext.Display(), config)
if err != nil { if err != nil {
log.Errorf("Failed to initialize wayland manager: %v", err) log.Errorf("Failed to initialize wayland manager: %v", err)
return err return err
@@ -385,8 +386,8 @@ func InitializeDbusManager() error {
return nil return nil
} }
func InitializeThemeModeManager(geoClient geolocation.Client) error { func InitializeThemeModeManager() error {
manager := thememode.NewManager(geoClient) manager := thememode.NewManager()
themeModeManager = manager themeModeManager = manager
log.Info("Theme mode automation manager initialized") log.Info("Theme mode automation manager initialized")
@@ -1330,6 +1331,9 @@ func cleanupManagers() {
if locationManager != nil { if locationManager != nil {
locationManager.Close() locationManager.Close()
} }
if geoClientInstance != nil {
geoClientInstance.Close()
}
} }
func Start(printDocs bool) error { func Start(printDocs bool) error {
@@ -1545,9 +1549,6 @@ func Start(printDocs bool) error {
loginctlReady := make(chan struct{}) loginctlReady := make(chan struct{})
freedesktopReady := make(chan struct{}) freedesktopReady := make(chan struct{})
geoClient := geolocation.NewClient()
defer geoClient.Close()
go func() { go func() {
defer close(loginctlReady) defer close(loginctlReady)
if err := InitializeLoginctlManager(); err != nil { if err := InitializeLoginctlManager(); err != nil {
@@ -1592,10 +1593,41 @@ func Start(printDocs bool) error {
} }
}() }()
if err := InitializeWaylandManager(geoClient); err != nil { if err := InitializeWaylandManager(); err != nil {
log.Warnf("Wayland manager unavailable: %v", err) log.Warnf("Wayland manager unavailable: %v", err)
} }
if err := InitializeThemeModeManager(); err != nil {
log.Warnf("Theme mode manager unavailable: %v", err)
} else {
notifyCapabilityChange()
go func() {
<-loginctlReady
if loginctlManager == nil {
return
}
themeModeManager.WatchLoginctl(loginctlManager)
}()
}
go func() {
geoClient := geolocation.NewClient()
geoClientInstance = geoClient
if waylandManager != nil {
waylandManager.SetGeoClient(geoClient)
}
if themeModeManager != nil {
themeModeManager.SetGeoClient(geoClient)
}
if err := InitializeLocationManager(geoClient); err != nil {
log.Warnf("Location manager unavailable: %v", err)
} else {
notifyCapabilityChange()
}
}()
go func() { go func() {
if err := InitializeBluezManager(); err != nil { if err := InitializeBluezManager(); err != nil {
log.Warnf("Bluez manager unavailable: %v", err) log.Warnf("Bluez manager unavailable: %v", err)
@@ -1624,25 +1656,6 @@ func Start(printDocs bool) error {
log.Debugf("WlrOutput manager unavailable: %v", err) log.Debugf("WlrOutput manager unavailable: %v", err)
} }
if err := InitializeThemeModeManager(geoClient); err != nil {
log.Warnf("Theme mode manager unavailable: %v", err)
} else {
notifyCapabilityChange()
go func() {
<-loginctlReady
if loginctlManager == nil {
return
}
themeModeManager.WatchLoginctl(loginctlManager)
}()
}
if err := InitializeLocationManager(geoClient); err != nil {
log.Warnf("Location manager unavailable: %v", err)
} else {
notifyCapabilityChange()
}
fatalErrChan := make(chan error, 1) fatalErrChan := make(chan error, 1)
if wlrOutputManager != nil { if wlrOutputManager != nil {
go func() { go func() {
+8 -2
View File
@@ -40,7 +40,7 @@ type Manager struct {
wg sync.WaitGroup wg sync.WaitGroup
} }
func NewManager(geoClient geolocation.Client) *Manager { func NewManager() *Manager {
m := &Manager{ m := &Manager{
config: Config{ config: Config{
Enabled: false, Enabled: false,
@@ -54,7 +54,6 @@ func NewManager(geoClient geolocation.Client) *Manager {
}, },
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
updateTrigger: make(chan struct{}, 1), updateTrigger: make(chan struct{}, 1),
geoClient: geoClient,
} }
m.updateState(time.Now()) m.updateState(time.Now())
@@ -315,6 +314,10 @@ func (m *Manager) getConfig() Config {
return m.config return m.config
} }
func (m *Manager) SetGeoClient(client geolocation.Client) {
m.geoClient = client
}
func (m *Manager) getLocation(config Config) (*float64, *float64) { func (m *Manager) getLocation(config Config) (*float64, *float64) {
if config.Latitude != nil && config.Longitude != nil { if config.Latitude != nil && config.Longitude != nil {
return config.Latitude, config.Longitude return config.Latitude, config.Longitude
@@ -322,6 +325,9 @@ func (m *Manager) getLocation(config Config) (*float64, *float64) {
if !config.UseIPLocation { if !config.UseIPLocation {
return nil, nil return nil, nil
} }
if m.geoClient == nil {
return nil, nil
}
m.locationMutex.RLock() m.locationMutex.RLock()
if m.cachedIPLat != nil && m.cachedIPLon != nil { if m.cachedIPLat != nil && m.cachedIPLon != nil {
+29 -22
View File
@@ -20,7 +20,7 @@ import (
const animKelvinStep = 25 const animKelvinStep = 25
func NewManager(display wlclient.WaylandDisplay, geoClient geolocation.Client, config Config) (*Manager, error) { func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error) {
if err := config.Validate(); err != nil { if err := config.Validate(); err != nil {
return nil, err return nil, err
} }
@@ -41,7 +41,6 @@ func NewManager(display wlclient.WaylandDisplay, geoClient geolocation.Client, c
updateTrigger: make(chan struct{}, 1), updateTrigger: make(chan struct{}, 1),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
dbusSignal: make(chan *dbus.Signal, 16), dbusSignal: make(chan *dbus.Signal, 16),
geoClient: geoClient,
} }
if err := m.setupRegistry(); err != nil { if err := m.setupRegistry(); err != nil {
@@ -422,6 +421,10 @@ func (m *Manager) recalcSchedule(now time.Time) {
} }
} }
func (m *Manager) SetGeoClient(client geolocation.Client) {
m.geoClient = client
}
func (m *Manager) getLocation() (*float64, *float64) { func (m *Manager) getLocation() (*float64, *float64) {
m.configMutex.RLock() m.configMutex.RLock()
config := m.config config := m.config
@@ -430,27 +433,31 @@ func (m *Manager) getLocation() (*float64, *float64) {
if config.Latitude != nil && config.Longitude != nil { if config.Latitude != nil && config.Longitude != nil {
return config.Latitude, config.Longitude return config.Latitude, config.Longitude
} }
if config.UseIPLocation { if !config.UseIPLocation {
m.locationMutex.RLock() return nil, nil
if m.cachedIPLat != nil && m.cachedIPLon != nil {
lat, lon := m.cachedIPLat, m.cachedIPLon
m.locationMutex.RUnlock()
return lat, lon
}
m.locationMutex.RUnlock()
location, err := m.geoClient.GetLocation()
if err != nil {
return nil, nil
}
m.locationMutex.Lock()
m.cachedIPLat = &location.Latitude
m.cachedIPLon = &location.Longitude
m.locationMutex.Unlock()
return m.cachedIPLat, m.cachedIPLon
} }
return nil, nil if m.geoClient == nil {
return nil, nil
}
m.locationMutex.RLock()
if m.cachedIPLat != nil && m.cachedIPLon != nil {
lat, lon := m.cachedIPLat, m.cachedIPLon
m.locationMutex.RUnlock()
return lat, lon
}
m.locationMutex.RUnlock()
location, err := m.geoClient.GetLocation()
if err != nil {
return nil, nil
}
m.locationMutex.Lock()
m.cachedIPLat = &location.Latitude
m.cachedIPLon = &location.Longitude
m.locationMutex.Unlock()
return m.cachedIPLat, m.cachedIPLon
} }
func (m *Manager) hasValidSchedule() bool { func (m *Manager) hasValidSchedule() bool {
+2 -5
View File
@@ -8,7 +8,6 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
mocks_geolocation "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/geolocation"
mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient" mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient"
) )
@@ -391,20 +390,18 @@ func TestNotifySubscribers_NonBlocking(t *testing.T) {
func TestNewManager_GetRegistryError(t *testing.T) { func TestNewManager_GetRegistryError(t *testing.T) {
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t) mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
mockGeoclient := mocks_geolocation.NewMockClient(t)
mockDisplay.EXPECT().Context().Return(nil) mockDisplay.EXPECT().Context().Return(nil)
mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry")) mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry"))
config := DefaultConfig() config := DefaultConfig()
_, err := NewManager(mockDisplay, mockGeoclient, config) _, err := NewManager(mockDisplay, config)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.Error(), "get registry") assert.Contains(t, err.Error(), "get registry")
} }
func TestNewManager_InvalidConfig(t *testing.T) { func TestNewManager_InvalidConfig(t *testing.T) {
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t) mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
mockGeoclient := mocks_geolocation.NewMockClient(t)
config := Config{ config := Config{
LowTemp: 500, LowTemp: 500,
@@ -412,6 +409,6 @@ func TestNewManager_InvalidConfig(t *testing.T) {
Gamma: 1.0, Gamma: 1.0,
} }
_, err := NewManager(mockDisplay, mockGeoclient, config) _, err := NewManager(mockDisplay, config)
assert.Error(t, err) assert.Error(t, err)
} }
+17 -4
View File
@@ -2,10 +2,10 @@ package wlcontext
import ( import (
"fmt" "fmt"
"golang.org/x/sys/unix"
"os" "os"
"sync" "sync"
"time"
"golang.org/x/sys/unix"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
@@ -123,6 +123,9 @@ func (sc *SharedContext) eventDispatcher() {
{Fd: int32(sc.wakeR), Events: unix.POLLIN}, {Fd: int32(sc.wakeR), Events: unix.POLLIN},
} }
consecutiveErrors := 0
const maxConsecutiveErrors = 20
for { for {
sc.drainCmdQueue() sc.drainCmdQueue()
@@ -153,9 +156,19 @@ func (sc *SharedContext) eventDispatcher() {
} }
if err := ctx.Dispatch(); err != nil && !os.IsTimeout(err) { if err := ctx.Dispatch(); err != nil && !os.IsTimeout(err) {
log.Errorf("Wayland connection error: %v", err) consecutiveErrors++
return log.Warnf("Wayland connection error (%d/%d): %v", consecutiveErrors, maxConsecutiveErrors, err)
if consecutiveErrors >= maxConsecutiveErrors {
log.Errorf("Fatal: Wayland connection unrecoverable after %d attempts. Exiting dispatcher.", maxConsecutiveErrors)
return
}
time.Sleep(100 * time.Millisecond * time.Duration(consecutiveErrors))
continue
} }
consecutiveErrors = 0
} }
} }
+1 -1
View File
@@ -40,7 +40,7 @@ func (m Model) viewDeployingConfigs() string {
spinner := m.spinner.View() spinner := m.spinner.View()
status := m.styles.Normal.Render("Setting up configuration files...") status := m.styles.Normal.Render("Setting up configuration files...")
b.WriteString(fmt.Sprintf("%s %s", spinner, status)) fmt.Fprintf(&b, "%s %s", spinner, status)
b.WriteString("\n\n") b.WriteString("\n\n")
// Show progress information // Show progress information
+1 -1
View File
@@ -23,7 +23,7 @@ func (m Model) viewDetectingDeps() string {
spinner := m.spinner.View() spinner := m.spinner.View()
status := m.styles.Normal.Render("Scanning system for existing packages and configurations...") status := m.styles.Normal.Render("Scanning system for existing packages and configurations...")
b.WriteString(fmt.Sprintf("%s %s", spinner, status)) fmt.Fprintf(&b, "%s %s", spinner, status)
return b.String() return b.String()
} }
+2 -2
View File
@@ -52,7 +52,7 @@ func (m Model) viewInstallingPackages() string {
if !m.packageProgress.isComplete { if !m.packageProgress.isComplete {
spinner := m.spinner.View() spinner := m.spinner.View()
status := m.styles.Normal.Render(m.packageProgress.step) status := m.styles.Normal.Render(m.packageProgress.step)
b.WriteString(fmt.Sprintf("%s %s", spinner, status)) fmt.Fprintf(&b, "%s %s", spinner, status)
b.WriteString("\n\n") b.WriteString("\n\n")
// Show progress bar // Show progress bar
@@ -387,7 +387,7 @@ func (m Model) viewDebugLogs() string {
for i := startIdx; i < len(allLogs); i++ { for i := startIdx; i < len(allLogs); i++ {
if allLogs[i] != "" { if allLogs[i] != "" {
b.WriteString(fmt.Sprintf("%d: %s\n", i, allLogs[i])) fmt.Fprintf(&b, "%d: %s\n", i, allLogs[i])
} }
} }
+1 -1
View File
@@ -75,7 +75,7 @@ func (m Model) viewFingerprintAuth() string {
spinner := m.spinner.View() spinner := m.spinner.View()
status := m.styles.Normal.Render("Waiting for fingerprint...") status := m.styles.Normal.Render("Waiting for fingerprint...")
b.WriteString(fmt.Sprintf("%s %s", spinner, status)) fmt.Fprintf(&b, "%s %s", spinner, status)
} }
return b.String() return b.String()
+3 -3
View File
@@ -132,9 +132,9 @@ func (m Model) viewWelcome() string {
contentStyle = contentStyle.Bold(true) contentStyle = contentStyle.Bold(true)
} }
b.WriteString(fmt.Sprintf(" %s %s\n", fmt.Fprintf(&b, " %s %s\n",
prefixStyle.Render(prefix), prefixStyle.Render(prefix),
contentStyle.Render(content))) contentStyle.Render(content))
} }
b.WriteString("\n") b.WriteString("\n")
@@ -158,7 +158,7 @@ func (m Model) viewWelcome() string {
} else if m.isLoading { } else if m.isLoading {
spinner := m.spinner.View() spinner := m.spinner.View()
loading := m.styles.Normal.Render("Detecting system...") loading := m.styles.Normal.Render("Detecting system...")
b.WriteString(fmt.Sprintf("%s %s\n\n", spinner, loading)) fmt.Fprintf(&b, "%s %s\n\n", spinner, loading)
} }
// Footer with better visual separation // Footer with better visual separation
+1 -1
View File
@@ -13,7 +13,7 @@ Architecture: any
Depends: ${misc:Depends}, Depends: ${misc:Depends},
greetd, greetd,
quickshell-git | quickshell quickshell-git | quickshell
Recommends: niri | hyprland | sway Suggests: niri | hyprland | sway
Description: DankMaterialShell greeter for greetd Description: DankMaterialShell greeter for greetd
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3 DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
inspired greeter interface built with Quickshell for Wayland compositors. inspired greeter interface built with Quickshell for Wayland compositors.
+1 -1
View File
@@ -3,7 +3,7 @@
%global debug_package %{nil} %global debug_package %{nil}
%global version {{{ git_repo_version }}} %global version {{{ git_repo_version }}}
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors %global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
%global go_toolchain_version 1.25.7 %global go_toolchain_version 1.26.1
Name: dms Name: dms
Epoch: 2 Epoch: 2
+4 -1
View File
@@ -24,6 +24,7 @@ let
lib.makeBinPath [ lib.makeBinPath [
cfg.quickshell.package cfg.quickshell.package
compositorPackage compositorPackage
pkgs.glib # provides gdbus, used by the fprintd hardware probe in GreeterContent.qml
] ]
} }
${ ${
@@ -195,7 +196,9 @@ in
fi fi
if [ -f settings.json ]; then if [ -f settings.json ]; then
if cp "$(${jq} -r '.customThemeFile' settings.json)" custom-theme.json; then theme_file="$(${jq} -r '.customThemeFile // empty' settings.json)"
if [ -f "$theme_file" ] && [ -r "$theme_file" ]; then
cp "$theme_file" custom-theme.json
mv settings.json settings.orig.json mv settings.json settings.orig.json
${jq} '.customThemeFile = "${cacheDir}/custom-theme.json"' settings.orig.json > settings.json ${jq} '.customThemeFile = "${cacheDir}/custom-theme.json"' settings.orig.json > settings.json
fi fi
+23 -26
View File
@@ -2,11 +2,9 @@
config, config,
lib, lib,
... ...
}: }: let
let
cfg = config.programs.dank-material-shell; cfg = config.programs.dank-material-shell;
in in {
{
imports = [ imports = [
./dms-rename.nix ./dms-rename.nix
]; ];
@@ -16,9 +14,11 @@ in
enableKeybinds = lib.mkEnableOption "DankMaterialShell niri keybinds"; enableKeybinds = lib.mkEnableOption "DankMaterialShell niri keybinds";
enableSpawn = lib.mkEnableOption "DankMaterialShell niri spawn-at-startup"; enableSpawn = lib.mkEnableOption "DankMaterialShell niri spawn-at-startup";
includes = { includes = {
enable = (lib.mkEnableOption "includes for niri-flake") // { enable =
default = true; (lib.mkEnableOption "includes for niri-flake")
}; // {
default = true;
};
override = lib.mkOption { override = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
description = '' description = ''
@@ -44,8 +44,10 @@ in
"alttab" "alttab"
"binds" "binds"
"colors" "colors"
"cursor"
"layout" "layout"
"outputs" "outputs"
"windowrules"
"wpblur" "wpblur"
]; ];
example = [ example = [
@@ -70,24 +72,21 @@ in
let let
cfg' = cfg.niri.includes; cfg' = cfg.niri.includes;
withOriginalConfig = withOriginalConfig = dmsFiles:
dmsFiles: if cfg'.override
if cfg'.override then then [cfg'.originalFileName] ++ dmsFiles
[ cfg'.originalFileName ] ++ dmsFiles else dmsFiles ++ [cfg'.originalFileName];
else
dmsFiles ++ [ cfg'.originalFileName ];
fixes = map (fix: "\n${fix}") ( fixes = map (fix: "\n${fix}") (
lib.optional (cfg'.enable && config.programs.niri.settings.layout.border.enable) lib.optional (cfg'.enable && config.programs.niri.settings.layout.border.enable)
# kdl # kdl
'' ''
// Border fix // Border fix
// See https://yalter.github.io/niri/Configuration%3A-Include.html#border-special-case for details // See https://yalter.github.io/niri/Configuration%3A-Include.html#border-special-case for details
layout { border { on; }; } layout { border { on; }; }
'' ''
); );
in in {
{
niri-config.target = lib.mkForce "niri/${cfg'.originalFileName}.kdl"; niri-config.target = lib.mkForce "niri/${cfg'.originalFileName}.kdl";
niri-config-dms = { niri-config-dms = {
target = "niri/config.kdl"; target = "niri/config.kdl";
@@ -104,11 +103,9 @@ in
programs.niri.settings = lib.mkMerge [ programs.niri.settings = lib.mkMerge [
(lib.mkIf cfg.niri.enableKeybinds { (lib.mkIf cfg.niri.enableKeybinds {
binds = binds = with config.lib.niri.actions; let
with config.lib.niri.actions; dms-ipc = spawn "dms" "ipc";
let in
dms-ipc = spawn "dms" "ipc";
in
{ {
"Mod+Space" = { "Mod+Space" = {
action = dms-ipc "spotlight" "toggle"; action = dms-ipc "spotlight" "toggle";
+31 -3
View File
@@ -102,6 +102,19 @@ if [[ ! -d "distro/debian" ]]; then
echo "Error: Run this script from the repository root" echo "Error: Run this script from the repository root"
exit 1 exit 1
fi fi
# Retry wrapper for osc commands (mitigates SSL "Connection reset by peer" from api.opensuse.org)
osc_retry() {
local max=3 attempt=1
while true; do
if osc "$@"; then return 0; fi
((attempt >= max)) && return 1
echo "Retrying in $((5*attempt))s (attempt $attempt/$max)..."
sleep $((5*attempt))
((attempt++))
done
}
# Parameters: # Parameters:
# $1 = PROJECT # $1 = PROJECT
# $2 = PACKAGE # $2 = PACKAGE
@@ -309,8 +322,23 @@ mkdir -p "$OBS_BASE"
if [[ ! -d "$OBS_BASE/$OBS_PROJECT/$PACKAGE" ]]; then if [[ ! -d "$OBS_BASE/$OBS_PROJECT/$PACKAGE" ]]; then
echo "Checking out $OBS_PROJECT/$PACKAGE..." echo "Checking out $OBS_PROJECT/$PACKAGE..."
cd "$OBS_BASE" cd "$OBS_BASE"
osc co "$OBS_PROJECT/$PACKAGE" CHECKOUT_OK=false
for attempt in 1 2 3; do
if osc co "$OBS_PROJECT/$PACKAGE"; then
CHECKOUT_OK=true
break
fi
if [[ $attempt -lt 3 ]]; then
echo "Checkout failed (attempt $attempt/3). Removing partial copy and retrying in $((5*attempt))s..."
rm -rf "${OBS_BASE:?}/${OBS_PROJECT:?}"
sleep $((5*attempt))
fi
done
cd "$REPO_ROOT" cd "$REPO_ROOT"
if [[ "$CHECKOUT_OK" != "true" ]]; then
echo "Error: Checkout failed after 3 attempts"
exit 1
fi
fi fi
WORK_DIR="$OBS_BASE/$OBS_PROJECT/$PACKAGE" WORK_DIR="$OBS_BASE/$OBS_PROJECT/$PACKAGE"
@@ -1064,7 +1092,7 @@ fi
# Update working copy to latest revision (without expanding service files to avoid revision conflicts) # Update working copy to latest revision (without expanding service files to avoid revision conflicts)
echo "==> Updating working copy" echo "==> Updating working copy"
if ! osc up 2>/dev/null; then if ! osc_retry up 2>/dev/null; then
echo "Error: Failed to update working copy" echo "Error: Failed to update working copy"
exit 1 exit 1
fi fi
@@ -1145,7 +1173,7 @@ if ! osc status 2>/dev/null | grep -qE '^[MAD]|^[?]'; then
else else
echo "==> Committing to OBS" echo "==> Committing to OBS"
set +e set +e
osc commit --skip-local-service-run -m "$MESSAGE" 2>&1 | grep -v "Git SCM package" | grep -v "apiurl\|project\|_ObsPrj\|_manifest\|git-obs" osc_retry commit --skip-local-service-run -m "$MESSAGE" 2>&1 | grep -v "Git SCM package" | grep -v "apiurl\|project\|_ObsPrj\|_manifest\|git-obs"
COMMIT_EXIT=${PIPESTATUS[0]} COMMIT_EXIT=${PIPESTATUS[0]}
set -e set -e
if [[ $COMMIT_EXIT -ne 0 ]]; then if [[ $COMMIT_EXIT -ne 0 ]]; then
+1 -1
View File
@@ -13,7 +13,7 @@ Architecture: any
Depends: ${misc:Depends}, Depends: ${misc:Depends},
greetd, greetd,
quickshell-git | quickshell quickshell-git | quickshell
Recommends: niri | hyprland | sway Suggests: niri | hyprland | sway
Description: DankMaterialShell greeter for greetd Description: DankMaterialShell greeter for greetd
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3 DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
inspired greeter interface built with Quickshell for Wayland compositors. inspired greeter interface built with Quickshell for Wayland compositors.
+82 -57
View File
@@ -17,6 +17,25 @@
... ...
}: }:
let let
goModVersion =
let
content = builtins.readFile ./core/go.mod;
lines = builtins.filter builtins.isString (builtins.split "\n" content);
goLines = builtins.filter (l: builtins.match "go [0-9]+\\..*" l != null) lines;
matched =
if goLines != [ ] then builtins.match "go ([0-9]+)\\.([0-9]+).*" (builtins.head goLines) else null;
in
if matched != null then
{
major = builtins.elemAt matched 0;
minor = builtins.elemAt matched 1;
}
else
{
major = "1";
minor = "25";
};
goForPkgs = pkgs: pkgs.${"go_${goModVersion.major}_${goModVersion.minor}"};
forEachSystem = forEachSystem =
fn: fn:
nixpkgs.lib.genAttrs [ "aarch64-darwin" "aarch64-linux" "x86_64-darwin" "x86_64-linux" ] ( nixpkgs.lib.genAttrs [ "aarch64-darwin" "aarch64-linux" "x86_64-darwin" "x86_64-linux" ] (
@@ -72,76 +91,82 @@
"${cleanVersion}${dateSuffix}${revSuffix}"; "${cleanVersion}${dateSuffix}${revSuffix}";
in in
{ {
dms-shell = pkgs.buildGoModule ( dms-shell = pkgs.lib.makeOverridable (
let
rootSrc = ./.;
in
{ {
inherit version; extraQtPackages ? [ ],
pname = "dms-shell"; }:
src = ./core; (pkgs.buildGoModule.override { go = goForPkgs pkgs; }) (
vendorHash = "sha256-dEk7IOd6aQwaxZruxQclN7TGMyb8EJOl6NBWRsoZ9HQ="; let
rootSrc = ./.;
qtPackages = (qmlPkgs pkgs) ++ extraQtPackages;
in
{
inherit version;
pname = "dms-shell";
src = ./core;
vendorHash = "sha256-dEk7IOd6aQwaxZruxQclN7TGMyb8EJOl6NBWRsoZ9HQ=";
subPackages = [ "cmd/dms" ]; subPackages = [ "cmd/dms" ];
ldflags = [ ldflags = [
"-s" "-s"
"-w" "-w"
"-X 'main.Version=${version}'" "-X 'main.Version=${version}'"
]; ];
nativeBuildInputs = with pkgs; [ nativeBuildInputs = with pkgs; [
installShellFiles installShellFiles
makeWrapper makeWrapper
]; ];
postInstall = '' postInstall = ''
mkdir -p $out/share/quickshell/dms mkdir -p $out/share/quickshell/dms
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/ cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
chmod u+w $out/share/quickshell/dms/VERSION chmod u+w $out/share/quickshell/dms/VERSION
echo "${version}" > $out/share/quickshell/dms/VERSION echo "${version}" > $out/share/quickshell/dms/VERSION
# Install desktop file and icon # Install desktop file and icon
install -D ${rootSrc}/assets/dms-open.desktop \ install -D ${rootSrc}/assets/dms-open.desktop \
$out/share/applications/dms-open.desktop $out/share/applications/dms-open.desktop
install -D ${rootSrc}/core/assets/danklogo.svg \ install -D ${rootSrc}/core/assets/danklogo.svg \
$out/share/hicolor/scalable/apps/danklogo.svg $out/share/hicolor/scalable/apps/danklogo.svg
wrapProgram $out/bin/dms \ wrapProgram $out/bin/dms \
--add-flags "-c $out/share/quickshell/dms" \ --add-flags "-c $out/share/quickshell/dms" \
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs (qmlPkgs pkgs)}" \ --prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs qtPackages}" \
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs (qmlPkgs pkgs)}" --prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs qtPackages}"
install -Dm644 ${rootSrc}/assets/systemd/dms.service \ install -Dm644 ${rootSrc}/assets/systemd/dms.service \
$out/lib/systemd/user/dms.service $out/lib/systemd/user/dms.service
substituteInPlace $out/lib/systemd/user/dms.service \ substituteInPlace $out/lib/systemd/user/dms.service \
--replace-fail /usr/bin/dms $out/bin/dms \ --replace-fail /usr/bin/dms $out/bin/dms \
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill --replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \ substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash --replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \ substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so --replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
installShellCompletion --cmd dms \ installShellCompletion --cmd dms \
--bash <($out/bin/dms completion bash) \ --bash <($out/bin/dms completion bash) \
--fish <($out/bin/dms completion fish) \ --fish <($out/bin/dms completion fish) \
--zsh <($out/bin/dms completion zsh) --zsh <($out/bin/dms completion zsh)
''; '';
meta = { meta = {
description = "Desktop shell for wayland compositors built with Quickshell & GO"; description = "Desktop shell for wayland compositors built with Quickshell & GO";
homepage = "https://danklinux.com"; homepage = "https://danklinux.com";
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}"; changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}";
license = pkgs.lib.licenses.mit; license = pkgs.lib.licenses.mit;
mainProgram = "dms"; mainProgram = "dms";
platforms = pkgs.lib.platforms.linux; platforms = pkgs.lib.platforms.linux;
}; };
} }
); )
) { };
quickshell = quickshell.packages.${system}.default; quickshell = quickshell.packages.${system}.default;
@@ -181,7 +206,7 @@
buildInputs = buildInputs =
with pkgs; with pkgs;
[ [
go_1_25 (goForPkgs pkgs)
go-mockery_2 go-mockery_2
gopls gopls
delve delve
+2 -2
View File
@@ -99,7 +99,7 @@ qs -v -p shell.qml # Verbose debugging
# Code formatting and linting # Code formatting and linting
qmlfmt -t 4 -i 4 -b 250 -w /path/to/file.qml # Format QML (don't use qmlformat) qmlfmt -t 4 -i 4 -b 250 -w /path/to/file.qml # Format QML (don't use qmlformat)
qmllint **/*.qml # Lint all QML files make -C .. lint-qml # From quickshell/, call the repo-root lint target; requires the generated .qmlls.ini VFS from `qs -p .`
./qmlformat-all.sh # Format all QML files ./qmlformat-all.sh # Format all QML files
``` ```
@@ -783,7 +783,7 @@ When modifying the shell:
**QML Frontend:** **QML Frontend:**
1. **Test changes**: `qs -p .` (automatic reload on file changes) 1. **Test changes**: `qs -p .` (automatic reload on file changes)
2. **Code quality**: Run `./qmlformat-all.sh` or `qmlformat -i **/*.qml` and `qmllint **/*.qml` 2. **Code quality**: Run `./qmlformat-all.sh` or `qmlformat -i **/*.qml`, then from repo root run `make lint-qml` after Quickshell has generated the local `.qmlls.ini` VFS with `qs -p .`
3. **Performance**: Ensure animations remain smooth (60 FPS target) 3. **Performance**: Ensure animations remain smooth (60 FPS target)
4. **Theming**: Use `Theme.propertyName` for Material Design 3 consistency 4. **Theming**: Use `Theme.propertyName` for Material Design 3 consistency
-174
View File
@@ -1,174 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
// AnimVariants Central tuning for animation and Motion Effects variants
// (Material/Fluent/Dynamic) (Standard/Directional/Depth)
Singleton {
id: root
readonly property list<real> variantEnterCurve: {
if (typeof SettingsData === "undefined")
return Anims.expressiveDefaultSpatial;
switch (SettingsData.animationVariant) {
case 1:
return Anims.standardDecel;
case 2:
return Anims.expressiveFastSpatial;
default:
return Anims.expressiveDefaultSpatial;
}
}
readonly property list<real> variantExitCurve: {
if (typeof SettingsData === "undefined")
return Anims.emphasized;
switch (SettingsData.animationVariant) {
case 1:
return Anims.standard;
case 2:
return Anims.emphasized;
default:
return Anims.emphasized;
}
}
// Modal-specific entry curve
readonly property list<real> variantModalEnterCurve: {
if (typeof SettingsData === "undefined")
return Anims.expressiveDefaultSpatial;
if (isDirectionalEffect) {
if (SettingsData.animationVariant === 1)
return Anims.standardDecel;
if (SettingsData.animationVariant === 2)
return Anims.expressiveFastSpatial;
}
return variantEnterCurve;
}
readonly property list<real> variantModalExitCurve: {
if (typeof SettingsData === "undefined")
return Anims.emphasized;
if (isDirectionalEffect) {
if (SettingsData.animationVariant === 1)
return Anims.emphasizedAccel;
if (SettingsData.animationVariant === 2)
return Anims.emphasizedAccel;
}
return variantExitCurve;
}
// Popout-specific entry curve
readonly property list<real> variantPopoutEnterCurve: {
if (typeof SettingsData === "undefined")
return Anims.expressiveDefaultSpatial;
if (isDirectionalEffect) {
if (SettingsData.animationVariant === 1)
return Anims.standardDecel;
if (SettingsData.animationVariant === 2)
return Anims.expressiveFastSpatial;
return Anims.standardDecel;
}
return variantEnterCurve;
}
readonly property list<real> variantPopoutExitCurve: {
if (typeof SettingsData === "undefined")
return Anims.emphasized;
if (isDirectionalEffect) {
if (SettingsData.animationVariant === 1)
return Anims.emphasizedAccel;
if (SettingsData.animationVariant === 2)
return Anims.emphasizedAccel;
}
return variantExitCurve;
}
readonly property real variantEnterDurationFactor: {
if (typeof SettingsData === "undefined")
return 1.0;
switch (SettingsData.animationVariant) {
case 1:
return 0.9;
case 2:
return 1.08;
default:
return 1.0;
}
}
readonly property real variantExitDurationFactor: {
if (typeof SettingsData === "undefined")
return 1.0;
switch (SettingsData.animationVariant) {
case 1:
return 0.85;
case 2:
return 0.92;
default:
return 1.0;
}
}
// Fluent: opacity at ~55% of duration; Material/Dynamic: 1:1 with position
readonly property real variantOpacityDurationScale: {
if (typeof SettingsData === "undefined")
return 1.0;
return SettingsData.animationVariant === 1 ? 0.55 : 1.0;
}
function variantDuration(baseDuration, entering) {
const factor = entering ? variantEnterDurationFactor : variantExitDurationFactor;
return Math.max(0, Math.round(baseDuration * factor));
}
function variantExitCleanupPadding() {
if (typeof SettingsData === "undefined")
return 50;
switch (SettingsData.motionEffect) {
case 1:
return 8;
case 2:
return 24;
default:
return 50;
}
}
function variantCloseInterval(baseDuration) {
return variantDuration(baseDuration, false) + variantExitCleanupPadding();
}
readonly property bool isDirectionalEffect: typeof SettingsData !== "undefined" && SettingsData.motionEffect === 1
readonly property bool isDepthEffect: typeof SettingsData !== "undefined" && SettingsData.motionEffect === 2
readonly property real effectScaleCollapsed: {
if (typeof SettingsData === "undefined")
return 0.96;
switch (SettingsData.motionEffect) {
case 1:
return 1.0;
case 2:
return 0.88;
default:
return 0.96;
}
}
readonly property real effectAnimOffset: {
if (typeof SettingsData === "undefined")
return 16;
switch (SettingsData.motionEffect) {
case 1:
return 144;
case 2:
return 56;
default:
return 16;
}
}
}
-5
View File
@@ -22,9 +22,4 @@ Singleton {
readonly property var standard: [0.20, 0.00, 0.00, 1.00, 1.00, 1.00] readonly property var standard: [0.20, 0.00, 0.00, 1.00, 1.00, 1.00]
readonly property var standardDecel: [0.00, 0.00, 0.00, 1.00, 1.00, 1.00] readonly property var standardDecel: [0.00, 0.00, 0.00, 1.00, 1.00, 1.00]
readonly property var standardAccel: [0.30, 0.00, 1.00, 1.00, 1.00, 1.00] readonly property var standardAccel: [0.30, 0.00, 1.00, 1.00, 1.00, 1.00]
// Used by AnimVariants for variant/effect logic
readonly property var expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
readonly property var expressiveFastSpatial: [0.34, 1.5, 0.2, 1.0, 1.0, 1.0]
readonly property var expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
} }
+8 -36
View File
@@ -22,8 +22,8 @@ Singleton {
property bool _hasUnsavedChanges: false property bool _hasUnsavedChanges: false
property var _loadedSessionSnapshot: null property var _loadedSessionSnapshot: null
readonly property var _hooks: ({ readonly property var _hooks: ({
"updateLocale": updateLocale "updateLocale": updateLocale
}) })
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation) readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
readonly property string _stateDir: Paths.strip(_stateUrl) readonly property string _stateDir: Paths.strip(_stateUrl)
@@ -580,14 +580,7 @@ Singleton {
} }
} }
if (!newSettings[identifier]) { newSettings[identifier] = getMonitorCyclingSettings(screenName);
newSettings[identifier] = {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
};
}
newSettings[identifier].enabled = enabled; newSettings[identifier].enabled = enabled;
monitorCyclingSettings = newSettings; monitorCyclingSettings = newSettings;
saveSettings(); saveSettings();
@@ -618,14 +611,7 @@ Singleton {
} }
} }
if (!newSettings[identifier]) { newSettings[identifier] = getMonitorCyclingSettings(screenName);
newSettings[identifier] = {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
};
}
newSettings[identifier].mode = mode; newSettings[identifier].mode = mode;
monitorCyclingSettings = newSettings; monitorCyclingSettings = newSettings;
saveSettings(); saveSettings();
@@ -656,14 +642,7 @@ Singleton {
} }
} }
if (!newSettings[identifier]) { newSettings[identifier] = getMonitorCyclingSettings(screenName);
newSettings[identifier] = {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
};
}
newSettings[identifier].interval = interval; newSettings[identifier].interval = interval;
monitorCyclingSettings = newSettings; monitorCyclingSettings = newSettings;
saveSettings(); saveSettings();
@@ -694,14 +673,7 @@ Singleton {
} }
} }
if (!newSettings[identifier]) { newSettings[identifier] = getMonitorCyclingSettings(screenName);
newSettings[identifier] = {
"enabled": false,
"mode": "interval",
"interval": 300,
"time": "06:00"
};
}
newSettings[identifier].time = time; newSettings[identifier].time = time;
monitorCyclingSettings = newSettings; monitorCyclingSettings = newSettings;
saveSettings(); saveSettings();
@@ -1218,7 +1190,7 @@ Singleton {
"time": "06:00" "time": "06:00"
}; };
var value = _findMonitorValue(monitorCyclingSettings, screenName); var value = _findMonitorValue(monitorCyclingSettings, screenName);
return value !== undefined ? value : defaults; return Object.assign({}, defaults, value !== undefined ? value : {});
} }
FileView { FileView {
@@ -1245,7 +1217,7 @@ Singleton {
id: greeterSessionFile id: greeterSessionFile
path: { path: {
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms"; const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
return greetCfgDir + "/session.json"; return greetCfgDir + "/session.json";
} }
preload: isGreeterMode preload: isGreeterMode
+96 -27
View File
@@ -14,7 +14,7 @@ import "settings/SettingsStore.js" as Store
Singleton { Singleton {
id: root id: root
readonly property int settingsConfigVersion: 6 readonly property int settingsConfigVersion: 5
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true" readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
@@ -37,18 +37,6 @@ Singleton {
Custom Custom
} }
enum AnimationVariant {
Material,
Fluent,
Dynamic
}
enum AnimationEffect {
Standard, // 0 M3: scale-in, rises from below
Directional, // 1 pure large slide, no scale
Depth // 2 medium slide with deep depth scale pop
}
enum SuspendBehavior { enum SuspendBehavior {
Suspend, Suspend,
Hibernate, Hibernate,
@@ -162,6 +150,7 @@ Singleton {
property int mangoLayoutBorderSize: -1 property int mangoLayoutBorderSize: -1
property int firstDayOfWeek: -1 property int firstDayOfWeek: -1
property bool showWeekNumber: false
property bool use24HourClock: true property bool use24HourClock: true
property bool showSeconds: false property bool showSeconds: false
property bool padHours12Hour: false property bool padHours12Hour: false
@@ -178,12 +167,6 @@ Singleton {
property int modalCustomAnimationDuration: 150 property int modalCustomAnimationDuration: 150
property bool enableRippleEffects: true property bool enableRippleEffects: true
onEnableRippleEffectsChanged: saveSettings() onEnableRippleEffectsChanged: saveSettings()
property int animationVariant: SettingsData.AnimationVariant.Material
onAnimationVariantChanged: saveSettings()
property int motionEffect: SettingsData.AnimationEffect.Standard
onMotionEffectChanged: saveSettings()
property int directionalAnimationMode: 0
onDirectionalAnimationModeChanged: saveSettings()
property bool m3ElevationEnabled: true property bool m3ElevationEnabled: true
onM3ElevationEnabledChanged: saveSettings() onM3ElevationEnabledChanged: saveSettings()
property int m3ElevationIntensity: 12 property int m3ElevationIntensity: 12
@@ -298,6 +281,7 @@ Singleton {
property bool showOccupiedWorkspacesOnly: false property bool showOccupiedWorkspacesOnly: false
property bool reverseScrolling: false property bool reverseScrolling: false
property bool dwlShowAllTags: false property bool dwlShowAllTags: false
property bool workspaceActiveAppHighlightEnabled: false
property string workspaceColorMode: "default" property string workspaceColorMode: "default"
property string workspaceOccupiedColorMode: "none" property string workspaceOccupiedColorMode: "none"
property string workspaceUnfocusedColorMode: "default" property string workspaceUnfocusedColorMode: "default"
@@ -331,6 +315,17 @@ Singleton {
property string centeringMode: "index" property string centeringMode: "index"
property string clockDateFormat: "" property string clockDateFormat: ""
property string lockDateFormat: "" property string lockDateFormat: ""
property bool greeterRememberLastSession: true
property bool greeterRememberLastUser: true
property bool greeterEnableFprint: false
property bool greeterEnableU2f: false
property string greeterWallpaperPath: ""
property bool greeterUse24HourClock: true
property bool greeterShowSeconds: false
property bool greeterPadHours12Hour: false
property string greeterLockDateFormat: ""
property string greeterFontFamily: ""
property string greeterWallpaperFillMode: ""
property int mediaSize: 1 property int mediaSize: 1
property string appLauncherViewMode: "list" property string appLauncherViewMode: "list"
@@ -459,6 +454,11 @@ Singleton {
property bool syncModeWithPortal: true property bool syncModeWithPortal: true
property bool terminalsAlwaysDark: false property bool terminalsAlwaysDark: false
property string muxType: "tmux"
property bool muxUseCustomCommand: false
property string muxCustomCommand: ""
property string muxSessionFilter: ""
property bool runDmsMatugenTemplates: true property bool runDmsMatugenTemplates: true
property bool matugenTemplateGtk: true property bool matugenTemplateGtk: true
property bool matugenTemplateNiri: true property bool matugenTemplateNiri: true
@@ -474,18 +474,25 @@ Singleton {
property bool matugenTemplateGhostty: true property bool matugenTemplateGhostty: true
property bool matugenTemplateKitty: true property bool matugenTemplateKitty: true
property bool matugenTemplateFoot: true property bool matugenTemplateFoot: true
property bool matugenTemplateNeovim: true property bool matugenTemplateNeovim: false
property bool matugenTemplateAlacritty: true property bool matugenTemplateAlacritty: true
property bool matugenTemplateWezterm: true property bool matugenTemplateWezterm: true
property bool matugenTemplateDgop: true property bool matugenTemplateDgop: true
property bool matugenTemplateKcolorscheme: true property bool matugenTemplateKcolorscheme: true
property bool matugenTemplateVscode: true property bool matugenTemplateVscode: true
property bool matugenTemplateEmacs: true property bool matugenTemplateEmacs: true
property bool matugenTemplateZed: true
property var matugenTemplateNeovimSettings: ({
"dark": { "baseTheme": "github_dark", "harmony": 0.5 },
"light": { "baseTheme": "github_light", "harmony": 0.5 }
})
property bool showDock: false property bool showDock: false
property bool dockAutoHide: false property bool dockAutoHide: false
property bool dockSmartAutoHide: false property bool dockSmartAutoHide: false
property bool dockGroupByApp: false property bool dockGroupByApp: false
property bool dockRestoreSpecialWorkspaceOnClick: false
property bool dockOpenOnOverview: false property bool dockOpenOnOverview: false
property int dockPosition: SettingsData.Position.Bottom property int dockPosition: SettingsData.Position.Bottom
property real dockSpacing: 4 property real dockSpacing: 4
@@ -531,9 +538,23 @@ Singleton {
property bool enableFprint: false property bool enableFprint: false
property int maxFprintTries: 15 property int maxFprintTries: 15
property bool fprintdAvailable: false property bool fprintdAvailable: false
property bool lockFingerprintCanEnable: false
property bool lockFingerprintReady: false
property string lockFingerprintReason: "probe_failed"
property bool greeterFingerprintCanEnable: false
property bool greeterFingerprintReady: false
property string greeterFingerprintReason: "probe_failed"
property string greeterFingerprintSource: "none"
property bool enableU2f: false property bool enableU2f: false
property string u2fMode: "or" property string u2fMode: "or"
property bool u2fAvailable: false property bool u2fAvailable: false
property bool lockU2fCanEnable: false
property bool lockU2fReady: false
property string lockU2fReason: "probe_failed"
property bool greeterU2fCanEnable: false
property bool greeterU2fReady: false
property string greeterU2fReason: "probe_failed"
property string greeterU2fSource: "none"
property string lockScreenActiveMonitor: "all" property string lockScreenActiveMonitor: "all"
property string lockScreenInactiveColor: "#000000" property string lockScreenInactiveColor: "#000000"
property int lockScreenNotificationMode: 0 property int lockScreenNotificationMode: 0
@@ -556,6 +577,7 @@ Singleton {
property bool notificationHistorySaveNormal: true property bool notificationHistorySaveNormal: true
property bool notificationHistorySaveCritical: true property bool notificationHistorySaveCritical: true
property var notificationRules: [] property var notificationRules: []
property bool notificationFocusedMonitor: false
property bool osdAlwaysShowValue: false property bool osdAlwaysShowValue: false
property int osdPosition: SettingsData.Position.BottomCenter property int osdPosition: SettingsData.Position.BottomCenter
@@ -1019,13 +1041,19 @@ Singleton {
signal widgetDataChanged signal widgetDataChanged
signal workspaceIconsUpdated signal workspaceIconsUpdated
function refreshAuthAvailability() {
if (isGreeterMode)
return;
Processes.settingsRoot = root;
Processes.detectAuthCapabilities();
}
Component.onCompleted: { Component.onCompleted: {
if (!isGreeterMode) { if (!isGreeterMode) {
Processes.settingsRoot = root; Processes.settingsRoot = root;
loadSettings(); loadSettings();
initializeListModels(); initializeListModels();
Processes.detectFprintd(); refreshAuthAvailability();
Processes.detectU2f();
Processes.checkPluginSettings(); Processes.checkPluginSettings();
} }
} }
@@ -1173,7 +1201,7 @@ Singleton {
"updateCompositorLayout": updateCompositorLayout, "updateCompositorLayout": updateCompositorLayout,
"applyStoredIconTheme": applyStoredIconTheme, "applyStoredIconTheme": applyStoredIconTheme,
"updateBarConfigs": updateBarConfigs, "updateBarConfigs": updateBarConfigs,
"updateCompositorCursor": updateCompositorCursor, "updateCompositorCursor": updateCompositorCursor
}) })
function set(key, value) { function set(key, value) {
@@ -1265,10 +1293,47 @@ Singleton {
return JSON.stringify(Store.toJson(root), null, 2); return JSON.stringify(Store.toJson(root), null, 2);
} }
function _resetPluginSettings() {
_pluginParseError = false;
pluginSettings = {};
}
function _pluginSettingsErrorCode(error) {
if (typeof error === "number")
return error;
if (error && typeof error === "object") {
if (typeof error.code === "number")
return error.code;
if (typeof error.errno === "number")
return error.errno;
}
const msg = String(error || "").trim();
if (/^\d+$/.test(msg))
return Number(msg);
return -1;
}
function _isMissingPluginSettingsError(error) {
if (_pluginSettingsErrorCode(error) === 2)
return true;
const msg = String(error || "").toLowerCase();
return msg.indexOf("file does not exist") !== -1
|| msg.indexOf("no such file") !== -1
|| msg.indexOf("enoent") !== -1;
}
function loadPluginSettings() { function loadPluginSettings() {
_pluginSettingsLoading = true; try {
parsePluginSettings(pluginSettingsFile.text()); parsePluginSettings(pluginSettingsFile.text());
_pluginSettingsLoading = false; } catch (e) {
const msg = e.message || String(e);
if (!_isMissingPluginSettingsError(e))
console.warn("SettingsData: Failed to load plugin_settings.json. Error:", msg);
_resetPluginSettings();
}
} }
function parsePluginSettings(content) { function parsePluginSettings(content) {
@@ -2704,6 +2769,7 @@ Singleton {
blockLoading: true blockLoading: true
blockWrites: true blockWrites: true
atomicWrites: true atomicWrites: true
printErrors: false
watchChanges: !isGreeterMode watchChanges: !isGreeterMode
onLoaded: { onLoaded: {
if (!isGreeterMode) { if (!isGreeterMode) {
@@ -2712,7 +2778,10 @@ Singleton {
} }
onLoadFailed: error => { onLoadFailed: error => {
if (!isGreeterMode) { if (!isGreeterMode) {
pluginSettings = {}; const msg = String(error || "");
if (!_isMissingPluginSettingsError(error))
console.warn("SettingsData: Failed to load plugin_settings.json. Error:", msg);
_resetPluginSettings();
} }
} }
} }
+5 -21
View File
@@ -960,24 +960,6 @@ Singleton {
"expressiveEffects": [0.34, 0.8, 0.34, 1, 1, 1] "expressiveEffects": [0.34, 0.8, 0.34, 1, 1, 1]
} }
// Delegates to AnimVariants.qml for curves, timing, scale, and offsets.
readonly property list<real> variantEnterCurve: AnimVariants.variantEnterCurve
readonly property list<real> variantExitCurve: AnimVariants.variantExitCurve
readonly property list<real> variantModalEnterCurve: AnimVariants.variantModalEnterCurve
readonly property list<real> variantModalExitCurve: AnimVariants.variantModalExitCurve
readonly property list<real> variantPopoutEnterCurve: AnimVariants.variantPopoutEnterCurve
readonly property list<real> variantPopoutExitCurve: AnimVariants.variantPopoutExitCurve
readonly property real variantEnterDurationFactor: AnimVariants.variantEnterDurationFactor
readonly property real variantExitDurationFactor: AnimVariants.variantExitDurationFactor
readonly property real variantOpacityDurationScale: AnimVariants.variantOpacityDurationScale
readonly property bool isDirectionalEffect: AnimVariants.isDirectionalEffect
readonly property bool isDepthEffect: AnimVariants.isDepthEffect
readonly property real effectScaleCollapsed: AnimVariants.effectScaleCollapsed
readonly property real effectAnimOffset: AnimVariants.effectAnimOffset
function variantDuration(baseDuration, entering) { return AnimVariants.variantDuration(baseDuration, entering); }
function variantExitCleanupPadding() { return AnimVariants.variantExitCleanupPadding(); }
function variantCloseInterval(baseDuration) { return AnimVariants.variantCloseInterval(baseDuration); }
readonly property var animationPresetDurations: { readonly property var animationPresetDurations: {
"none": 0, "none": 0,
"short": 250, "short": 250,
@@ -1102,7 +1084,7 @@ Singleton {
property string fontFamily: { property string fontFamily: {
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") { if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
return GreetdSettings.fontFamily; return GreetdSettings.getEffectiveFontFamily();
} }
return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable"; return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable";
} }
@@ -1569,7 +1551,7 @@ Singleton {
if (typeof SettingsData !== "undefined") { if (typeof SettingsData !== "undefined") {
const skipTemplates = []; const skipTemplates = [];
if (!SettingsData.runDmsMatugenTemplates) { if (!SettingsData.runDmsMatugenTemplates) {
skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode", "emacs"); skipTemplates.push("gtk", "nvim", "niri", "qt5ct", "qt6ct", "firefox", "pywalfox", "zenbrowser", "vesktop", "equibop", "ghostty", "kitty", "foot", "alacritty", "wezterm", "dgop", "kcolorscheme", "vscode", "emacs", "zed");
} else { } else {
if (!SettingsData.matugenTemplateGtk) if (!SettingsData.matugenTemplateGtk)
skipTemplates.push("gtk"); skipTemplates.push("gtk");
@@ -1613,6 +1595,8 @@ Singleton {
skipTemplates.push("vscode"); skipTemplates.push("vscode");
if (!SettingsData.matugenTemplateEmacs) if (!SettingsData.matugenTemplateEmacs)
skipTemplates.push("emacs"); skipTemplates.push("emacs");
if (!SettingsData.matugenTemplateZed)
skipTemplates.push("zed");
} }
if (skipTemplates.length > 0) { if (skipTemplates.length > 0) {
args.push("--skip-templates", skipTemplates.join(",")); args.push("--skip-templates", skipTemplates.join(","));
@@ -2005,7 +1989,7 @@ Singleton {
FileView { FileView {
id: dynamicColorsFileView id: dynamicColorsFileView
path: { path: {
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms"; const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json"; const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json";
return colorsPath; return colorsPath;
} }
+509 -21
View File
@@ -10,22 +10,352 @@ Singleton {
property var settingsRoot: null property var settingsRoot: null
property string greetdPamText: ""
property string systemAuthPamText: ""
property string commonAuthPamText: ""
property string passwordAuthPamText: ""
property string systemLoginPamText: ""
property string systemLocalLoginPamText: ""
property string commonAuthPcPamText: ""
property string loginPamText: ""
property string dankshellU2fPamText: ""
property string u2fKeysText: ""
property string fingerprintProbeOutput: ""
property int fingerprintProbeExitCode: 0
property bool fingerprintProbeStreamFinished: false
property bool fingerprintProbeExited: false
property string fingerprintProbeState: "probe_failed"
property string pamSupportProbeOutput: ""
property bool pamSupportProbeStreamFinished: false
property bool pamSupportProbeExited: false
property int pamSupportProbeExitCode: 0
property bool pamFprintSupportDetected: false
property bool pamU2fSupportDetected: false
readonly property string homeDir: Quickshell.env("HOME") || ""
readonly property string u2fKeysPath: homeDir ? homeDir + "/.config/Yubico/u2f_keys" : ""
readonly property bool homeU2fKeysDetected: u2fKeysPath !== "" && u2fKeysWatcher.loaded && u2fKeysText.trim() !== ""
readonly property bool lockU2fCustomConfigDetected: pamModuleEnabled(dankshellU2fPamText, "pam_u2f")
readonly property bool greeterPamHasFprint: greeterPamStackHasModule("pam_fprintd")
readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f")
function envFlag(name) {
const value = (Quickshell.env(name) || "").trim().toLowerCase();
if (value === "1" || value === "true" || value === "yes" || value === "on")
return true;
if (value === "0" || value === "false" || value === "no" || value === "off")
return false;
return null;
}
readonly property var forcedFprintAvailable: envFlag("DMS_FORCE_FPRINT_AVAILABLE")
readonly property var forcedU2fAvailable: envFlag("DMS_FORCE_U2F_AVAILABLE")
function detectQtTools() { function detectQtTools() {
qtToolsDetectionProcess.running = true; qtToolsDetectionProcess.running = true;
} }
function detectAuthCapabilities() {
if (!settingsRoot)
return;
if (forcedFprintAvailable === null) {
fingerprintProbeOutput = "";
fingerprintProbeStreamFinished = false;
fingerprintProbeExited = false;
fingerprintProbeProcess.running = true;
} else {
fingerprintProbeState = forcedFprintAvailable ? "ready" : "probe_failed";
}
if (forcedFprintAvailable === null || forcedU2fAvailable === null) {
pamFprintSupportDetected = false;
pamU2fSupportDetected = false;
pamSupportProbeOutput = "";
pamSupportProbeStreamFinished = false;
pamSupportProbeExited = false;
pamSupportDetectionProcess.running = true;
}
recomputeAuthCapabilities();
}
function detectFprintd() { function detectFprintd() {
fprintdDetectionProcess.running = true; detectAuthCapabilities();
} }
function detectU2f() { function detectU2f() {
u2fDetectionProcess.running = true; detectAuthCapabilities();
} }
function checkPluginSettings() { function checkPluginSettings() {
pluginSettingsCheckProcess.running = true; pluginSettingsCheckProcess.running = true;
} }
function stripPamComment(line) {
if (!line)
return "";
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#"))
return "";
const hashIdx = trimmed.indexOf("#");
if (hashIdx >= 0)
return trimmed.substring(0, hashIdx).trim();
return trimmed;
}
function pamModuleEnabled(pamText, moduleName) {
if (!pamText || !moduleName)
return false;
const lines = pamText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (!line)
continue;
if (line.includes(moduleName))
return true;
}
return false;
}
function pamTextIncludesFile(pamText, filename) {
if (!pamText || !filename)
return false;
const lines = pamText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (!line)
continue;
if (line.includes(filename) && (line.includes("include") || line.includes("substack") || line.startsWith("@include")))
return true;
}
return false;
}
function greeterPamStackHasModule(moduleName) {
if (pamModuleEnabled(greetdPamText, moduleName))
return true;
const includedPamStacks = [
["system-auth", systemAuthPamText],
["common-auth", commonAuthPamText],
["password-auth", passwordAuthPamText],
["system-login", systemLoginPamText],
["system-local-login", systemLocalLoginPamText],
["common-auth-pc", commonAuthPcPamText],
["login", loginPamText]
];
for (let i = 0; i < includedPamStacks.length; i++) {
const stack = includedPamStacks[i];
if (pamTextIncludesFile(greetdPamText, stack[0]) && pamModuleEnabled(stack[1], moduleName))
return true;
}
return false;
}
function hasEnrolledFingerprintOutput(output) {
const lower = (output || "").toLowerCase();
if (lower.includes("has fingers enrolled") || lower.includes("has fingerprints enrolled"))
return true;
const lines = lower.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const trimmed = lines[i].trim();
if (trimmed.startsWith("finger:"))
return true;
if (trimmed.startsWith("- ") && trimmed.includes("finger"))
return true;
}
return false;
}
function hasMissingFingerprintEnrollmentOutput(output) {
const lower = (output || "").toLowerCase();
return lower.includes("no fingers enrolled")
|| lower.includes("no fingerprints enrolled")
|| lower.includes("no prints enrolled");
}
function hasMissingFingerprintReaderOutput(output) {
const lower = (output || "").toLowerCase();
return lower.includes("no devices available")
|| lower.includes("no device available")
|| lower.includes("no devices found")
|| lower.includes("list_devices failed")
|| lower.includes("no device");
}
function parseFingerprintProbe(exitCode, output) {
if (hasEnrolledFingerprintOutput(output))
return "ready";
if (hasMissingFingerprintEnrollmentOutput(output))
return "missing_enrollment";
if (hasMissingFingerprintReaderOutput(output))
return "missing_reader";
if (exitCode === 0)
return "missing_enrollment";
if (exitCode === 127 || (output || "").includes("__missing_command__"))
return "probe_failed";
return pamFprintSupportDetected ? "probe_failed" : "missing_pam_support";
}
function setLockFingerprintCapability(canEnable, ready, reason) {
settingsRoot.lockFingerprintCanEnable = canEnable;
settingsRoot.lockFingerprintReady = ready;
settingsRoot.lockFingerprintReason = reason;
}
function setLockU2fCapability(canEnable, ready, reason) {
settingsRoot.lockU2fCanEnable = canEnable;
settingsRoot.lockU2fReady = ready;
settingsRoot.lockU2fReason = reason;
}
function setGreeterFingerprintCapability(canEnable, ready, reason, source) {
settingsRoot.greeterFingerprintCanEnable = canEnable;
settingsRoot.greeterFingerprintReady = ready;
settingsRoot.greeterFingerprintReason = reason;
settingsRoot.greeterFingerprintSource = source;
}
function setGreeterU2fCapability(canEnable, ready, reason, source) {
settingsRoot.greeterU2fCanEnable = canEnable;
settingsRoot.greeterU2fReady = ready;
settingsRoot.greeterU2fReason = reason;
settingsRoot.greeterU2fSource = source;
}
function recomputeFingerprintCapabilities() {
if (forcedFprintAvailable !== null) {
const reason = forcedFprintAvailable ? "ready" : "probe_failed";
const source = forcedFprintAvailable ? "dms" : "none";
setLockFingerprintCapability(forcedFprintAvailable, forcedFprintAvailable, reason);
setGreeterFingerprintCapability(forcedFprintAvailable, forcedFprintAvailable, reason, source);
return;
}
const state = fingerprintProbeState;
switch (state) {
case "ready":
setLockFingerprintCapability(true, true, "ready");
break;
case "missing_enrollment":
setLockFingerprintCapability(true, false, "missing_enrollment");
break;
case "missing_reader":
setLockFingerprintCapability(false, false, "missing_reader");
break;
case "missing_pam_support":
setLockFingerprintCapability(false, false, "missing_pam_support");
break;
default:
setLockFingerprintCapability(false, false, "probe_failed");
break;
}
if (greeterPamHasFprint) {
switch (state) {
case "ready":
setGreeterFingerprintCapability(true, true, "configured_externally", "pam");
break;
case "missing_enrollment":
setGreeterFingerprintCapability(true, false, "missing_enrollment", "pam");
break;
case "missing_reader":
setGreeterFingerprintCapability(false, false, "missing_reader", "pam");
break;
default:
setGreeterFingerprintCapability(true, false, "probe_failed", "pam");
break;
}
return;
}
switch (state) {
case "ready":
setGreeterFingerprintCapability(true, true, "ready", "dms");
break;
case "missing_enrollment":
setGreeterFingerprintCapability(true, false, "missing_enrollment", "dms");
break;
case "missing_reader":
setGreeterFingerprintCapability(false, false, "missing_reader", "none");
break;
case "missing_pam_support":
setGreeterFingerprintCapability(false, false, "missing_pam_support", "none");
break;
default:
setGreeterFingerprintCapability(false, false, "probe_failed", "none");
break;
}
}
function recomputeU2fCapabilities() {
if (forcedU2fAvailable !== null) {
const reason = forcedU2fAvailable ? "ready" : "probe_failed";
const source = forcedU2fAvailable ? "dms" : "none";
setLockU2fCapability(forcedU2fAvailable, forcedU2fAvailable, reason);
setGreeterU2fCapability(forcedU2fAvailable, forcedU2fAvailable, reason, source);
return;
}
const lockReady = lockU2fCustomConfigDetected || homeU2fKeysDetected;
const lockCanEnable = lockReady || pamU2fSupportDetected;
const lockReason = lockReady ? "ready" : (lockCanEnable ? "missing_key_registration" : "missing_pam_support");
setLockU2fCapability(lockCanEnable, lockReady, lockReason);
if (greeterPamHasU2f) {
setGreeterU2fCapability(true, true, "configured_externally", "pam");
return;
}
const greeterReady = homeU2fKeysDetected;
const greeterCanEnable = greeterReady || pamU2fSupportDetected;
const greeterReason = greeterReady ? "ready" : (greeterCanEnable ? "missing_key_registration" : "missing_pam_support");
setGreeterU2fCapability(greeterCanEnable, greeterReady, greeterReason, greeterCanEnable ? "dms" : "none");
}
function recomputeAuthCapabilities() {
if (!settingsRoot)
return;
recomputeFingerprintCapabilities();
recomputeU2fCapabilities();
settingsRoot.fprintdAvailable = settingsRoot.lockFingerprintReady || settingsRoot.greeterFingerprintReady;
settingsRoot.u2fAvailable = settingsRoot.lockU2fReady || settingsRoot.greeterU2fReady;
}
function finalizeFingerprintProbe() {
if (!fingerprintProbeStreamFinished || !fingerprintProbeExited)
return;
fingerprintProbeState = parseFingerprintProbe(fingerprintProbeExitCode, fingerprintProbeOutput);
recomputeAuthCapabilities();
}
function finalizePamSupportProbe() {
if (!pamSupportProbeStreamFinished || !pamSupportProbeExited)
return;
pamFprintSupportDetected = false;
pamU2fSupportDetected = false;
const lines = (pamSupportProbeOutput || "").trim().split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const parts = lines[i].split(":");
if (parts.length !== 2)
continue;
if (parts[0] === "pam_fprintd.so")
pamFprintSupportDetected = parts[1] === "true";
else if (parts[0] === "pam_u2f.so")
pamU2fSupportDetected = parts[1] === "true";
}
if (forcedFprintAvailable === null && fingerprintProbeState === "missing_pam_support")
fingerprintProbeState = parseFingerprintProbe(fingerprintProbeExitCode, fingerprintProbeOutput);
recomputeAuthCapabilities();
}
property var qtToolsDetectionProcess: Process { property var qtToolsDetectionProcess: Process {
command: ["sh", "-c", "echo -n 'qt5ct:'; command -v qt5ct >/dev/null && echo 'true' || echo 'false'; echo -n 'qt6ct:'; command -v qt6ct >/dev/null && echo 'true' || echo 'false'; echo -n 'gtk:'; (command -v gsettings >/dev/null || command -v dconf >/dev/null) && echo 'true' || echo 'false'"] command: ["sh", "-c", "echo -n 'qt5ct:'; command -v qt5ct >/dev/null && echo 'true' || echo 'false'; echo -n 'qt6ct:'; command -v qt6ct >/dev/null && echo 'true' || echo 'false'; echo -n 'gtk:'; (command -v gsettings >/dev/null || command -v dconf >/dev/null) && echo 'true' || echo 'false'"]
running: false running: false
@@ -35,15 +365,15 @@ Singleton {
if (!settingsRoot) if (!settingsRoot)
return; return;
if (text && text.trim()) { if (text && text.trim()) {
var lines = text.trim().split('\n'); const lines = text.trim().split("\n");
for (var i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
var line = lines[i]; const line = lines[i];
if (line.startsWith('qt5ct:')) { if (line.startsWith("qt5ct:")) {
settingsRoot.qt5ctAvailable = line.split(':')[1] === 'true'; settingsRoot.qt5ctAvailable = line.split(":")[1] === "true";
} else if (line.startsWith('qt6ct:')) { } else if (line.startsWith("qt6ct:")) {
settingsRoot.qt6ctAvailable = line.split(':')[1] === 'true'; settingsRoot.qt6ctAvailable = line.split(":")[1] === "true";
} else if (line.startsWith('gtk:')) { } else if (line.startsWith("gtk:")) {
settingsRoot.gtkAvailable = line.split(':')[1] === 'true'; settingsRoot.gtkAvailable = line.split(":")[1] === "true";
} }
} }
} }
@@ -51,23 +381,181 @@ Singleton {
} }
} }
property var fprintdDetectionProcess: Process { property var fingerprintProbeProcess: Process {
command: ["sh", "-c", "command -v fprintd-list >/dev/null 2>&1"] command: ["sh", "-c", "if command -v fprintd-list >/dev/null 2>&1; then fprintd-list \"${USER:-$(id -un)}\" 2>&1; else printf '__missing_command__\\n'; exit 127; fi"]
running: false running: false
stdout: StdioCollector {
onStreamFinished: {
root.fingerprintProbeOutput = text || "";
root.fingerprintProbeStreamFinished = true;
root.finalizeFingerprintProbe();
}
}
onExited: function (exitCode) { onExited: function (exitCode) {
if (!settingsRoot) root.fingerprintProbeExitCode = exitCode;
return; root.fingerprintProbeExited = true;
settingsRoot.fprintdAvailable = (exitCode === 0); root.finalizeFingerprintProbe();
} }
} }
property var u2fDetectionProcess: Process { property var pamSupportDetectionProcess: Process {
command: ["sh", "-c", "(test -f /usr/lib/security/pam_u2f.so || test -f /usr/lib64/security/pam_u2f.so) && (test -f /etc/pam.d/dankshell-u2f || test -f \"$HOME/.config/Yubico/u2f_keys\")"] command: ["sh", "-c", "for module in pam_fprintd.so pam_u2f.so; do found=false; for dir in /usr/lib64/security /usr/lib/security /lib/security /lib/x86_64-linux-gnu/security /usr/lib/x86_64-linux-gnu/security /usr/lib/aarch64-linux-gnu/security /run/current-system/sw/lib/security; do if [ -f \"$dir/$module\" ]; then found=true; break; fi; done; printf '%s:%s\\n' \"$module\" \"$found\"; done"]
running: false running: false
stdout: StdioCollector {
onStreamFinished: {
root.pamSupportProbeOutput = text || "";
root.pamSupportProbeStreamFinished = true;
root.finalizePamSupportProbe();
}
}
onExited: function (exitCode) { onExited: function (exitCode) {
if (!settingsRoot) root.pamSupportProbeExitCode = exitCode;
return; root.pamSupportProbeExited = true;
settingsRoot.u2fAvailable = (exitCode === 0); root.finalizePamSupportProbe();
}
}
FileView {
id: greetdPamWatcher
path: "/etc/pam.d/greetd"
printErrors: false
onLoaded: {
root.greetdPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.greetdPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: systemAuthPamWatcher
path: "/etc/pam.d/system-auth"
printErrors: false
onLoaded: {
root.systemAuthPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.systemAuthPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: commonAuthPamWatcher
path: "/etc/pam.d/common-auth"
printErrors: false
onLoaded: {
root.commonAuthPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.commonAuthPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: passwordAuthPamWatcher
path: "/etc/pam.d/password-auth"
printErrors: false
onLoaded: {
root.passwordAuthPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.passwordAuthPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: systemLoginPamWatcher
path: "/etc/pam.d/system-login"
printErrors: false
onLoaded: {
root.systemLoginPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.systemLoginPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: systemLocalLoginPamWatcher
path: "/etc/pam.d/system-local-login"
printErrors: false
onLoaded: {
root.systemLocalLoginPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.systemLocalLoginPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: commonAuthPcPamWatcher
path: "/etc/pam.d/common-auth-pc"
printErrors: false
onLoaded: {
root.commonAuthPcPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.commonAuthPcPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: loginPamWatcher
path: "/etc/pam.d/login"
printErrors: false
onLoaded: {
root.loginPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.loginPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: dankshellU2fPamWatcher
path: "/etc/pam.d/dankshell-u2f"
printErrors: false
onLoaded: {
root.dankshellU2fPamText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.dankshellU2fPamText = "";
root.recomputeAuthCapabilities();
}
}
FileView {
id: u2fKeysWatcher
path: root.u2fKeysPath
printErrors: false
onLoaded: {
root.u2fKeysText = text();
root.recomputeAuthCapabilities();
}
onLoadFailed: {
root.u2fKeysText = "";
root.recomputeAuthCapabilities();
} }
} }
+43 -4
View File
@@ -33,6 +33,7 @@ var SPEC = {
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" }, mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
firstDayOfWeek: { def: -1 }, firstDayOfWeek: { def: -1 },
showWeekNumber: { def: false },
use24HourClock: { def: true }, use24HourClock: { def: true },
showSeconds: { def: false }, showSeconds: { def: false },
padHours12Hour: { def: false }, padHours12Hour: { def: false },
@@ -47,9 +48,6 @@ var SPEC = {
modalAnimationSpeed: { def: 1 }, modalAnimationSpeed: { def: 1 },
modalCustomAnimationDuration: { def: 150 }, modalCustomAnimationDuration: { def: 150 },
enableRippleEffects: { def: true }, enableRippleEffects: { def: true },
animationVariant: { def: 0 },
motionEffect: { def: 0 },
directionalAnimationMode: { def: 0 },
m3ElevationEnabled: { def: true }, m3ElevationEnabled: { def: true },
m3ElevationIntensity: { def: 12 }, m3ElevationIntensity: { def: 12 },
m3ElevationOpacity: { def: 30 }, m3ElevationOpacity: { def: 30 },
@@ -126,6 +124,7 @@ var SPEC = {
showOccupiedWorkspacesOnly: { def: false }, showOccupiedWorkspacesOnly: { def: false },
reverseScrolling: { def: false }, reverseScrolling: { def: false },
dwlShowAllTags: { def: false }, dwlShowAllTags: { def: false },
workspaceActiveAppHighlightEnabled: { def: false },
workspaceColorMode: { def: "default" }, workspaceColorMode: { def: "default" },
workspaceOccupiedColorMode: { def: "none" }, workspaceOccupiedColorMode: { def: "none" },
workspaceUnfocusedColorMode: { def: "default" }, workspaceUnfocusedColorMode: { def: "default" },
@@ -167,6 +166,17 @@ var SPEC = {
centeringMode: { def: "index" }, centeringMode: { def: "index" },
clockDateFormat: { def: "" }, clockDateFormat: { def: "" },
lockDateFormat: { def: "" }, lockDateFormat: { def: "" },
greeterRememberLastSession: { def: true },
greeterRememberLastUser: { def: true },
greeterEnableFprint: { def: false },
greeterEnableU2f: { def: false },
greeterWallpaperPath: { def: "" },
greeterUse24HourClock: { def: true },
greeterShowSeconds: { def: false },
greeterPadHours12Hour: { def: false },
greeterLockDateFormat: { def: "" },
greeterFontFamily: { def: "" },
greeterWallpaperFillMode: { def: "" },
mediaSize: { def: 1 }, mediaSize: { def: 1 },
appLauncherViewMode: { def: "list" }, appLauncherViewMode: { def: "list" },
@@ -259,6 +269,11 @@ var SPEC = {
syncModeWithPortal: { def: true }, syncModeWithPortal: { def: true },
terminalsAlwaysDark: { def: false, onChange: "regenSystemThemes" }, terminalsAlwaysDark: { def: false, onChange: "regenSystemThemes" },
muxType: { def: "tmux" },
muxUseCustomCommand: { def: false },
muxCustomCommand: { def: "" },
muxSessionFilter: { def: "" },
runDmsMatugenTemplates: { def: true }, runDmsMatugenTemplates: { def: true },
matugenTemplateGtk: { def: true }, matugenTemplateGtk: { def: true },
matugenTemplateNiri: { def: true }, matugenTemplateNiri: { def: true },
@@ -275,17 +290,26 @@ var SPEC = {
matugenTemplateKitty: { def: true }, matugenTemplateKitty: { def: true },
matugenTemplateFoot: { def: true }, matugenTemplateFoot: { def: true },
matugenTemplateAlacritty: { def: true }, matugenTemplateAlacritty: { def: true },
matugenTemplateNeovim: { def: true }, matugenTemplateNeovim: { def: false },
matugenTemplateWezterm: { def: true }, matugenTemplateWezterm: { def: true },
matugenTemplateDgop: { def: true }, matugenTemplateDgop: { def: true },
matugenTemplateKcolorscheme: { def: true }, matugenTemplateKcolorscheme: { def: true },
matugenTemplateVscode: { def: true }, matugenTemplateVscode: { def: true },
matugenTemplateEmacs: { def: true }, matugenTemplateEmacs: { def: true },
matugenTemplateZed: { def: true },
matugenTemplateNeovimSettings: {
def: {
dark: { baseTheme: "github_dark", harmony: 0.5 },
light: { baseTheme: "github_light", harmony: 0.5 }
}
},
showDock: { def: false }, showDock: { def: false },
dockAutoHide: { def: false }, dockAutoHide: { def: false },
dockSmartAutoHide: { def: false }, dockSmartAutoHide: { def: false },
dockGroupByApp: { def: false }, dockGroupByApp: { def: false },
dockRestoreSpecialWorkspaceOnClick: { def: false },
dockOpenOnOverview: { def: false }, dockOpenOnOverview: { def: false },
dockPosition: { def: 1 }, dockPosition: { def: 1 },
dockSpacing: { def: 4 }, dockSpacing: { def: 4 },
@@ -330,9 +354,23 @@ var SPEC = {
enableFprint: { def: false }, enableFprint: { def: false },
maxFprintTries: { def: 15 }, maxFprintTries: { def: 15 },
fprintdAvailable: { def: false, persist: false }, fprintdAvailable: { def: false, persist: false },
lockFingerprintCanEnable: { def: false, persist: false },
lockFingerprintReady: { def: false, persist: false },
lockFingerprintReason: { def: "probe_failed", persist: false },
greeterFingerprintCanEnable: { def: false, persist: false },
greeterFingerprintReady: { def: false, persist: false },
greeterFingerprintReason: { def: "probe_failed", persist: false },
greeterFingerprintSource: { def: "none", persist: false },
enableU2f: { def: false }, enableU2f: { def: false },
u2fMode: { def: "or" }, u2fMode: { def: "or" },
u2fAvailable: { def: false, persist: false }, u2fAvailable: { def: false, persist: false },
lockU2fCanEnable: { def: false, persist: false },
lockU2fReady: { def: false, persist: false },
lockU2fReason: { def: "probe_failed", persist: false },
greeterU2fCanEnable: { def: false, persist: false },
greeterU2fReady: { def: false, persist: false },
greeterU2fReason: { def: "probe_failed", persist: false },
greeterU2fSource: { def: "none", persist: false },
lockScreenActiveMonitor: { def: "all" }, lockScreenActiveMonitor: { def: "all" },
lockScreenInactiveColor: { def: "#000000" }, lockScreenInactiveColor: { def: "#000000" },
lockScreenNotificationMode: { def: 0 }, lockScreenNotificationMode: { def: 0 },
@@ -355,6 +393,7 @@ var SPEC = {
notificationHistorySaveNormal: { def: true }, notificationHistorySaveNormal: { def: true },
notificationHistorySaveCritical: { def: true }, notificationHistorySaveCritical: { def: true },
notificationRules: { def: [] }, notificationRules: { def: [] },
notificationFocusedMonitor: { def: false },
osdAlwaysShowValue: { def: false }, osdAlwaysShowValue: { def: false },
osdPosition: { def: 5 }, osdPosition: { def: 5 },
-2
View File
@@ -1,7 +1,5 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Services.Greetd
import qs.Common
import qs.Modules.Greetd import qs.Modules.Greetd
Scope { Scope {
+6 -1
View File
@@ -7,6 +7,7 @@ import qs.Modals.Clipboard
import qs.Modals.Greeter import qs.Modals.Greeter
import qs.Modals.Settings import qs.Modals.Settings
import qs.Modals.DankLauncherV2 import qs.Modals.DankLauncherV2
import qs.Modals
import qs.Modules import qs.Modules
import qs.Modules.AppDrawer import qs.Modules.AppDrawer
import qs.Modules.DankDash import qs.Modules.DankDash
@@ -313,7 +314,7 @@ Item {
} }
Variants { Variants {
model: SettingsData.getFilteredScreens("notifications") model: SettingsData.notificationFocusedMonitor ? Quickshell.screens : SettingsData.getFilteredScreens("notifications")
delegate: NotificationPopupManager { delegate: NotificationPopupManager {
modelData: item modelData: item
@@ -619,6 +620,10 @@ Item {
} }
} }
MuxModal {
id: muxModal
}
ClipboardHistoryModal { ClipboardHistoryModal {
id: clipboardHistoryModalPopup id: clipboardHistoryModalPopup
+50 -169
View File
@@ -26,10 +26,10 @@ Item {
property bool closeOnBackgroundClick: true property bool closeOnBackgroundClick: true
property string animationType: "scale" property string animationType: "scale"
property int animationDuration: Theme.modalAnimationDuration property int animationDuration: Theme.modalAnimationDuration
property real animationScaleCollapsed: Theme.effectScaleCollapsed property real animationScaleCollapsed: 0.96
property real animationOffset: Theme.effectAnimOffset property real animationOffset: Theme.spacingL
property list<real> animationEnterCurve: Theme.variantModalEnterCurve property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
property list<real> animationExitCurve: Theme.variantModalExitCurve property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
property color backgroundColor: Theme.surfaceContainer property color backgroundColor: Theme.surfaceContainer
property color borderColor: Theme.outlineMedium property color borderColor: Theme.outlineMedium
property real borderWidth: 0 property real borderWidth: 0
@@ -44,13 +44,11 @@ Item {
property bool keepPopoutsOpen: false property bool keepPopoutsOpen: false
property var customKeyboardFocus: null property var customKeyboardFocus: null
property bool useOverlayLayer: false property bool useOverlayLayer: false
property real frozenMotionOffsetX: 0
property real frozenMotionOffsetY: 0
readonly property alias contentWindow: contentWindow readonly property alias contentWindow: contentWindow
readonly property alias clickCatcher: clickCatcher readonly property alias clickCatcher: clickCatcher
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground
readonly property bool useSingleWindow: CompositorService.isHyprland readonly property bool useSingleWindow: CompositorService.isHyprland || useBackground
signal opened signal opened
signal dialogClosed signal dialogClosed
@@ -60,34 +58,19 @@ Item {
function open() { function open() {
closeTimer.stop(); closeTimer.stop();
animationsEnabled = false;
frozenMotionOffsetX = modalContainer ? modalContainer.offsetX : 0;
frozenMotionOffsetY = modalContainer ? modalContainer.offsetY : animationOffset;
const focusedScreen = CompositorService.getFocusedScreen(); const focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen) { if (focusedScreen) {
contentWindow.screen = focusedScreen; contentWindow.screen = focusedScreen;
if (!useSingleWindow) if (!useSingleWindow)
clickCatcher.screen = focusedScreen; clickCatcher.screen = focusedScreen;
} }
if (Theme.isDirectionalEffect || root.useBackground) {
if (!useSingleWindow)
clickCatcher.visible = true;
contentWindow.visible = true;
}
ModalManager.openModal(root); ModalManager.openModal(root);
shouldBeVisible = true;
Qt.callLater(() => { if (!useSingleWindow)
animationsEnabled = true; clickCatcher.visible = true;
shouldBeVisible = true; contentWindow.visible = true;
if (!useSingleWindow && !clickCatcher.visible) shouldHaveFocus = false;
clickCatcher.visible = true; Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
if (!contentWindow.visible)
contentWindow.visible = true;
shouldHaveFocus = false;
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
});
} }
function close() { function close() {
@@ -148,7 +131,7 @@ Item {
Timer { Timer {
id: closeTimer id: closeTimer
interval: Theme.variantCloseInterval(animationDuration) interval: animationDuration + 50
onTriggered: { onTriggered: {
if (shouldBeVisible) if (shouldBeVisible)
return; return;
@@ -162,17 +145,7 @@ Item {
readonly property var shadowLevel: Theme.elevationLevel3 readonly property var shadowLevel: Theme.elevationLevel3
readonly property real shadowFallbackOffset: 6 readonly property real shadowFallbackOffset: 6
readonly property real shadowRenderPadding: (root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0 readonly property real shadowRenderPadding: (root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
readonly property real shadowMotionPadding: { readonly property real shadowMotionPadding: animationType === "slide" ? 30 : Math.max(0, animationOffset)
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0 && Theme.isDirectionalEffect)
return 0; // Wayland native overlap mask
if (animationType === "slide")
return 30;
if (Theme.isDirectionalEffect)
return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.9);
if (Theme.isDepthEffect)
return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.35);
return Math.max(0, animationOffset);
}
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr) readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
readonly property real alignedWidth: Theme.px(modalWidth, dpr) readonly property real alignedWidth: Theme.px(modalWidth, dpr)
readonly property real alignedHeight: Theme.px(modalHeight, dpr) readonly property real alignedHeight: Theme.px(modalHeight, dpr)
@@ -232,26 +205,9 @@ Item {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
enabled: !root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible enabled: root.closeOnBackgroundClick && root.shouldBeVisible
onClicked: root.backgroundClicked() onClicked: root.backgroundClicked()
} }
Rectangle {
anchors.fill: parent
z: -1
color: "black"
opacity: (!root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: opacity > 0
Behavior on opacity {
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
NumberAnimation {
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
}
} }
PanelWindow { PanelWindow {
@@ -294,12 +250,9 @@ Item {
bottom: root.useSingleWindow bottom: root.useSingleWindow
} }
readonly property real actualMarginLeft: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
readonly property real actualMarginTop: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
WlrLayershell.margins { WlrLayershell.margins {
left: actualMarginLeft left: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
top: actualMarginTop top: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
right: 0 right: 0
bottom: 0 bottom: 0
} }
@@ -329,14 +282,13 @@ Item {
anchors.fill: parent anchors.fill: parent
z: -1 z: -1
color: "black" color: "black"
opacity: (root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0 opacity: root.useBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: opacity > 0 visible: root.useBackground
Behavior on opacity { Behavior on opacity {
enabled: root.animationsEnabled && !Theme.isDirectionalEffect enabled: root.animationsEnabled
NumberAnimation { DankAnim {
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale) duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
@@ -344,8 +296,8 @@ Item {
Item { Item {
id: modalContainer id: modalContainer
x: (root.useSingleWindow ? root.alignedX : (root.alignedX - contentWindow.actualMarginLeft)) + Theme.snap(animX, root.dpr) x: root.useSingleWindow ? root.alignedX : shadowBuffer
y: (root.useSingleWindow ? root.alignedY : (root.alignedY - contentWindow.actualMarginTop)) + Theme.snap(animY, root.dpr) y: root.useSingleWindow ? root.alignedY : shadowBuffer
width: root.alignedWidth width: root.alignedWidth
height: root.alignedHeight height: root.alignedHeight
@@ -361,117 +313,45 @@ Item {
} }
readonly property bool slide: root.animationType === "slide" readonly property bool slide: root.animationType === "slide"
readonly property bool directionalEffect: Theme.isDirectionalEffect readonly property real offsetX: slide ? 15 : 0
readonly property bool depthEffect: Theme.isDepthEffect readonly property real offsetY: slide ? -30 : root.animationOffset
readonly property real directionalTravel: Math.max(root.animationOffset, Math.max(root.alignedWidth, root.alignedHeight) * 0.8)
readonly property real depthTravel: Math.max(root.animationOffset * 0.8, 36)
readonly property real customAnchorX: root.alignedX + root.alignedWidth * 0.5
readonly property real customAnchorY: root.alignedY + root.alignedHeight * 0.5
readonly property real customDistLeft: customAnchorX
readonly property real customDistRight: root.screenWidth - customAnchorX
readonly property real customDistTop: customAnchorY
readonly property real customDistBottom: root.screenHeight - customAnchorY
readonly property real offsetX: {
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
return 0;
if (slide && !directionalEffect && !depthEffect)
return 15;
if (directionalEffect) {
switch (root.positioning) {
case "top-right":
return 0;
case "custom":
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
return -directionalTravel;
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
return directionalTravel;
return 0;
default:
return 0;
}
}
if (depthEffect) {
switch (root.positioning) {
case "top-right":
return 0;
case "custom":
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
return -depthTravel;
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
return depthTravel;
return 0;
default:
return 0;
}
}
return 0;
}
readonly property real offsetY: {
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
return 0;
if (slide && !directionalEffect && !depthEffect)
return -30;
if (directionalEffect) {
switch (root.positioning) {
case "top-right":
return -Math.max(directionalTravel * 0.65, 96);
case "custom":
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
return -directionalTravel;
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
return directionalTravel;
return 0;
default:
// Default to sliding down from top when centered
return -Math.max(directionalTravel, root.screenHeight * 0.24);
}
}
if (depthEffect) {
switch (root.positioning) {
case "top-right":
return -depthTravel * 0.75;
case "custom":
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
return -depthTravel;
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
return depthTravel;
return depthTravel * 0.45;
default:
return -depthTravel;
}
}
return root.animationOffset;
}
property real animX: root.shouldBeVisible ? 0 : root.frozenMotionOffsetX property real animX: 0
property real animY: root.shouldBeVisible ? 0 : root.frozenMotionOffsetY property real animY: 0
property real scaleValue: root.animationScaleCollapsed
readonly property real computedScaleCollapsed: (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) ? 0.0 : root.animationScaleCollapsed onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
property real scaleValue: root.shouldBeVisible ? 1.0 : computedScaleCollapsed onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
Connections {
target: root
function onShouldBeVisibleChanged() {
modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr);
modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr);
modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed;
}
}
Behavior on animX { Behavior on animX {
enabled: root.animationsEnabled enabled: root.animationsEnabled
NumberAnimation { DankAnim {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible) duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
Behavior on animY { Behavior on animY {
enabled: root.animationsEnabled enabled: root.animationsEnabled
NumberAnimation { DankAnim {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible) duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
Behavior on scaleValue { Behavior on scaleValue {
enabled: root.animationsEnabled enabled: root.animationsEnabled
NumberAnimation { DankAnim {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible) duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
@@ -487,14 +367,15 @@ Item {
id: animatedContent id: animatedContent
anchors.fill: parent anchors.fill: parent
clip: false clip: false
opacity: Theme.isDirectionalEffect ? 1 : (root.shouldBeVisible ? 1 : 0) opacity: root.shouldBeVisible ? 1 : 0
scale: modalContainer.scaleValue scale: modalContainer.scaleValue
transformOrigin: Item.Center x: Theme.snap(modalContainer.animX, root.dpr) + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5
y: Theme.snap(modalContainer.animY, root.dpr) + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5
Behavior on opacity { Behavior on opacity {
enabled: root.animationsEnabled && !Theme.isDirectionalEffect enabled: root.animationsEnabled
NumberAnimation { NumberAnimation {
duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale) duration: animationDuration
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
+312
View File
@@ -0,0 +1,312 @@
import QtQuick
import qs.Common
import qs.Modals.Common
import qs.Widgets
DankModal {
id: root
layerNamespace: "dms:input-modal"
keepPopoutsOpen: true
property string inputTitle: ""
property string inputMessage: ""
property string inputPlaceholder: ""
property string inputText: ""
property string confirmButtonText: "Confirm"
property string cancelButtonText: "Cancel"
property color confirmButtonColor: Theme.primary
property var onConfirm: function (text) {}
property var onCancel: function () {}
property int selectedButton: -1
property bool keyboardNavigation: false
function show(title, message, onConfirmCallback, onCancelCallback) {
inputTitle = title || "";
inputMessage = message || "";
inputPlaceholder = "";
inputText = "";
confirmButtonText = "Confirm";
cancelButtonText = "Cancel";
confirmButtonColor = Theme.primary;
onConfirm = onConfirmCallback || ((text) => {});
onCancel = onCancelCallback || (() => {});
selectedButton = -1;
keyboardNavigation = false;
open();
}
function showWithOptions(options) {
inputTitle = options.title || "";
inputMessage = options.message || "";
inputPlaceholder = options.placeholder || "";
inputText = options.initialText || "";
confirmButtonText = options.confirmText || "Confirm";
cancelButtonText = options.cancelText || "Cancel";
confirmButtonColor = options.confirmColor || Theme.primary;
onConfirm = options.onConfirm || ((text) => {});
onCancel = options.onCancel || (() => {});
selectedButton = -1;
keyboardNavigation = false;
open();
}
function confirmAndClose() {
const text = inputText;
close();
if (onConfirm) {
onConfirm(text);
}
}
function cancelAndClose() {
close();
if (onCancel) {
onCancel();
}
}
function selectButton() {
if (selectedButton === 0) {
cancelAndClose();
} else {
confirmAndClose();
}
}
shouldBeVisible: false
allowStacking: true
modalWidth: 350
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 200
enableShadow: true
shouldHaveFocus: true
onBackgroundClicked: cancelAndClose()
onOpened: {
Qt.callLater(function () {
if (contentLoader.item && contentLoader.item.textInputRef) {
contentLoader.item.textInputRef.forceActiveFocus();
}
});
}
content: Component {
FocusScope {
anchors.fill: parent
implicitHeight: mainColumn.implicitHeight
focus: true
property alias textInputRef: textInput
Keys.onPressed: function (event) {
const textFieldFocused = textInput.activeFocus;
switch (event.key) {
case Qt.Key_Escape:
root.cancelAndClose();
event.accepted = true;
break;
case Qt.Key_Tab:
if (textFieldFocused) {
root.keyboardNavigation = true;
root.selectedButton = 0;
textInput.focus = false;
} else {
root.keyboardNavigation = true;
if (root.selectedButton === -1) {
root.selectedButton = 0;
} else if (root.selectedButton === 0) {
root.selectedButton = 1;
} else {
root.selectedButton = -1;
textInput.forceActiveFocus();
}
}
event.accepted = true;
break;
case Qt.Key_Left:
if (!textFieldFocused) {
root.keyboardNavigation = true;
root.selectedButton = 0;
event.accepted = true;
}
break;
case Qt.Key_Right:
if (!textFieldFocused) {
root.keyboardNavigation = true;
root.selectedButton = 1;
event.accepted = true;
}
break;
case Qt.Key_Return:
case Qt.Key_Enter:
if (root.selectedButton !== -1) {
root.selectButton();
} else {
root.confirmAndClose();
}
event.accepted = true;
break;
}
}
Column {
id: mainColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
anchors.topMargin: Theme.spacingL
spacing: 0
StyledText {
text: root.inputTitle
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
width: parent.width
horizontalAlignment: Text.AlignHCenter
}
Item {
width: 1
height: Theme.spacingL
}
StyledText {
text: root.inputMessage
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
width: parent.width
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
visible: root.inputMessage !== ""
}
Item {
width: 1
height: root.inputMessage !== "" ? Theme.spacingL : 0
visible: root.inputMessage !== ""
}
Rectangle {
width: parent.width
height: 40
radius: Theme.cornerRadius
color: Theme.surfaceVariantAlpha
border.color: textInput.activeFocus ? Theme.primary : "transparent"
border.width: textInput.activeFocus ? 1 : 0
TextInput {
id: textInput
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
verticalAlignment: TextInput.AlignVCenter
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
selectionColor: Theme.primary
selectedTextColor: Theme.primaryText
clip: true
text: root.inputText
onTextChanged: root.inputText = text
StyledText {
anchors.fill: parent
verticalAlignment: Text.AlignVCenter
font.pixelSize: Theme.fontSizeMedium
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
text: root.inputPlaceholder
visible: textInput.text === "" && !textInput.activeFocus
}
}
}
Item {
width: 1
height: Theme.spacingL * 1.5
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Rectangle {
width: 120
height: 40
radius: Theme.cornerRadius
color: {
if (root.keyboardNavigation && root.selectedButton === 0) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
} else if (cancelButton.containsMouse) {
return Theme.surfacePressed;
} else {
return Theme.surfaceVariantAlpha;
}
}
border.color: (root.keyboardNavigation && root.selectedButton === 0) ? Theme.primary : "transparent"
border.width: (root.keyboardNavigation && root.selectedButton === 0) ? 1 : 0
StyledText {
text: root.cancelButtonText
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: cancelButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.cancelAndClose()
}
}
Rectangle {
width: 120
height: 40
radius: Theme.cornerRadius
color: {
const baseColor = root.confirmButtonColor;
if (root.keyboardNavigation && root.selectedButton === 1) {
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1);
} else if (confirmButton.containsMouse) {
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9);
} else {
return baseColor;
}
}
border.color: (root.keyboardNavigation && root.selectedButton === 1) ? "white" : "transparent"
border.width: (root.keyboardNavigation && root.selectedButton === 1) ? 1 : 0
StyledText {
text: root.confirmButtonText
font.pixelSize: Theme.fontSizeMedium
color: Theme.primaryText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: confirmButton
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.confirmAndClose()
}
}
}
Item {
width: 1
height: Theme.spacingL
}
}
}
}
}
@@ -207,9 +207,12 @@ Rectangle {
selectedActionIndex = 0; selectedActionIndex = 0;
} }
function cycleAction() { function cycleAction(reverse = false) {
if (actions.length > 0) { if (actions.length > 0) {
selectedActionIndex = (selectedActionIndex + 1) % actions.length; if (! reverse)
selectedActionIndex = (selectedActionIndex + 1) % actions.length;
else
selectedActionIndex = (selectedActionIndex - 1) % actions.length;
ensureSelectedVisible(); ensureSelectedVisible();
} }
} }
@@ -353,10 +353,13 @@ Item {
performSearch(); performSearch();
} }
function cycleMode() { function cycleMode(reverse = false) {
var modes = ["all", "apps", "files", "plugins"]; var modes = ["all", "apps", "files", "plugins"];
var currentIndex = modes.indexOf(searchMode); var currentIndex = modes.indexOf(searchMode);
var nextIndex = (currentIndex + 1) % modes.length; if (!reverse)
var nextIndex = (currentIndex + 1) % modes.length;
else
var nextIndex = (currentIndex - 1 + modes.length) % modes.length;
setMode(modes[nextIndex]); setMode(modes[nextIndex]);
} }
@@ -1006,9 +1009,7 @@ Item {
_applyHighlights(newSections, searchQuery); _applyHighlights(newSections, searchQuery);
flatModel = Scorer.flattenSections(newSections); flatModel = Scorer.flattenSections(newSections);
sections = newSections; sections = newSections;
if (selectedFlatIndex >= flatModel.length) { selectedFlatIndex = getFirstItemIndex();
selectedFlatIndex = getFirstItemIndex();
}
updateSelectedItem(); updateSelectedItem();
}); });
} }
@@ -14,7 +14,6 @@ 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
readonly property bool launcherMotionVisible: Theme.isDirectionalEffect ? spotlightOpen : _motionActive
property var spotlightContent: launcherContentLoader.item property var spotlightContent: launcherContentLoader.item
property bool openedFromOverview: false property bool openedFromOverview: false
property bool isClosing: false property bool isClosing: false
@@ -24,14 +23,8 @@ Item {
property string _pendingMode: "" property string _pendingMode: ""
readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
// Animation state matches DankPopout/DankModal pattern
property bool animationsEnabled: true
property bool _motionActive: false
property real _frozenMotionX: 0
property real _frozenMotionY: 0
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
readonly property var effectiveScreen: contentWindow.screen readonly property var effectiveScreen: launcherWindow.screen
readonly property real screenWidth: effectiveScreen?.width ?? 1920 readonly property real screenWidth: effectiveScreen?.width ?? 1920
readonly property real screenHeight: effectiveScreen?.height ?? 1080 readonly property real screenHeight: effectiveScreen?.height ?? 1080
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1 readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
@@ -85,34 +78,6 @@ Item {
} }
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0 readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
// Shadow padding for the content window (render padding only, no motion padding)
readonly property var shadowLevel: Theme.elevationLevel3
readonly property real shadowFallbackOffset: 6
readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
readonly property real shadowPad: Theme.snap(shadowRenderPadding, dpr)
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
readonly property real alignedX: Theme.snap(modalX, dpr)
readonly property real alignedY: Theme.snap(modalY, dpr)
// For directional/depth: window extends from screen top (content slides within)
// For standard: small window tightly around the modal + shadow padding
readonly property bool _needsExtendedWindow: Theme.isDirectionalEffect || Theme.isDepthEffect
// Content window geometry
readonly property real _cwMarginLeft: Theme.snap(alignedX - shadowPad, dpr)
readonly property real _cwMarginTop: _needsExtendedWindow ? 0 : Theme.snap(alignedY - shadowPad, dpr)
readonly property real _cwWidth: alignedWidth + shadowPad * 2
readonly property real _cwHeight: {
if (Theme.isDirectionalEffect)
return screenHeight + shadowPad;
if (Theme.isDepthEffect)
return alignedY + alignedHeight + shadowPad;
return alignedHeight + shadowPad * 2;
}
// Where the content container sits inside the content window
readonly property real _ccX: shadowPad
readonly property real _ccY: _needsExtendedWindow ? alignedY : shadowPad
signal dialogClosed signal dialogClosed
function _ensureContentLoadedAndInitialize(query, mode) { function _ensureContentLoadedAndInitialize(query, mode) {
@@ -132,8 +97,7 @@ Item {
if (!spotlightContent) if (!spotlightContent)
return; return;
contentVisible = true; contentVisible = true;
// NOTE: forceActiveFocus() is deliberately NOT called here. spotlightContent.searchField.forceActiveFocus();
// It is deferred to after animation starts to avoid compositor IPC stalls.
if (spotlightContent.searchField) { if (spotlightContent.searchField) {
spotlightContent.searchField.text = query; spotlightContent.searchField.text = query;
@@ -166,59 +130,40 @@ Item {
} }
} }
function _openCommon(query, mode) { function show() {
closeCleanupTimer.stop(); closeCleanupTimer.stop();
isClosing = false; isClosing = false;
openedFromOverview = false; openedFromOverview = false;
// Disable animations so the snap is instant
animationsEnabled = false;
// Freeze the collapsed offsets (they depend on height which could change)
_frozenMotionX = contentContainer ? contentContainer.collapsedMotionX : 0;
_frozenMotionY = contentContainer ? contentContainer.collapsedMotionY : (Theme.isDirectionalEffect ? Math.max(root.screenHeight - root._ccY + root.shadowPad, Theme.effectAnimOffset * 1.1) : -Theme.effectAnimOffset);
var focusedScreen = CompositorService.getFocusedScreen(); var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen) { if (focusedScreen)
backgroundWindow.screen = focusedScreen; launcherWindow.screen = focusedScreen;
contentWindow.screen = focusedScreen;
}
// _motionActive = false ensures motionX/Y snap to frozen collapsed position
_motionActive = false;
// Make windows visible but do NOT request keyboard focus yet
ModalManager.openModal(root);
spotlightOpen = true; spotlightOpen = true;
backgroundWindow.visible = true; keyboardActive = true;
contentWindow.visible = true; ModalManager.openModal(root);
if (useHyprlandFocusGrab) if (useHyprlandFocusGrab)
focusGrab.active = true; focusGrab.active = true;
// Load content and initialize (but no forceActiveFocus that's deferred) _ensureContentLoadedAndInitialize("", "");
_ensureContentLoadedAndInitialize(query || "", mode || "");
// Frame 1: enable animations and trigger enter motion
Qt.callLater(() => {
root.animationsEnabled = true;
root._motionActive = true;
// Frame 2: request keyboard focus + activate search field
// Double-deferred to avoid compositor IPC competing with animation frames
Qt.callLater(() => {
root.keyboardActive = true;
if (root.spotlightContent && root.spotlightContent.searchField)
root.spotlightContent.searchField.forceActiveFocus();
});
});
}
function show() {
_openCommon("", "");
} }
function showWithQuery(query) { function showWithQuery(query) {
_openCommon(query, ""); closeCleanupTimer.stop();
isClosing = false;
openedFromOverview = false;
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen)
launcherWindow.screen = focusedScreen;
spotlightOpen = true;
keyboardActive = true;
ModalManager.openModal(root);
if (useHyprlandFocusGrab)
focusGrab.active = true;
_ensureContentLoadedAndInitialize(query, "");
} }
function hide() { function hide() {
@@ -226,17 +171,13 @@ Item {
return; return;
openedFromOverview = false; openedFromOverview = false;
isClosing = true; isClosing = true;
// For directional effects, defer contentVisible=false so content stays rendered during exit slide contentVisible = false;
if (!Theme.isDirectionalEffect)
contentVisible = false;
// Trigger exit animation Behaviors will animate motionX/Y to frozen collapsed position
_motionActive = false;
keyboardActive = false; keyboardActive = false;
spotlightOpen = false; spotlightOpen = false;
focusGrab.active = false; focusGrab.active = false;
ModalManager.closeModal(root); ModalManager.closeModal(root);
closeCleanupTimer.start(); closeCleanupTimer.start();
} }
@@ -245,7 +186,21 @@ Item {
} }
function showWithMode(mode) { function showWithMode(mode) {
_openCommon("", mode); closeCleanupTimer.stop();
isClosing = false;
openedFromOverview = false;
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen)
launcherWindow.screen = focusedScreen;
spotlightOpen = true;
keyboardActive = true;
ModalManager.openModal(root);
if (useHyprlandFocusGrab)
focusGrab.active = true;
_ensureContentLoadedAndInitialize("", mode);
} }
function toggleWithMode(mode) { function toggleWithMode(mode) {
@@ -266,13 +221,10 @@ Item {
Timer { Timer {
id: closeCleanupTimer id: closeCleanupTimer
interval: Theme.variantCloseInterval(Theme.modalAnimationDuration) interval: Theme.modalAnimationDuration + 50
repeat: false repeat: false
onTriggered: { onTriggered: {
isClosing = false; isClosing = false;
contentVisible = false;
contentWindow.visible = false;
backgroundWindow.visible = false;
if (root.unloadContentOnClose) if (root.unloadContentOnClose)
launcherContentLoader.active = false; launcherContentLoader.active = false;
dialogClosed(); dialogClosed();
@@ -290,7 +242,7 @@ Item {
HyprlandFocusGrab { HyprlandFocusGrab {
id: focusGrab id: focusGrab
windows: [contentWindow] windows: [launcherWindow]
active: false active: false
onCleared: { onCleared: {
@@ -315,7 +267,7 @@ Item {
if (Quickshell.screens.length === 0) if (Quickshell.screens.length === 0)
return; return;
const screen = contentWindow.screen; const screen = launcherWindow.screen;
const screenName = screen?.name; const screenName = screen?.name;
let needsReset = !screen || !screenName; let needsReset = !screen || !screenName;
@@ -337,31 +289,35 @@ Item {
return; return;
root._windowEnabled = false; root._windowEnabled = false;
backgroundWindow.screen = newScreen; launcherWindow.screen = newScreen;
contentWindow.screen = newScreen;
Qt.callLater(() => { Qt.callLater(() => {
root._windowEnabled = true; root._windowEnabled = true;
}); });
} }
} }
// Background window: fullscreen, handles darkening + click-to-dismiss
PanelWindow { PanelWindow {
id: backgroundWindow id: launcherWindow
visible: false visible: root._windowEnabled && (spotlightOpen || isClosing)
color: "transparent" color: "transparent"
exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "dms:spotlight:bg" WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: WlrLayershell.Top WlrLayershell.layer: {
WlrLayershell.exclusiveZone: -1 switch (Quickshell.env("DMS_MODAL_LAYER")) {
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None case "bottom":
console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
WlrLayershell.margins { return WlrLayershell.Top;
top: contentContainer.dockTop ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 0 ? Theme.px(42, root.dpr) : 0) case "background":
bottom: contentContainer.dockBottom ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 1 ? Theme.px(42, root.dpr) : 0) console.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
left: contentContainer.dockLeft ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 2 ? Theme.px(42, root.dpr) : 0) return WlrLayershell.Top;
right: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0) case "overlay":
return WlrLayershell.Overlay;
default:
return WlrLayershell.Top;
}
} }
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
anchors { anchors {
top: true top: true
@@ -371,11 +327,11 @@ Item {
} }
mask: Region { mask: Region {
item: (spotlightOpen || isClosing) ? bgFullScreenMask : null item: spotlightOpen ? fullScreenMask : null
} }
Item { Item {
id: bgFullScreenMask id: fullScreenMask
anchors.fill: parent anchors.fill: parent
} }
@@ -383,14 +339,13 @@ Item {
id: backgroundDarken id: backgroundDarken
anchors.fill: parent anchors.fill: parent
color: "black" color: "black"
opacity: launcherMotionVisible && SettingsData.modalDarkenBackground ? 0.5 : 0 opacity: contentVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
visible: launcherMotionVisible || opacity > 0 visible: contentVisible || opacity > 0
Behavior on opacity { Behavior on opacity {
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
DankAnim { DankAnim {
duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale) duration: Theme.modalAnimationDuration
easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
} }
} }
} }
@@ -398,240 +353,88 @@ Item {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
enabled: spotlightOpen enabled: spotlightOpen
onClicked: root.hide() onClicked: mouse => {
} var contentX = modalContainer.x;
} var contentY = modalContainer.y;
var contentW = modalContainer.width;
var contentH = modalContainer.height;
// Content window: SMALL, positioned with margins only renders the modal area if (mouse.x < contentX || mouse.x > contentX + contentW || mouse.y < contentY || mouse.y > contentY + contentH) {
PanelWindow { root.hide();
id: contentWindow }
visible: false
color: "transparent"
WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
console.error("DankLauncherV2Modal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
console.error("DankLauncherV2Modal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "overlay":
return WlrLayershell.Overlay;
default:
return WlrLayershell.Top;
} }
} }
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
anchors {
left: true
top: true
}
WlrLayershell.margins {
left: root._cwMarginLeft
top: root._cwMarginTop
}
implicitWidth: root._cwWidth
implicitHeight: root._cwHeight
mask: Region {
item: contentInputMask
}
Item { Item {
id: contentInputMask id: modalContainer
visible: false x: root.modalX
x: contentContainer.x + contentWrapper.x y: root.modalY
y: contentContainer.y + contentWrapper.y width: root.modalWidth
width: root.alignedWidth height: root.modalHeight
height: root.alignedHeight visible: contentVisible || opacity > 0
}
Item { opacity: contentVisible ? 1 : 0
id: contentContainer scale: contentVisible ? 1 : 0.96
transformOrigin: Item.Center
// For directional/depth: contentContainer is at alignedY from window top (window starts at screen top) Behavior on opacity {
// For standard: contentContainer is at shadowPad from window top (window starts near modal)
x: root._ccX
y: root._ccY
width: root.alignedWidth
height: root.alignedHeight
readonly property int dockEdge: typeof SettingsData !== "undefined" ? SettingsData.dockPosition : 1
readonly property bool dockTop: dockEdge === 0
readonly property bool dockBottom: dockEdge === 1
readonly property bool dockLeft: dockEdge === 2
readonly property bool dockRight: dockEdge === 3
readonly property real dockThickness: typeof SettingsData !== "undefined" && SettingsData.showDock ? Theme.px(SettingsData.dockIconSize + (SettingsData.dockMargin * 2) + SettingsData.dockSpacing + 8, root.dpr) : Theme.px(60, root.dpr)
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real collapsedMotionX: {
if (directionalEffect) {
if (dockLeft)
return -(root._ccX + root.alignedWidth + Theme.effectAnimOffset);
if (dockRight)
return root.screenWidth - root._ccX + Theme.effectAnimOffset;
}
if (depthEffect)
return Theme.effectAnimOffset * 0.25;
return 0;
}
readonly property real collapsedMotionY: {
if (directionalEffect) {
if (dockTop)
return -(root._ccY + root.alignedHeight + Theme.effectAnimOffset);
if (dockBottom)
return root.screenHeight - root._ccY + root.shadowPad + Theme.effectAnimOffset;
return 0;
}
if (depthEffect)
return -Math.max(Theme.effectAnimOffset * 0.85, 34);
return -Math.max((root.shadowPad || 0) + Theme.effectAnimOffset, 40);
}
// animX/animY are Behavior-animated DankPopout pattern
property real animX: 0
property real animY: 0
property real scaleValue: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed)
Component.onCompleted: {
animX = Theme.snap(root._motionActive ? 0 : collapsedMotionX, root.dpr);
animY = Theme.snap(root._motionActive ? 0 : collapsedMotionY, root.dpr);
scaleValue = root._motionActive ? 1.0 : (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed));
}
Connections {
target: root
function on_MotionActiveChanged() {
contentContainer.animX = Theme.snap(root._motionActive ? 0 : root._frozenMotionX, root.dpr);
contentContainer.animY = Theme.snap(root._motionActive ? 0 : root._frozenMotionY, root.dpr);
contentContainer.scaleValue = root._motionActive ? 1.0 : (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed));
}
}
Behavior on animX {
enabled: root.animationsEnabled
DankAnim { DankAnim {
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive) duration: Theme.modalAnimationDuration
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
} }
} }
Behavior on animY { Behavior on scale {
enabled: root.animationsEnabled
DankAnim { DankAnim {
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive) duration: Theme.modalAnimationDuration
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
} }
} }
Behavior on scaleValue { ElevationShadow {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2)) id: launcherShadowLayer
DankAnim { anchors.fill: parent
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive) level: Theme.elevationLevel3
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve fallbackOffset: 6
} targetColor: root.backgroundColor
borderColor: root.borderColor
borderWidth: root.borderWidth
targetRadius: root.cornerRadius
shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
} }
Item { MouseArea {
id: directionalClipMask anchors.fill: parent
readonly property bool shouldClip: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0 onPressed: mouse => mouse.accepted = true
readonly property real clipOversize: 2000 }
clip: shouldClip FocusScope {
anchors.fill: parent
focus: keyboardActive
x: shouldClip ? (contentContainer.dockRight ? -clipOversize : (contentContainer.dockLeft ? contentContainer.dockThickness - root._ccX : -clipOversize)) : 0 Loader {
y: shouldClip ? (contentContainer.dockBottom ? -clipOversize : (contentContainer.dockTop ? contentContainer.dockThickness - root._ccY : -clipOversize)) : 0 id: launcherContentLoader
anchors.fill: parent
width: shouldClip ? parent.width + clipOversize + (contentContainer.dockRight ? (root.screenWidth - contentContainer.dockThickness - root._ccX - parent.width) : (contentContainer.dockLeft ? clipOversize : clipOversize)) : parent.width active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
height: shouldClip ? parent.height + clipOversize + (contentContainer.dockBottom ? (root.screenHeight - contentContainer.dockThickness - root._ccY - parent.height) : (contentContainer.dockTop ? clipOversize : clipOversize)) : parent.height asynchronous: false
sourceComponent: LauncherContent {
Item { focus: true
id: aligner parentModal: root
x: directionalClipMask.x !== 0 ? -directionalClipMask.x : 0
y: directionalClipMask.y !== 0 ? -directionalClipMask.y : 0
width: contentContainer.width
height: contentContainer.height
// Shadow mirrors contentWrapper position/scale/opacity
ElevationShadow {
id: launcherShadowLayer
width: parent.width
height: parent.height
opacity: contentWrapper.opacity
scale: contentWrapper.scale
x: contentWrapper.x
y: contentWrapper.y
level: root.shadowLevel
fallbackOffset: root.shadowFallbackOffset
targetColor: root.backgroundColor
borderColor: root.borderColor
borderWidth: root.borderWidth
targetRadius: root.cornerRadius
shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
} }
// contentWrapper moves inside static contentContainer DankPopout pattern onLoaded: {
Item { if (root._pendingInitialize) {
id: contentWrapper root._initializeAndShow(root._pendingQuery, root._pendingMode);
width: parent.width root._pendingInitialize = false;
height: parent.height
opacity: Theme.isDirectionalEffect ? 1 : (launcherMotionVisible ? 1 : 0)
visible: opacity > 0
scale: contentContainer.scaleValue
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
Behavior on opacity {
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
DankAnim {
duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
} }
}
}
MouseArea { Keys.onEscapePressed: event => {
anchors.fill: parent root.hide();
onPressed: mouse => mouse.accepted = true event.accepted = true;
} }
}
FocusScope { }
anchors.fill: parent }
focus: keyboardActive
Loader {
id: launcherContentLoader
anchors.fill: parent
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 => {
root.hide();
event.accepted = true;
}
}
} // contentWrapper
} // aligner
} // directionalClipMask
} // contentContainer
} // PanelWindow
} }
@@ -86,7 +86,7 @@ FocusScope {
Controller { Controller {
id: controller id: controller
active: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true active: root.parentModal?.spotlightOpen ?? true
viewModeContext: root.viewModeContext viewModeContext: root.viewModeContext
onItemExecuted: { onItemExecuted: {
@@ -158,6 +158,10 @@ FocusScope {
controller.selectPageUp(8); controller.selectPageUp(8);
return; return;
case Qt.Key_Right: case Qt.Key_Right:
if (hasCtrl) {
controller.cycleMode();
return;
}
if (controller.getCurrentSectionViewMode() !== "list") { if (controller.getCurrentSectionViewMode() !== "list") {
controller.selectRight(); controller.selectRight();
return; return;
@@ -165,12 +169,25 @@ FocusScope {
event.accepted = false; event.accepted = false;
return; return;
case Qt.Key_Left: case Qt.Key_Left:
if (hasCtrl) {
const reverse = true;
controller.cycleMode(reverse);
return;
}
if (controller.getCurrentSectionViewMode() !== "list") { if (controller.getCurrentSectionViewMode() !== "list") {
controller.selectLeft(); controller.selectLeft();
return; return;
} }
event.accepted = false; event.accepted = false;
return; return;
case Qt.Key_H:
if (hasCtrl) {
const reverse = true;
controller.cycleMode(reverse);
return;
}
event.accepted = false;
return;
case Qt.Key_J: case Qt.Key_J:
if (hasCtrl) { if (hasCtrl) {
controller.selectNext(); controller.selectNext();
@@ -185,6 +202,13 @@ FocusScope {
} }
event.accepted = false; event.accepted = false;
return; return;
case Qt.Key_L:
if (hasCtrl) {
controller.cycleMode();
return;
}
event.accepted = false;
return;
case Qt.Key_N: case Qt.Key_N:
if (hasCtrl) { if (hasCtrl) {
controller.selectNextSection(); controller.selectNextSection();
@@ -200,13 +224,19 @@ FocusScope {
event.accepted = false; event.accepted = false;
return; return;
case Qt.Key_Tab: case Qt.Key_Tab:
if (actionPanel.hasActions) { if (hasCtrl && actionPanel.hasActions) {
actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show(); actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show();
return;
} }
controller.selectNext();
return; return;
case Qt.Key_Backtab: case Qt.Key_Backtab:
if (actionPanel.expanded) if (hasCtrl && actionPanel.expanded) {
actionPanel.hide(); const reverse = true
actionPanel.expanded ? actionPanel.cycleAction(reverse) : actionPanel.show();
return;
}
controller.selectPrevious();
return; return;
case Qt.Key_Return: case Qt.Key_Return:
case Qt.Key_Enter: case Qt.Key_Enter:
@@ -388,7 +418,7 @@ FocusScope {
StyledText { StyledText {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "Tab " + I18n.tr("actions") text: "Ctrl-Tab " + I18n.tr("actions")
font.pixelSize: Theme.fontSizeSmall - 1 font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
visible: actionPanel.hasActions visible: actionPanel.hasActions
@@ -462,7 +492,7 @@ FocusScope {
showClearButton: true showClearButton: true
textColor: Theme.surfaceText textColor: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
enabled: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true enabled: root.parentModal ? root.parentModal.spotlightOpen : true
placeholderText: "" placeholderText: ""
ignoreUpDownKeys: true ignoreUpDownKeys: true
ignoreTabKeys: true ignoreTabKeys: true
@@ -548,6 +578,7 @@ FocusScope {
} }
} }
} }
} }
Item { Item {
@@ -696,13 +727,7 @@ FocusScope {
Item { Item {
width: parent.width width: parent.width
height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2) height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2)
opacity: { opacity: root.parentModal?.isClosing ? 0 : 1
if (!root.parentModal)
return 1;
if (Theme.isDirectionalEffect && root.parentModal.isClosing)
return 1;
return root.parentModal.isClosing ? 0 : 1;
}
ResultsList { ResultsList {
id: resultsList id: resultsList
@@ -468,7 +468,7 @@ Item {
switch (mode) { switch (mode) {
case "files": case "files":
if (!DSearchService.dsearchAvailable) if (!DSearchService.dsearchAvailable)
return I18n.tr("File search requires dsearch\nInstall from github.com/morelazers/dsearch"); return I18n.tr("File search requires dsearch\nInstall from github.com/AvengeMedia/danksearch");
if (!hasQuery) if (!hasQuery)
return I18n.tr("Type to search files"); return I18n.tr("Type to search files");
if (root.controller.searchQuery.length < 2) if (root.controller.searchQuery.length < 2)
+621
View File
@@ -0,0 +1,621 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Hyprland
import Quickshell.Io
import Quickshell
import qs.Common
import qs.Modals.Common
import qs.Services
import qs.Widgets
DankModal {
id: muxModal
layerNamespace: "dms:mux"
property int selectedIndex: -1
property string searchText: ""
property var filteredSessions: []
function updateFilteredSessions() {
var filtered = []
var lowerSearch = searchText.trim().toLowerCase()
for (var i = 0; i < MuxService.sessions.length; i++) {
var session = MuxService.sessions[i]
if (lowerSearch.length > 0 && !session.name.toLowerCase().includes(lowerSearch))
continue
filtered.push(session)
}
filteredSessions = filtered
if (selectedIndex >= filteredSessions.length) {
selectedIndex = Math.max(0, filteredSessions.length - 1)
}
}
onSearchTextChanged: updateFilteredSessions()
Connections {
target: MuxService
function onSessionsChanged() {
updateFilteredSessions()
}
}
HyprlandFocusGrab {
id: grab
windows: [muxModal.contentWindow]
active: CompositorService.isHyprland && muxModal.shouldHaveFocus
}
function toggle() {
if (shouldBeVisible) {
hide()
} else {
show()
}
}
function show() {
open()
selectedIndex = -1
searchText = ""
MuxService.refreshSessions()
shouldHaveFocus = true
Qt.callLater(() => {
if (muxPanel && muxPanel.searchField) {
muxPanel.searchField.forceActiveFocus();
}
})
}
function hide() {
close()
selectedIndex = -1
searchText = ""
}
function attachToSession(name) {
MuxService.attachToSession(name)
hide()
}
function renameSession(name) {
inputModal.showWithOptions({
title: I18n.tr("Rename Session"),
message: I18n.tr("Enter a new name for session \"%1\"").arg(name),
initialText: name,
onConfirm: function (newName) {
MuxService.renameSession(name, newName)
}
})
}
function killSession(name) {
confirmModal.showWithOptions({
title: I18n.tr("Kill Session"),
message: I18n.tr("Are you sure you want to kill session \"%1\"?").arg(name),
confirmText: I18n.tr("Kill"),
confirmColor: Theme.primary,
onConfirm: function () {
MuxService.killSession(name)
}
})
}
function createNewSession() {
inputModal.showWithOptions({
title: I18n.tr("New Session"),
message: I18n.tr("Please write a name for your new %1 session").arg(MuxService.displayName),
onConfirm: function (name) {
MuxService.createSession(name)
hide()
}
})
}
function selectNext() {
selectedIndex = Math.min(selectedIndex + 1, filteredSessions.length - 1)
}
function selectPrevious() {
selectedIndex = Math.max(selectedIndex - 1, -1)
}
function activateSelected() {
if (selectedIndex === -1) {
createNewSession()
} else if (selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
attachToSession(filteredSessions[selectedIndex].name)
}
}
visible: false
modalWidth: 600
modalHeight: 600
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
cornerRadius: Theme.cornerRadius
borderColor: Theme.outlineMedium
borderWidth: 1
enableShadow: true
keepContentLoaded: true
onBackgroundClicked: hide()
Timer {
interval: 3000
running: muxModal.shouldBeVisible
repeat: true
onTriggered: MuxService.refreshSessions()
}
IpcHandler {
function open(): string {
muxModal.show()
return "MUX_OPEN_SUCCESS"
}
function close(): string {
muxModal.hide()
return "MUX_CLOSE_SUCCESS"
}
function toggle(): string {
muxModal.toggle()
return "MUX_TOGGLE_SUCCESS"
}
target: "mux"
}
// Backwards compatibility
IpcHandler {
function open(): string {
muxModal.show()
return "TMUX_OPEN_SUCCESS"
}
function close(): string {
muxModal.hide()
return "TMUX_CLOSE_SUCCESS"
}
function toggle(): string {
muxModal.toggle()
return "TMUX_TOGGLE_SUCCESS"
}
target: "tmux"
}
InputModal {
id: inputModal
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
muxModal.shouldHaveFocus = false;
muxModal.contentWindow.visible = false;
return;
}
if (muxModal.shouldBeVisible) {
muxModal.contentWindow.visible = true;
}
Qt.callLater(function () {
if (!muxModal.shouldBeVisible) {
return;
}
muxModal.shouldHaveFocus = true;
muxModal.modalFocusScope.forceActiveFocus();
if (muxPanel.searchField) {
muxPanel.searchField.forceActiveFocus();
}
});
}
}
ConfirmModal {
id: confirmModal
onShouldBeVisibleChanged: {
if (shouldBeVisible) {
muxModal.shouldHaveFocus = false;
muxModal.contentWindow.visible = false;
return;
}
if (muxModal.shouldBeVisible) {
muxModal.contentWindow.visible = true;
}
Qt.callLater(function () {
if (!muxModal.shouldBeVisible) {
return;
}
muxModal.shouldHaveFocus = true;
muxModal.modalFocusScope.forceActiveFocus();
if (muxPanel.searchField) {
muxPanel.searchField.forceActiveFocus();
}
});
}
}
directContent: Item {
id: muxPanel
clip: false
property alias searchField: searchField
Keys.onPressed: event => {
if ((event.key === Qt.Key_J && (event.modifiers & Qt.ControlModifier)) ||
(event.key === Qt.Key_Down)) {
selectNext()
event.accepted = true
} else if ((event.key === Qt.Key_K && (event.modifiers & Qt.ControlModifier)) ||
(event.key === Qt.Key_Up)) {
selectPrevious()
event.accepted = true
} else if (event.key === Qt.Key_N && (event.modifiers & Qt.ControlModifier)) {
createNewSession()
event.accepted = true
} else if (event.key === Qt.Key_R && (event.modifiers & Qt.ControlModifier)) {
if (MuxService.supportsRename && selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
renameSession(filteredSessions[selectedIndex].name)
}
event.accepted = true
} else if (event.key === Qt.Key_D && (event.modifiers & Qt.ControlModifier)) {
if (selectedIndex >= 0 && selectedIndex < filteredSessions.length) {
killSession(filteredSessions[selectedIndex].name)
}
event.accepted = true
} else if (event.key === Qt.Key_Escape) {
hide()
event.accepted = true
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
activateSelected()
event.accepted = true
}
}
Column {
width: parent.width - Theme.spacingM * 2
height: parent.height - Theme.spacingM * 2
x: Theme.spacingM
y: Theme.spacingM
spacing: Theme.spacingS
// Header
Item {
width: parent.width
height: 40
StyledText {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("%1 Sessions").arg(MuxService.displayName)
font.pixelSize: Theme.fontSizeLarge + 4
font.weight: Font.Bold
color: Theme.surfaceText
}
StyledText {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("%1 active, %2 filtered").arg(MuxService.sessions.length).arg(muxModal.filteredSessions.length)
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
}
}
// Search field
DankTextField {
id: searchField
width: parent.width
height: 48
cornerRadius: Theme.cornerRadius
backgroundColor: Theme.surfaceContainerHigh
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
leftIconName: "search"
leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText
leftIconFocusedColor: Theme.primary
showClearButton: true
font.pixelSize: Theme.fontSizeMedium
placeholderText: I18n.tr("Search sessions...")
keyForwardTargets: [muxPanel]
onTextEdited: {
muxModal.searchText = text
muxModal.selectedIndex = 0
}
}
// New Session Button
Rectangle {
width: parent.width
height: 56
radius: Theme.cornerRadius
color: muxModal.selectedIndex === -1 ? Theme.primaryContainer :
(newMouse.containsMouse ? Theme.surfaceContainerHigh : Theme.surfaceContainer)
RowLayout {
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
Rectangle {
Layout.preferredWidth: 40
Layout.preferredHeight: 40
radius: 20
color: Theme.primaryContainer
DankIcon {
anchors.centerIn: parent
name: "add"
size: Theme.iconSize
color: Theme.primary
}
}
Column {
Layout.fillWidth: true
spacing: 2
StyledText {
text: I18n.tr("New Session")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
}
StyledText {
text: I18n.tr("Create a new %1 session (n)").arg(MuxService.displayName)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
MouseArea {
id: newMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: muxModal.createNewSession()
}
}
// Sessions List
Rectangle {
width: parent.width
height: parent.height - 88 - 48 - shortcutsBar.height - Theme.spacingS * 3
radius: Theme.cornerRadius
color: "transparent"
ScrollView {
anchors.fill: parent
clip: true
Column {
width: parent.width
spacing: Theme.spacingXS
Repeater {
model: ScriptModel {
values: muxModal.filteredSessions
}
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 64
radius: Theme.cornerRadius
color: muxModal.selectedIndex === index ? Theme.primaryContainer :
(sessionMouse.containsMouse ? Theme.surfaceContainerHigh : "transparent")
MouseArea {
id: sessionMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: muxModal.attachToSession(modelData.name)
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
// Avatar
Rectangle {
Layout.preferredWidth: 40
Layout.preferredHeight: 40
radius: 20
color: modelData.attached ? Theme.primaryContainer : Theme.surfaceContainerHigh
StyledText {
anchors.centerIn: parent
text: modelData.name.charAt(0).toUpperCase()
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: modelData.attached ? Theme.primary : Theme.surfaceText
}
}
// Info
Column {
Layout.fillWidth: true
spacing: 2
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
elide: Text.ElideRight
}
StyledText {
text: {
var parts = []
if (modelData.windows !== "N/A")
parts.push(I18n.tr("%1 windows").arg(modelData.windows))
parts.push(modelData.attached ? I18n.tr("attached") : I18n.tr("detached"))
return parts.join(" \u2022 ")
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
// Rename button (tmux only)
Rectangle {
Layout.preferredWidth: 36
Layout.preferredHeight: 36
radius: 18
visible: MuxService.supportsRename
color: renameMouse.containsMouse ? Theme.surfaceContainerHighest : "transparent"
DankIcon {
anchors.centerIn: parent
name: "edit"
size: Theme.iconSizeSmall
color: renameMouse.containsMouse ? Theme.primary : Theme.surfaceVariantText
}
MouseArea {
id: renameMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: muxModal.renameSession(modelData.name)
}
}
// Delete button
Rectangle {
Layout.preferredWidth: 36
Layout.preferredHeight: 36
radius: 18
color: deleteMouse.containsMouse ? Theme.errorContainer : "transparent"
DankIcon {
anchors.centerIn: parent
name: "delete"
size: Theme.iconSizeSmall
color: deleteMouse.containsMouse ? Theme.error : Theme.surfaceVariantText
}
MouseArea {
id: deleteMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
muxModal.killSession(modelData.name)
}
}
}
}
}
}
// Empty state
Item {
width: parent.width
height: muxModal.filteredSessions.length === 0 ? 200 : 0
visible: muxModal.filteredSessions.length === 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingM
DankIcon {
name: muxModal.searchText.length > 0 ? "search_off" : "terminal"
size: 48
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: muxModal.searchText.length > 0 ? I18n.tr("No sessions found") : I18n.tr("No active %1 sessions").arg(MuxService.displayName)
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: muxModal.searchText.length > 0 ? I18n.tr("Try a different search") : I18n.tr("Press 'n' or click 'New Session' to create one")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
}
}
// Shortcuts bar
Row {
id: shortcutsBar
width: parent.width
spacing: Theme.spacingM
bottomPadding: Theme.spacingS
Repeater {
model: {
var shortcuts = [
{ key: "↑↓", label: I18n.tr("Navigate") },
{ key: "↵", label: I18n.tr("Attach") },
{ key: "^N", label: I18n.tr("New") },
{ key: "^D", label: I18n.tr("Kill") },
{ key: "Esc", label: I18n.tr("Close") }
]
if (MuxService.supportsRename)
shortcuts.splice(3, 0, { key: "^R", label: I18n.tr("Rename") })
return shortcuts
}
delegate: Row {
required property var modelData
spacing: 4
Rectangle {
width: keyText.width + Theme.spacingS
height: keyText.height + 4
radius: 4
color: Theme.surfaceContainerHighest
anchors.verticalCenter: parent.verticalCenter
StyledText {
id: keyText
anchors.centerIn: parent
text: modelData.key
font.pixelSize: Theme.fontSizeSmall - 1
font.weight: Font.Medium
color: Theme.surfaceVariantText
}
}
StyledText {
text: modelData.label
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
}
}
+114 -30
View File
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -11,8 +12,45 @@ FloatingWindow {
property string passwordInput: "" property string passwordInput: ""
property var currentFlow: PolkitService.agent?.flow property var currentFlow: PolkitService.agent?.flow
property bool isLoading: false property bool isLoading: false
property bool awaitingFprintForPassword: false
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2 readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
property string polkitEtcPamText: ""
property string polkitLibPamText: ""
property string systemAuthPamText: ""
property string commonAuthPamText: ""
property string passwordAuthPamText: ""
readonly property bool polkitPamHasFprint: {
const polkitText = polkitEtcPamText !== "" ? polkitEtcPamText : polkitLibPamText;
if (!polkitText)
return false;
return pamModuleEnabled(polkitText, "pam_fprintd") || (polkitText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_fprintd")) || (polkitText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_fprintd")) || (polkitText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_fprintd"));
}
function stripPamComment(line) {
if (!line)
return "";
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#"))
return "";
const hashIdx = trimmed.indexOf("#");
if (hashIdx >= 0)
return trimmed.substring(0, hashIdx).trim();
return trimmed;
}
function pamModuleEnabled(pamText, moduleName) {
if (!pamText || !moduleName)
return false;
const lines = pamText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (line && line.includes(moduleName))
return true;
}
return false;
}
function focusPasswordField() { function focusPasswordField() {
passwordField.forceActiveFocus(); passwordField.forceActiveFocus();
} }
@@ -20,6 +58,7 @@ FloatingWindow {
function show() { function show() {
passwordInput = ""; passwordInput = "";
isLoading = false; isLoading = false;
awaitingFprintForPassword = false;
visible = true; visible = true;
Qt.callLater(focusPasswordField); Qt.callLater(focusPasswordField);
} }
@@ -28,17 +67,27 @@ FloatingWindow {
visible = false; visible = false;
} }
function _commitSubmit() {
isLoading = true;
awaitingFprintForPassword = false;
currentFlow.submit(passwordInput);
passwordInput = "";
}
function submitAuth() { function submitAuth() {
if (!currentFlow || isLoading) if (!currentFlow || isLoading)
return; return;
isLoading = true; if (!currentFlow.isResponseRequired) {
currentFlow.submit(passwordInput); awaitingFprintForPassword = true;
passwordInput = ""; return;
}
_commitSubmit();
} }
function cancelAuth() { function cancelAuth() {
if (isLoading) if (isLoading)
return; return;
awaitingFprintForPassword = false;
if (currentFlow) { if (currentFlow) {
currentFlow.cancelAuthenticationRequest(); currentFlow.cancelAuthenticationRequest();
return; return;
@@ -60,6 +109,7 @@ FloatingWindow {
} }
passwordInput = ""; passwordInput = "";
isLoading = false; isLoading = false;
awaitingFprintForPassword = false;
} }
Connections { Connections {
@@ -83,6 +133,11 @@ FloatingWindow {
function onIsResponseRequiredChanged() { function onIsResponseRequiredChanged() {
if (!currentFlow.isResponseRequired) if (!currentFlow.isResponseRequired)
return; return;
if (awaitingFprintForPassword && passwordInput !== "") {
_commitSubmit();
return;
}
awaitingFprintForPassword = false;
isLoading = false; isLoading = false;
passwordInput = ""; passwordInput = "";
passwordField.forceActiveFocus(); passwordField.forceActiveFocus();
@@ -101,6 +156,41 @@ FloatingWindow {
} }
} }
FileView {
path: "/etc/pam.d/polkit-1"
printErrors: false
onLoaded: root.polkitEtcPamText = text()
onLoadFailed: root.polkitEtcPamText = ""
}
FileView {
path: "/usr/lib/pam.d/polkit-1"
printErrors: false
onLoaded: root.polkitLibPamText = text()
onLoadFailed: root.polkitLibPamText = ""
}
FileView {
path: "/etc/pam.d/system-auth"
printErrors: false
onLoaded: root.systemAuthPamText = text()
onLoadFailed: root.systemAuthPamText = ""
}
FileView {
path: "/etc/pam.d/common-auth"
printErrors: false
onLoaded: root.commonAuthPamText = text()
onLoadFailed: root.commonAuthPamText = ""
}
FileView {
path: "/etc/pam.d/password-auth"
printErrors: false
onLoaded: root.passwordAuthPamText = text()
onLoadFailed: root.passwordAuthPamText = ""
}
FocusScope { FocusScope {
id: contentFocusScope id: contentFocusScope
@@ -205,36 +295,30 @@ FloatingWindow {
visible: text !== "" visible: text !== ""
} }
Rectangle { DankTextField {
id: passwordField
width: parent.width width: parent.width
height: inputFieldHeight height: inputFieldHeight
radius: Theme.cornerRadius backgroundColor: Theme.surfaceHover
color: Theme.surfaceHover normalBorderColor: Theme.outlineStrong
border.color: passwordField.activeFocus ? Theme.primary : Theme.outlineStrong focusedBorderColor: Theme.primary
border.width: passwordField.activeFocus ? 2 : 1 borderWidth: 1
focusedBorderWidth: 2
leftIconName: polkitPamHasFprint ? "fingerprint" : ""
leftIconSize: 20
leftIconColor: Theme.primary
leftIconFocusedColor: Theme.primary
opacity: isLoading ? 0.5 : 1 opacity: isLoading ? 0.5 : 1
font.pixelSize: Theme.fontSizeMedium
MouseArea { textColor: Theme.surfaceText
anchors.fill: parent text: passwordInput
enabled: !isLoading showPasswordToggle: !(currentFlow?.responseVisible ?? false)
onClicked: passwordField.forceActiveFocus() echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
} placeholderText: ""
enabled: !isLoading
DankTextField { onTextEdited: passwordInput = text
id: passwordField onAccepted: submitAuth()
anchors.fill: parent
font.pixelSize: Theme.fontSizeMedium
textColor: Theme.surfaceText
text: passwordInput
showPasswordToggle: !(currentFlow?.responseVisible ?? false)
echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
placeholderText: ""
backgroundColor: "transparent"
enabled: !isLoading
onTextEdited: passwordInput = text
onAccepted: submitAuth()
}
} }
StyledText { StyledText {
+32 -2
View File
@@ -241,6 +241,21 @@ FocusScope {
} }
} }
Loader {
id: greeterLoader
anchors.fill: parent
active: root.currentIndex === 31
visible: active
focus: active
sourceComponent: GreeterTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
Loader { Loader {
id: pluginsLoader id: pluginsLoader
anchors.fill: parent anchors.fill: parent
@@ -470,7 +485,7 @@ FocusScope {
onActiveChanged: { onActiveChanged: {
if (active && item) if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
} }
} }
@@ -485,7 +500,22 @@ FocusScope {
onActiveChanged: { onActiveChanged: {
if (active && item) if (active && item)
Qt.callLater(() => item.forceActiveFocus()); Qt.callLater(() => item.forceActiveFocus());
}
}
Loader {
id: muxLoader
anchors.fill: parent
active: root.currentIndex === 30
visible: active
focus: active
sourceComponent: MuxTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
} }
} }
} }
@@ -266,6 +266,12 @@ Rectangle {
"tabIndex": 8, "tabIndex": 8,
"cupsOnly": true "cupsOnly": true
}, },
{
"id": "multiplexers",
"text": I18n.tr("Multiplexers"),
"icon": "terminal",
"tabIndex": 30
},
{ {
"id": "window_rules", "id": "window_rules",
"text": I18n.tr("Window Rules"), "text": I18n.tr("Window Rules"),
@@ -287,6 +293,12 @@ Rectangle {
"icon": "lock", "icon": "lock",
"tabIndex": 11 "tabIndex": 11
}, },
{
"id": "greeter",
"text": I18n.tr("Greeter"),
"icon": "login",
"tabIndex": 31
},
{ {
"id": "power_sleep", "id": "power_sleep",
"text": I18n.tr("Power & Sleep"), "text": I18n.tr("Power & Sleep"),
@@ -8,6 +8,9 @@ DankPopout {
layerNamespace: "dms:app-launcher" layerNamespace: "dms:app-launcher"
readonly property real screenWidth: screen?.width ?? 1920
readonly property real screenHeight: screen?.height ?? 1080
property string _pendingMode: "" property string _pendingMode: ""
property string _pendingQuery: "" property string _pendingQuery: ""
@@ -41,8 +44,35 @@ DankPopout {
openWithQuery(query); openWithQuery(query);
} }
popupWidth: 560 readonly property int _baseWidth: {
popupHeight: 640 switch (SettingsData.dankLauncherV2Size) {
case "micro":
return 500;
case "medium":
return 720;
case "large":
return 860;
default:
return 620;
}
}
readonly property int _baseHeight: {
switch (SettingsData.dankLauncherV2Size) {
case "micro":
return 480;
case "medium":
return 720;
case "large":
return 860;
default:
return 600;
}
}
popupWidth: Math.min(_baseWidth, screenWidth - 100)
popupHeight: Math.min(_baseHeight, screenHeight - 100)
triggerWidth: 40 triggerWidth: 40
positioning: "" positioning: ""
contentHandlesKeys: contentLoader.item?.launcherContent?.editMode ?? false contentHandlesKeys: contentLoader.item?.launcherContent?.editMode ?? false
@@ -106,7 +136,7 @@ DankPopout {
QtObject { QtObject {
id: modalAdapter id: modalAdapter
property bool spotlightOpen: appDrawerPopout.shouldBeVisible property bool spotlightOpen: appDrawerPopout.shouldBeVisible
property bool isClosing: appDrawerPopout.isClosing property bool isClosing: false
function hide() { function hide() {
appDrawerPopout.close(); appDrawerPopout.close();
@@ -12,7 +12,7 @@ DankPopout {
id: root id: root
layerNamespace: "dms:control-center" layerNamespace: "dms:control-center"
fullHeightSurface: false fullHeightSurface: true
property string expandedSection: "" property string expandedSection: ""
property var triggerScreen: null property var triggerScreen: null
@@ -70,6 +70,16 @@ DankPopout {
backgroundInteractive: !anyModalOpen backgroundInteractive: !anyModalOpen
onCredentialsPromptOpenChanged: {
if (credentialsPromptOpen && shouldBeVisible)
close();
}
onPolkitModalOpenChanged: {
if (polkitModalOpen && shouldBeVisible)
close();
}
customKeyboardFocus: { customKeyboardFocus: {
if (!shouldBeVisible) if (!shouldBeVisible)
return WlrKeyboardFocus.None; return WlrKeyboardFocus.None;
@@ -126,11 +136,9 @@ DankPopout {
z: 5000 z: 5000
Behavior on opacity { Behavior on opacity {
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: Theme.shortDuration duration: 200
easing.type: Easing.BezierSpline easing.type: Easing.OutCubic
easing.bezierCurve: root.shouldBeVisible ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
} }
@@ -1117,7 +1117,6 @@ Item {
if (!notificationCenterLoader.item) { if (!notificationCenterLoader.item) {
return; return;
} }
notificationCenterLoader.item.triggerScreen = barWindow.screen;
const effectiveBarConfig = topBarContent.barConfig; const effectiveBarConfig = topBarContent.barConfig;
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
if (notificationCenterLoader.item.setBarContext) { if (notificationCenterLoader.item.setBarContext) {
+5 -4
View File
@@ -289,7 +289,7 @@ PanelWindow {
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
if (!onThisScreen) if (!onThisScreen)
return false; return false;
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name) if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
return false; return false;
return true; return true;
}); });
@@ -312,7 +312,7 @@ PanelWindow {
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
if (!onThisScreen) if (!onThisScreen)
return false; return false;
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name) if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
return false; return false;
return true; return true;
}); });
@@ -336,7 +336,7 @@ PanelWindow {
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
if (!onThisScreen) if (!onThisScreen)
return false; return false;
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name) if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
return false; return false;
return true; return true;
}); });
@@ -360,7 +360,7 @@ PanelWindow {
const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all"); const onThisScreen = bc.screenPreferences.includes(screenName) || bc.screenPreferences.length === 0 || bc.screenPreferences.includes("all");
if (!onThisScreen) if (!onThisScreen)
return false; return false;
if (bc.showOnLastDisplay && screenName !== barWindow.screen.name) if (bc.showOnLastDisplay && screenName !== barWindow.screenName)
return false; return false;
return true; return true;
}); });
@@ -686,6 +686,7 @@ PanelWindow {
onHasActivePopoutChanged: evaluateReveal() onHasActivePopoutChanged: evaluateReveal()
function updateActivePopoutState() { function updateActivePopoutState() {
if (!barWindow.screen) return;
const screenName = barWindow.screen.name; const screenName = barWindow.screen.name;
const activePopout = PopoutManager.currentPopoutsByScreen[screenName]; const activePopout = PopoutManager.currentPopoutsByScreen[screenName];
const activeTrayMenu = TrayMenuManager.activeTrayMenus[screenName]; const activeTrayMenu = TrayMenuManager.activeTrayMenus[screenName];
@@ -1,3 +1,4 @@
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import qs.Common import qs.Common
@@ -38,12 +39,20 @@ BasePill {
property var _vAudio: null property var _vAudio: null
property var _vBrightness: null property var _vBrightness: null
property var _vMic: null property var _vMic: null
property var _interactionDelegates: []
readonly property var defaultControlCenterGroupOrder: ["network", "vpn", "bluetooth", "audio", "microphone", "brightness", "battery", "printer", "screenSharing"]
readonly property var effectiveControlCenterGroupOrder: getEffectiveControlCenterGroupOrder()
readonly property var controlCenterRenderModel: getControlCenterRenderModel()
onIsVerticalOrientationChanged: root.clearInteractionRefs()
onWheel: function (wheelEvent) { onWheel: function (wheelEvent) {
const delta = wheelEvent.angleDelta.y; const delta = wheelEvent.angleDelta.y;
if (delta === 0) if (delta === 0)
return; return;
root.refreshInteractionRefs();
const rootX = wheelEvent.x - root.leftMargin; const rootX = wheelEvent.x - root.leftMargin;
const rootY = wheelEvent.y - root.topMargin; const rootY = wheelEvent.y - root.topMargin;
@@ -72,6 +81,8 @@ BasePill {
} }
onRightClicked: function (rootX, rootY) { onRightClicked: function (rootX, rootY) {
root.refreshInteractionRefs();
if (root.isVerticalOrientation && _vCol) { if (root.isVerticalOrientation && _vCol) {
const pos = root.mapToItem(_vCol, rootX, rootY); const pos = root.mapToItem(_vCol, rootX, rootY);
if (_vAudio?.visible && pos.y >= _vAudio.y && pos.y < _vAudio.y + _vAudio.height) { if (_vAudio?.visible && pos.y >= _vAudio.y && pos.y < _vAudio.y + _vAudio.height) {
@@ -279,26 +290,142 @@ BasePill {
return CupsService.getTotalJobsNum() > 0; return CupsService.getTotalJobsNum() > 0;
} }
function getControlCenterIconSize() {
return Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale);
}
function getEffectiveControlCenterGroupOrder() {
const knownIds = root.defaultControlCenterGroupOrder;
const savedOrder = root.widgetData?.controlCenterGroupOrder;
const result = [];
const seen = {};
if (savedOrder && typeof savedOrder.length === "number") {
for (let i = 0; i < savedOrder.length; ++i) {
const groupId = savedOrder[i];
if (knownIds.indexOf(groupId) === -1 || seen[groupId])
continue;
seen[groupId] = true;
result.push(groupId);
}
}
for (let i = 0; i < knownIds.length; ++i) {
const groupId = knownIds[i];
if (seen[groupId])
continue;
seen[groupId] = true;
result.push(groupId);
}
return result;
}
function isGroupVisible(groupId) {
switch (groupId) {
case "screenSharing":
return root.showScreenSharingIcon && NiriService.hasCasts;
case "network":
return root.showNetworkIcon && NetworkService.networkAvailable;
case "vpn":
return root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected;
case "bluetooth":
return root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled;
case "audio":
return root.showAudioIcon;
case "microphone":
return root.showMicIcon;
case "brightness":
return root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice();
case "battery":
return root.showBatteryIcon && BatteryService.batteryAvailable;
case "printer":
return root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs();
default:
return false;
}
}
function isCompositeGroup(groupId) {
return groupId === "audio" || groupId === "microphone" || groupId === "brightness";
}
function getControlCenterRenderModel() {
return root.effectiveControlCenterGroupOrder.map(groupId => ({
"id": groupId,
"visible": root.isGroupVisible(groupId),
"composite": root.isCompositeGroup(groupId)
}));
}
function clearInteractionRefs() {
root._hAudio = null;
root._hBrightness = null;
root._hMic = null;
root._vAudio = null;
root._vBrightness = null;
root._vMic = null;
}
function registerInteractionDelegate(isVertical, item) {
if (!item)
return;
for (let i = 0; i < root._interactionDelegates.length; ++i) {
const entry = root._interactionDelegates[i];
if (entry && entry.item === item) {
entry.isVertical = isVertical;
return;
}
}
root._interactionDelegates = root._interactionDelegates.concat([
{
"isVertical": isVertical,
"item": item
}
]);
}
function unregisterInteractionDelegate(item) {
if (!item)
return;
root._interactionDelegates = root._interactionDelegates.filter(entry => entry && entry.item !== item);
}
function refreshInteractionRefs() {
root.clearInteractionRefs();
for (let i = 0; i < root._interactionDelegates.length; ++i) {
const entry = root._interactionDelegates[i];
const item = entry?.item;
if (!item || !item.visible)
continue;
const groupId = item.interactionGroupId;
if (entry.isVertical) {
if (groupId === "audio")
root._vAudio = item;
else if (groupId === "microphone")
root._vMic = item;
else if (groupId === "brightness")
root._vBrightness = item;
} else {
if (groupId === "audio")
root._hAudio = item;
else if (groupId === "microphone")
root._hMic = item;
else if (groupId === "brightness")
root._hBrightness = item;
}
}
}
function hasNoVisibleIcons() { function hasNoVisibleIcons() {
if (root.showScreenSharingIcon && NiriService.hasCasts) return !root.controlCenterRenderModel.some(entry => entry.visible);
return false;
if (root.showNetworkIcon && NetworkService.networkAvailable)
return false;
if (root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected)
return false;
if (root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled)
return false;
if (root.showAudioIcon)
return false;
if (root.showMicIcon)
return false;
if (root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice())
return false;
if (root.showBatteryIcon && BatteryService.batteryAvailable)
return false;
if (root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs())
return false;
return true;
} }
content: Component { content: Component {
@@ -309,12 +436,7 @@ BasePill {
Component.onCompleted: { Component.onCompleted: {
root._hRow = controlIndicators; root._hRow = controlIndicators;
root._vCol = controlColumn; root._vCol = controlColumn;
root._hAudio = audioIcon.parent; root.clearInteractionRefs();
root._hBrightness = brightnessIcon.parent;
root._hMic = micIcon.parent;
root._vAudio = audioIconV.parent;
root._vBrightness = brightnessIconV.parent;
root._vMic = micIconV.parent;
} }
Column { Column {
@@ -324,159 +446,151 @@ BasePill {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXS spacing: Theme.spacingXS
Item { Repeater {
width: parent.width model: root.controlCenterRenderModel
height: root.vIconSize Item {
visible: root.showScreenSharingIcon && NiriService.hasCasts id: verticalGroupItem
required property var modelData
required property int index
property string interactionGroupId: modelData.id
DankIcon { width: parent.width
name: "screen_record" height: {
size: root.vIconSize switch (modelData.id) {
color: NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText case "audio":
anchors.centerIn: parent return root.vIconSize + (audioPercentV.visible ? audioPercentV.implicitHeight + 2 : 0);
} case "microphone":
} return root.vIconSize + (micPercentV.visible ? micPercentV.implicitHeight + 2 : 0);
case "brightness":
return root.vIconSize + (brightnessPercentV.visible ? brightnessPercentV.implicitHeight + 2 : 0);
default:
return root.vIconSize;
}
}
visible: modelData.visible
Item { Component.onCompleted: {
width: parent.width root.registerInteractionDelegate(true, verticalGroupItem);
height: root.vIconSize root.refreshInteractionRefs();
visible: root.showNetworkIcon && NetworkService.networkAvailable }
Component.onDestruction: {
if (root) {
root.unregisterInteractionDelegate(verticalGroupItem);
root.refreshInteractionRefs();
}
}
onVisibleChanged: root.refreshInteractionRefs()
onInteractionGroupIdChanged: {
root.refreshInteractionRefs();
}
DankIcon { DankIcon {
name: root.getNetworkIconName() anchors.centerIn: parent
size: root.vIconSize visible: !verticalGroupItem.modelData.composite
color: root.getNetworkIconColor() name: {
anchors.centerIn: parent switch (verticalGroupItem.modelData.id) {
} case "screenSharing":
} return "screen_record";
case "network":
return root.getNetworkIconName();
case "vpn":
return "vpn_lock";
case "bluetooth":
return "bluetooth";
case "battery":
return Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable);
case "printer":
return "print";
default:
return "settings";
}
}
size: root.vIconSize
color: {
switch (verticalGroupItem.modelData.id) {
case "screenSharing":
return NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText;
case "network":
return root.getNetworkIconColor();
case "vpn":
return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText;
case "bluetooth":
return BluetoothService.connected ? Theme.primary : Theme.surfaceText;
case "battery":
return root.getBatteryIconColor();
case "printer":
return Theme.primary;
default:
return Theme.widgetIconColor;
}
}
}
Item { DankIcon {
width: parent.width id: audioIconV
height: root.vIconSize visible: verticalGroupItem.modelData.id === "audio"
visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected name: root.getVolumeIconName()
size: root.vIconSize
color: Theme.widgetIconColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
}
DankIcon { NumericText {
name: "vpn_lock" id: audioPercentV
size: root.vIconSize visible: verticalGroupItem.modelData.id === "audio" && root.showAudioPercent && isFinite(AudioService.sink?.audio?.volume)
color: NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
anchors.centerIn: parent reserveText: "100%"
} font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
} color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: audioIconV.bottom
anchors.topMargin: 2
}
Item { DankIcon {
width: parent.width id: micIconV
height: root.vIconSize visible: verticalGroupItem.modelData.id === "microphone"
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled name: root.getMicIconName()
size: root.vIconSize
color: root.getMicIconColor()
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
}
DankIcon { NumericText {
name: "bluetooth" id: micPercentV
size: root.vIconSize visible: verticalGroupItem.modelData.id === "microphone" && root.showMicPercent && isFinite(AudioService.source?.audio?.volume)
color: BluetoothService.connected ? Theme.primary : Theme.surfaceText text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
anchors.centerIn: parent reserveText: "100%"
} font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
} color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: micIconV.bottom
anchors.topMargin: 2
}
Item { DankIcon {
width: parent.width id: brightnessIconV
height: root.vIconSize + (root.showAudioPercent ? audioPercentV.implicitHeight + 2 : 0) visible: verticalGroupItem.modelData.id === "brightness"
visible: root.showAudioIcon name: root.getBrightnessIconName()
size: root.vIconSize
color: Theme.widgetIconColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
}
DankIcon { NumericText {
id: audioIconV id: brightnessPercentV
name: root.getVolumeIconName() visible: verticalGroupItem.modelData.id === "brightness" && root.showBrightnessPercent && isFinite(getBrightness())
size: root.vIconSize text: Math.round(getBrightness() * 100) + "%"
color: Theme.widgetIconColor reserveText: "100%"
anchors.horizontalCenter: parent.horizontalCenter font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
anchors.top: parent.top color: Theme.widgetTextColor
} anchors.horizontalCenter: parent.horizontalCenter
anchors.top: brightnessIconV.bottom
StyledText { anchors.topMargin: 2
id: audioPercentV }
visible: root.showAudioPercent
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: audioIconV.bottom
anchors.topMargin: 2
}
}
Item {
width: parent.width
height: root.vIconSize + (root.showMicPercent ? micPercentV.implicitHeight + 2 : 0)
visible: root.showMicIcon
DankIcon {
id: micIconV
name: root.getMicIconName()
size: root.vIconSize
color: root.getMicIconColor()
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
}
StyledText {
id: micPercentV
visible: root.showMicPercent
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: micIconV.bottom
anchors.topMargin: 2
}
}
Item {
width: parent.width
height: root.vIconSize + (root.showBrightnessPercent ? brightnessPercentV.implicitHeight + 2 : 0)
visible: root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice()
DankIcon {
id: brightnessIconV
name: root.getBrightnessIconName()
size: root.vIconSize
color: Theme.widgetIconColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
}
StyledText {
id: brightnessPercentV
visible: root.showBrightnessPercent
text: Math.round(getBrightness() * 100) + "%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: brightnessIconV.bottom
anchors.topMargin: 2
}
}
Item {
width: parent.width
height: root.vIconSize
visible: root.showBatteryIcon && BatteryService.batteryAvailable
DankIcon {
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
size: root.vIconSize
color: root.getBatteryIconColor()
anchors.centerIn: parent
}
}
Item {
width: parent.width
height: root.vIconSize
visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs()
DankIcon {
name: "print"
size: root.vIconSize
color: Theme.primary
anchors.centerIn: parent
} }
} }
@@ -500,149 +614,206 @@ BasePill {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Theme.spacingXS spacing: Theme.spacingXS
DankIcon { Repeater {
name: "screen_record" model: root.controlCenterRenderModel
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: root.showScreenSharingIcon && NiriService.hasCasts
}
DankIcon { Item {
id: networkIcon id: horizontalGroupItem
name: root.getNetworkIconName() required property var modelData
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) required property int index
color: root.getNetworkIconColor() property string interactionGroupId: modelData.id
anchors.verticalCenter: parent.verticalCenter
visible: root.showNetworkIcon && NetworkService.networkAvailable
}
DankIcon { width: {
id: vpnIcon switch (modelData.id) {
name: "vpn_lock" case "audio":
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) return audioGroup.width;
color: NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText case "microphone":
anchors.verticalCenter: parent.verticalCenter return micGroup.width;
visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected case "brightness":
} return brightnessGroup.width;
default:
return root.getControlCenterIconSize();
}
}
implicitWidth: width
height: root.widgetThickness - root.horizontalPadding * 2
visible: modelData.visible
DankIcon { Component.onCompleted: {
id: bluetoothIcon root.registerInteractionDelegate(false, horizontalGroupItem);
name: "bluetooth" root.refreshInteractionRefs();
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) }
color: BluetoothService.connected ? Theme.primary : Theme.surfaceText Component.onDestruction: {
anchors.verticalCenter: parent.verticalCenter if (root) {
visible: root.showBluetoothIcon && BluetoothService.available && BluetoothService.enabled root.unregisterInteractionDelegate(horizontalGroupItem);
} root.refreshInteractionRefs();
}
}
onVisibleChanged: root.refreshInteractionRefs()
onInteractionGroupIdChanged: {
root.refreshInteractionRefs();
}
Rectangle { DankIcon {
width: audioIcon.implicitWidth + (root.showAudioPercent ? audioPercent.implicitWidth : 0) + 4 id: iconOnlyItem
height: root.widgetThickness - root.horizontalPadding * 2 anchors.verticalCenter: parent.verticalCenter
color: "transparent" anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter visible: !horizontalGroupItem.modelData.composite
visible: root.showAudioIcon name: {
switch (horizontalGroupItem.modelData.id) {
case "screenSharing":
return "screen_record";
case "network":
return root.getNetworkIconName();
case "vpn":
return "vpn_lock";
case "bluetooth":
return "bluetooth";
case "battery":
return Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable);
case "printer":
return "print";
default:
return "settings";
}
}
size: root.getControlCenterIconSize()
color: {
switch (horizontalGroupItem.modelData.id) {
case "screenSharing":
return NiriService.hasActiveCast ? Theme.primary : Theme.surfaceText;
case "network":
return root.getNetworkIconColor();
case "vpn":
return NetworkService.vpnConnected ? Theme.primary : Theme.surfaceText;
case "bluetooth":
return BluetoothService.connected ? Theme.primary : Theme.surfaceText;
case "battery":
return root.getBatteryIconColor();
case "printer":
return Theme.primary;
default:
return Theme.widgetIconColor;
}
}
}
DankIcon { Rectangle {
id: audioIcon id: audioGroup
name: root.getVolumeIconName() width: audioContent.implicitWidth + 2
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) implicitWidth: width
color: Theme.widgetIconColor height: parent.height
anchors.verticalCenter: parent.verticalCenter color: "transparent"
anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 2 visible: horizontalGroupItem.modelData.id === "audio"
Row {
id: audioContent
anchors.left: parent.left
anchors.leftMargin: 1
anchors.verticalCenter: parent.verticalCenter
spacing: 2
DankIcon {
id: audioIcon
name: root.getVolumeIconName()
size: root.getControlCenterIconSize()
color: Theme.widgetIconColor
anchors.verticalCenter: parent.verticalCenter
}
NumericText {
id: audioPercent
visible: root.showAudioPercent && isFinite(AudioService.sink?.audio?.volume)
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
width: visible ? implicitWidth : 0
}
}
}
Rectangle {
id: micGroup
width: micContent.implicitWidth + 2
implicitWidth: width
height: parent.height
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
visible: horizontalGroupItem.modelData.id === "microphone"
Row {
id: micContent
anchors.left: parent.left
anchors.leftMargin: 1
anchors.verticalCenter: parent.verticalCenter
spacing: 2
DankIcon {
id: micIcon
name: root.getMicIconName()
size: root.getControlCenterIconSize()
color: root.getMicIconColor()
anchors.verticalCenter: parent.verticalCenter
}
NumericText {
id: micPercent
visible: root.showMicPercent && isFinite(AudioService.source?.audio?.volume)
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
width: visible ? implicitWidth : 0
}
}
}
Rectangle {
id: brightnessGroup
width: brightnessContent.implicitWidth + 2
implicitWidth: width
height: parent.height
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
visible: horizontalGroupItem.modelData.id === "brightness"
Row {
id: brightnessContent
anchors.left: parent.left
anchors.leftMargin: 1
anchors.verticalCenter: parent.verticalCenter
spacing: 2
DankIcon {
id: brightnessIcon
name: root.getBrightnessIconName()
size: root.getControlCenterIconSize()
color: Theme.widgetIconColor
anchors.verticalCenter: parent.verticalCenter
}
NumericText {
id: brightnessPercent
visible: root.showBrightnessPercent && isFinite(getBrightness())
text: Math.round(getBrightness() * 100) + "%"
reserveText: "100%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
width: visible ? implicitWidth : 0
}
}
}
} }
StyledText {
id: audioPercent
visible: root.showAudioPercent
text: Math.round((AudioService.sink?.audio?.volume ?? 0) * 100) + "%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
anchors.left: audioIcon.right
anchors.leftMargin: 2
}
}
Rectangle {
width: micIcon.implicitWidth + (root.showMicPercent ? micPercent.implicitWidth : 0) + 4
height: root.widgetThickness - root.horizontalPadding * 2
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
visible: root.showMicIcon
DankIcon {
id: micIcon
name: root.getMicIconName()
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: root.getMicIconColor()
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 2
}
StyledText {
id: micPercent
visible: root.showMicPercent
text: Math.round((AudioService.source?.audio?.volume ?? 0) * 100) + "%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
anchors.left: micIcon.right
anchors.leftMargin: 2
}
}
Rectangle {
width: brightnessIcon.implicitWidth + (root.showBrightnessPercent ? brightnessPercent.implicitWidth : 0) + 4
height: root.widgetThickness - root.horizontalPadding * 2
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
visible: root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice()
DankIcon {
id: brightnessIcon
name: root.getBrightnessIconName()
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: Theme.widgetIconColor
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 2
}
StyledText {
id: brightnessPercent
visible: root.showBrightnessPercent
text: Math.round(getBrightness() * 100) + "%"
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
color: Theme.widgetTextColor
anchors.verticalCenter: parent.verticalCenter
anchors.left: brightnessIcon.right
anchors.leftMargin: 2
}
}
DankIcon {
id: batteryIcon
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: root.getBatteryIconColor()
anchors.verticalCenter: parent.verticalCenter
visible: root.showBatteryIcon && BatteryService.batteryAvailable
}
DankIcon {
id: printerIcon
name: "print"
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs()
} }
DankIcon { DankIcon {
name: "settings" name: "settings"
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale) size: root.getControlCenterIconSize()
color: root.isActive ? Theme.primary : Theme.widgetIconColor color: root.isActive ? Theme.primary : Theme.widgetIconColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: root.hasNoVisibleIcons() visible: root.hasNoVisibleIcons()
@@ -87,11 +87,11 @@ BasePill {
} }
const workspaceWindows = NiriService.windows.filter(w => w.workspace_id === currentWorkspaceId); const workspaceWindows = NiriService.windows.filter(w => w.workspace_id === currentWorkspaceId);
return workspaceWindows.length > 0 && activeWindow && activeWindow.title; return workspaceWindows.length > 0 && activeWindow && (activeWindow.title || activeWindow.appId);
} }
if (CompositorService.isHyprland) { if (CompositorService.isHyprland) {
if (!Hyprland.focusedWorkspace || !activeWindow || !activeWindow.title) { if (!Hyprland.focusedWorkspace || !activeWindow || !(activeWindow.title || activeWindow.appId)) {
return false; return false;
} }
@@ -111,7 +111,7 @@ BasePill {
} }
} }
return activeWindow && activeWindow.title; return activeWindow && (activeWindow.title || activeWindow.appId);
} }
width: hasWindowsOnCurrentWorkspace ? (isVerticalOrientation ? barThickness : visualWidth) : 0 width: hasWindowsOnCurrentWorkspace ? (isVerticalOrientation ? barThickness : visualWidth) : 0
@@ -211,17 +211,20 @@ BasePill {
text: { text: {
const title = activeWindow && activeWindow.title ? activeWindow.title : ""; const title = activeWindow && activeWindow.title ? activeWindow.title : "";
const appName = appText.text; const appName = appText.text;
if (!title || !appName) {
if (compactMode) {
if (!title || title === appName)
return title || appName;
if (title.endsWith(appName))
return title.substring(0, title.length - appName.length).replace(/ (-|—) $/, "") || appName;
return title; return title;
} }
if (title.endsWith(" - " + appName)) { if (!title || !appName)
return title.substring(0, title.length - (" - " + appName).length); return title;
}
if (title.endsWith(appName)) { if (title.endsWith(appName))
return title.substring(0, title.length - appName.length).replace(/ - $/, ""); return title.substring(0, title.length - appName.length).replace(/ (-|—) $/, "");
}
return title; return title;
} }
@@ -1,5 +1,4 @@
import QtQuick import QtQuick
import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Hyprland import Quickshell.Hyprland
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
@@ -162,6 +161,23 @@ BasePill {
return 0; return 0;
} }
readonly property string autoBarShadowDirection: {
const edge = root.axis?.edge;
switch (edge) {
case "top":
return "top";
case "bottom":
return "bottom";
case "left":
return "left";
case "right":
return "right";
default:
return "bottom";
}
}
readonly property string effectiveShadowDirection: Theme.elevationLightDirection === "autoBar" ? autoBarShadowDirection : Theme.elevationLightDirection
property bool menuOpen: false property bool menuOpen: false
property var currentTrayMenu: null property var currentTrayMenu: null
@@ -940,13 +956,6 @@ BasePill {
} }
})(), overflowMenu.dpr) })(), overflowMenu.dpr)
readonly property var elev: Theme.elevationLevel2
property real shadowBlurPx: elev && elev.blurPx !== undefined ? elev.blurPx : 8
property real shadowSpreadPx: elev && elev.spreadPx !== undefined ? elev.spreadPx : 0
property real shadowBaseAlpha: elev && elev.alpha !== undefined ? elev.alpha : 0.25
readonly property real popupSurfaceAlpha: Theme.popupTransparency
readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha))
opacity: root.menuOpen ? 1 : 0 opacity: root.menuOpen ? 1 : 0
scale: root.menuOpen ? 1 : 0.85 scale: root.menuOpen ? 1 : 0.85
@@ -967,19 +976,14 @@ BasePill {
ElevationShadow { ElevationShadow {
id: bgShadowLayer id: bgShadowLayer
anchors.fill: parent anchors.fill: parent
level: menuContainer.elev level: Theme.elevationLevel3
fallbackOffset: 4 direction: root.effectiveShadowDirection
shadowBlurPx: menuContainer.shadowBlurPx fallbackOffset: 6
shadowSpreadPx: menuContainer.shadowSpreadPx
shadowColor: {
const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest;
return Theme.withAlpha(baseColor, menuContainer.effectiveShadowAlpha);
}
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
targetRadius: Theme.cornerRadius targetRadius: Theme.cornerRadius
sourceRect.antialiasing: true sourceRect.antialiasing: true
sourceRect.smooth: true sourceRect.smooth: true
shadowEnabled: Theme.elevationEnabled shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled
layer.smooth: true layer.smooth: true
layer.textureSize: Qt.size(Math.round(width * overflowMenu.dpr * 2), Math.round(height * overflowMenu.dpr * 2)) layer.textureSize: Qt.size(Math.round(width * overflowMenu.dpr * 2), Math.round(height * overflowMenu.dpr * 2))
layer.textureMirroring: ShaderEffectSource.MirrorVertically layer.textureMirroring: ShaderEffectSource.MirrorVertically
@@ -1402,13 +1406,6 @@ BasePill {
} }
})(), menuWindow.dpr) })(), menuWindow.dpr)
readonly property var elev: Theme.elevationLevel2
property real shadowBlurPx: elev && elev.blurPx !== undefined ? elev.blurPx : 8
property real shadowSpreadPx: elev && elev.spreadPx !== undefined ? elev.spreadPx : 0
property real shadowBaseAlpha: elev && elev.alpha !== undefined ? elev.alpha : 0.25
readonly property real popupSurfaceAlpha: Theme.popupTransparency
readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha))
opacity: menuRoot.showMenu ? 1 : 0 opacity: menuRoot.showMenu ? 1 : 0
scale: menuRoot.showMenu ? 1 : 0.85 scale: menuRoot.showMenu ? 1 : 0.85
@@ -1429,18 +1426,13 @@ BasePill {
ElevationShadow { ElevationShadow {
id: menuBgShadowLayer id: menuBgShadowLayer
anchors.fill: parent anchors.fill: parent
level: menuContainer.elev level: Theme.elevationLevel3
fallbackOffset: 4 direction: root.effectiveShadowDirection
shadowBlurPx: menuContainer.shadowBlurPx fallbackOffset: 6
shadowSpreadPx: menuContainer.shadowSpreadPx
shadowColor: {
const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest;
return Theme.withAlpha(baseColor, menuContainer.effectiveShadowAlpha);
}
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
targetRadius: Theme.cornerRadius targetRadius: Theme.cornerRadius
sourceRect.antialiasing: true sourceRect.antialiasing: true
shadowEnabled: Theme.elevationEnabled shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled
layer.smooth: true layer.smooth: true
layer.textureSize: Qt.size(Math.round(width * menuWindow.dpr), Math.round(height * menuWindow.dpr)) layer.textureSize: Qt.size(Math.round(width * menuWindow.dpr), Math.round(height * menuWindow.dpr))
layer.textureMirroring: ShaderEffectSource.MirrorVertically layer.textureMirroring: ShaderEffectSource.MirrorVertically
@@ -1470,6 +1470,10 @@ Item {
delegate: Item { delegate: Item {
width: root.appIconSize width: root.appIconSize
height: root.appIconSize height: root.appIconSize
readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active
readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected
readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary
readonly property real appOpacity: (modelData.active || isActive) ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
IconImage { IconImage {
id: rowAppIcon id: rowAppIcon
@@ -1485,14 +1489,14 @@ Item {
color: Theme.surfaceContainer color: Theme.surfaceContainer
radius: Theme.cornerRadius * (root.appIconSize / 40) radius: Theme.cornerRadius * (root.appIconSize / 40)
border.width: 1 border.width: 1
border.color: Theme.primarySelected border.color: appBorderColor
opacity: (modelData.active || isActive) ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6 opacity: appOpacity
StyledText { StyledText {
anchors.centerIn: parent anchors.centerIn: parent
text: (modelData.fallbackText || "?").charAt(0).toUpperCase() text: (modelData.fallbackText || "?").charAt(0).toUpperCase()
font.pixelSize: parent.width * 0.45 font.pixelSize: parent.width * 0.45
color: Theme.primary color: appGlyphColor
font.weight: Font.Bold font.weight: Font.Bold
} }
} }
@@ -1503,14 +1507,14 @@ Item {
color: Theme.surfaceContainer color: Theme.surfaceContainer
radius: Theme.cornerRadius * (root.appIconSize / 40) radius: Theme.cornerRadius * (root.appIconSize / 40)
border.width: 1 border.width: 1
border.color: Theme.primarySelected border.color: appBorderColor
opacity: (modelData.active || isActive) ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6 opacity: appOpacity
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
size: parent.width * 0.7 size: parent.width * 0.7
name: "sports_esports" name: "sports_esports"
color: Theme.primary color: appGlyphColor
} }
} }
@@ -1523,11 +1527,12 @@ Item {
layer.effect: MultiEffect { layer.effect: MultiEffect {
saturation: 0 saturation: 0
colorization: 1 colorization: 1
colorizationColor: isActive ? quickshellIconActiveColor : quickshellIconInactiveColor colorizationColor: appHighlightActive ? focusedBorderColor : (isActive ? quickshellIconActiveColor : quickshellIconInactiveColor)
} }
} }
IconImage { IconImage {
id: rowSteamIcon
anchors.fill: parent anchors.fill: parent
source: modelData.icon source: modelData.icon
opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6 opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
@@ -1538,11 +1543,21 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
size: root.appIconSize size: root.appIconSize
name: "sports_esports" name: "sports_esports"
color: Theme.widgetTextColor color: appHighlightActive ? focusedBorderColor : Theme.widgetTextColor
opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6 opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
visible: modelData.isSteamApp && !modelData.icon visible: modelData.isSteamApp && !modelData.icon
} }
Rectangle {
anchors.fill: parent
visible: (rowAppIcon.visible || rowSteamIcon.visible || modelData.isQuickshell) && appHighlightActive
color: "transparent"
radius: Theme.cornerRadius * (root.appIconSize / 40)
border.width: 1
border.color: focusedBorderColor
z: 1
}
MouseArea { MouseArea {
id: rowAppMouseArea id: rowAppMouseArea
anchors.fill: parent anchors.fill: parent
@@ -1624,6 +1639,10 @@ Item {
delegate: Item { delegate: Item {
width: root.appIconSize width: root.appIconSize
height: root.appIconSize height: root.appIconSize
readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active
readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected
readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary
readonly property real appOpacity: (modelData.active || isActive) ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
IconImage { IconImage {
id: colAppIcon id: colAppIcon
@@ -1639,14 +1658,14 @@ Item {
color: Theme.surfaceContainer color: Theme.surfaceContainer
radius: Theme.cornerRadius * (root.appIconSize / 40) radius: Theme.cornerRadius * (root.appIconSize / 40)
border.width: 1 border.width: 1
border.color: Theme.primarySelected border.color: appBorderColor
opacity: (modelData.active || isActive) ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6 opacity: appOpacity
StyledText { StyledText {
anchors.centerIn: parent anchors.centerIn: parent
text: (modelData.fallbackText || "?").charAt(0).toUpperCase() text: (modelData.fallbackText || "?").charAt(0).toUpperCase()
font.pixelSize: parent.width * 0.45 font.pixelSize: parent.width * 0.45
color: Theme.primary color: appGlyphColor
font.weight: Font.Bold font.weight: Font.Bold
} }
} }
@@ -1657,14 +1676,14 @@ Item {
color: Theme.surfaceContainer color: Theme.surfaceContainer
radius: Theme.cornerRadius * (root.appIconSize / 40) radius: Theme.cornerRadius * (root.appIconSize / 40)
border.width: 1 border.width: 1
border.color: Theme.primarySelected border.color: appBorderColor
opacity: (modelData.active || isActive) ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6 opacity: appOpacity
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
size: parent.width * 0.7 size: parent.width * 0.7
name: "sports_esports" name: "sports_esports"
color: Theme.primary color: appGlyphColor
} }
} }
@@ -1677,11 +1696,12 @@ Item {
layer.effect: MultiEffect { layer.effect: MultiEffect {
saturation: 0 saturation: 0
colorization: 1 colorization: 1
colorizationColor: isActive ? quickshellIconActiveColor : quickshellIconInactiveColor colorizationColor: appHighlightActive ? focusedBorderColor : (isActive ? quickshellIconActiveColor : quickshellIconInactiveColor)
} }
} }
IconImage { IconImage {
id: colSteamIcon
anchors.fill: parent anchors.fill: parent
source: modelData.icon source: modelData.icon
opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6 opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
@@ -1692,11 +1712,21 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
size: root.appIconSize size: root.appIconSize
name: "sports_esports" name: "sports_esports"
color: Theme.widgetTextColor color: appHighlightActive ? focusedBorderColor : Theme.widgetTextColor
opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6 opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
visible: modelData.isSteamApp && !modelData.icon visible: modelData.isSteamApp && !modelData.icon
} }
Rectangle {
anchors.fill: parent
visible: (colAppIcon.visible || colSteamIcon.visible || modelData.isQuickshell) && appHighlightActive
color: "transparent"
radius: Theme.cornerRadius * (root.appIconSize / 40)
border.width: 1
border.color: focusedBorderColor
z: 1
}
MouseArea { MouseArea {
id: colAppMouseArea id: colAppMouseArea
anchors.fill: parent anchors.fill: parent
@@ -12,10 +12,11 @@ DankPopout {
property var triggerScreen: null property var triggerScreen: null
property int currentTabIndex: 0 property int currentTabIndex: 0
popupWidth: 700 popupWidth: SettingsData.showWeekNumber ? 736 : 700
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500 popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
triggerWidth: 80 triggerWidth: 80
screen: triggerScreen screen: triggerScreen
shouldBeVisible: dashVisible
property bool __focusArmed: false property bool __focusArmed: false
property bool __contentReady: false property bool __contentReady: false
@@ -167,6 +168,7 @@ DankPopout {
LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true LayoutMirroring.childrenInherit: true
implicitWidth: Math.max(700, pages.implicitWidth + (Theme.spacingM * 2))
implicitHeight: contentColumn.height + Theme.spacingM * 2 implicitHeight: contentColumn.height + Theme.spacingM * 2
color: "transparent" color: "transparent"
focus: true focus: true
@@ -315,6 +317,7 @@ DankPopout {
id: pages id: pages
width: parent.width width: parent.width
height: implicitHeight height: implicitHeight
implicitWidth: currentItem && currentItem.implicitWidth > 0 ? currentItem.implicitWidth : (700 - Theme.spacingM * 2)
implicitHeight: { implicitHeight: {
if (root.currentTabIndex === 0) if (root.currentTabIndex === 0)
return overviewLoader.item?.implicitHeight ?? 410; return overviewLoader.item?.implicitHeight ?? 410;
@@ -44,43 +44,6 @@ Item {
property int __volumeHoverCount: 0 property int __volumeHoverCount: 0
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
function panelMotionX(panelWidth, active) {
if (active)
return 0;
if (directionalEffect) {
const travel = Math.max(Theme.effectAnimOffset, panelWidth * 0.85);
return isRightEdge ? -travel : travel;
}
if (depthEffect) {
const travel = Math.max(Theme.effectAnimOffset * 0.7, panelWidth * 0.32);
return isRightEdge ? -travel : travel;
}
return 0;
}
function panelMotionY(panelType, panelHeight, active) {
if (active)
return 0;
if (directionalEffect) {
if (panelType === 2)
return panelHeight * 0.08;
if (panelType === 3)
return -panelHeight * 0.08;
return 0;
}
if (depthEffect) {
if (panelType === 2)
return panelHeight * 0.04;
if (panelType === 3)
return -panelHeight * 0.04;
return 0;
}
return 0;
}
function volumeAreaEntered() { function volumeAreaEntered() {
__volumeHoverCount++; __volumeHoverCount++;
panelEntered(); panelEntered();
@@ -99,47 +62,30 @@ Item {
visible: dropdownType === 1 && volumeAvailable visible: dropdownType === 1 && volumeAvailable
width: 60 width: 60
height: 180 height: 180
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 1) x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2 + panelMotionY(1, height, dropdownType === 1) y: anchorPos.y - height / 2
radius: Theme.cornerRadius * 2 radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95) color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
border.width: 1 border.width: 1
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : 0) opacity: dropdownType === 1 ? 1 : 0
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : Theme.effectScaleCollapsed) scale: dropdownType === 1 ? 1 : 0.96
transformOrigin: isRightEdge ? Item.Left : Item.Right transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity { Behavior on opacity {
enabled: !Theme.isDirectionalEffect NumberAnimation {
DankAnim { duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1) * Theme.variantOpacityDurationScale) easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
} }
} }
Behavior on scale { Behavior on scale {
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1) duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on y {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
@@ -251,50 +197,33 @@ Item {
Rectangle { Rectangle {
id: audioDevicesPanel id: audioDevicesPanel
visible: dropdownType === 2 && activePlayer !== null visible: dropdownType === 2
width: 280 width: 280
height: Math.max(200, Math.min(280, availableDevices.length * 50 + 100)) height: Math.max(200, Math.min(280, availableDevices.length * 50 + 100))
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 2) x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2 + panelMotionY(2, height, dropdownType === 2) y: anchorPos.y - height / 2
radius: Theme.cornerRadius * 2 radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98) color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
border.width: 2 border.width: 2
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : 0) opacity: dropdownType === 2 ? 1 : 0
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : Theme.effectScaleCollapsed) scale: dropdownType === 2 ? 1 : 0.96
transformOrigin: isRightEdge ? Item.Left : Item.Right transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity { Behavior on opacity {
enabled: !Theme.isDirectionalEffect NumberAnimation {
DankAnim { duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2) * Theme.variantOpacityDurationScale) easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
} }
} }
Behavior on scale { Behavior on scale {
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2) duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on y {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
@@ -425,47 +354,30 @@ Item {
visible: dropdownType === 3 visible: dropdownType === 3
width: 240 width: 240
height: Math.max(180, Math.min(240, (allPlayers?.length || 0) * 50 + 80)) height: Math.max(180, Math.min(240, (allPlayers?.length || 0) * 50 + 80))
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 3) x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2 + panelMotionY(3, height, dropdownType === 3) y: anchorPos.y - height / 2
radius: Theme.cornerRadius * 2 radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98) color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
border.width: 2 border.width: 2
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : 0) opacity: dropdownType === 3 ? 1 : 0
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : Theme.effectScaleCollapsed) scale: dropdownType === 3 ? 1 : 0.96
transformOrigin: isRightEdge ? Item.Left : Item.Right transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity { Behavior on opacity {
enabled: !Theme.isDirectionalEffect NumberAnimation {
DankAnim { duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3) * Theme.variantOpacityDurationScale) easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
} }
} }
Behavior on scale { Behavior on scale {
enabled: !Theme.isDirectionalEffect
NumberAnimation { NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3) duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on y {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
} }
} }
@@ -105,7 +105,7 @@ Item {
return Math.max(0, Math.min(1, calculatedRatio)); return Math.max(0, Math.min(1, calculatedRatio));
} }
implicitWidth: 700 implicitWidth: SettingsData.showWeekNumber ? 736 : 700
implicitHeight: playerContent.height + playerContent.anchors.topMargin * 2 implicitHeight: playerContent.height + playerContent.anchors.topMargin * 2
Connections { Connections {
@@ -7,6 +7,8 @@ import qs.Widgets
Rectangle { Rectangle {
id: root id: root
implicitWidth: SettingsData.showWeekNumber ? 736 : 700
property bool showEventDetails: false property bool showEventDetails: false
property date selectedDate: systemClock.date property date selectedDate: systemClock.date
property var selectedDateEvents: [] property var selectedDateEvents: []
@@ -41,6 +43,40 @@ Rectangle {
return d; return d;
} }
function getWeekNumber(dateObj) {
// Set time to noon to avoid potential Daylight Saving Time related bugs
const weekStartDay = startOfWeek(dateObj);
weekStartDay.setHours(12, 0, 0, 0);
let week1Start;
if (weekStartJs() === 1) {
// ISO 8601 Standard, week start on Monday
// A week belongs to the year its Thursday falls in
// So we have to get the yearTarget from weekStartDay instead of dateObj
let yearTarget = weekStartDay;
yearTarget.setDate(yearTarget.getDate() + 3); // Monday + 3 = Thursday
// Week 1 is the week containing Jan 4th
const jan4 = new Date(yearTarget.getFullYear(), 0, 4);
week1Start = startOfWeek(jan4);
} else {
// Traditional / US Standard, week start on Sunday
// A week belongs to the year its Sunday falls in
let yearTarget = weekStartDay;
yearTarget.setDate(yearTarget.getDate() + 6); // Monday + 6 = Sunday
// Week 1 is the week containing Jan 1st
const jan1 = new Date(yearTarget.getFullYear(), 0, 1);
week1Start = startOfWeek(jan1);
}
week1Start.setHours(12, 0, 0, 0);
const diffDays = Math.round((weekStartDay.getTime() - week1Start.getTime()) / 86400000); // Number of miliseconds in a day
return Math.floor(diffDays / 7) + 1;
}
function updateSelectedDateEvents() { function updateSelectedDateEvents() {
if (CalendarService && CalendarService.khalAvailable) { if (CalendarService && CalendarService.khalAvailable) {
const events = CalendarService.getEventsForDate(selectedDate); const events = CalendarService.getEventsForDate(selectedDate);
@@ -151,6 +187,7 @@ Rectangle {
elide: Text.ElideRight elide: Text.ElideRight
} }
} }
Row { Row {
width: parent.width width: parent.width
height: 28 height: 28
@@ -224,120 +261,172 @@ Rectangle {
Row { Row {
width: parent.width width: parent.width
height: 18 height: parent.height - 28 - Theme.spacingS
visible: !showEventDetails visible: !showEventDetails
spacing: SettingsData.showWeekNumber ? Theme.spacingS : 0
Repeater { Column {
model: { id: weekNumberColumn
const days = []; visible: SettingsData.showWeekNumber
const qtFirst = weekStartQt(); width: SettingsData.showWeekNumber ? 28 : 0
for (let i = 0; i < 7; ++i) { height: parent.height
const qtDay = ((qtFirst - 1 + i) % 7) + 1; spacing: Theme.spacingS
days.push(I18n.locale().dayName(qtDay, Locale.ShortFormat));
}
return days;
}
Rectangle { Item {
width: parent.width / 7 width: parent.width
height: 18 height: 18
color: "transparent" }
StyledText { Grid {
anchors.centerIn: parent width: parent.width
text: modelData height: parent.height - 18 - Theme.spacingS
font.pixelSize: Theme.fontSizeSmall columns: 1
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6) rows: 6
font.weight: Font.Medium
Repeater {
model: 6
Rectangle {
width: parent.width
height: parent.height / 6
color: "transparent"
StyledText {
anchors.centerIn: parent
text: {
const rowDate = new Date(calendarGrid.firstDay);
rowDate.setDate(rowDate.getDate() + index * 7);
return root.getWeekNumber(rowDate);
}
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
font.weight: Font.Medium
}
}
} }
} }
} }
}
Grid { Column {
id: calendarGrid width: SettingsData.showWeekNumber ? (parent.width - weekNumberColumn.width - parent.spacing) : parent.width
visible: !showEventDetails height: parent.height
spacing: Theme.spacingS
property date displayDate: systemClock.date Row {
property date selectedDate: systemClock.date width: parent.width
height: 18
readonly property date firstDay: { Repeater {
const firstOfMonth = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1); model: {
return startOfWeek(firstOfMonth); const days = [];
} const qtFirst = weekStartQt();
for (let i = 0; i < 7; ++i) {
width: parent.width const qtDay = ((qtFirst - 1 + i) % 7) + 1;
height: parent.height - 28 - 18 - Theme.spacingS * 2 days.push(I18n.locale().dayName(qtDay, Locale.ShortFormat));
columns: 7 }
rows: 6 return days;
Repeater {
model: 42
Rectangle {
readonly property date dayDate: {
const date = new Date(parent.firstDay);
date.setDate(date.getDate() + index);
return date;
}
readonly property bool isCurrentMonth: dayDate.getMonth() === calendarGrid.displayDate.getMonth()
readonly property bool isToday: dayDate.toDateString() === new Date().toDateString()
readonly property bool isSelected: dayDate.toDateString() === calendarGrid.selectedDate.toDateString()
width: parent.width / 7
height: parent.height / 6
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: Math.min(parent.width - 4, parent.height - 4, 32)
height: width
color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius
StyledText {
anchors.centerIn: parent
text: dayDate.getDate()
font.pixelSize: Theme.fontSizeSmall
color: isToday ? Theme.primary : isCurrentMonth ? Theme.surfaceText : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
font.weight: isToday ? Font.Medium : Font.Normal
} }
Rectangle { Rectangle {
anchors.bottom: parent.bottom width: parent.width / 7
anchors.horizontalCenter: parent.horizontalCenter height: 18
anchors.bottomMargin: 4 color: "transparent"
width: 12
height: 2
radius: Theme.cornerRadius
visible: CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)
color: isToday ? Qt.lighter(Theme.primary, 1.3) : Theme.primary
opacity: isToday ? 0.9 : 0.7
Behavior on opacity { StyledText {
NumberAnimation { anchors.centerIn: parent
duration: Theme.shortDuration text: modelData
easing.type: Theme.standardEasing font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
font.weight: Font.Medium
}
}
}
}
Grid {
id: calendarGrid
width: parent.width
height: parent.height - 18 - Theme.spacingS
columns: 7
rows: 6
property date displayDate: systemClock.date
property date selectedDate: systemClock.date
readonly property date firstDay: {
const firstOfMonth = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1);
return startOfWeek(firstOfMonth);
}
Repeater {
model: 42
Rectangle {
readonly property date dayDate: {
const date = new Date(parent.firstDay);
date.setDate(date.getDate() + index);
return date;
}
readonly property bool isCurrentMonth: dayDate.getMonth() === calendarGrid.displayDate.getMonth()
readonly property bool isToday: dayDate.toDateString() === new Date().toDateString()
readonly property bool isSelected: dayDate.toDateString() === calendarGrid.selectedDate.toDateString()
width: parent.width / 7
height: parent.height / 6
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: Math.min(parent.width - 4, parent.height - 4, 32)
height: width
color: isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
radius: Theme.cornerRadius
StyledText {
anchors.centerIn: parent
text: dayDate.getDate()
font.pixelSize: Theme.fontSizeSmall
color: isToday ? Theme.primary : isCurrentMonth ? Theme.surfaceText : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
font.weight: isToday ? Font.Medium : Font.Normal
}
Rectangle {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: 4
width: 12
height: 2
radius: Theme.cornerRadius
visible: CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)
color: isToday ? Qt.lighter(Theme.primary, 1.3) : Theme.primary
opacity: isToday ? 0.9 : 0.7
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
MouseArea {
id: dayArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)) {
root.selectedDate = dayDate;
root.showEventDetails = true;
}
} }
} }
} }
} }
MouseArea {
id: dayArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)) {
root.selectedDate = dayDate;
root.showEventDetails = true;
}
}
}
} }
} }
} }
DankListView { DankListView {
width: parent.width - Theme.spacingS * 2 width: parent.width - Theme.spacingS * 2
height: parent.height - (showEventDetails ? 40 : 28 + 18) - Theme.spacingS height: parent.height - (showEventDetails ? 40 : 28 + 18) - Theme.spacingS
+1 -1
View File
@@ -8,7 +8,7 @@ Item {
LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true LayoutMirroring.childrenInherit: true
implicitWidth: 700 implicitWidth: SettingsData.showWeekNumber ? 736 : 700
implicitHeight: 410 implicitHeight: 410
signal switchToWeatherTab signal switchToWeatherTab
+1 -1
View File
@@ -12,7 +12,7 @@ Item {
LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true LayoutMirroring.childrenInherit: true
implicitWidth: 700 implicitWidth: SettingsData.showWeekNumber ? 736 : 700
implicitHeight: 410 implicitHeight: 410
property string wallpaperDir: "" property string wallpaperDir: ""
+1 -1
View File
@@ -11,7 +11,7 @@ Item {
LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true LayoutMirroring.childrenInherit: true
implicitWidth: 700 implicitWidth: SettingsData.showWeekNumber ? 736 : 700
implicitHeight: 410 implicitHeight: 410
property bool syncing: false property bool syncing: false
property bool showHourly: false property bool showHourly: false
+43 -2
View File
@@ -1,6 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Effects import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Widgets import Quickshell.Widgets
import qs.Common import qs.Common
@@ -133,6 +134,40 @@ Item {
function getGroupedToplevels() { function getGroupedToplevels() {
return appData?.allWindows?.map(w => w.toplevel).filter(t => t !== null) || []; return appData?.allWindows?.map(w => w.toplevel).filter(t => t !== null) || [];
} }
function getHyprToplevelForWayland(waylandToplevel) {
if (!waylandToplevel || !CompositorService.isHyprland || !Hyprland.toplevels)
return null;
const hyprToplevels = Array.from(Hyprland.toplevels.values);
for (let i = 0; i < hyprToplevels.length; i++) {
if (hyprToplevels[i].wayland === waylandToplevel)
return hyprToplevels[i];
}
return null;
}
function getSpecialWorkspaceName(waylandToplevel) {
const hyprToplevel = getHyprToplevelForWayland(waylandToplevel);
if (!hyprToplevel)
return "";
const wsName = String(hyprToplevel.lastIpcObject?.workspace?.name || hyprToplevel.workspace?.name || "");
if (!wsName.startsWith("special:"))
return "";
return wsName.slice("special:".length);
}
function restoreSpecialWorkspaceWindow(waylandToplevel) {
if (!SettingsData.dockRestoreSpecialWorkspaceOnClick || !CompositorService.isHyprland || !waylandToplevel)
return false;
const specialName = getSpecialWorkspaceName(waylandToplevel);
if (!specialName)
return false;
Hyprland.dispatch("togglespecialworkspace " + specialName);
Qt.callLater(() => waylandToplevel.activate());
return true;
}
onIsHoveredChanged: { onIsHoveredChanged: {
if (mouseArea.pressed || dragging) if (mouseArea.pressed || dragging)
return; return;
@@ -276,8 +311,11 @@ Item {
break; break;
case "window": case "window":
const windowToplevel = getToplevelObject(); const windowToplevel = getToplevelObject();
if (windowToplevel) if (windowToplevel) {
if (restoreSpecialWorkspaceWindow(windowToplevel))
return;
windowToplevel.activate(); windowToplevel.activate();
}
break; break;
case "grouped": case "grouped":
if (appData.windowCount === 0) { if (appData.windowCount === 0) {
@@ -300,8 +338,11 @@ Item {
SessionService.launchDesktopEntry(groupedEntry); SessionService.launchDesktopEntry(groupedEntry);
} else if (appData.windowCount === 1) { } else if (appData.windowCount === 1) {
const groupedToplevel = getToplevelObject(); const groupedToplevel = getToplevelObject();
if (groupedToplevel) if (groupedToplevel) {
if (restoreSpecialWorkspaceWindow(groupedToplevel))
return;
groupedToplevel.activate(); groupedToplevel.activate();
}
} else if (contextMenu) { } else if (contextMenu) {
const shouldHidePin = appData.appId === "org.quickshell"; const shouldHidePin = appData.appId === "org.quickshell";
contextMenu.showForButton(root, appData, root.height + 25, shouldHidePin, cachedDesktopEntry, parentDockScreen, dockApps); contextMenu.showForButton(root, appData, root.height + 25, shouldHidePin, cachedDesktopEntry, parentDockScreen, dockApps);
+20
View File
@@ -0,0 +1,20 @@
.pragma library
function readBoolOverride(envReader, names, fallbackValue) {
for (let i = 0; i < names.length; i++) {
const name = names[i];
const raw = envReader(name);
if (raw === undefined || raw === null || raw === "")
continue;
const normalized = String(raw).trim().toLowerCase();
if (normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on")
return true;
if (normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off")
return false;
console.warn("Invalid boolean override for", name + ":", raw, "- trying next override/fallback");
}
return fallbackValue;
}
+29 -8
View File
@@ -4,13 +4,16 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import "GreetdEnv.js" as GreetdEnv
Singleton { Singleton {
id: root id: root
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms" readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
readonly property string sessionConfigPath: greetCfgDir + "/session.json" readonly property string sessionConfigPath: greetCfgDir + "/session.json"
readonly property string memoryFile: greetCfgDir + "/memory.json" readonly property string memoryFile: greetCfgDir + "/.local/state/memory.json"
readonly property bool rememberLastSession: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], true)
readonly property bool rememberLastUser: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], true)
property string lastSessionId: "" property string lastSessionId: ""
property string lastSuccessfulUser: "" property string lastSuccessfulUser: ""
@@ -49,26 +52,44 @@ Singleton {
if (!content || !content.trim()) if (!content || !content.trim())
return; return;
const memory = JSON.parse(content); const memory = JSON.parse(content);
lastSessionId = memory.lastSessionId || ""; lastSessionId = rememberLastSession ? (memory.lastSessionId || "") : "";
lastSuccessfulUser = memory.lastSuccessfulUser || ""; lastSuccessfulUser = rememberLastUser ? (memory.lastSuccessfulUser || "") : "";
if (!rememberLastSession || !rememberLastUser)
saveMemory();
} catch (e) { } catch (e) {
console.warn("Failed to parse greetd memory:", e); console.warn("Failed to parse greetd memory:", e);
} }
} }
function saveMemory() { function saveMemory() {
memoryFileView.setText(JSON.stringify({ let memory = {};
"lastSessionId": lastSessionId, if (rememberLastSession && lastSessionId)
"lastSuccessfulUser": lastSuccessfulUser memory.lastSessionId = lastSessionId;
}, null, 2)); if (rememberLastUser && lastSuccessfulUser)
memory.lastSuccessfulUser = lastSuccessfulUser;
memoryFileView.setText(JSON.stringify(memory, null, 2));
} }
function setLastSessionId(id) { function setLastSessionId(id) {
if (!rememberLastSession) {
if (lastSessionId !== "") {
lastSessionId = "";
saveMemory();
}
return;
}
lastSessionId = id || ""; lastSessionId = id || "";
saveMemory(); saveMemory();
} }
function setLastSuccessfulUser(username) { function setLastSuccessfulUser(username) {
if (!rememberLastUser) {
if (lastSuccessfulUser !== "") {
lastSuccessfulUser = "";
saveMemory();
}
return;
}
lastSuccessfulUser = username || ""; lastSuccessfulUser = username || "";
saveMemory(); saveMemory();
} }
+107 -48
View File
@@ -5,15 +5,22 @@ import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import "GreetdEnv.js" as GreetdEnv
Singleton { Singleton {
id: root id: root
readonly property string configPath: { readonly property string configPath: {
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms"; const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
return greetCfgDir + "/settings.json"; return greetCfgDir + "/settings.json";
} }
readonly property string _greeterCacheDir: {
const i = root.configPath.lastIndexOf("/");
return i >= 0 ? root.configPath.substring(0, i) : "";
}
readonly property string greeterWallpaperOverridePath: root._greeterCacheDir ? (root._greeterCacheDir + "/greeter_wallpaper_override.jpg") : ""
property string currentThemeName: "purple" property string currentThemeName: "purple"
property bool settingsLoaded: false property bool settingsLoaded: false
property string customThemeFile: "" property string customThemeFile: ""
@@ -21,6 +28,12 @@ Singleton {
property bool use24HourClock: true property bool use24HourClock: true
property bool showSeconds: false property bool showSeconds: false
property bool padHours12Hour: false property bool padHours12Hour: false
property bool greeterUse24HourClock: true
property bool greeterShowSeconds: false
property bool greeterPadHours12Hour: false
property string greeterLockDateFormat: ""
property string greeterFontFamily: ""
property string greeterWallpaperFillMode: ""
property bool useFahrenheit: false property bool useFahrenheit: false
property bool nightModeEnabled: false property bool nightModeEnabled: false
property string weatherLocation: "New York, NY" property string weatherLocation: "New York, NY"
@@ -41,6 +54,11 @@ Singleton {
property string lockDateFormat: "" property string lockDateFormat: ""
property bool lockScreenShowPowerActions: true property bool lockScreenShowPowerActions: true
property bool lockScreenShowProfileImage: true property bool lockScreenShowProfileImage: true
property bool rememberLastSession: true
property bool rememberLastUser: true
property bool greeterEnableFprint: false
property bool greeterEnableU2f: false
property string greeterWallpaperPath: ""
property bool powerActionConfirm: true property bool powerActionConfirm: true
property real powerActionHoldDuration: 0.5 property real powerActionHoldDuration: 0.5
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"] property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
@@ -52,66 +70,103 @@ Singleton {
function parseSettings(content) { function parseSettings(content) {
try { try {
let settings = {};
if (content && content.trim()) { if (content && content.trim()) {
const settings = JSON.parse(content); settings = JSON.parse(content);
currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "purple"; }
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : "";
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot";
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false;
padHours12Hour = settings.padHours12Hour !== undefined ? settings.padHours12Hour : false;
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false;
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY";
weatherCoordinates = settings.weatherCoordinates !== undefined ? settings.weatherCoordinates : "40.7128,-74.0060";
useAutoLocation = settings.useAutoLocation !== undefined ? settings.useAutoLocation : false;
weatherEnabled = settings.weatherEnabled !== undefined ? settings.weatherEnabled : true;
iconTheme = settings.iconTheme !== undefined ? settings.iconTheme : "System Default";
useOSLogo = settings.useOSLogo !== undefined ? settings.useOSLogo : false;
osLogoColorOverride = settings.osLogoColorOverride !== undefined ? settings.osLogoColorOverride : "";
osLogoBrightness = settings.osLogoBrightness !== undefined ? settings.osLogoBrightness : 0.5;
osLogoContrast = settings.osLogoContrast !== undefined ? settings.osLogoContrast : 1;
fontFamily = settings.fontFamily !== undefined ? settings.fontFamily : Theme.defaultFontFamily;
monoFontFamily = settings.monoFontFamily !== undefined ? settings.monoFontFamily : Theme.defaultMonoFontFamily;
fontWeight = settings.fontWeight !== undefined ? settings.fontWeight : Font.Normal;
fontScale = settings.fontScale !== undefined ? settings.fontScale : 1.0;
cornerRadius = settings.cornerRadius !== undefined ? settings.cornerRadius : 12;
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch";
lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : "";
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true;
lockScreenShowProfileImage = settings.lockScreenShowProfileImage !== undefined ? settings.lockScreenShowProfileImage : true;
powerActionConfirm = settings.powerActionConfirm !== undefined ? settings.powerActionConfirm : true;
powerActionHoldDuration = settings.powerActionHoldDuration !== undefined ? settings.powerActionHoldDuration : 0.5;
powerMenuActions = settings.powerMenuActions !== undefined ? settings.powerMenuActions : ["reboot", "logout", "poweroff", "lock", "suspend", "restart"];
powerMenuDefaultAction = settings.powerMenuDefaultAction !== undefined ? settings.powerMenuDefaultAction : "logout";
powerMenuGridLayout = settings.powerMenuGridLayout !== undefined ? settings.powerMenuGridLayout : false;
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({});
animationSpeed = settings.animationSpeed !== undefined ? settings.animationSpeed : 2;
wallpaperFillMode = settings.wallpaperFillMode !== undefined ? settings.wallpaperFillMode : "Fill";
settingsLoaded = true;
if (typeof Theme !== "undefined") { const envRememberLastSession = GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], undefined);
if (currentThemeName === "custom" && customThemeFile) { const envRememberLastUser = GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], undefined);
Theme.loadCustomThemeFromFile(customThemeFile);
} currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "purple";
Theme.applyGreeterTheme(currentThemeName); customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : "";
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot";
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false;
padHours12Hour = settings.padHours12Hour !== undefined ? settings.padHours12Hour : false;
greeterUse24HourClock = settings.greeterUse24HourClock !== undefined ? settings.greeterUse24HourClock : use24HourClock;
greeterShowSeconds = settings.greeterShowSeconds !== undefined ? settings.greeterShowSeconds : showSeconds;
greeterPadHours12Hour = settings.greeterPadHours12Hour !== undefined ? settings.greeterPadHours12Hour : padHours12Hour;
greeterLockDateFormat = settings.greeterLockDateFormat !== undefined ? settings.greeterLockDateFormat : "";
greeterFontFamily = settings.greeterFontFamily !== undefined ? settings.greeterFontFamily : "";
greeterWallpaperFillMode = settings.greeterWallpaperFillMode !== undefined ? settings.greeterWallpaperFillMode : "";
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false;
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY";
weatherCoordinates = settings.weatherCoordinates !== undefined ? settings.weatherCoordinates : "40.7128,-74.0060";
useAutoLocation = settings.useAutoLocation !== undefined ? settings.useAutoLocation : false;
weatherEnabled = settings.weatherEnabled !== undefined ? settings.weatherEnabled : true;
iconTheme = settings.iconTheme !== undefined ? settings.iconTheme : "System Default";
useOSLogo = settings.useOSLogo !== undefined ? settings.useOSLogo : false;
osLogoColorOverride = settings.osLogoColorOverride !== undefined ? settings.osLogoColorOverride : "";
osLogoBrightness = settings.osLogoBrightness !== undefined ? settings.osLogoBrightness : 0.5;
osLogoContrast = settings.osLogoContrast !== undefined ? settings.osLogoContrast : 1;
fontFamily = settings.fontFamily !== undefined ? settings.fontFamily : Theme.defaultFontFamily;
monoFontFamily = settings.monoFontFamily !== undefined ? settings.monoFontFamily : Theme.defaultMonoFontFamily;
fontWeight = settings.fontWeight !== undefined ? settings.fontWeight : Font.Normal;
fontScale = settings.fontScale !== undefined ? settings.fontScale : 1.0;
cornerRadius = settings.cornerRadius !== undefined ? settings.cornerRadius : 12;
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch";
lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : "";
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true;
lockScreenShowProfileImage = settings.lockScreenShowProfileImage !== undefined ? settings.lockScreenShowProfileImage : true;
if (envRememberLastSession !== undefined) {
rememberLastSession = envRememberLastSession;
} else {
rememberLastSession = settings.greeterRememberLastSession !== undefined ? settings.greeterRememberLastSession : settings.rememberLastSession !== undefined ? settings.rememberLastSession : true;
}
if (envRememberLastUser !== undefined) {
rememberLastUser = envRememberLastUser;
} else {
rememberLastUser = settings.greeterRememberLastUser !== undefined ? settings.greeterRememberLastUser : settings.rememberLastUser !== undefined ? settings.rememberLastUser : true;
}
greeterEnableFprint = settings.greeterEnableFprint !== undefined ? settings.greeterEnableFprint : false;
greeterEnableU2f = settings.greeterEnableU2f !== undefined ? settings.greeterEnableU2f : false;
greeterWallpaperPath = settings.greeterWallpaperPath !== undefined ? settings.greeterWallpaperPath : "";
powerActionConfirm = settings.powerActionConfirm !== undefined ? settings.powerActionConfirm : true;
powerActionHoldDuration = settings.powerActionHoldDuration !== undefined ? settings.powerActionHoldDuration : 0.5;
powerMenuActions = settings.powerMenuActions !== undefined ? settings.powerMenuActions : ["reboot", "logout", "poweroff", "lock", "suspend", "restart"];
powerMenuDefaultAction = settings.powerMenuDefaultAction !== undefined ? settings.powerMenuDefaultAction : "logout";
powerMenuGridLayout = settings.powerMenuGridLayout !== undefined ? settings.powerMenuGridLayout : false;
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({});
animationSpeed = settings.animationSpeed !== undefined ? settings.animationSpeed : 2;
wallpaperFillMode = settings.wallpaperFillMode !== undefined ? settings.wallpaperFillMode : "Fill";
if (typeof Theme !== "undefined") {
if (currentThemeName === "custom" && customThemeFile) {
Theme.loadCustomThemeFromFile(customThemeFile);
} }
Theme.applyGreeterTheme(currentThemeName);
} }
} catch (e) { } catch (e) {
console.warn("Failed to parse greetd settings:", e); console.warn("Failed to parse greetd settings:", e);
} finally {
settingsLoaded = true;
} }
} }
function getEffectiveTimeFormat() { function getEffectiveTimeFormat() {
if (use24HourClock) const use24 = greeterUse24HourClock;
return showSeconds ? "hh:mm:ss" : "hh:mm"; const secs = greeterShowSeconds;
if (padHours12Hour) const pad = greeterPadHours12Hour;
return showSeconds ? "hh:mm:ss AP" : "hh:mm AP"; if (use24)
return showSeconds ? "h:mm:ss AP" : "h:mm AP"; return secs ? "hh:mm:ss" : "hh:mm";
if (pad)
return secs ? "hh:mm:ss AP" : "hh:mm AP";
return secs ? "h:mm:ss AP" : "h:mm AP";
} }
function getEffectiveLockDateFormat() { function getEffectiveLockDateFormat() {
return lockDateFormat && lockDateFormat.length > 0 ? lockDateFormat : Locale.LongFormat; const fmt = (greeterLockDateFormat !== undefined && greeterLockDateFormat !== "") ? greeterLockDateFormat : lockDateFormat;
return fmt && fmt.length > 0 ? fmt : Locale.LongFormat;
}
function getEffectiveWallpaperFillMode() {
return (greeterWallpaperFillMode && greeterWallpaperFillMode !== "") ? greeterWallpaperFillMode : wallpaperFillMode;
}
function getEffectiveFontFamily() {
return (greeterFontFamily && greeterFontFamily !== "") ? greeterFontFamily : fontFamily;
} }
function getFilteredScreens(componentId) { function getFilteredScreens(componentId) {
@@ -133,5 +188,9 @@ Singleton {
onLoaded: { onLoaded: {
parseSettings(settingsFile.text()); parseSettings(settingsFile.text());
} }
onLoadFailed: error => {
console.warn("Failed to load greetd settings:", error);
root.parseSettings("");
}
} }
} }
+711 -49
View File
@@ -31,6 +31,38 @@ Item {
signal launchRequested signal launchRequested
property bool weatherInitialized: false property bool weatherInitialized: false
property bool awaitingExternalAuth: false
property bool pendingPasswordResponse: false
property bool passwordSubmitRequested: false
property bool cancelingExternalAuthForPassword: false
property int defaultAuthTimeoutMs: 10000
property int externalAuthTimeoutMs: 30000
property int memoryFlushDelayMs: 120
property string pendingLaunchCommand: ""
property var pendingLaunchEnv: []
property int passwordFailureCount: 0
property int passwordAttemptLimitHint: 0
property string authFeedbackMessage: ""
property string greetdPamText: ""
property string systemAuthPamText: ""
property string commonAuthPamText: ""
property string passwordAuthPamText: ""
property string systemLoginPamText: ""
property string systemLocalLoginPamText: ""
property string commonAuthPcPamText: ""
property string loginPamText: ""
property string faillockConfigText: ""
property bool greeterWallpaperOverrideExists: false
property string externalAuthAutoStartedForUser: ""
property int passwordSessionTransitionRetryCount: 0
property int maxPasswordSessionTransitionRetries: 2
property bool fprintdProbeComplete: false
property bool fprintdHasDevice: false
// Falls back to PAM-only detection until the fprintd D-Bus probe completes.
readonly property bool greeterPamHasFprint: greeterPamStackHasModule("pam_fprintd") && (!fprintdProbeComplete || fprintdHasDevice)
readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f")
readonly property bool greeterExternalAuthAvailable: (greeterPamHasFprint && GreetdSettings.greeterEnableFprint) || (greeterPamHasU2f && GreetdSettings.greeterEnableU2f)
readonly property bool greeterPamHasExternalAuth: greeterPamHasFprint || greeterPamHasU2f
function initWeatherService() { function initWeatherService() {
if (weatherInitialized) if (weatherInitialized)
@@ -44,34 +76,492 @@ Item {
WeatherService.forceRefresh(); WeatherService.forceRefresh();
} }
function stripPamComment(line) {
if (!line)
return "";
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#"))
return "";
const hashIdx = trimmed.indexOf("#");
if (hashIdx >= 0)
return trimmed.substring(0, hashIdx).trim();
return trimmed;
}
function pamModuleEnabled(pamText, moduleName) {
if (!pamText || !moduleName)
return false;
const lines = pamText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (!line)
continue;
if (line.includes(moduleName))
return true;
}
return false;
}
function pamTextIncludesFile(pamText, filename) {
if (!pamText || !filename)
return false;
const lines = pamText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (!line)
continue;
if (line.includes(filename) && (line.includes("include") || line.includes("substack") || line.startsWith("@include")))
return true;
}
return false;
}
function greeterPamStackHasModule(moduleName) {
if (pamModuleEnabled(greetdPamText, moduleName))
return true;
const includedPamStacks = [
["system-auth", systemAuthPamText],
["common-auth", commonAuthPamText],
["password-auth", passwordAuthPamText],
["system-login", systemLoginPamText],
["system-local-login", systemLocalLoginPamText],
["common-auth-pc", commonAuthPcPamText],
["login", loginPamText]
];
for (let i = 0; i < includedPamStacks.length; i++) {
const stack = includedPamStacks[i];
if (pamTextIncludesFile(greetdPamText, stack[0]) && pamModuleEnabled(stack[1], moduleName))
return true;
}
return false;
}
function usesPamLockoutPolicy(pamText) {
if (!pamText)
return false;
const lines = pamText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (!line)
continue;
if (line.includes("pam_faillock.so") || line.includes("pam_tally2.so") || line.includes("pam_tally.so"))
return true;
}
return false;
}
function parsePamLineDenyValue(pamText) {
if (!pamText)
return -1;
const lines = pamText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (!line)
continue;
if (!line.includes("pam_faillock.so") && !line.includes("pam_tally2.so") && !line.includes("pam_tally.so"))
continue;
const denyMatch = line.match(/\bdeny\s*=\s*(\d+)\b/i);
if (!denyMatch)
continue;
const parsed = parseInt(denyMatch[1], 10);
if (!isNaN(parsed))
return parsed;
}
return -1;
}
function parseFaillockDenyValue(configText) {
if (!configText)
return -1;
const lines = configText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (!line)
continue;
const denyMatch = line.match(/^deny\s*=\s*(\d+)\s*$/i);
if (!denyMatch)
continue;
const parsed = parseInt(denyMatch[1], 10);
if (!isNaN(parsed))
return parsed;
}
return -1;
}
function refreshPasswordAttemptPolicyHint() {
const pamSources = [greetdPamText, systemAuthPamText, commonAuthPamText, passwordAuthPamText, systemLoginPamText, systemLocalLoginPamText, commonAuthPcPamText, loginPamText];
let lockoutConfigured = false;
let denyFromPam = -1;
for (let i = 0; i < pamSources.length; i++) {
const source = pamSources[i];
if (!source)
continue;
if (usesPamLockoutPolicy(source))
lockoutConfigured = true;
const denyValue = parsePamLineDenyValue(source);
if (denyValue >= 0 && (denyFromPam < 0 || denyValue < denyFromPam))
denyFromPam = denyValue;
}
if (!lockoutConfigured) {
passwordAttemptLimitHint = 0;
return;
}
const denyFromConfig = parseFaillockDenyValue(faillockConfigText);
if (denyFromConfig >= 0) {
passwordAttemptLimitHint = denyFromConfig;
return;
}
if (denyFromPam >= 0) {
passwordAttemptLimitHint = denyFromPam;
return;
}
// pam_faillock default deny value when no explicit config is set.
passwordAttemptLimitHint = 3;
}
function isLikelyLockoutMessage(message) {
const lower = (message || "").toLowerCase();
return lower.includes("account is locked") || lower.includes("too many") || lower.includes("maximum number of") || lower.includes("auth_err");
}
function currentAuthMessage() {
if (GreeterState.pamState === "error")
return "Authentication error - try again";
if (GreeterState.pamState === "max")
return "Too many failed attempts - account may be locked";
if (GreeterState.pamState === "fail") {
if (passwordAttemptLimitHint > 0) {
const attempt = Math.max(1, Math.min(passwordFailureCount, passwordAttemptLimitHint));
const remaining = Math.max(passwordAttemptLimitHint - attempt, 0);
if (remaining > 0) {
return "Incorrect password - attempt " + attempt + " of " + passwordAttemptLimitHint + " (lockout may follow)";
}
return "Incorrect password - next failures may trigger account lockout";
}
return "Incorrect password";
}
return "";
}
function clearAuthFeedback() {
GreeterState.pamState = "";
authFeedbackMessage = "";
}
function resetPasswordSessionTransition(clearSubmitRequest) {
cancelingExternalAuthForPassword = false;
passwordSessionTransitionRetryCount = 0;
if (clearSubmitRequest)
passwordSubmitRequested = false;
}
Connections { Connections {
target: GreetdSettings target: GreetdSettings
function onSettingsLoadedChanged() { function onSettingsLoadedChanged() {
if (GreetdSettings.settingsLoaded) if (GreetdSettings.settingsLoaded) {
initWeatherService(); initWeatherService();
if (isPrimaryScreen) {
applyLastSuccessfulUser();
finalizeSessionSelection();
}
}
}
function onRememberLastUserChanged() {
if (!isPrimaryScreen)
return;
if (!GreetdSettings.rememberLastUser && GreetdMemory.lastSuccessfulUser) {
GreetdMemory.setLastSuccessfulUser("");
}
applyLastSuccessfulUser();
}
function onRememberLastSessionChanged() {
if (!isPrimaryScreen)
return;
if (!GreetdSettings.rememberLastSession && GreetdMemory.lastSessionId) {
GreetdMemory.setLastSessionId("");
}
finalizeSessionSelection();
}
}
FileView {
id: greetdPamWatcher
path: "/etc/pam.d/greetd"
printErrors: false
onLoaded: {
root.greetdPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.greetdPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: systemAuthPamWatcher
path: "/etc/pam.d/system-auth"
printErrors: false
onLoaded: {
root.systemAuthPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.systemAuthPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: commonAuthPamWatcher
path: "/etc/pam.d/common-auth"
printErrors: false
onLoaded: {
root.commonAuthPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.commonAuthPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: passwordAuthPamWatcher
path: "/etc/pam.d/password-auth"
printErrors: false
onLoaded: {
root.passwordAuthPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.passwordAuthPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: systemLoginPamWatcher
path: "/etc/pam.d/system-login"
printErrors: false
onLoaded: {
root.systemLoginPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.systemLoginPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: systemLocalLoginPamWatcher
path: "/etc/pam.d/system-local-login"
printErrors: false
onLoaded: {
root.systemLocalLoginPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.systemLocalLoginPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: commonAuthPcPamWatcher
path: "/etc/pam.d/common-auth-pc"
printErrors: false
onLoaded: {
root.commonAuthPcPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.commonAuthPcPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: loginPamWatcher
path: "/etc/pam.d/login"
printErrors: false
onLoaded: {
root.loginPamText = text();
root.refreshPasswordAttemptPolicyHint();
root.maybeAutoStartExternalAuth();
}
onLoadFailed: {
root.loginPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: faillockConfigWatcher
path: "/etc/security/faillock.conf"
printErrors: false
onLoaded: {
root.faillockConfigText = text();
root.refreshPasswordAttemptPolicyHint();
}
onLoadFailed: {
root.faillockConfigText = "";
root.refreshPasswordAttemptPolicyHint();
} }
} }
Component.onCompleted: { Component.onCompleted: {
initWeatherService(); initWeatherService();
refreshPasswordAttemptPolicyHint();
if (isPrimaryScreen) if (isPrimaryScreen)
applyLastSuccessfulUser(); applyLastSuccessfulUser();
if (CompositorService.isHyprland) if (CompositorService.isHyprland)
updateHyprlandLayout(); updateHyprlandLayout();
fprintdDeviceProbe.running = true;
} }
function applyLastSuccessfulUser() { function applyLastSuccessfulUser() {
if (!GreetdSettings.settingsLoaded || !GreetdSettings.rememberLastUser)
return;
const lastUser = GreetdMemory.lastSuccessfulUser; const lastUser = GreetdMemory.lastSuccessfulUser;
if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) { if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) {
GreeterState.username = lastUser; GreeterState.username = lastUser;
GreeterState.usernameInput = lastUser; GreeterState.usernameInput = lastUser;
GreeterState.showPasswordInput = true; GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(lastUser); PortalService.getGreeterUserProfileImage(lastUser);
maybeAutoStartExternalAuth();
} }
} }
function submitUsername(rawValue) {
const user = (rawValue || "").trim();
if (!user)
return;
if (GreeterState.username !== user) {
passwordFailureCount = 0;
clearAuthFeedback();
externalAuthAutoStartedForUser = "";
}
GreeterState.username = user;
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(user);
GreeterState.passwordBuffer = "";
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
maybeAutoStartExternalAuth();
}
function submitBufferedPassword() {
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
awaitingExternalAuth = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.restart();
// Some PAM stacks expect an explicit empty response to advance U2F/fprint or fail normally.
Greetd.respond(GreeterState.passwordBuffer || "");
GreeterState.passwordBuffer = "";
inputField.text = "";
return true;
}
function requestPasswordSessionTransition() {
const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
if (!passwordSubmitRequested && !hasPasswordBuffer)
return;
if (cancelingExternalAuthForPassword)
return;
if (passwordSessionTransitionRetryCount >= maxPasswordSessionTransitionRetries) {
pendingPasswordResponse = false;
awaitingExternalAuth = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
resetPasswordSessionTransition(true);
GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
placeholderDelay.restart();
Greetd.cancelSession();
return;
}
cancelingExternalAuthForPassword = true;
passwordSessionTransitionRetryCount = passwordSessionTransitionRetryCount + 1;
awaitingExternalAuth = false;
pendingPasswordResponse = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
Greetd.cancelSession();
}
function startAuthSession(submitPassword) {
submitPassword = submitPassword === true;
if (!GreeterState.showPasswordInput || !GreeterState.username)
return;
if (GreeterState.unlocking)
return;
const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
if (Greetd.state !== GreetdState.Inactive) {
if (pendingPasswordResponse && submitPassword)
submitBufferedPassword();
else if (submitPassword)
passwordSubmitRequested = true;
return;
}
if (cancelingExternalAuthForPassword) {
if (submitPassword)
passwordSubmitRequested = true;
return;
}
if (!submitPassword && !hasPasswordBuffer && !root.greeterExternalAuthAvailable)
return;
pendingPasswordResponse = false;
passwordSubmitRequested = submitPassword;
awaitingExternalAuth = !submitPassword && !hasPasswordBuffer && root.greeterExternalAuthAvailable;
// Use greeterExternalAuthAvailable so systems with pam_fprintd but no hardware don't incur the 30 s wait.
const waitingOnPamExternalBeforePassword = submitPassword && root.greeterExternalAuthAvailable;
authTimeout.interval = (awaitingExternalAuth || waitingOnPamExternalBeforePassword) ? externalAuthTimeoutMs : defaultAuthTimeoutMs;
authTimeout.restart();
Greetd.createSession(GreeterState.username);
}
function maybeAutoStartExternalAuth() {
if (!GreeterState.showPasswordInput || !GreeterState.username)
return;
if (!root.greeterExternalAuthAvailable)
return;
if (GreeterState.unlocking || Greetd.state !== GreetdState.Inactive)
return;
if (passwordSubmitRequested || cancelingExternalAuthForPassword)
return;
if (GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0)
return;
if (externalAuthAutoStartedForUser === GreeterState.username)
return;
externalAuthAutoStartedForUser = GreeterState.username;
startAuthSession(false);
}
function isExternalAuthPrompt(message, responseRequired) {
// Non-response PAM messages commonly represent waiting states (fprint/U2F/token touch).
return !responseRequired;
}
Component.onDestruction: { Component.onDestruction: {
if (weatherInitialized) if (weatherInitialized)
WeatherService.removeRef(); WeatherService.removeRef();
@@ -113,6 +603,34 @@ Item {
} }
} }
// Probe fprintd D-Bus for physically enrolled scanners to eliminate PAM stack false-positives.
Process {
id: fprintdDeviceProbe
running: false
// sh wrapper: emits PROBE_UNAVAILABLE if gdbus is absent or fprintd unreachable,
// keeping the PAM-only fallback active in those cases.
command: ["sh", "-c",
"command -v gdbus >/dev/null 2>&1 || { echo PROBE_UNAVAILABLE; exit 0; }; " +
"gdbus call --system " +
"--dest net.reactivated.Fprint " +
"--object-path /net/reactivated/Fprint/Manager " +
"--method net.reactivated.Fprint.Manager.GetDevices 2>/dev/null " +
"|| echo PROBE_UNAVAILABLE"]
stdout: StdioCollector {
onStreamFinished: {
if (text.includes("PROBE_UNAVAILABLE"))
return; // PAM-only fallback stays active
root.fprintdHasDevice = text.includes("objectpath");
root.fprintdProbeComplete = true;
root.maybeAutoStartExternalAuth();
}
}
onExited: function(exitCode, exitStatus) {
if (!root.fprintdProbeComplete)
root.maybeAutoStartExternalAuth(); // PAM-only fallback stays active
}
}
Connections { Connections {
target: CompositorService.isHyprland ? Hyprland : null target: CompositorService.isHyprland ? Hyprland : null
enabled: CompositorService.isHyprland enabled: CompositorService.isHyprland
@@ -143,10 +661,39 @@ Item {
} }
} }
FileView {
id: greeterWallpaperOverrideFile
path: GreetdSettings.greeterWallpaperOverridePath
printErrors: false
watchChanges: true
onLoaded: root.greeterWallpaperOverrideExists = true
onLoadFailed: root.greeterWallpaperOverrideExists = false
}
Connections {
target: GreetdSettings
function onGreeterWallpaperOverridePathChanged() {
if (!GreetdSettings.greeterWallpaperOverridePath) {
root.greeterWallpaperOverrideExists = false;
return;
}
greeterWallpaperOverrideFile.reload();
}
function onGreeterWallpaperPathChanged() {
if (!GreetdSettings.greeterWallpaperPath) {
root.greeterWallpaperOverrideExists = false;
return;
}
greeterWallpaperOverrideFile.reload();
}
}
DankBackdrop { DankBackdrop {
anchors.fill: parent anchors.fill: parent
screenName: root.screenName screenName: root.screenName
visible: { visible: {
if (GreetdSettings.greeterWallpaperPath !== "" && root.greeterWallpaperOverrideExists)
return false;
var _ = SessionData.perMonitorWallpaper; var _ = SessionData.perMonitorWallpaper;
var __ = SessionData.monitorWallpapers; var __ = SessionData.monitorWallpapers;
var currentWallpaper = SessionData.getMonitorWallpaper(screenName); var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
@@ -159,12 +706,14 @@ Item {
anchors.fill: parent anchors.fill: parent
source: { source: {
if (GreetdSettings.greeterWallpaperPath !== "" && root.greeterWallpaperOverrideExists)
return encodeFileUrl(GreetdSettings.greeterWallpaperOverridePath);
var _ = SessionData.perMonitorWallpaper; var _ = SessionData.perMonitorWallpaper;
var __ = SessionData.monitorWallpapers; var __ = SessionData.monitorWallpapers;
var currentWallpaper = SessionData.getMonitorWallpaper(screenName); var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
return (currentWallpaper && !currentWallpaper.startsWith("#")) ? encodeFileUrl(currentWallpaper) : ""; return (currentWallpaper && !currentWallpaper.startsWith("#")) ? encodeFileUrl(currentWallpaper) : "";
} }
fillMode: Theme.getFillMode(GreetdSettings.wallpaperFillMode) fillMode: Theme.getFillMode(GreetdSettings.getEffectiveWallpaperFillMode())
smooth: true smooth: true
asynchronous: false asynchronous: false
cache: true cache: true
@@ -327,10 +876,7 @@ Item {
anchors.top: clockContainer.bottom anchors.top: clockContainer.bottom
anchors.topMargin: 4 anchors.topMargin: 4
text: { text: {
if (GreetdSettings.lockDateFormat && GreetdSettings.lockDateFormat.length > 0) { return systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.getEffectiveLockDateFormat());
return systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.lockDateFormat);
}
return systemClock.date.toLocaleDateString(I18n.locale(), Locale.LongFormat);
} }
font.pixelSize: Theme.fontSizeXLarge font.pixelSize: Theme.fontSizeXLarge
color: "white" color: "white"
@@ -399,6 +945,9 @@ Item {
if (GreeterState.showPasswordInput && revealButton.visible) { if (GreeterState.showPasswordInput && revealButton.visible) {
margin += revealButton.width; margin += revealButton.width;
} }
if (externalAuthButton.visible) {
margin += externalAuthButton.width;
}
if (virtualKeyboardButton.visible) { if (virtualKeyboardButton.visible) {
margin += virtualKeyboardButton.width; margin += virtualKeyboardButton.width;
} }
@@ -415,21 +964,18 @@ Item {
return; return;
if (GreeterState.showPasswordInput) { if (GreeterState.showPasswordInput) {
GreeterState.passwordBuffer = text; GreeterState.passwordBuffer = text;
if (!text || text.length === 0)
root.passwordSubmitRequested = false;
} else { } else {
GreeterState.usernameInput = text; GreeterState.usernameInput = text;
} }
} }
onAccepted: { onAccepted: {
if (GreeterState.showPasswordInput) { if (GreeterState.showPasswordInput) {
if (Greetd.state === GreetdState.Inactive && GreeterState.username) { root.startAuthSession(true);
Greetd.createSession(GreeterState.username);
}
} else { } else {
if (text.trim()) { if (text.trim()) {
GreeterState.username = text.trim(); root.submitUsername(text);
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(GreeterState.username);
GreeterState.passwordBuffer = "";
syncingFromState = true; syncingFromState = true;
text = ""; text = "";
syncingFromState = false; syncingFromState = false;
@@ -461,14 +1007,14 @@ Item {
anchors.left: lockIcon.right anchors.left: lockIcon.right
anchors.leftMargin: Theme.spacingM anchors.leftMargin: Theme.spacingM
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right))) anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right))))
anchors.rightMargin: 2 anchors.rightMargin: 2
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: { text: {
if (GreeterState.unlocking) { if (GreeterState.unlocking) {
return "Logging in..."; return "Logging in...";
} }
if (Greetd.state !== GreetdState.Inactive) { if (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse) {
return "Authenticating..."; return "Authenticating...";
} }
if (GreeterState.showPasswordInput) { if (GreeterState.showPasswordInput) {
@@ -476,7 +1022,7 @@ Item {
} }
return "Username..."; return "Username...";
} }
color: GreeterState.unlocking ? Theme.primary : (Greetd.state !== GreetdState.Inactive ? Theme.primary : Theme.outline) color: (GreeterState.unlocking || (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse)) ? Theme.primary : Theme.outline
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : GreeterState.usernameInput.length === 0) ? 1 : 0 opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : GreeterState.usernameInput.length === 0) ? 1 : 0
@@ -498,7 +1044,7 @@ Item {
StyledText { StyledText {
anchors.left: lockIcon.right anchors.left: lockIcon.right
anchors.leftMargin: Theme.spacingM anchors.leftMargin: Theme.spacingM
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right))) anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right))))
anchors.rightMargin: 2 anchors.rightMargin: 2
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: { text: {
@@ -528,15 +1074,27 @@ Item {
DankActionButton { DankActionButton {
id: revealButton id: revealButton
anchors.right: virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right) anchors.right: externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right))
anchors.rightMargin: 0 anchors.rightMargin: 0
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
iconName: parent.showPassword ? "visibility_off" : "visibility" iconName: parent.showPassword ? "visibility_off" : "visibility"
buttonSize: 32 buttonSize: 32
visible: GreeterState.showPasswordInput && GreeterState.passwordBuffer.length > 0 && Greetd.state === GreetdState.Inactive && !GreeterState.unlocking visible: GreeterState.showPasswordInput && GreeterState.passwordBuffer.length > 0 && (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
enabled: visible enabled: visible
onClicked: parent.showPassword = !parent.showPassword onClicked: parent.showPassword = !parent.showPassword
} }
DankActionButton {
id: externalAuthButton
anchors.right: virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)
anchors.rightMargin: 0
anchors.verticalCenter: parent.verticalCenter
iconName: root.greeterPamHasFprint ? "fingerprint" : "key"
buttonSize: 32
visible: GreeterState.showPasswordInput && root.greeterExternalAuthAvailable && GreeterState.passwordBuffer.length === 0 && (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
enabled: visible
onClicked: root.startAuthSession(false)
}
DankActionButton { DankActionButton {
id: virtualKeyboardButton id: virtualKeyboardButton
@@ -545,7 +1103,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
iconName: "keyboard" iconName: "keyboard"
buttonSize: 32 buttonSize: 32
visible: Greetd.state === GreetdState.Inactive && !GreeterState.unlocking visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
enabled: visible enabled: visible
onClicked: { onClicked: {
if (keyboard_controller.isKeyboardActive) { if (keyboard_controller.isKeyboardActive) {
@@ -564,19 +1122,14 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
iconName: "keyboard_return" iconName: "keyboard_return"
buttonSize: 36 buttonSize: 36
visible: Greetd.state === GreetdState.Inactive && !GreeterState.unlocking visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
enabled: true enabled: true
onClicked: { onClicked: {
if (GreeterState.showPasswordInput) { if (GreeterState.showPasswordInput) {
if (GreeterState.username) { root.startAuthSession(true);
Greetd.createSession(GreeterState.username);
}
} else { } else {
if (inputField.text.trim()) { if (inputField.text.trim()) {
GreeterState.username = inputField.text.trim(); root.submitUsername(inputField.text);
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(GreeterState.username);
GreeterState.passwordBuffer = "";
inputField.text = ""; inputField.text = "";
} }
} }
@@ -601,20 +1154,16 @@ Item {
StyledText { StyledText {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 20 Layout.preferredHeight: 38
Layout.topMargin: -Theme.spacingS Layout.topMargin: -Theme.spacingS
Layout.bottomMargin: -Theme.spacingS Layout.bottomMargin: -Theme.spacingS
text: { text: root.authFeedbackMessage
if (GreeterState.pamState === "error")
return "Authentication error - try again";
if (GreeterState.pamState === "fail")
return "Incorrect password";
return "";
}
color: Theme.error color: Theme.error
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
opacity: GreeterState.pamState !== "" ? 1 : 0 wrapMode: Text.WordWrap
maximumLineCount: 2
opacity: root.authFeedbackMessage !== "" ? 1 : 0
Behavior on opacity { Behavior on opacity {
NumberAnimation { NumberAnimation {
@@ -667,6 +1216,7 @@ Item {
enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput
onClicked: { onClicked: {
GreeterState.reset(); GreeterState.reset();
root.externalAuthAutoStartedForUser = "";
inputField.text = ""; inputField.text = "";
PortalService.profileImage = ""; PortalService.profileImage = "";
} }
@@ -1029,9 +1579,11 @@ Item {
return; return;
if (!GreetdMemory.memoryReady) if (!GreetdMemory.memoryReady)
return; return;
if (!GreetdSettings.settingsLoaded)
return;
const savedSession = GreetdMemory.lastSessionId; const savedSession = GreetdSettings.rememberLastSession ? GreetdMemory.lastSessionId : "";
if (savedSession) { if (savedSession && GreetdSettings.rememberLastSession) {
for (var i = 0; i < GreeterState.sessionPaths.length; i++) { for (var i = 0; i < GreeterState.sessionPaths.length; i++) {
if (GreeterState.sessionPaths[i] === savedSession) { if (GreeterState.sessionPaths[i] === savedSession) {
GreeterState.currentSessionIndex = i; GreeterState.currentSessionIndex = i;
@@ -1164,44 +1716,151 @@ Item {
function onAuthMessage(message, error, responseRequired, echoResponse) { function onAuthMessage(message, error, responseRequired, echoResponse) {
if (responseRequired) { if (responseRequired) {
Greetd.respond(GreeterState.passwordBuffer); cancelingExternalAuthForPassword = false;
GreeterState.passwordBuffer = ""; passwordSessionTransitionRetryCount = 0;
inputField.text = ""; awaitingExternalAuth = false;
pendingPasswordResponse = true;
const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
if (!passwordSubmitRequested && hasPasswordBuffer)
passwordSubmitRequested = true;
if (passwordSubmitRequested && !root.submitBufferedPassword())
passwordSubmitRequested = false;
if (passwordSubmitRequested || hasPasswordBuffer) {
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.restart();
} else {
authTimeout.stop();
}
return; return;
} }
if (!error) pendingPasswordResponse = false;
Greetd.respond(""); const externalPrompt = root.isExternalAuthPrompt(message, responseRequired);
if (!passwordSubmitRequested)
awaitingExternalAuth = root.greeterExternalAuthAvailable && externalPrompt;
if (awaitingExternalAuth || (passwordSubmitRequested && externalPrompt && root.greeterPamHasExternalAuth))
authTimeout.interval = externalAuthTimeoutMs;
else
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.restart();
Greetd.respond("");
}
function onStateChanged() {
if (Greetd.state === GreetdState.Inactive) {
const resumePasswordSubmit = cancelingExternalAuthForPassword && passwordSubmitRequested;
awaitingExternalAuth = false;
pendingPasswordResponse = false;
cancelingExternalAuthForPassword = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
if (resumePasswordSubmit) {
Qt.callLater(function() {
root.startAuthSession(true);
});
return;
}
resetPasswordSessionTransition(true);
}
} }
function onReadyToLaunch() { function onReadyToLaunch() {
awaitingExternalAuth = false;
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
passwordFailureCount = 0;
clearAuthFeedback();
const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex]; const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex];
const sessionPath = GreeterState.selectedSessionPath || GreeterState.sessionPaths[GreeterState.currentSessionIndex]; const sessionPath = GreeterState.selectedSessionPath || GreeterState.sessionPaths[GreeterState.currentSessionIndex];
if (!sessionCmd) { if (!sessionCmd) {
GreeterState.pamState = "error"; GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
placeholderDelay.restart(); placeholderDelay.restart();
return; return;
} }
GreeterState.unlocking = true; GreeterState.unlocking = true;
launchTimeout.restart(); launchTimeout.restart();
GreetdMemory.setLastSessionId(sessionPath); if (GreetdSettings.rememberLastSession) {
GreetdMemory.setLastSuccessfulUser(GreeterState.username); GreetdMemory.setLastSessionId(sessionPath);
Greetd.launch(sessionCmd.split(" "), ["XDG_SESSION_TYPE=wayland"]); } else if (GreetdMemory.lastSessionId) {
GreetdMemory.setLastSessionId("");
}
if (GreetdSettings.rememberLastUser) {
GreetdMemory.setLastSuccessfulUser(GreeterState.username);
} else if (GreetdMemory.lastSuccessfulUser) {
GreetdMemory.setLastSuccessfulUser("");
}
pendingLaunchCommand = sessionCmd;
pendingLaunchEnv = ["XDG_SESSION_TYPE=wayland"];
memoryFlushTimer.restart();
} }
function onAuthFailure(message) { function onAuthFailure(message) {
awaitingExternalAuth = false;
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
launchTimeout.stop(); launchTimeout.stop();
GreeterState.unlocking = false; GreeterState.unlocking = false;
GreeterState.pamState = "fail"; if (isLikelyLockoutMessage(message)) {
GreeterState.pamState = "max";
} else {
GreeterState.pamState = "fail";
passwordFailureCount = passwordFailureCount + 1;
}
authFeedbackMessage = currentAuthMessage();
GreeterState.passwordBuffer = ""; GreeterState.passwordBuffer = "";
inputField.text = ""; inputField.text = "";
placeholderDelay.restart(); placeholderDelay.restart();
Greetd.cancelSession();
} }
function onError(error) { function onError(error) {
awaitingExternalAuth = false;
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
launchTimeout.stop(); launchTimeout.stop();
GreeterState.unlocking = false; GreeterState.unlocking = false;
GreeterState.pamState = "error"; GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
GreeterState.passwordBuffer = "";
inputField.text = "";
placeholderDelay.restart();
Greetd.cancelSession();
}
}
Timer {
id: memoryFlushTimer
interval: memoryFlushDelayMs
onTriggered: {
if (!pendingLaunchCommand)
return;
const sessionCommand = pendingLaunchCommand;
const launchEnv = pendingLaunchEnv;
pendingLaunchCommand = "";
pendingLaunchEnv = [];
Greetd.launch(sessionCommand.split(" "), launchEnv);
}
}
Timer {
id: authTimeout
interval: defaultAuthTimeoutMs
onTriggered: {
if (GreeterState.unlocking || Greetd.state === GreetdState.Inactive)
return;
awaitingExternalAuth = false;
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
authTimeout.interval = defaultAuthTimeoutMs;
GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
GreeterState.passwordBuffer = ""; GreeterState.passwordBuffer = "";
inputField.text = ""; inputField.text = "";
placeholderDelay.restart(); placeholderDelay.restart();
@@ -1215,8 +1874,11 @@ Item {
onTriggered: { onTriggered: {
if (!GreeterState.unlocking) if (!GreeterState.unlocking)
return; return;
pendingPasswordResponse = false;
resetPasswordSessionTransition(true);
GreeterState.unlocking = false; GreeterState.unlocking = false;
GreeterState.pamState = "error"; GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
placeholderDelay.restart(); placeholderDelay.restart();
Greetd.cancelSession(); Greetd.cancelSession();
} }
@@ -1225,7 +1887,7 @@ Item {
Timer { Timer {
id: placeholderDelay id: placeholderDelay
interval: 4000 interval: 4000
onTriggered: GreeterState.pamState = "" onTriggered: clearAuthFeedback()
} }
LockPowerMenu { LockPowerMenu {
+2
View File
@@ -9,6 +9,7 @@ A greeter for [greetd](https://github.com/kennylevinsen/greetd) that follows the
- **Multiple compositors**: Supports niri, Hyprland, Sway, or mangowc. - **Multiple compositors**: Supports niri, Hyprland, Sway, or mangowc.
- **Custom PAM**: Supports custom PAM configuration in `/etc/pam.d/greetd` - **Custom PAM**: Supports custom PAM configuration in `/etc/pam.d/greetd`
- **Session Memory**: Remembers last selected session and user - **Session Memory**: Remembers last selected session and user
- Can be disabled via `settings.json` keys: `greeterRememberLastSession` and `greeterRememberLastUser`
## Installation ## Installation
@@ -212,6 +213,7 @@ dms-greeter --command hyprland
dms-greeter --command sway dms-greeter --command sway
dms-greeter --command mangowc dms-greeter --command mangowc
dms-greeter --command niri -C /path/to/custom-niri.kdl dms-greeter --command niri -C /path/to/custom-niri.kdl
dms-greeter --command niri --remember-last-user false --remember-last-session false
``` ```
Configure greetd to use it in `/etc/greetd/config.toml`: Configure greetd to use it in `/etc/greetd/config.toml`:
+162 -12
View File
@@ -6,6 +6,9 @@ COMPOSITOR=""
COMPOSITOR_CONFIG="" COMPOSITOR_CONFIG=""
DMS_PATH="dms-greeter" DMS_PATH="dms-greeter"
CACHE_DIR="/var/cache/dms-greeter" CACHE_DIR="/var/cache/dms-greeter"
REMEMBER_LAST_SESSION=""
REMEMBER_LAST_USER=""
DEBUG_MODE=0
show_help() { show_help() {
cat << EOF cat << EOF
@@ -22,6 +25,15 @@ Options:
(default: dms-greeter) (default: dms-greeter)
--cache-dir PATH Cache directory for greeter data --cache-dir PATH Cache directory for greeter data
(default: /var/cache/dms-greeter) (default: /var/cache/dms-greeter)
--remember-last-session BOOL
Persist selected session to greeter memory
(BOOL: true/false, default: from settings.json)
--remember-last-user BOOL
Persist last successful username to greeter memory
(BOOL: true/false, default: from settings.json)
--no-save-session Alias for --remember-last-session false
--no-save-username Alias for --remember-last-user false
--debug Enable verbose startup logging to stderr
-h, --help Show this help message -h, --help Show this help message
Examples: Examples:
@@ -30,6 +42,7 @@ Examples:
dms-greeter --command sway -p /home/user/.config/quickshell/custom-dms dms-greeter --command sway -p /home/user/.config/quickshell/custom-dms
dms-greeter --command scroll -p /home/user/.config/quickshell/custom-dms dms-greeter --command scroll -p /home/user/.config/quickshell/custom-dms
dms-greeter --command niri --cache-dir /tmp/dmsgreeter dms-greeter --command niri --cache-dir /tmp/dmsgreeter
dms-greeter --command niri --no-save-session --no-save-username
dms-greeter --command mango dms-greeter --command mango
dms-greeter --command labwc dms-greeter --command labwc
EOF EOF
@@ -43,6 +56,41 @@ require_command() {
fi fi
} }
normalize_bool_flag() {
local flag_name="$1"
local value="$2"
local normalized="${value,,}"
case "$normalized" in
1|true|yes|on)
echo "1"
;;
0|false|no|off)
echo "0"
;;
*)
echo "Error: $flag_name must be true/false (or 1/0, yes/no, on/off)" >&2
exit 1
;;
esac
}
exec_compositor() {
local log_tag="$1"
shift
if [[ "$DEBUG_MODE" == "1" ]]; then
exec "$@"
fi
if command -v systemd-cat >/dev/null 2>&1; then
exec "$@" > >(systemd-cat -t "dms-greeter/$log_tag" -p info) 2>&1
fi
local log_file="$CACHE_DIR/$log_tag.log"
exec "$@" >> "$log_file" 2>&1
}
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case $1 in case $1 in
--command) --command)
@@ -61,6 +109,26 @@ while [[ $# -gt 0 ]]; do
CACHE_DIR="$2" CACHE_DIR="$2"
shift 2 shift 2
;; ;;
--remember-last-session)
REMEMBER_LAST_SESSION="$2"
shift 2
;;
--remember-last-user)
REMEMBER_LAST_USER="$2"
shift 2
;;
--no-save-session)
REMEMBER_LAST_SESSION="0"
shift
;;
--no-save-username)
REMEMBER_LAST_USER="0"
shift
;;
--debug)
DEBUG_MODE=1
shift
;;
-h|--help) -h|--help)
show_help show_help
exit 0 exit 0
@@ -111,9 +179,71 @@ export QT_QPA_PLATFORM=wayland
export QT_WAYLAND_DISABLE_WINDOWDECORATION=1 export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
export EGL_PLATFORM=gbm export EGL_PLATFORM=gbm
export DMS_RUN_GREETER=1 export DMS_RUN_GREETER=1
ensure_cache_tree() {
local base="$1"
mkdir -p "$base/.local/state" "$base/.local/state/wireplumber" "$base/.local/share" "$base/.cache"
}
if ! ensure_cache_tree "$CACHE_DIR" 2>/dev/null; then
FALLBACK_CACHE_DIR="/tmp/dms-greeter-${UID:-$(id -u)}"
echo "Warning: cache directory '$CACHE_DIR' is not writable; falling back to '$FALLBACK_CACHE_DIR'" >&2
CACHE_DIR="$FALLBACK_CACHE_DIR"
if ! ensure_cache_tree "$CACHE_DIR"; then
echo "Error: failed to initialize fallback cache directory '$CACHE_DIR'" >&2
exit 1
fi
fi
export DMS_GREET_CFG_DIR="$CACHE_DIR" export DMS_GREET_CFG_DIR="$CACHE_DIR"
mkdir -p "$CACHE_DIR" if [[ -n "$REMEMBER_LAST_SESSION" ]]; then
DMS_GREET_REMEMBER_LAST_SESSION=$(normalize_bool_flag "--remember-last-session" "$REMEMBER_LAST_SESSION")
export DMS_GREET_REMEMBER_LAST_SESSION
if [[ "$DMS_GREET_REMEMBER_LAST_SESSION" == "1" ]]; then
DMS_SAVE_SESSION=true
else
DMS_SAVE_SESSION=false
fi
export DMS_SAVE_SESSION
fi
if [[ -n "$REMEMBER_LAST_USER" ]]; then
DMS_GREET_REMEMBER_LAST_USER=$(normalize_bool_flag "--remember-last-user" "$REMEMBER_LAST_USER")
export DMS_GREET_REMEMBER_LAST_USER
if [[ "$DMS_GREET_REMEMBER_LAST_USER" == "1" ]]; then
DMS_SAVE_USERNAME=true
else
DMS_SAVE_USERNAME=false
fi
export DMS_SAVE_USERNAME
fi
export HOME="$CACHE_DIR"
export XDG_STATE_HOME="$CACHE_DIR/.local/state"
export XDG_DATA_HOME="$CACHE_DIR/.local/share"
export XDG_CACHE_HOME="$CACHE_DIR/.cache"
# Propagate correct XDG dirs into the systemd user session so socket-activated
# services (e.g. wireplumber) don't inherit HOME=/ from /etc/passwd.
if command -v systemctl >/dev/null 2>&1; then
systemctl --user set-environment \
HOME="$CACHE_DIR" \
XDG_STATE_HOME="$CACHE_DIR/.local/state" \
XDG_DATA_HOME="$CACHE_DIR/.local/share" \
XDG_CACHE_HOME="$CACHE_DIR/.cache" 2>/dev/null || true
if systemctl --user is-active --quiet wireplumber.service 2>/dev/null; then
systemctl --user restart wireplumber.service 2>/dev/null || true
fi
fi
# Keep greeter VT clean by default; callers can override via env or --debug.
if [[ -z "${RUST_LOG:-}" ]]; then
export RUST_LOG=warn
fi
if [[ -z "${NIRI_LOG:-}" ]]; then
export NIRI_LOG=warn
fi
if command -v qs >/dev/null 2>&1; then if command -v qs >/dev/null 2>&1; then
QS_BIN="qs" QS_BIN="qs"
@@ -130,7 +260,9 @@ if [[ "$DMS_PATH" == /* ]]; then
else else
RESOLVED_PATH=$(locate_dms_config "$DMS_PATH") RESOLVED_PATH=$(locate_dms_config "$DMS_PATH")
if [[ $? -eq 0 && -n "$RESOLVED_PATH" ]]; then if [[ $? -eq 0 && -n "$RESOLVED_PATH" ]]; then
echo "Located DMS config at: $RESOLVED_PATH" >&2 if [[ "$DEBUG_MODE" == "1" ]]; then
echo "Located DMS config at: $RESOLVED_PATH" >&2
fi
QS_CMD="$QS_BIN -p $RESOLVED_PATH" QS_CMD="$QS_BIN -p $RESOLVED_PATH"
else else
echo "Error: Could not find DMS config '$DMS_PATH' (shell.qml) in any valid config path" >&2 echo "Error: Could not find DMS config '$DMS_PATH' (shell.qml) in any valid config path" >&2
@@ -151,6 +283,10 @@ hotkey-overlay {
environment { environment {
DMS_RUN_GREETER "1" DMS_RUN_GREETER "1"
HOME "$CACHE_DIR"
XDG_STATE_HOME "$CACHE_DIR/.local/state"
XDG_DATA_HOME "$CACHE_DIR/.local/share"
XDG_CACHE_HOME "$CACHE_DIR/.cache"
} }
debug { debug {
@@ -192,7 +328,7 @@ NIRI_EOF
spawn-at-startup "sh" "-c" "$QS_CMD; niri msg action quit --skip-confirmation" spawn-at-startup "sh" "-c" "$QS_CMD; niri msg action quit --skip-confirmation"
NIRI_EOF NIRI_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
exec niri -c "$COMPOSITOR_CONFIG" exec_compositor "niri" niri -c "$COMPOSITOR_CONFIG"
;; ;;
hyprland) hyprland)
@@ -204,11 +340,16 @@ NIRI_EOF
TEMP_CONFIG=$(mktemp) TEMP_CONFIG=$(mktemp)
cat > "$TEMP_CONFIG" << HYPRLAND_EOF cat > "$TEMP_CONFIG" << HYPRLAND_EOF
env = DMS_RUN_GREETER,1 env = DMS_RUN_GREETER,1
env = HOME,$CACHE_DIR
env = XDG_STATE_HOME,$CACHE_DIR/.local/state
env = XDG_DATA_HOME,$CACHE_DIR/.local/share
env = XDG_CACHE_HOME,$CACHE_DIR/.cache
misc { misc {
disable_hyprland_logo = true disable_hyprland_logo = true
} }
exec-once = systemctl --user import-environment HOME XDG_STATE_HOME XDG_DATA_HOME XDG_CACHE_HOME 2>/dev/null || true
exec-once = sh -c "$QS_CMD; hyprctl dispatch exit" exec-once = sh -c "$QS_CMD; hyprctl dispatch exit"
HYPRLAND_EOF HYPRLAND_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
@@ -217,14 +358,19 @@ HYPRLAND_EOF
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG" cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
cat >> "$TEMP_CONFIG" << HYPRLAND_EOF cat >> "$TEMP_CONFIG" << HYPRLAND_EOF
env = HOME,$CACHE_DIR
env = XDG_STATE_HOME,$CACHE_DIR/.local/state
env = XDG_DATA_HOME,$CACHE_DIR/.local/share
env = XDG_CACHE_HOME,$CACHE_DIR/.cache
exec-once = systemctl --user import-environment HOME XDG_STATE_HOME XDG_DATA_HOME XDG_CACHE_HOME 2>/dev/null || true
exec-once = sh -c "$QS_CMD; hyprctl dispatch exit" exec-once = sh -c "$QS_CMD; hyprctl dispatch exit"
HYPRLAND_EOF HYPRLAND_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi fi
if command -v start-hyprland >/dev/null 2>&1; then if command -v start-hyprland >/dev/null 2>&1; then
exec start-hyprland -- --config "$COMPOSITOR_CONFIG" exec_compositor "hyprland" start-hyprland -- --config "$COMPOSITOR_CONFIG"
else else
exec Hyprland -c "$COMPOSITOR_CONFIG" exec_compositor "hyprland" Hyprland -c "$COMPOSITOR_CONFIG"
fi fi
;; ;;
@@ -233,6 +379,7 @@ HYPRLAND_EOF
if [[ -z "$COMPOSITOR_CONFIG" ]]; then if [[ -z "$COMPOSITOR_CONFIG" ]]; then
TEMP_CONFIG=$(mktemp) TEMP_CONFIG=$(mktemp)
cat > "$TEMP_CONFIG" << SWAY_EOF cat > "$TEMP_CONFIG" << SWAY_EOF
exec --no-startup-id dbus-update-activation-environment --systemd HOME XDG_STATE_HOME XDG_DATA_HOME XDG_CACHE_HOME
exec "$QS_CMD; swaymsg exit" exec "$QS_CMD; swaymsg exit"
SWAY_EOF SWAY_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
@@ -241,11 +388,12 @@ SWAY_EOF
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG" cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
cat >> "$TEMP_CONFIG" << SWAY_EOF cat >> "$TEMP_CONFIG" << SWAY_EOF
exec --no-startup-id dbus-update-activation-environment --systemd HOME XDG_STATE_HOME XDG_DATA_HOME XDG_CACHE_HOME
exec "$QS_CMD; swaymsg exit" exec "$QS_CMD; swaymsg exit"
SWAY_EOF SWAY_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi fi
exec sway --unsupported-gpu -c "$COMPOSITOR_CONFIG" exec_compositor "sway" sway --unsupported-gpu -c "$COMPOSITOR_CONFIG"
;; ;;
scroll) scroll)
@@ -253,6 +401,7 @@ SWAY_EOF
if [[ -z "$COMPOSITOR_CONFIG" ]]; then if [[ -z "$COMPOSITOR_CONFIG" ]]; then
TEMP_CONFIG=$(mktemp) TEMP_CONFIG=$(mktemp)
cat > "$TEMP_CONFIG" << SCROLL_EOF cat > "$TEMP_CONFIG" << SCROLL_EOF
exec --no-startup-id dbus-update-activation-environment --systemd HOME XDG_STATE_HOME XDG_DATA_HOME XDG_CACHE_HOME
exec "$QS_CMD; scrollmsg exit" exec "$QS_CMD; scrollmsg exit"
SCROLL_EOF SCROLL_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
@@ -261,11 +410,12 @@ SCROLL_EOF
cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG" cat "$COMPOSITOR_CONFIG" > "$TEMP_CONFIG"
cat >> "$TEMP_CONFIG" << SCROLL_EOF cat >> "$TEMP_CONFIG" << SCROLL_EOF
exec --no-startup-id dbus-update-activation-environment --systemd HOME XDG_STATE_HOME XDG_DATA_HOME XDG_CACHE_HOME
exec "$QS_CMD; scrollmsg exit" exec "$QS_CMD; scrollmsg exit"
SCROLL_EOF SCROLL_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi fi
exec scroll -c "$COMPOSITOR_CONFIG" exec_compositor "scroll" scroll -c "$COMPOSITOR_CONFIG"
;; ;;
miracle|miracle-wm) miracle|miracle-wm)
@@ -285,24 +435,24 @@ exec "$QS_CMD; miraclemsg exit"
MIRACLE_EOF MIRACLE_EOF
COMPOSITOR_CONFIG="$TEMP_CONFIG" COMPOSITOR_CONFIG="$TEMP_CONFIG"
fi fi
exec miracle-wm -c "$COMPOSITOR_CONFIG" exec_compositor "miracle" miracle-wm -c "$COMPOSITOR_CONFIG"
;; ;;
labwc) labwc)
require_command "labwc" require_command "labwc"
if [[ -n "$COMPOSITOR_CONFIG" ]]; then if [[ -n "$COMPOSITOR_CONFIG" ]]; then
exec labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD" exec_compositor "labwc" labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD"
else else
exec labwc --session "$QS_CMD" exec_compositor "labwc" labwc --session "$QS_CMD"
fi fi
;; ;;
mango|mangowc) mango|mangowc)
require_command "mango" require_command "mango"
if [[ -n "$COMPOSITOR_CONFIG" ]]; then if [[ -n "$COMPOSITOR_CONFIG" ]]; then
exec mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg -d quit" exec_compositor "mango" mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg -d quit"
else else
exec mango -s "$QS_CMD && mmsg -d quit" exec_compositor "mango" mango -s "$QS_CMD && mmsg -d quit"
fi fi
;; ;;
+17 -2
View File
@@ -755,7 +755,7 @@ Item {
} }
} }
onAccepted: { onAccepted: {
if (!demoMode && !pam.passwd.active && !pam.u2fPending) { if (!demoMode && !root.unlocking && !pam.passwd.active && !pam.u2fPending) {
pam.passwd.start(); pam.passwd.start();
} }
} }
@@ -764,6 +764,11 @@ Item {
return; return;
} }
if (root.unlocking) {
event.accepted = true;
return;
}
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
if (pam.u2fPending) { if (pam.u2fPending) {
pam.cancelU2fPending(); pam.cancelU2fPending();
@@ -1017,7 +1022,7 @@ Item {
visible: (demoMode || (!pam.passwd.active && !root.unlocking && !pam.u2fPending)) visible: (demoMode || (!pam.passwd.active && !root.unlocking && !pam.u2fPending))
enabled: !demoMode enabled: !demoMode
onClicked: { onClicked: {
if (!demoMode && !pam.u2fPending) { if (!demoMode && !root.unlocking && !pam.u2fPending) {
pam.passwd.start(); pam.passwd.start();
} }
} }
@@ -1626,6 +1631,7 @@ Item {
onStateChanged: { onStateChanged: {
root.pamState = state; root.pamState = state;
if (state !== "") { if (state !== "") {
root.unlocking = false;
placeholderDelay.restart(); placeholderDelay.restart();
passwordField.text = ""; passwordField.text = "";
root.passwordBuffer = ""; root.passwordBuffer = "";
@@ -1641,6 +1647,15 @@ Item {
} }
} }
Connections {
target: pam
function onUnlockInProgressChanged() {
if (!pam.unlockInProgress && root.unlocking)
root.unlocking = false;
}
}
Binding { Binding {
target: pam target: pam
property: "buffer" property: "buffer"
+88 -36
View File
@@ -25,6 +25,29 @@ Scope {
signal flashMsg signal flashMsg
signal unlockRequested signal unlockRequested
function resetAuthFlows(): void {
passwd.abort();
fprint.abort();
u2f.abort();
errorRetry.running = false;
u2fErrorRetry.running = false;
u2fPendingTimeout.running = false;
passwdActiveTimeout.running = false;
unlockRequestTimeout.running = false;
u2fPending = false;
u2fState = "";
unlockInProgress = false;
}
function recoverFromAuthStall(newState: string): void {
resetAuthFlows();
state = newState;
flashMsg();
stateReset.restart();
fprint.checkAvail();
u2f.checkAvail();
}
function completeUnlock(): void { function completeUnlock(): void {
if (!unlockInProgress) { if (!unlockInProgress) {
unlockInProgress = true; unlockInProgress = true;
@@ -36,6 +59,7 @@ Scope {
u2fPendingTimeout.running = false; u2fPendingTimeout.running = false;
u2fPending = false; u2fPending = false;
u2fState = ""; u2fState = "";
unlockRequestTimeout.restart();
unlockRequested(); unlockRequested();
} }
} }
@@ -66,6 +90,13 @@ Scope {
printErrors: false printErrors: false
} }
FileView {
id: loginConfigWatcher
path: "/etc/pam.d/login"
printErrors: false
}
FileView { FileView {
id: u2fConfigWatcher id: u2fConfigWatcher
@@ -77,7 +108,7 @@ Scope {
id: passwd id: passwd
config: dankshellConfigWatcher.loaded ? "dankshell" : "login" config: dankshellConfigWatcher.loaded ? "dankshell" : "login"
configDirectory: dankshellConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam" configDirectory: dankshellConfigWatcher.loaded || loginConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
onMessageChanged: { onMessageChanged: {
if (message.startsWith("The account is locked")) if (message.startsWith("The account is locked"))
@@ -102,6 +133,13 @@ Scope {
return; return;
} }
unlockRequestTimeout.running = false;
root.unlockInProgress = false;
root.u2fPending = false;
root.u2fState = "";
u2fPendingTimeout.running = false;
u2f.abort();
if (res === PamResult.Error) if (res === PamResult.Error)
root.state = "error"; root.state = "error";
else if (res === PamResult.MaxTries) else if (res === PamResult.MaxTries)
@@ -114,10 +152,22 @@ Scope {
} }
} }
Connections {
target: passwd
function onActiveChanged() {
if (passwd.active) {
passwdActiveTimeout.restart();
} else {
passwdActiveTimeout.running = false;
}
}
}
PamContext { PamContext {
id: fprint id: fprint
property bool available property bool available: SettingsData.lockFingerprintReady
property int tries property int tries
property int errorTries property int errorTries
@@ -173,7 +223,7 @@ Scope {
PamContext { PamContext {
id: u2f id: u2f
property bool available property bool available: SettingsData.lockU2fReady
function checkAvail(): void { function checkAvail(): void {
if (!available || !SettingsData.enableU2f || !root.lockSecured) { if (!available || !SettingsData.enableU2f || !root.lockSecured) {
@@ -202,7 +252,7 @@ Scope {
configDirectory: u2fConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam" configDirectory: u2fConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
onMessageChanged: { onMessageChanged: {
if (message.toLowerCase().includes("touch")) if (message !== "")
root.u2fState = "waiting"; root.u2fState = "waiting";
} }
@@ -238,26 +288,6 @@ Scope {
} }
} }
Process {
id: availProc
command: ["sh", "-c", "fprintd-list $USER"]
onExited: code => {
fprint.available = code === 0;
fprint.checkAvail();
}
}
Process {
id: u2fAvailProc
command: ["sh", "-c", "(test -f /usr/lib/security/pam_u2f.so || test -f /usr/lib64/security/pam_u2f.so) && (test -f /etc/pam.d/dankshell-u2f || test -f \"$HOME/.config/Yubico/u2f_keys\")"]
onExited: code => {
u2f.available = code === 0;
u2f.checkAvail();
}
}
Timer { Timer {
id: errorRetry id: errorRetry
@@ -279,6 +309,26 @@ Scope {
onTriggered: root.cancelU2fPending() onTriggered: root.cancelU2fPending()
} }
Timer {
id: passwdActiveTimeout
interval: 15000
onTriggered: {
if (passwd.active)
root.recoverFromAuthStall("error");
}
}
Timer {
id: unlockRequestTimeout
interval: 8000
onTriggered: {
if (root.unlockInProgress)
root.recoverFromAuthStall("error");
}
}
Timer { Timer {
id: stateReset id: stateReset
@@ -301,24 +351,17 @@ Scope {
onLockSecuredChanged: { onLockSecuredChanged: {
if (lockSecured) { if (lockSecured) {
availProc.running = true; SettingsData.refreshAuthAvailability();
u2fAvailProc.running = true;
root.state = ""; root.state = "";
root.fprintState = ""; root.fprintState = "";
root.u2fState = ""; root.u2fState = "";
root.u2fPending = false; root.u2fPending = false;
root.lockMessage = ""; root.lockMessage = "";
root.unlockInProgress = false; root.resetAuthFlows();
fprint.checkAvail();
u2f.checkAvail();
} else { } else {
fprint.abort(); root.resetAuthFlows();
passwd.abort();
u2f.abort();
errorRetry.running = false;
u2fErrorRetry.running = false;
u2fPendingTimeout.running = false;
root.u2fPending = false;
root.u2fState = "";
root.unlockInProgress = false;
} }
} }
@@ -329,15 +372,24 @@ Scope {
fprint.checkAvail(); fprint.checkAvail();
} }
function onLockFingerprintReadyChanged(): void {
fprint.checkAvail();
}
function onEnableU2fChanged(): void { function onEnableU2fChanged(): void {
u2f.checkAvail(); u2f.checkAvail();
} }
function onLockU2fReadyChanged(): void {
u2f.checkAvail();
}
function onU2fModeChanged(): void { function onU2fModeChanged(): void {
if (root.lockSecured) { if (root.lockSecured) {
u2f.abort(); u2f.abort();
u2fErrorRetry.running = false; u2fErrorRetry.running = false;
u2fPendingTimeout.running = false; u2fPendingTimeout.running = false;
unlockRequestTimeout.running = false;
root.u2fPending = false; root.u2fPending = false;
root.u2fState = ""; root.u2fState = "";
u2f.checkAvail(); u2f.checkAvail();
@@ -7,7 +7,7 @@ DankPopout {
id: root id: root
layerNamespace: "dms:notification-center-popout" layerNamespace: "dms:notification-center-popout"
fullHeightSurface: false fullHeightSurface: true
property bool notificationHistoryVisible: false property bool notificationHistoryVisible: false
property var triggerScreen: null property var triggerScreen: null
@@ -39,9 +39,11 @@ DankPopout {
} }
} }
popupWidth: 400 popupWidth: triggerScreen ? Math.min(500, Math.max(380, triggerScreen.width - 48)) : 400
popupHeight: stablePopupHeight popupHeight: stablePopupHeight
positioning: "" positioning: ""
animationScaleCollapsed: 0.94
animationOffset: 0
suspendShadowWhileResizing: false suspendShadowWhileResizing: false
screen: triggerScreen screen: triggerScreen
@@ -24,29 +24,6 @@ PanelWindow {
property real _lastReportedAlignedHeight: -1 property real _lastReportedAlignedHeight: -1
property real _storedTopMargin: 0 property real _storedTopMargin: 0
property real _storedBottomMargin: 0 property real _storedBottomMargin: 0
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real entryTravel: {
const base = Math.abs(Theme.effectAnimOffset);
if (directionalEffect) {
if (isCenterPosition)
return Math.max(base, Math.round(content.height * 1.1));
return Math.max(base, Math.round(content.width * 0.95));
}
if (depthEffect)
return Math.max(base, 44);
return base;
}
readonly property real exitTravel: {
if (directionalEffect) {
if (isCenterPosition)
return content.height + entryTravel;
return content.width + entryTravel;
}
if (depthEffect)
return Math.round(entryTravel * 1.35);
return Anims.slidePx;
}
readonly property string clearText: I18n.tr("Dismiss") readonly property string clearText: I18n.tr("Dismiss")
property bool descriptionExpanded: false property bool descriptionExpanded: false
readonly property bool hasExpandableBody: (notificationData?.htmlBody || "").replace(/<[^>]*>/g, "").trim().length > 0 readonly property bool hasExpandableBody: (notificationData?.htmlBody || "").replace(/<[^>]*>/g, "").trim().length > 0
@@ -160,9 +137,9 @@ PanelWindow {
enabled: !exiting && !_isDestroying enabled: !exiting && !_isDestroying
NumberAnimation { NumberAnimation {
id: implicitHeightAnim id: implicitHeightAnim
duration: Theme.variantDuration(descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration, descriptionExpanded) duration: descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: descriptionExpanded ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve easing.bezierCurve: Theme.expressiveCurves.emphasized
} }
} }
@@ -934,9 +911,9 @@ PanelWindow {
if (isCenterPosition) if (isCenterPosition)
return 0; return 0;
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom; const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
return isLeft ? -entryTravel : entryTravel; return isLeft ? -Anims.slidePx : Anims.slidePx;
} }
y: isTopCenter ? -entryTravel : isBottomCenter ? entryTravel : 0 y: isTopCenter ? -Anims.slidePx : isBottomCenter ? Anims.slidePx : 0
} }
] ]
} }
@@ -948,16 +925,16 @@ PanelWindow {
property: isCenterPosition ? "y" : "x" property: isCenterPosition ? "y" : "x"
from: { from: {
if (isTopCenter) if (isTopCenter)
return -entryTravel; return -Anims.slidePx;
if (isBottomCenter) if (isBottomCenter)
return entryTravel; return Anims.slidePx;
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom; const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
return isLeft ? -entryTravel : entryTravel; return isLeft ? -Anims.slidePx : Anims.slidePx;
} }
to: 0 to: 0
duration: Theme.variantDuration(Theme.notificationEnterDuration, true) duration: Theme.notificationEnterDuration
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantPopoutEnterCurve easing.bezierCurve: isCenterPosition ? Theme.expressiveCurves.standardDecel : Theme.expressiveCurves.emphasizedDecel
onStopped: { onStopped: {
if (!win.exiting && !win._isDestroying) { if (!win.exiting && !win._isDestroying) {
if (isCenterPosition) { if (isCenterPosition) {
@@ -982,35 +959,35 @@ PanelWindow {
from: 0 from: 0
to: { to: {
if (isTopCenter) if (isTopCenter)
return -exitTravel; return -Anims.slidePx;
if (isBottomCenter) if (isBottomCenter)
return exitTravel; return Anims.slidePx;
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom; const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
return isLeft ? -exitTravel : exitTravel; return isLeft ? -Anims.slidePx : Anims.slidePx;
} }
duration: Theme.variantDuration(Theme.notificationExitDuration, false) duration: Theme.notificationExitDuration
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantPopoutExitCurve easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
} }
NumberAnimation { NumberAnimation {
target: content target: content
property: "opacity" property: "opacity"
from: 1 from: 1
to: Theme.isDirectionalEffect ? 1 : 0 to: 0
duration: Theme.variantDuration(Theme.notificationExitDuration, false) duration: Theme.notificationExitDuration
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantPopoutExitCurve easing.bezierCurve: Theme.expressiveCurves.standardAccel
} }
NumberAnimation { NumberAnimation {
target: content target: content
property: "scale" property: "scale"
from: 1 from: 1
to: Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed to: 0.98
duration: Theme.variantDuration(Theme.notificationExitDuration, false) duration: Theme.notificationExitDuration
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantPopoutExitCurve easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
} }
} }

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