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

Compare commits

...

42 Commits

Author SHA1 Message Date
bbedward 6e6416c8ba blur: fix dankbar auto-hide blur, fix synchronization in popouts and
modals
2026-03-30 10:40:52 -04:00
bbedward a0b2debd7e blur: add blur support with ext-bg-effect 2026-03-30 09:33:26 -04:00
Sheershak sharma c471cff456 False error Fix (#2109)
* fix: use UnsetWorkspaceName for empty input in workspace rename
Previously, empty input would set workspace name to empty string,
causing issues with Niri's unique workspace name requirement.
Now uses UnsetWorkspaceName action when input is empty.

* prevent false failed to load config toast on niri validation
Move error toast logic from StdioCollector.onStreamFinished to Process.onExited
so it only displays when niri validate actually fails (non-zero exit code),
not when stderr outputs early progress messages during config processing.
2026-03-30 09:21:59 -04:00
Graeme Foster f83bb10e0c fix(osd): coerce optional chain to bool in VolumeOSD enabled bindings (#2101)
Fixes #2100
2026-03-30 09:16:58 -04:00
Triệu Kha 74ad58b1e1 feat(color-picker): add --raw flag (#2103)
* Add --raw flag

* Fix typo

* cleanup duped code
2026-03-30 09:12:48 -04:00
Triệu Kha 577863b969 feat(danklauncher): add launcher history (#2086)
* remember last search query

* add launcherLastQuery SessionSpec

* add rememberLastQuery option

* Add remember last query for appdrawer

* Add query history sessiondata

* Complete and cleanup

* Discard changes to quickshell/Modules/Settings/LauncherTab.qml

* Cleanup logic

* Add rememberLastQuery option

* Add rememberLastQuery option description

* Move setLauncherLastQuery above validation

* Fix logic bug with empty query
2026-03-30 09:09:39 -04:00
Tulip Blossom 03d2a3fd39 chore(niri): use satty screenshot tool as default (#2105)
Swappy seems to not be quite as well maintained as satty, latest commit (https://github.com/jtheoof/swappy/commit/ff7d641b8c0d461b8a90448a5893e4aa3a0533b1) is a translation, before that the latest commit was in august 2025. Latest release is also from Aug 27 2025.

Satty however has had commits quite recently and releases as well.
- https://github.com/Satty-org/Satty/commit/590253c8bba01f5c559808dd432cbb80bc4bd7c8 - This was from yesterday
- https://github.com/Satty-org/Satty/releases/tag/v0.20.1 - Febuary 6th

Signed-off-by: Tulip Blossom <tulilirockz@outlook.com>
2026-03-29 21:32:15 -04:00
purian23 802b23ed60 auth: Add Nix store fallback detection to PAM configs 2026-03-29 18:37:08 -04:00
Sheershak sharma 2b9f3a9eef fix: use UnsetWorkspaceName for empty input in workspace rename (#2094)
Previously, empty input would set workspace name to empty string,
causing issues with Niri's unique workspace name requirement.
Now uses UnsetWorkspaceName action when input is empty.
2026-03-29 12:28:21 -04:00
bbedward 62c60900eb fix(clipboard): wait for forked child to register Wayland source before returning 2026-03-27 13:54:34 -04:00
Patrick Fischer b381e1e54c fix(nix): patch U2F PAM config with full Nix store path (#2071) 2026-03-27 13:39:12 -04:00
purian23 e7ee26ce74 feat(Auth): Unify shared PAM sync across greeter & lockscreen
- Add a neutral `dms auth sync` command and reuse the shared auth flow from:
- Settings auth toggle auto-apply
- `dms greeter sync`
- `dms greeter install`
- greeter auth cleanup paths

- Rework lockscreen PAM so DMS builds /etc/pam.d/dankshell from the system login stack, but removes fingerprint and U2F from that password path. Keep /etc/pam.d/dankshell-u2f separate.

- Preserve custom PAM files in place to avoid adding duplicate greeter auth when the distro already provides it, and keep NixOS on the non-writing path.
2026-03-27 12:52:31 -04:00
bbedward 521a3fa6e8 fix syncWallpaperForCurrentMode 2026-03-27 09:35:39 -04:00
Connor Welsh 5ee93a67fe fix(bar): exclude niri from fullscreen toplevel detection (#2091)
Niri already renders fullscreen windows above the top layer-shell layer
(see render_above_top_layer() in niri's scrolling.rs). Aside from
redundancy, this check also hides the bar for windowed-fullscreen
windows (aka fake fullscreen), since the Wayland protocol reports
identical state for both.
2026-03-27 08:57:18 -04:00
Kangheng Liu 5d0a03c822 fix: show bar when scrolling away from fullscreen window (#2089)
In niri there can be multiple fullscreen windows in a workspace
2026-03-27 08:56:35 -04:00
purian23 293c2a0035 refactor: Remove faillock support and related properties from settings 2026-03-25 22:19:09 -04:00
purian23 9a5fa50541 fix(pam): Update config selection logic for PAM context 2026-03-25 17:04:57 -04:00
purian23 d5ceea8a56 fix(lock): Restore system PAM fallback, faillock support, and auth feedback
- Re-add loginConfigWatcher so installs can still fall through to
  /etc/pam.d instead of the bundled PAM assets
- Add login-faillock bundled PAM asset at runtime. Use it as the bundled fallback when dankshell config is absent
- Fix invalid bare property writes (u2fPending, u2fState, unlockInProgress,
  state) in Pam.qml
- Improve lockscreen auth feedback
2026-03-25 16:39:37 -04:00
Jonas Bloch faa5e7e02d fix: set default value for matugenTemplateNeovimSetBackground (#2081) 2026-03-25 11:35:17 -04:00
Jonas Bloch 516c478f3d Neovim template enhancements (#2078)
* feat: add neovim-lualine template, set vim.o.background automatically based on dms light/dark mode

* feat(matugen): add option to follow dms background color or not on neovim

* chore: regenerate settings and translation index after merging master
2026-03-25 09:16:01 -04:00
Kangheng Liu 906c6a2501 feat: FileBrowser video thumbnail (#2077)
* feat(filebrowser): add filebrowser video thumbnails display

- Find cached thumbnails first
- If not found, generate with ffmpegthumbnailer
- Fallback to placeholder icon if dependency not met

* fix(filebrowser): create thumbnail cache dir if not exists

* refactor(filebrowser): prefer using Paths lib

* fix(filebrowser): only check filetype once for each file

* fix(filebrowser): early test for thumbnails

* feat: add xdgCache path
2026-03-25 09:14:59 -04:00
Viet Dinh 86d8fe4fa4 fix: pywalfox light theme template (#2075)
The current template doesn't work for an OOTB config of pywalfox
without manual configuration. This commit fixes the colors to work
better with its defaults.
2026-03-25 09:12:41 -04:00
Kangheng Liu 9b44bc3259 feat: add FullscreenToplevel detection (#2069)
* feat: add FullscreenToplevel detection

For animating bar for fullscreen events

* fix: respect overview reveal settings

perform the overview check first
2026-03-24 09:21:38 -04:00
bbedward 59b6d2237b i18n: add swedish and german 2026-03-23 12:04:37 -04:00
bbedward 7e559cc0bb cli/notify: append file:// prefix for --file arguments
fixes #1962
2026-03-23 11:57:17 -04:00
bbedward fd1facfce8 core: execute quickshell IPC with pid 2026-03-23 10:06:31 -04:00
bbedward 8f26193cc3 widgets: convert DankButtonGroup to Row instead of Flow 2026-03-23 09:50:56 -04:00
bbedward 43b2e5315d popout: avoid calling close on bad reference 2026-03-23 09:40:53 -04:00
bbedward 5cad89e9cc wallpaper: updatesEnabled set on screen changes 2026-03-23 09:26:49 -04:00
Jon Rogers 3804d2f00b fix(Scorer): honour _preScored for no-query when value exceeds typeBonus (#2065)
Plugin items can set _preScored to signal a priority boost (e.g. recently
used items). Previously _preScored was only respected when a search query
was active, so no-query default lists always fell back to typeBonus+frecency
scoring, making plugin-controlled ordering impossible.

Change the condition from:
  if (query && item._preScored !== undefined)
to:
  if (item._preScored !== undefined && (query || item._preScored > 900))

This respects _preScored in no-query mode only when the value exceeds 900
(the plugin typeBonus), which avoids changing behaviour for "all" mode items
whose _preScored is set to 900-j by the controller (≤ 900). Items without
_preScored set continue to use the existing typeBonus + frecency formula.
2026-03-23 09:25:20 -04:00
Jeff Corcoran 4d649468d5 fix(dropdown): sort fuzzy search results by score and fix empty results on reopen (#2051)
fzf.js relied on stable Array.sort to preserve score ordering, which is
not guaranteed in QML's JS engine. Results appeared in arbitrary order
with low-relevance matches above exact matches. The sort comparator now
explicitly sorts by score descending, with a length-based tiebreaker so
shorter matches rank first when scores are tied.

Also fixed Object.assign mutating the shared defaultOpts object, which
could cause options to leak between Finder instances.

DankDropdown's onOpened handler now reinitializes the search when previous
search text exists, fixing the empty results shown on reopen.

Added resetSearch() for consumers to clear search state externally.
2026-03-23 09:24:51 -04:00
Kangheng Liu c5f145be36 feat: animate dock apps on entry addition/deletion (#2064) 2026-03-23 09:21:21 -04:00
Kangheng Liu 76dff870a7 fix: fallback icon does not dim when not focused (#2063) 2026-03-23 09:19:18 -04:00
lpv 6c8d3fc007 feat(screenshot): add --no-confirm and --reset flags (#2059) 2026-03-23 09:18:56 -04:00
Patrick Fischer e7ffa23016 fix: restore lock screen U2F/fingerprint auth to working state (#2052)
* fix: restore lock screen U2F/fingerprint auth to working state

* fix(pam): Keep SettingsData as single source of truth for auth availability
- Restores SettingsData for fingerprint/U2F, keeping lock screen and New Greeter Settings UI in sync

---------

Co-authored-by: purian23 <purian23@gmail.com>
2026-03-22 19:23:10 -04:00
purian23 4266c064a9 refactor(Ubuntu): Update dual-series upload logic 2026-03-21 20:11:40 -04:00
purian23 5f631b36cd feat(Ubuntu): Initial Ubuntu 26.04 LTS Resolute Raccoon distro support 2026-03-21 17:56:08 -04:00
purian23 be8326f497 fix(Dock): Replace hardcoded max height mask in vertical mode 2026-03-21 13:28:17 -04:00
purian23 07dbba6c53 notifications(Settings): Update notifs popout settings overflow 2026-03-20 19:09:03 -04:00
Linken Quy Dinh a53b9afb44 fix: multi-monitor wallpaper cycling not working (#2042)
Fixed a QML property binding timing issue where dynamically created timers
and processes for per-monitor wallpaper cycling were being assigned to
properties and then immediately read back, which could return undefined
or stale values.

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

Affected functions:
- startMonitorCycling() - timer creation
- cycleToNextWallpaper() - process creation
- cycleToPrevWallpaper() - process creation
2026-03-20 17:38:36 -04:00
purian23 a0c7ffd6b9 dankinstall(Arch): improve AUR package installation logic 2026-03-20 17:03:30 -04:00
bbedward 7ca1d2325a core: use QS_APP_ID instead of pragma 2026-03-20 12:38:42 -04:00
108 changed files with 19977 additions and 2320 deletions
+5 -1
View File
@@ -242,7 +242,11 @@ jobs:
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE" echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
fi fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bash distro/scripts/ppa-upload.sh "$PKG" "$PPA_NAME" questing ${REBUILD_RELEASE:+"$REBUILD_RELEASE"} # ppa-upload.sh uploads to questing + resolute when series is omitted
if ! bash distro/scripts/ppa-upload.sh "$PKG" "$PPA_NAME" ${REBUILD_RELEASE:+"$REBUILD_RELEASE"}; then
echo "::error::Upload failed for $PKG"
exit 1
fi
done done
- name: Summary - name: Summary
+76
View File
@@ -0,0 +1,76 @@
package main
import (
"fmt"
"strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
"github.com/spf13/cobra"
)
var authCmd = &cobra.Command{
Use: "auth",
Short: "Manage DMS authentication sync",
Long: "Manage shared PAM/authentication setup for DMS greeter and lock screen",
}
var authSyncCmd = &cobra.Command{
Use: "sync",
Short: "Sync DMS authentication configuration",
Long: "Apply shared PAM/authentication changes for the lock screen and greeter based on current DMS settings",
Run: func(cmd *cobra.Command, args []string) {
yes, _ := cmd.Flags().GetBool("yes")
term, _ := cmd.Flags().GetBool("terminal")
if term {
if err := syncAuthInTerminal(yes); err != nil {
log.Fatalf("Error launching auth sync in terminal: %v", err)
}
return
}
if err := syncAuth(yes); err != nil {
log.Fatalf("Error syncing authentication: %v", err)
}
},
}
func init() {
authSyncCmd.Flags().BoolP("yes", "y", false, "Non-interactive mode: skip prompts")
authSyncCmd.Flags().BoolP("terminal", "t", false, "Run auth sync in a new terminal (for entering sudo password)")
}
func syncAuth(nonInteractive bool) error {
if !nonInteractive {
fmt.Println("=== DMS Authentication Sync ===")
fmt.Println()
}
logFunc := func(msg string) {
fmt.Println(msg)
}
if err := sharedpam.SyncAuthConfig(logFunc, "", sharedpam.SyncAuthOptions{}); err != nil {
return err
}
if !nonInteractive {
fmt.Println("\n=== Authentication Sync Complete ===")
fmt.Println("\nAuthentication changes have been applied.")
}
return nil
}
func syncAuthInTerminal(nonInteractive bool) error {
syncFlags := make([]string, 0, 1)
if nonInteractive {
syncFlags = append(syncFlags, "--yes")
}
shellSyncCmd := "dms auth sync"
if len(syncFlags) > 0 {
shellSyncCmd += " " + strings.Join(syncFlags, " ")
}
shellCmd := shellSyncCmd + `; echo; echo "Authentication sync finished. Closing in 3 seconds..."; sleep 3`
return runCommandInTerminal(shellCmd)
}
+13 -1
View File
@@ -37,6 +37,9 @@ Output format flags (mutually exclusive, default: --hex):
--cmyk - CMYK values (C% M% Y% K%) --cmyk - CMYK values (C% M% Y% K%)
--json - JSON with all formats --json - JSON with all formats
Optional:
--raw - Removes ANSI escape codes and background colors. Use this when piping to other commands
Examples: Examples:
dms color pick # Pick color, output as hex dms color pick # Pick color, output as hex
dms color pick --rgb # Output as RGB dms color pick --rgb # Output as RGB
@@ -53,6 +56,7 @@ func init() {
colorPickCmd.Flags().Bool("hsv", false, "Output as HSV (H S% V%)") colorPickCmd.Flags().Bool("hsv", false, "Output as HSV (H S% V%)")
colorPickCmd.Flags().Bool("cmyk", false, "Output as CMYK (C% M% Y% K%)") colorPickCmd.Flags().Bool("cmyk", false, "Output as CMYK (C% M% Y% K%)")
colorPickCmd.Flags().Bool("json", false, "Output all formats as JSON") colorPickCmd.Flags().Bool("json", false, "Output all formats as JSON")
colorPickCmd.Flags().Bool("raw", false, "Removes ANSI escape codes and background colors. Use this when piping to other commands")
colorPickCmd.Flags().StringVarP(&colorOutputFmt, "output-format", "o", "", "Custom output format template") colorPickCmd.Flags().StringVarP(&colorOutputFmt, "output-format", "o", "", "Custom output format template")
colorPickCmd.Flags().BoolVarP(&colorAutocopy, "autocopy", "a", false, "Copy result to clipboard") colorPickCmd.Flags().BoolVarP(&colorAutocopy, "autocopy", "a", false, "Copy result to clipboard")
colorPickCmd.Flags().BoolVarP(&colorLowercase, "lowercase", "l", false, "Output hex in lowercase") colorPickCmd.Flags().BoolVarP(&colorLowercase, "lowercase", "l", false, "Output hex in lowercase")
@@ -113,7 +117,15 @@ func runColorPick(cmd *cobra.Command, args []string) {
if jsonOutput { if jsonOutput {
fmt.Println(output) fmt.Println(output)
} else if color.IsDark() { return
}
if raw, _ := cmd.Flags().GetBool("raw"); raw {
fmt.Printf("%s\n", output)
return
}
if color.IsDark() {
fmt.Printf("\033[48;2;%d;%d;%dm\033[97m %s \033[0m\n", color.R, color.G, color.B, output) fmt.Printf("\033[48;2;%d;%d;%dm\033[97m %s \033[0m\n", color.R, color.G, color.B, output)
} else { } else {
fmt.Printf("\033[48;2;%d;%d;%dm\033[30m %s \033[0m\n", color.R, color.G, color.B, output) fmt.Printf("\033[48;2;%d;%d;%dm\033[30m %s \033[0m\n", color.R, color.G, color.B, output)
+2 -3
View File
@@ -64,9 +64,8 @@ var killCmd = &cobra.Command{
} }
var ipcCmd = &cobra.Command{ var ipcCmd = &cobra.Command{
Use: "ipc [target] [function] [args...]", Use: "ipc [target] [function] [args...]",
Short: "Send IPC commands to running DMS shell", Short: "Send IPC commands to running DMS shell",
PreRunE: findConfig,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
_ = findConfig(cmd, args) _ = findConfig(cmd, args)
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
+38 -47
View File
@@ -13,6 +13,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros" "github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter" "github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log" "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils" "github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/text/cases" "golang.org/x/text/cases"
@@ -25,6 +26,11 @@ var greeterCmd = &cobra.Command{
Long: "Manage DMS greeter (greetd)", Long: "Manage DMS greeter (greetd)",
} }
var (
greeterConfigSyncFn = greeter.SyncDMSConfigs
sharedAuthSyncFn = sharedpam.SyncAuthConfig
)
var greeterInstallCmd = &cobra.Command{ var greeterInstallCmd = &cobra.Command{
Use: "install", Use: "install",
Short: "Install and configure DMS greeter", Short: "Install and configure DMS greeter",
@@ -148,6 +154,16 @@ func init() {
greeterUninstallCmd.Flags().BoolP("terminal", "t", false, "Run in a new terminal (for entering sudo password)") greeterUninstallCmd.Flags().BoolP("terminal", "t", false, "Run in a new terminal (for entering sudo password)")
} }
func syncGreeterConfigsAndAuth(dmsPath, compositor string, logFunc func(string), options sharedpam.SyncAuthOptions, beforeAuth func()) error {
if err := greeterConfigSyncFn(dmsPath, compositor, logFunc, ""); err != nil {
return err
}
if beforeAuth != nil {
beforeAuth()
}
return sharedAuthSyncFn(logFunc, "", options)
}
func installGreeter(nonInteractive bool) error { func installGreeter(nonInteractive bool) error {
fmt.Println("=== DMS Greeter Installation ===") fmt.Println("=== DMS Greeter Installation ===")
@@ -243,7 +259,9 @@ func installGreeter(nonInteractive bool) error {
} }
fmt.Println("\nSynchronizing DMS configurations...") fmt.Println("\nSynchronizing DMS configurations...")
if err := greeter.SyncDMSConfigs(dmsPath, selectedCompositor, logFunc, "", false); err != nil { if err := syncGreeterConfigsAndAuth(dmsPath, selectedCompositor, logFunc, sharedpam.SyncAuthOptions{}, func() {
fmt.Println("\nConfiguring authentication...")
}); err != nil {
return err return err
} }
@@ -278,7 +296,7 @@ func uninstallGreeter(nonInteractive bool) error {
} }
if !nonInteractive { if !nonInteractive {
fmt.Print("\nThis will:\n • Stop and disable greetd\n • Remove the DMS PAM managed block\n • Remove the DMS AppArmor profile\n • Restore the most recent pre-DMS greetd config (if available)\n\nContinue? [y/N]: ") fmt.Print("\nThis will:\n • Stop and disable greetd\n • Remove the DMS-managed greeter auth block\n • Remove the DMS AppArmor profile\n • Restore the most recent pre-DMS greetd config (if available)\n\nContinue? [y/N]: ")
var response string var response string
fmt.Scanln(&response) fmt.Scanln(&response)
if strings.ToLower(strings.TrimSpace(response)) != "y" { if strings.ToLower(strings.TrimSpace(response)) != "y" {
@@ -297,8 +315,8 @@ func uninstallGreeter(nonInteractive bool) error {
fmt.Println(" ✓ greetd disabled") fmt.Println(" ✓ greetd disabled")
} }
fmt.Println("\nRemoving DMS PAM configuration...") fmt.Println("\nRemoving DMS authentication configuration...")
if err := greeter.RemoveGreeterPamManagedBlock(logFunc, ""); err != nil { if err := sharedpam.RemoveManagedGreeterPamBlock(logFunc, ""); err != nil {
fmt.Printf(" ⚠ PAM cleanup failed: %v\n", err) fmt.Printf(" ⚠ PAM cleanup failed: %v\n", err)
} }
@@ -535,7 +553,7 @@ func resolveLocalWrapperShell() (string, error) {
func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error { func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
if !nonInteractive { if !nonInteractive {
fmt.Println("=== DMS Greeter Theme Sync ===") fmt.Println("=== DMS Greeter Sync ===")
fmt.Println() fmt.Println()
} }
@@ -721,7 +739,11 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
} }
fmt.Println("\nSynchronizing DMS configurations...") fmt.Println("\nSynchronizing DMS configurations...")
if err := greeter.SyncDMSConfigs(dmsPath, compositor, logFunc, "", forceAuth); err != nil { if err := syncGreeterConfigsAndAuth(dmsPath, compositor, logFunc, sharedpam.SyncAuthOptions{
ForceGreeterAuth: forceAuth,
}, func() {
fmt.Println("\nConfiguring authentication...")
}); err != nil {
return err return err
} }
@@ -734,8 +756,9 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
fmt.Println("\n=== Sync Complete ===") fmt.Println("\n=== Sync Complete ===")
fmt.Println("\nYour theme, settings, and wallpaper configuration have been synced with the greeter.") fmt.Println("\nYour theme, settings, and wallpaper configuration have been synced with the greeter.")
fmt.Println("Shared authentication settings were also checked and reconciled where needed.")
if forceAuth { if forceAuth {
fmt.Println("PAM has been configured for fingerprint and U2F (where modules exist).") fmt.Println("Authentication has been configured for fingerprint and U2F (where modules exist).")
} }
fmt.Println("The changes will be visible on the next login screen.") fmt.Println("The changes will be visible on the next login screen.")
@@ -1297,39 +1320,7 @@ func extractGreeterPathOverrideFromCommand(command string) string {
} }
func parseManagedGreeterPamAuth(pamText string) (managed bool, fingerprint bool, u2f bool, legacy bool) { func parseManagedGreeterPamAuth(pamText string) (managed bool, fingerprint bool, u2f bool, legacy bool) {
if pamText == "" { return sharedpam.ParseManagedGreeterPamAuth(pamText)
return false, false, false, false
}
lines := strings.Split(pamText, "\n")
inManaged := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
switch trimmed {
case greeter.GreeterPamManagedBlockStart:
managed = true
inManaged = true
continue
case greeter.GreeterPamManagedBlockEnd:
inManaged = false
continue
}
if strings.HasPrefix(trimmed, "# DMS greeter fingerprint") || strings.HasPrefix(trimmed, "# DMS greeter U2F") {
legacy = true
}
if !inManaged {
continue
}
if strings.Contains(trimmed, "pam_fprintd") {
fingerprint = true
}
if strings.Contains(trimmed, "pam_u2f") {
u2f = true
}
}
return managed, fingerprint, u2f, legacy
} }
func packageInstallHint() string { func packageInstallHint() string {
@@ -1639,29 +1630,29 @@ func checkGreeterStatus() error {
fmt.Println(" No managed auth block present (DMS-managed fingerprint/U2F lines are disabled)") fmt.Println(" No managed auth block present (DMS-managed fingerprint/U2F lines are disabled)")
} }
if legacyManaged { if legacyManaged {
fmt.Println(" ⚠ Legacy unmanaged DMS PAM lines detected. Run 'dms greeter sync' to normalize.") fmt.Println(" ⚠ Legacy unmanaged DMS PAM lines detected. Run 'dms auth sync' to normalize.")
allGood = false allGood = false
} }
enableFprintToggle, enableU2fToggle := false, false enableFprintToggle, enableU2fToggle := false, false
if enableFprint, enableU2f, settingsErr := greeter.ReadGreeterAuthToggles(homeDir); settingsErr == nil { if enableFprint, enableU2f, settingsErr := sharedpam.ReadGreeterAuthToggles(homeDir); settingsErr == nil {
enableFprintToggle = enableFprint enableFprintToggle = enableFprint
enableU2fToggle = enableU2f enableU2fToggle = enableU2f
} else { } else {
fmt.Printf(" Could not read greeter auth toggles from settings: %v\n", settingsErr) fmt.Printf(" Could not read greeter auth toggles from settings: %v\n", settingsErr)
} }
includedFprintFile := greeter.DetectIncludedPamModule(string(pamData), "pam_fprintd.so") includedFprintFile := sharedpam.DetectIncludedPamModule(string(pamData), "pam_fprintd.so")
includedU2fFile := greeter.DetectIncludedPamModule(string(pamData), "pam_u2f.so") includedU2fFile := sharedpam.DetectIncludedPamModule(string(pamData), "pam_u2f.so")
fprintAvailableForCurrentUser := greeter.FingerprintAuthAvailableForCurrentUser() fprintAvailableForCurrentUser := sharedpam.FingerprintAuthAvailableForCurrentUser()
if managedFprint && includedFprintFile != "" { if managedFprint && includedFprintFile != "" {
fmt.Printf(" ⚠ pam_fprintd found in both DMS managed block and %s.\n", includedFprintFile) fmt.Printf(" ⚠ pam_fprintd found in both DMS managed block and %s.\n", includedFprintFile)
fmt.Println(" Double fingerprint auth detected — run 'dms greeter sync' to resolve.") fmt.Println(" Double fingerprint auth detected — run 'dms auth sync' to resolve.")
allGood = false allGood = false
} }
if managedU2f && includedU2fFile != "" { if managedU2f && includedU2fFile != "" {
fmt.Printf(" ⚠ pam_u2f found in both DMS managed block and %s.\n", includedU2fFile) fmt.Printf(" ⚠ pam_u2f found in both DMS managed block and %s.\n", includedU2fFile)
fmt.Println(" Double security-key auth detected — run 'dms greeter sync' to resolve.") fmt.Println(" Double security-key auth detected — run 'dms auth sync' to resolve.")
allGood = false allGood = false
} }
+87
View File
@@ -0,0 +1,87 @@
package main
import (
"errors"
"reflect"
"testing"
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
)
func TestSyncGreeterConfigsAndAuthDelegatesSharedAuth(t *testing.T) {
origGreeterConfigSyncFn := greeterConfigSyncFn
origSharedAuthSyncFn := sharedAuthSyncFn
t.Cleanup(func() {
greeterConfigSyncFn = origGreeterConfigSyncFn
sharedAuthSyncFn = origSharedAuthSyncFn
})
var calls []string
greeterConfigSyncFn = func(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
if dmsPath != "/tmp/dms" {
t.Fatalf("unexpected dmsPath %q", dmsPath)
}
if compositor != "niri" {
t.Fatalf("unexpected compositor %q", compositor)
}
if sudoPassword != "" {
t.Fatalf("expected empty sudoPassword, got %q", sudoPassword)
}
calls = append(calls, "configs")
return nil
}
var gotOptions sharedpam.SyncAuthOptions
sharedAuthSyncFn = func(logFunc func(string), sudoPassword string, options sharedpam.SyncAuthOptions) error {
if sudoPassword != "" {
t.Fatalf("expected empty sudoPassword, got %q", sudoPassword)
}
gotOptions = options
calls = append(calls, "auth")
return nil
}
err := syncGreeterConfigsAndAuth("/tmp/dms", "niri", func(string) {}, sharedpam.SyncAuthOptions{
ForceGreeterAuth: true,
}, func() {
calls = append(calls, "before-auth")
})
if err != nil {
t.Fatalf("syncGreeterConfigsAndAuth returned error: %v", err)
}
wantCalls := []string{"configs", "before-auth", "auth"}
if !reflect.DeepEqual(calls, wantCalls) {
t.Fatalf("call order = %v, want %v", calls, wantCalls)
}
if !gotOptions.ForceGreeterAuth {
t.Fatalf("expected ForceGreeterAuth to be true, got %+v", gotOptions)
}
}
func TestSyncGreeterConfigsAndAuthStopsOnConfigError(t *testing.T) {
origGreeterConfigSyncFn := greeterConfigSyncFn
origSharedAuthSyncFn := sharedAuthSyncFn
t.Cleanup(func() {
greeterConfigSyncFn = origGreeterConfigSyncFn
sharedAuthSyncFn = origSharedAuthSyncFn
})
greeterConfigSyncFn = func(string, string, func(string), string) error {
return errors.New("config sync failed")
}
authCalled := false
sharedAuthSyncFn = func(func(string), string, sharedpam.SyncAuthOptions) error {
authCalled = true
return nil
}
err := syncGreeterConfigsAndAuth("/tmp/dms", "niri", func(string) {}, sharedpam.SyncAuthOptions{}, nil)
if err == nil || err.Error() != "config sync failed" {
t.Fatalf("expected config sync error, got %v", err)
}
if authCalled {
t.Fatal("expected auth sync not to run after config sync failure")
}
}
+8
View File
@@ -22,6 +22,8 @@ var (
ssNoClipboard bool ssNoClipboard bool
ssNoFile bool ssNoFile bool
ssNoNotify bool ssNoNotify bool
ssNoConfirm bool
ssReset bool
ssStdout bool ssStdout bool
) )
@@ -50,8 +52,10 @@ Examples:
dms screenshot output -o DP-1 # Specific output dms screenshot output -o DP-1 # Specific output
dms screenshot window # Focused window (Hyprland) dms screenshot window # Focused window (Hyprland)
dms screenshot last # Last region (pre-selected) dms screenshot last # Last region (pre-selected)
dms screenshot --reset # Reset last region pre-selection
dms screenshot --no-clipboard # Save file only dms screenshot --no-clipboard # Save file only
dms screenshot --no-file # Clipboard only dms screenshot --no-file # Clipboard only
dms screenshot --no-confirm # Region capture on mouse release
dms screenshot --cursor=on # Include cursor dms screenshot --cursor=on # Include cursor
dms screenshot -f jpg -q 85 # JPEG with quality 85`, dms screenshot -f jpg -q 85 # JPEG with quality 85`,
} }
@@ -119,6 +123,8 @@ func init() {
screenshotCmd.PersistentFlags().BoolVar(&ssNoClipboard, "no-clipboard", false, "Don't copy to clipboard") screenshotCmd.PersistentFlags().BoolVar(&ssNoClipboard, "no-clipboard", false, "Don't copy to clipboard")
screenshotCmd.PersistentFlags().BoolVar(&ssNoFile, "no-file", false, "Don't save to file") screenshotCmd.PersistentFlags().BoolVar(&ssNoFile, "no-file", false, "Don't save to file")
screenshotCmd.PersistentFlags().BoolVar(&ssNoNotify, "no-notify", false, "Don't show notification") screenshotCmd.PersistentFlags().BoolVar(&ssNoNotify, "no-notify", false, "Don't show notification")
screenshotCmd.PersistentFlags().BoolVar(&ssNoConfirm, "no-confirm", false, "Region mode: capture on mouse release without Enter/Space confirmation")
screenshotCmd.PersistentFlags().BoolVar(&ssReset, "reset", false, "Reset saved last-region preselection before capturing")
screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)") screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)")
screenshotCmd.AddCommand(ssRegionCmd) screenshotCmd.AddCommand(ssRegionCmd)
@@ -142,6 +148,8 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
config.Clipboard = !ssNoClipboard config.Clipboard = !ssNoClipboard
config.SaveFile = !ssNoFile config.SaveFile = !ssNoFile
config.Notify = !ssNoNotify config.Notify = !ssNoNotify
config.NoConfirm = ssNoConfirm
config.Reset = ssReset
config.Stdout = ssStdout config.Stdout = ssStdout
if ssOutputDir != "" { if ssOutputDir != "" {
+2
View File
@@ -17,11 +17,13 @@ func init() {
runCmd.Flags().MarkHidden("daemon-child") runCmd.Flags().MarkHidden("daemon-child")
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd) greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
authCmd.AddCommand(authSyncCmd)
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
updateCmd.AddCommand(updateCheckCmd) updateCmd.AddCommand(updateCheckCmd)
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
rootCmd.AddCommand(getCommonCommands()...) rootCmd.AddCommand(getCommonCommands()...)
rootCmd.AddCommand(authCmd)
rootCmd.AddCommand(updateCmd) rootCmd.AddCommand(updateCmd)
rootCmd.SetHelpTemplate(getHelpTemplate()) rootCmd.SetHelpTemplate(getHelpTemplate())
+2
View File
@@ -17,9 +17,11 @@ func init() {
runCmd.Flags().MarkHidden("daemon-child") runCmd.Flags().MarkHidden("daemon-child")
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd) greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
authCmd.AddCommand(authSyncCmd)
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
rootCmd.AddCommand(getCommonCommands()...) rootCmd.AddCommand(getCommonCommands()...)
rootCmd.AddCommand(authCmd)
rootCmd.SetHelpTemplate(getHelpTemplate()) rootCmd.SetHelpTemplate(getHelpTemplate())
} }
+57 -3
View File
@@ -192,6 +192,9 @@ func runShellInteractive(session bool) {
} }
} }
// ! TODO - remove when QS 0.3 is up and we can use the pragma
cmd.Env = append(cmd.Env, "QS_APP_ID=com.danklinux.dms")
if isSessionManaged && hasSystemdRun() { if isSessionManaged && hasSystemdRun() {
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope") cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
} }
@@ -432,6 +435,9 @@ func runShellDaemon(session bool) {
} }
} }
// ! TODO - remove when QS 0.3 is up and we can use the pragma
cmd.Env = append(cmd.Env, "QS_APP_ID=com.danklinux.dms")
if isSessionManaged && hasSystemdRun() { if isSessionManaged && hasSystemdRun() {
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope") cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
} }
@@ -616,6 +622,43 @@ func getShellIPCCompletions(args []string, _ string) []string {
return nil return nil
} }
func getFirstDMSPID() (int, bool) {
dir := getRuntimeDir()
entries, err := os.ReadDir(dir)
if err != nil {
return 0, false
}
for _, entry := range entries {
if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".pid") {
continue
}
data, err := os.ReadFile(filepath.Join(dir, entry.Name()))
if err != nil {
continue
}
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
if err != nil {
continue
}
proc, err := os.FindProcess(pid)
if err != nil {
continue
}
if proc.Signal(syscall.Signal(0)) != nil {
continue
}
return pid, true
}
return 0, false
}
func runShellIPCCommand(args []string) { func runShellIPCCommand(args []string) {
if len(args) == 0 { if len(args) == 0 {
printIPCHelp() printIPCHelp()
@@ -627,10 +670,21 @@ func runShellIPCCommand(args []string) {
} }
cmdArgs := []string{"ipc"} cmdArgs := []string{"ipc"}
if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display") switch pid, ok := getFirstDMSPID(); {
case ok:
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
default:
if err := findConfig(nil, nil); err != nil {
log.Fatalf("Error finding config: %v", err)
}
// ! TODO - remove check when QS 0.3 is released
if qsHasAnyDisplay() {
cmdArgs = append(cmdArgs, "--any-display")
}
cmdArgs = append(cmdArgs, "-p", configPath)
} }
cmdArgs = append(cmdArgs, "-p", configPath)
cmdArgs = append(cmdArgs, args...) cmdArgs = append(cmdArgs, args...)
cmd := exec.Command("qs", cmdArgs...) cmd := exec.Command("qs", cmdArgs...)
cmd.Stdin = os.Stdin cmd.Stdin = os.Stdin
+36 -17
View File
@@ -52,35 +52,53 @@ func copyFork(data io.Reader, 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.Stdout = nil
cmd.Stderr = nil cmd.Stderr = nil
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
cmd.Env = append(os.Environ(), "DMS_CLIP_FORKED=1")
if stdinSource, ok := data.(*os.File); ok { stdout, err := cmd.StdoutPipe()
cmd.Stdin = stdinSource
return cmd.Start()
}
stdin, err := cmd.StdinPipe()
if err != nil { if err != nil {
return fmt.Errorf("stdin pipe: %w", err) return fmt.Errorf("stdout pipe: %w", err)
} }
if err := cmd.Start(); err != nil { switch src := data.(type) {
return fmt.Errorf("start: %w", err) case *os.File:
cmd.Stdin = src
if err := cmd.Start(); err != nil {
return fmt.Errorf("start: %w", err)
}
default:
stdin, err := cmd.StdinPipe()
if err != nil {
return fmt.Errorf("stdin pipe: %w", err)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("start: %w", err)
}
if _, err := io.Copy(stdin, data); err != nil {
stdin.Close()
return fmt.Errorf("write stdin: %w", err)
}
if err := stdin.Close(); err != nil {
return fmt.Errorf("close stdin: %w", err)
}
} }
if _, err := io.Copy(stdin, data); err != nil { var buf [1]byte
stdin.Close() if _, err := stdout.Read(buf[:]); err != nil {
return fmt.Errorf("write stdin: %w", err) return fmt.Errorf("waiting for clipboard ready: %w", err)
} }
if err := stdin.Close(); err != nil {
return fmt.Errorf("close stdin: %w", err)
}
return nil return nil
} }
func signalReady() {
if os.Getenv("DMS_CLIP_FORKED") == "" {
return
}
os.Stdout.Write([]byte{1})
}
func copyServeReader(data io.Reader, mimeType string, pasteOnce bool) error { func copyServeReader(data io.Reader, mimeType string, pasteOnce bool) error {
cachedData, err := createClipboardCacheFile() cachedData, err := createClipboardCacheFile()
if err != nil { if err != nil {
@@ -242,6 +260,7 @@ func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOn
} }
display.Roundtrip() display.Roundtrip()
signalReady()
for { for {
select { select {
+92 -24
View File
@@ -135,6 +135,42 @@ func (a *ArchDistribution) packageInstalled(pkg string) bool {
return err == nil return err == nil
} }
// parseSRCINFODeps reads a .SRCINFO file and returns runtime dep and makedep package
func parseSRCINFODeps(srcinfoPath string) (deps []string, makedeps []string, err error) {
data, err := os.ReadFile(srcinfoPath)
if err != nil {
return nil, nil, err
}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
var pkg string
var target *[]string
switch {
case strings.HasPrefix(line, "makedepends = "):
pkg = strings.TrimPrefix(line, "makedepends = ")
target = &makedeps
case strings.HasPrefix(line, "depends = "):
pkg = strings.TrimPrefix(line, "depends = ")
target = &deps
default:
continue
}
// Strip version constraint (>=, <=, >, <, =) and colon-descriptions
if idx := strings.IndexAny(pkg, "><:="); idx >= 0 {
pkg = pkg[:idx]
}
pkg = strings.TrimSpace(pkg)
if pkg != "" {
*target = append(*target, pkg)
}
}
return deps, makedeps, nil
}
func (a *ArchDistribution) isInSystemRepo(pkg string) bool {
return exec.Command("pacman", "-Si", pkg).Run() == nil
}
func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping { func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant)) return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
} }
@@ -524,6 +560,16 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
} }
func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error { func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error {
return a.installSingleAURPackageInternal(ctx, pkg, sudoPassword, progressChan, startProgress, endProgress, make(map[string]bool))
}
func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64, visited map[string]bool) error {
if visited[pkg] {
a.log(fmt.Sprintf("Skipping %s (already being installed, cycle detected)", pkg))
return nil
}
visited[pkg] = true
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err) return fmt.Errorf("failed to get user home directory: %w", err)
@@ -610,39 +656,61 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages, Phase: PhaseAURPackages,
Progress: startProgress + 0.3*(endProgress-startProgress), Progress: startProgress + 0.3*(endProgress-startProgress),
Step: fmt.Sprintf("Installing dependencies for %s...", pkg), Step: fmt.Sprintf("Resolving dependencies for %s...", pkg),
IsComplete: false, IsComplete: false,
CommandInfo: "Installing package dependencies and makedepends", CommandInfo: "Classifying dependencies as system or AUR",
} }
// Install dependencies from .SRCINFO runtimeDeps, makeDeps, err := parseSRCINFODeps(srcinfoPath)
depFilter := "" if err != nil {
if pkg == "dms-shell-git" { return fmt.Errorf("failed to parse .SRCINFO for %s: %w", pkg, err)
depFilter = ` | sed -E 's/[[:space:]]*(quickshell|dgop)[[:space:]]*/ /g' | tr -s ' '`
} }
depsCmd := exec.CommandContext(ctx, "bash", "-c", seen := make(map[string]bool)
fmt.Sprintf(` var systemPkgs []string
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' %s | sed 's/[[:space:]]*$//') var aurPkgs []string
if [ ! -z "$deps" ] && [ "$deps" != " " ]; then
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
fi
`, srcinfoPath, depFilter, sudoPassword))
if err := a.runWithProgress(depsCmd, progressChan, PhaseAURPackages, startProgress+0.3*(endProgress-startProgress), startProgress+0.35*(endProgress-startProgress)); err != nil { for _, dep := range append(runtimeDeps, makeDeps...) {
return fmt.Errorf("FAILED to install runtime dependencies for %s: %w", pkg, err) if seen[dep] || a.packageInstalled(dep) {
continue
}
seen[dep] = true
if a.isInSystemRepo(dep) {
systemPkgs = append(systemPkgs, dep)
} else {
aurPkgs = append(aurPkgs, dep)
}
} }
makedepsCmd := exec.CommandContext(ctx, "bash", "-c", if len(systemPkgs) > 0 {
fmt.Sprintf(` progressChan <- InstallProgressMsg{
makedeps=$(grep -E "^[[:space:]]*makedepends = " "%s" | sed 's/^[[:space:]]*makedepends = //' | tr '\n' ' ') Phase: PhaseAURPackages,
if [ ! -z "$makedeps" ]; then Progress: startProgress + 0.32*(endProgress-startProgress),
echo '%s' | sudo -S pacman -S --needed --noconfirm $makedeps Step: fmt.Sprintf("Installing %d system dependencies for %s...", len(systemPkgs), pkg),
fi IsComplete: false,
`, srcinfoPath, sudoPassword)) CommandInfo: fmt.Sprintf("sudo pacman -S --needed --noconfirm %s", strings.Join(systemPkgs, " ")),
}
if err := a.installSystemPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
return fmt.Errorf("failed to install system dependencies for %s: %w", pkg, err)
}
}
if err := a.runWithProgress(makedepsCmd, progressChan, PhaseAURPackages, startProgress+0.35*(endProgress-startProgress), startProgress+0.4*(endProgress-startProgress)); err != nil { for _, aurDep := range aurPkgs {
return fmt.Errorf("FAILED to install make dependencies for %s: %w", pkg, err) a.log(fmt.Sprintf("Dependency %s is AUR-only, building from source...", aurDep))
progressChan <- InstallProgressMsg{
Phase: PhaseAURPackages,
Progress: startProgress + 0.35*(endProgress-startProgress),
Step: fmt.Sprintf("Installing AUR dependency %s for %s...", aurDep, pkg),
IsComplete: false,
CommandInfo: fmt.Sprintf("Building AUR dependency: %s", aurDep),
}
if err := a.installSingleAURPackageInternal(ctx, aurDep, sudoPassword, progressChan,
startProgress+0.35*(endProgress-startProgress),
startProgress+0.39*(endProgress-startProgress),
visited,
); err != nil {
return fmt.Errorf("failed to install AUR dependency %s for %s: %w", aurDep, pkg, err)
}
} }
} }
+9 -441
View File
@@ -16,6 +16,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/config" "github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros" "github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen" "github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils" "github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
"github.com/sblinch/kdl-go" "github.com/sblinch/kdl-go"
"github.com/sblinch/kdl-go/document" "github.com/sblinch/kdl-go/document"
@@ -25,26 +26,7 @@ var appArmorProfileData []byte
const appArmorProfileDest = "/etc/apparmor.d/usr.bin.dms-greeter" const appArmorProfileDest = "/etc/apparmor.d/usr.bin.dms-greeter"
const ( const GreeterCacheDir = "/var/cache/dms-greeter"
GreeterCacheDir = "/var/cache/dms-greeter"
GreeterPamManagedBlockStart = "# BEGIN DMS GREETER AUTH (managed by dms greeter sync)"
GreeterPamManagedBlockEnd = "# END DMS GREETER AUTH"
legacyGreeterPamFprintComment = "# DMS greeter fingerprint"
legacyGreeterPamU2FComment = "# DMS greeter U2F"
)
// Common PAM auth stack names referenced by greetd across supported distros.
var includedPamAuthFiles = []string{
"system-auth",
"common-auth",
"password-auth",
"system-login",
"system-local-login",
"common-auth-pc",
"login",
}
func DetectDMSPath() (string, error) { func DetectDMSPath() (string, error) {
return config.LocateDMSConfig() return config.LocateDMSConfig()
@@ -749,49 +731,6 @@ func InstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
return nil return nil
} }
// RemoveGreeterPamManagedBlock strips the DMS managed auth block from /etc/pam.d/greetd
func RemoveGreeterPamManagedBlock(logFunc func(string), sudoPassword string) error {
if IsNixOS() {
return nil
}
const greetdPamPath = "/etc/pam.d/greetd"
data, err := os.ReadFile(greetdPamPath)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("failed to read %s: %w", greetdPamPath, err)
}
stripped, removed := stripManagedGreeterPamBlock(string(data))
strippedAgain, removedLegacy := stripLegacyGreeterPamLines(stripped)
if !removed && !removedLegacy {
return nil
}
tmp, err := os.CreateTemp("", "dms-pam-greetd-*")
if err != nil {
return fmt.Errorf("failed to create temp PAM file: %w", err)
}
tmpPath := tmp.Name()
defer os.Remove(tmpPath)
if _, err := tmp.WriteString(strippedAgain); err != nil {
tmp.Close()
return fmt.Errorf("failed to write temp PAM file: %w", err)
}
tmp.Close()
if err := runSudoCmd(sudoPassword, "cp", tmpPath, greetdPamPath); err != nil {
return fmt.Errorf("failed to write PAM config: %w", err)
}
if err := runSudoCmd(sudoPassword, "chmod", "644", greetdPamPath); err != nil {
return fmt.Errorf("failed to set PAM config permissions: %w", err)
}
logFunc(" ✓ Removed DMS managed PAM block from " + greetdPamPath)
return nil
}
// UninstallAppArmorProfile removes the DMS AppArmor profile and reloads AppArmor. // UninstallAppArmorProfile removes the DMS AppArmor profile and reloads AppArmor.
// It is a no-op when AppArmor is not active or the profile does not exist. // It is a no-op when AppArmor is not active or the profile does not exist.
func UninstallAppArmorProfile(logFunc func(string), sudoPassword string) error { func UninstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
@@ -1322,7 +1261,7 @@ func syncGreeterColorSource(homeDir, cacheDir string, state greeterThemeSyncStat
return nil return nil
} }
func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPassword string, forceAuth bool) error { func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err != nil { if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err) return fmt.Errorf("failed to get user home directory: %w", err)
@@ -1387,10 +1326,6 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
return fmt.Errorf("greeter wallpaper override sync failed: %w", err) return fmt.Errorf("greeter wallpaper override sync failed: %w", err)
} }
if err := syncGreeterPamConfig(homeDir, logFunc, sudoPassword, forceAuth); err != nil {
return fmt.Errorf("greeter PAM config sync failed: %w", err)
}
if strings.ToLower(compositor) != "niri" { if strings.ToLower(compositor) != "niri" {
return nil return nil
} }
@@ -1439,378 +1374,6 @@ func syncGreeterWallpaperOverride(cacheDir string, logFunc func(string), sudoPas
return nil return nil
} }
func pamModuleExists(module string) bool {
for _, libDir := range []string{
"/usr/lib64/security",
"/usr/lib/security",
"/lib64/security",
"/lib/security",
"/lib/x86_64-linux-gnu/security",
"/usr/lib/x86_64-linux-gnu/security",
"/lib/aarch64-linux-gnu/security",
"/usr/lib/aarch64-linux-gnu/security",
"/run/current-system/sw/lib64/security",
"/run/current-system/sw/lib/security",
} {
if _, err := os.Stat(filepath.Join(libDir, module)); err == nil {
return true
}
}
return false
}
func stripManagedGreeterPamBlock(content string) (string, bool) {
lines := strings.Split(content, "\n")
filtered := make([]string, 0, len(lines))
inManagedBlock := false
removed := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == GreeterPamManagedBlockStart {
inManagedBlock = true
removed = true
continue
}
if trimmed == GreeterPamManagedBlockEnd {
inManagedBlock = false
removed = true
continue
}
if inManagedBlock {
removed = true
continue
}
filtered = append(filtered, line)
}
return strings.Join(filtered, "\n"), removed
}
func stripLegacyGreeterPamLines(content string) (string, bool) {
lines := strings.Split(content, "\n")
filtered := make([]string, 0, len(lines))
removed := false
for i := 0; i < len(lines); i++ {
trimmed := strings.TrimSpace(lines[i])
if strings.HasPrefix(trimmed, legacyGreeterPamFprintComment) || strings.HasPrefix(trimmed, legacyGreeterPamU2FComment) {
removed = true
if i+1 < len(lines) {
nextLine := strings.TrimSpace(lines[i+1])
if strings.HasPrefix(nextLine, "auth") &&
(strings.Contains(nextLine, "pam_fprintd") || strings.Contains(nextLine, "pam_u2f")) {
i++
}
}
continue
}
filtered = append(filtered, lines[i])
}
return strings.Join(filtered, "\n"), removed
}
func insertManagedGreeterPamBlock(content string, blockLines []string, greetdPamPath string) (string, error) {
lines := strings.Split(content, "\n")
for i, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed != "" && !strings.HasPrefix(trimmed, "#") && strings.HasPrefix(trimmed, "auth") {
block := strings.Join(blockLines, "\n")
prefix := strings.Join(lines[:i], "\n")
suffix := strings.Join(lines[i:], "\n")
switch {
case prefix == "":
return block + "\n" + suffix, nil
case suffix == "":
return prefix + "\n" + block, nil
default:
return prefix + "\n" + block + "\n" + suffix, nil
}
}
}
return "", fmt.Errorf("no auth directive found in %s", greetdPamPath)
}
func PamTextIncludesFile(pamText, filename string) bool {
lines := strings.Split(pamText, "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
if strings.Contains(trimmed, filename) &&
(strings.Contains(trimmed, "include") || strings.Contains(trimmed, "substack") || strings.HasPrefix(trimmed, "@include")) {
return true
}
}
return false
}
func PamFileHasModule(pamFilePath, module string) bool {
data, err := os.ReadFile(pamFilePath)
if err != nil {
return false
}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
if strings.Contains(trimmed, module) {
return true
}
}
return false
}
func DetectIncludedPamModule(pamText, module string) string {
for _, includedFile := range includedPamAuthFiles {
if PamTextIncludesFile(pamText, includedFile) && PamFileHasModule("/etc/pam.d/"+includedFile, module) {
return includedFile
}
}
return ""
}
type greeterAuthSettings struct {
GreeterEnableFprint bool `json:"greeterEnableFprint"`
GreeterEnableU2f bool `json:"greeterEnableU2f"`
}
func readGreeterAuthSettings(homeDir string) (greeterAuthSettings, error) {
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
data, err := os.ReadFile(settingsPath)
if err != nil {
if os.IsNotExist(err) {
return greeterAuthSettings{}, nil
}
return greeterAuthSettings{}, fmt.Errorf("failed to read settings at %s: %w", settingsPath, err)
}
if strings.TrimSpace(string(data)) == "" {
return greeterAuthSettings{}, nil
}
var settings greeterAuthSettings
if err := json.Unmarshal(data, &settings); err != nil {
return greeterAuthSettings{}, fmt.Errorf("failed to parse settings at %s: %w", settingsPath, err)
}
return settings, nil
}
func ReadGreeterAuthToggles(homeDir string) (enableFprint bool, enableU2f bool, err error) {
settings, err := readGreeterAuthSettings(homeDir)
if err != nil {
return false, false, err
}
return settings.GreeterEnableFprint, settings.GreeterEnableU2f, nil
}
func hasEnrolledFingerprintOutput(output string) bool {
lower := strings.ToLower(output)
if strings.Contains(lower, "no fingers enrolled") ||
strings.Contains(lower, "no fingerprints enrolled") ||
strings.Contains(lower, "no prints enrolled") {
return false
}
if strings.Contains(lower, "has fingers enrolled") ||
strings.Contains(lower, "has fingerprints enrolled") {
return true
}
for _, line := range strings.Split(lower, "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "finger:") {
return true
}
if strings.HasPrefix(trimmed, "- ") && strings.Contains(trimmed, "finger") {
return true
}
}
return false
}
func FingerprintAuthAvailableForUser(username string) bool {
username = strings.TrimSpace(username)
if username == "" {
return false
}
if !pamModuleExists("pam_fprintd.so") {
return false
}
if _, err := exec.LookPath("fprintd-list"); err != nil {
return false
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
out, err := exec.CommandContext(ctx, "fprintd-list", username).CombinedOutput()
if err != nil {
return false
}
return hasEnrolledFingerprintOutput(string(out))
}
func FingerprintAuthAvailableForCurrentUser() bool {
username := strings.TrimSpace(os.Getenv("SUDO_USER"))
if username == "" {
username = strings.TrimSpace(os.Getenv("USER"))
}
if username == "" {
out, err := exec.Command("id", "-un").Output()
if err == nil {
username = strings.TrimSpace(string(out))
}
}
return FingerprintAuthAvailableForUser(username)
}
func pamManagerHintForCurrentDistro() string {
osInfo, err := distros.GetOSInfo()
if err != nil {
return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
}
config, exists := distros.Registry[osInfo.Distribution.ID]
if !exists {
return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
}
switch config.Family {
case distros.FamilyFedora:
return "Disable it in authselect to force password-only greeter login."
case distros.FamilyDebian, distros.FamilyUbuntu:
return "Disable it in pam-auth-update to force password-only greeter login."
default:
return "Disable it in your distro PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
}
}
func syncGreeterPamConfig(homeDir string, logFunc func(string), sudoPassword string, forceAuth bool) error {
var wantFprint, wantU2f bool
fprintToggleEnabled := forceAuth
u2fToggleEnabled := forceAuth
if forceAuth {
wantFprint = pamModuleExists("pam_fprintd.so")
wantU2f = pamModuleExists("pam_u2f.so")
} else {
settings, err := readGreeterAuthSettings(homeDir)
if err != nil {
return err
}
fprintToggleEnabled = settings.GreeterEnableFprint
u2fToggleEnabled = settings.GreeterEnableU2f
fprintModule := pamModuleExists("pam_fprintd.so")
u2fModule := pamModuleExists("pam_u2f.so")
wantFprint = settings.GreeterEnableFprint && fprintModule
wantU2f = settings.GreeterEnableU2f && u2fModule
if settings.GreeterEnableFprint && !fprintModule {
logFunc("⚠ Warning: greeter fingerprint toggle is enabled, but pam_fprintd.so was not found.")
}
if settings.GreeterEnableU2f && !u2fModule {
logFunc("⚠ Warning: greeter security key toggle is enabled, but pam_u2f.so was not found.")
}
}
if IsNixOS() {
logFunc(" NixOS detected: PAM config is managed by NixOS modules. Skipping DMS PAM block write.")
logFunc(" Configure fingerprint/U2F auth via your greetd NixOS module options (e.g. security.pam.services.greetd).")
return nil
}
greetdPamPath := "/etc/pam.d/greetd"
pamData, err := os.ReadFile(greetdPamPath)
if err != nil {
return fmt.Errorf("failed to read %s: %w", greetdPamPath, err)
}
originalContent := string(pamData)
content, _ := stripManagedGreeterPamBlock(originalContent)
content, _ = stripLegacyGreeterPamLines(content)
includedFprintFile := DetectIncludedPamModule(content, "pam_fprintd.so")
includedU2fFile := DetectIncludedPamModule(content, "pam_u2f.so")
fprintAvailableForCurrentUser := FingerprintAuthAvailableForCurrentUser()
if wantFprint && includedFprintFile != "" {
logFunc("⚠ pam_fprintd already present in included " + includedFprintFile + " (managed by authselect/pam-auth-update). Skipping DMS fprint block to avoid double-fingerprint auth.")
wantFprint = false
}
if wantU2f && includedU2fFile != "" {
logFunc("⚠ pam_u2f already present in included " + includedU2fFile + " (managed by authselect/pam-auth-update). Skipping DMS U2F block to avoid double security-key auth.")
wantU2f = false
}
if !wantFprint && includedFprintFile != "" {
if fprintToggleEnabled {
logFunc(" Fingerprint auth is still enabled via included " + includedFprintFile + ".")
if fprintAvailableForCurrentUser {
logFunc(" DMS toggle is enabled, and effective auth is provided by the included PAM stack.")
} else {
logFunc(" No enrolled fingerprints detected for the current user; password auth remains the effective path.")
}
} else {
if fprintAvailableForCurrentUser {
logFunc(" Fingerprint auth is active via included " + includedFprintFile + " while DMS fingerprint toggle is off.")
logFunc(" Password login will work but may be delayed while the fingerprint module runs first.")
logFunc(" To eliminate the delay, " + pamManagerHintForCurrentDistro())
} else {
logFunc(" pam_fprintd is present via included " + includedFprintFile + ", but no enrolled fingerprints were detected for the current user.")
logFunc(" Password auth remains the effective login path.")
}
}
}
if !wantU2f && includedU2fFile != "" {
if u2fToggleEnabled {
logFunc(" Security-key auth is still enabled via included " + includedU2fFile + ".")
logFunc(" DMS toggle is enabled, but effective auth is provided by the included PAM stack.")
} else {
logFunc("⚠ Security-key auth is active via included " + includedU2fFile + " while DMS security-key toggle is off.")
logFunc(" " + pamManagerHintForCurrentDistro())
}
}
if wantFprint || wantU2f {
blockLines := []string{GreeterPamManagedBlockStart}
if wantFprint {
blockLines = append(blockLines, "auth sufficient pam_fprintd.so max-tries=1 timeout=5")
}
if wantU2f {
blockLines = append(blockLines, "auth sufficient pam_u2f.so cue nouserok timeout=10")
}
blockLines = append(blockLines, GreeterPamManagedBlockEnd)
content, err = insertManagedGreeterPamBlock(content, blockLines, greetdPamPath)
if err != nil {
return err
}
}
if content == originalContent {
return nil
}
tmpFile, err := os.CreateTemp("", "greetd-pam-*.conf")
if err != nil {
return err
}
tmpPath := tmpFile.Name()
defer os.Remove(tmpPath)
if _, err := tmpFile.WriteString(content); err != nil {
tmpFile.Close()
return err
}
if err := tmpFile.Close(); err != nil {
return err
}
if err := runSudoCmd(sudoPassword, "cp", tmpPath, greetdPamPath); err != nil {
return fmt.Errorf("failed to install updated PAM config at %s: %w", greetdPamPath, err)
}
if err := runSudoCmd(sudoPassword, "chmod", "644", greetdPamPath); err != nil {
return fmt.Errorf("failed to set permissions on %s: %w", greetdPamPath, err)
}
if wantFprint || wantU2f {
logFunc("✓ Configured greetd PAM for fingerprint/U2F")
} else {
logFunc("✓ Cleared DMS-managed greeter PAM auth block")
}
return nil
}
type niriGreeterSync struct { type niriGreeterSync struct {
processed map[string]bool processed map[string]bool
nodes []*document.Node nodes []*document.Node
@@ -2484,10 +2047,15 @@ func AutoSetupGreeter(compositor, sudoPassword string, logFunc func(string)) err
} }
logFunc("Synchronizing DMS configurations...") logFunc("Synchronizing DMS configurations...")
if err := SyncDMSConfigs(dmsPath, compositor, logFunc, sudoPassword, false); err != nil { if err := SyncDMSConfigs(dmsPath, compositor, logFunc, sudoPassword); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: config sync error: %v", err)) logFunc(fmt.Sprintf("⚠ Warning: config sync error: %v", err))
} }
logFunc("Configuring authentication...")
if err := sharedpam.SyncAuthConfig(logFunc, sudoPassword, sharedpam.SyncAuthOptions{}); err != nil {
return fmt.Errorf("failed to sync authentication: %w", err)
}
logFunc("Checking for conflicting display managers...") logFunc("Checking for conflicting display managers...")
if err := DisableConflictingDisplayManagers(sudoPassword, logFunc); err != nil { if err := DisableConflictingDisplayManagers(sudoPassword, logFunc); err != nil {
logFunc(fmt.Sprintf("⚠ Warning: %v", err)) logFunc(fmt.Sprintf("⚠ Warning: %v", err))
+3 -3
View File
@@ -6,7 +6,7 @@ import (
"testing" "testing"
) )
func writeTestJSON(t *testing.T, path string, content string) { func writeTestFile(t *testing.T, path string, content string) {
t.Helper() t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("failed to create parent dir for %s: %v", path, err) t.Fatalf("failed to create parent dir for %s: %v", path, err)
@@ -70,8 +70,8 @@ func TestResolveGreeterThemeSyncState(t *testing.T) {
t.Parallel() t.Parallel()
homeDir := t.TempDir() homeDir := t.TempDir()
writeTestJSON(t, filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"), tt.settingsJSON) writeTestFile(t, filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"), tt.settingsJSON)
writeTestJSON(t, filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"), tt.sessionJSON) writeTestFile(t, filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"), tt.sessionJSON)
state, err := resolveGreeterThemeSyncState(homeDir) state, err := resolveGreeterThemeSyncState(homeDir)
if err != nil { if err != nil {
+6 -1
View File
@@ -6,6 +6,7 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings"
"syscall" "syscall"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
@@ -59,7 +60,11 @@ func Send(n Notification) error {
hints := map[string]dbus.Variant{} hints := map[string]dbus.Variant{}
if n.FilePath != "" { if n.FilePath != "" {
hints["image_path"] = dbus.MakeVariant(n.FilePath) imgPath := n.FilePath
if !strings.HasPrefix(imgPath, "file://") {
imgPath = "file://" + imgPath
}
hints["image_path"] = dbus.MakeVariant(imgPath)
} }
obj := conn.Object(notifyDest, notifyPath) obj := conn.Object(notifyDest, notifyPath)
+892
View File
@@ -0,0 +1,892 @@
package pam
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
)
const (
GreeterPamManagedBlockStart = "# BEGIN DMS GREETER AUTH (managed by dms greeter sync)"
GreeterPamManagedBlockEnd = "# END DMS GREETER AUTH"
LockscreenPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN AUTH (managed by dms greeter sync)"
LockscreenPamManagedBlockEnd = "# END DMS LOCKSCREEN AUTH"
LockscreenU2FPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN U2F AUTH (managed by dms auth sync)"
LockscreenU2FPamManagedBlockEnd = "# END DMS LOCKSCREEN U2F AUTH"
legacyGreeterPamFprintComment = "# DMS greeter fingerprint"
legacyGreeterPamU2FComment = "# DMS greeter U2F"
GreetdPamPath = "/etc/pam.d/greetd"
DankshellPamPath = "/etc/pam.d/dankshell"
DankshellU2FPamPath = "/etc/pam.d/dankshell-u2f"
)
var includedPamAuthFiles = []string{
"system-auth",
"common-auth",
"password-auth",
"system-login",
"system-local-login",
"common-auth-pc",
"login",
}
type AuthSettings struct {
EnableFprint bool `json:"enableFprint"`
EnableU2f bool `json:"enableU2f"`
GreeterEnableFprint bool `json:"greeterEnableFprint"`
GreeterEnableU2f bool `json:"greeterEnableU2f"`
}
type SyncAuthOptions struct {
HomeDir string
ForceGreeterAuth bool
}
type syncDeps struct {
pamDir string
greetdPath string
dankshellPath string
dankshellU2fPath string
isNixOS func() bool
readFile func(string) ([]byte, error)
stat func(string) (os.FileInfo, error)
createTemp func(string, string) (*os.File, error)
removeFile func(string) error
runSudoCmd func(string, string, ...string) error
pamModuleExists func(string) bool
fingerprintAvailableForCurrentUser func() bool
}
type lockscreenPamIncludeDirective struct {
target string
filterType string
}
type lockscreenPamResolver struct {
pamDir string
readFile func(string) ([]byte, error)
}
func defaultSyncDeps() syncDeps {
return syncDeps{
pamDir: "/etc/pam.d",
greetdPath: GreetdPamPath,
dankshellPath: DankshellPamPath,
dankshellU2fPath: DankshellU2FPamPath,
isNixOS: IsNixOS,
readFile: os.ReadFile,
stat: os.Stat,
createTemp: os.CreateTemp,
removeFile: os.Remove,
runSudoCmd: runSudoCmd,
pamModuleExists: pamModuleExists,
fingerprintAvailableForCurrentUser: FingerprintAuthAvailableForCurrentUser,
}
}
func IsNixOS() bool {
_, err := os.Stat("/etc/NIXOS")
return err == nil
}
func ReadAuthSettings(homeDir string) (AuthSettings, error) {
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
data, err := os.ReadFile(settingsPath)
if err != nil {
if os.IsNotExist(err) {
return AuthSettings{}, nil
}
return AuthSettings{}, fmt.Errorf("failed to read settings at %s: %w", settingsPath, err)
}
if strings.TrimSpace(string(data)) == "" {
return AuthSettings{}, nil
}
var settings AuthSettings
if err := json.Unmarshal(data, &settings); err != nil {
return AuthSettings{}, fmt.Errorf("failed to parse settings at %s: %w", settingsPath, err)
}
return settings, nil
}
func ReadGreeterAuthToggles(homeDir string) (enableFprint bool, enableU2f bool, err error) {
settings, err := ReadAuthSettings(homeDir)
if err != nil {
return false, false, err
}
return settings.GreeterEnableFprint, settings.GreeterEnableU2f, nil
}
func SyncAuthConfig(logFunc func(string), sudoPassword string, options SyncAuthOptions) error {
return syncAuthConfigWithDeps(logFunc, sudoPassword, options, defaultSyncDeps())
}
func RemoveManagedGreeterPamBlock(logFunc func(string), sudoPassword string) error {
return removeManagedGreeterPamBlockWithDeps(logFunc, sudoPassword, defaultSyncDeps())
}
func syncAuthConfigWithDeps(logFunc func(string), sudoPassword string, options SyncAuthOptions, deps syncDeps) error {
homeDir := strings.TrimSpace(options.HomeDir)
if homeDir == "" {
var err error
homeDir, err = os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get user home directory: %w", err)
}
}
settings, err := ReadAuthSettings(homeDir)
if err != nil {
return err
}
if err := syncLockscreenPamConfigWithDeps(logFunc, sudoPassword, deps); err != nil {
return err
}
if err := syncLockscreenU2FPamConfigWithDeps(logFunc, sudoPassword, settings.EnableU2f, deps); err != nil {
return err
}
if _, err := deps.stat(deps.greetdPath); err != nil {
if os.IsNotExist(err) {
logFunc(" /etc/pam.d/greetd not found. Skipping greeter PAM sync.")
return nil
}
return fmt.Errorf("failed to inspect %s: %w", deps.greetdPath, err)
}
if err := syncGreeterPamConfigWithDeps(logFunc, sudoPassword, settings, options.ForceGreeterAuth, deps); err != nil {
return err
}
return nil
}
func removeManagedGreeterPamBlockWithDeps(logFunc func(string), sudoPassword string, deps syncDeps) error {
if deps.isNixOS() {
return nil
}
data, err := deps.readFile(deps.greetdPath)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("failed to read %s: %w", deps.greetdPath, err)
}
originalContent := string(data)
stripped, removed := stripManagedGreeterPamBlock(originalContent)
strippedAgain, removedLegacy := stripLegacyGreeterPamLines(stripped)
if !removed && !removedLegacy {
return nil
}
if err := writeManagedPamFile(strippedAgain, deps.greetdPath, sudoPassword, deps); err != nil {
return fmt.Errorf("failed to write %s: %w", deps.greetdPath, err)
}
logFunc("✓ Removed DMS managed PAM block from " + deps.greetdPath)
return nil
}
func ParseManagedGreeterPamAuth(pamText string) (managed bool, fingerprint bool, u2f bool, legacy bool) {
if pamText == "" {
return false, false, false, false
}
lines := strings.Split(pamText, "\n")
inManaged := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
switch trimmed {
case GreeterPamManagedBlockStart:
managed = true
inManaged = true
continue
case GreeterPamManagedBlockEnd:
inManaged = false
continue
}
if strings.HasPrefix(trimmed, legacyGreeterPamFprintComment) || strings.HasPrefix(trimmed, legacyGreeterPamU2FComment) {
legacy = true
}
if !inManaged {
continue
}
if strings.Contains(trimmed, "pam_fprintd") {
fingerprint = true
}
if strings.Contains(trimmed, "pam_u2f") {
u2f = true
}
}
return managed, fingerprint, u2f, legacy
}
func StripManagedGreeterPamContent(pamText string) (string, bool) {
stripped, removed := stripManagedGreeterPamBlock(pamText)
stripped, removedLegacy := stripLegacyGreeterPamLines(stripped)
return stripped, removed || removedLegacy
}
func PamTextIncludesFile(pamText, filename string) bool {
lines := strings.Split(pamText, "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
if strings.Contains(trimmed, filename) &&
(strings.Contains(trimmed, "include") || strings.Contains(trimmed, "substack") || strings.HasPrefix(trimmed, "@include")) {
return true
}
}
return false
}
func PamFileHasModule(pamFilePath, module string) bool {
data, err := os.ReadFile(pamFilePath)
if err != nil {
return false
}
return pamContentHasModule(string(data), module)
}
func DetectIncludedPamModule(pamText, module string) string {
return detectIncludedPamModule(pamText, module, defaultSyncDeps())
}
func detectIncludedPamModule(pamText, module string, deps syncDeps) string {
for _, includedFile := range includedPamAuthFiles {
if !PamTextIncludesFile(pamText, includedFile) {
continue
}
path := filepath.Join(deps.pamDir, includedFile)
data, err := deps.readFile(path)
if err != nil {
continue
}
if pamContentHasModule(string(data), module) {
return includedFile
}
}
return ""
}
func pamContentHasModule(content, module string) bool {
lines := strings.Split(content, "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
if strings.Contains(trimmed, module) {
return true
}
}
return false
}
func hasManagedLockscreenPamFile(content string) bool {
return strings.Contains(content, LockscreenPamManagedBlockStart) &&
strings.Contains(content, LockscreenPamManagedBlockEnd)
}
func hasManagedLockscreenU2FPamFile(content string) bool {
return strings.Contains(content, LockscreenU2FPamManagedBlockStart) &&
strings.Contains(content, LockscreenU2FPamManagedBlockEnd)
}
func pamDirectiveType(line string) string {
fields := strings.Fields(line)
if len(fields) == 0 {
return ""
}
directiveType := strings.TrimPrefix(fields[0], "-")
switch directiveType {
case "auth", "account", "password", "session":
return directiveType
default:
return ""
}
}
func isExcludedLockscreenPamLine(line string) bool {
for _, field := range strings.Fields(line) {
if strings.HasPrefix(field, "#") {
break
}
if strings.Contains(field, "pam_u2f") || strings.Contains(field, "pam_fprintd") {
return true
}
}
return false
}
func parseLockscreenPamIncludeDirective(trimmed string, inheritedFilter string) (lockscreenPamIncludeDirective, bool) {
fields := strings.Fields(trimmed)
if len(fields) >= 2 && fields[0] == "@include" {
return lockscreenPamIncludeDirective{
target: fields[1],
filterType: inheritedFilter,
}, true
}
if len(fields) >= 3 && (fields[1] == "include" || fields[1] == "substack") {
lineType := pamDirectiveType(trimmed)
if lineType == "" {
return lockscreenPamIncludeDirective{}, false
}
return lockscreenPamIncludeDirective{
target: fields[2],
filterType: lineType,
}, true
}
if len(fields) >= 3 && fields[1] == "@include" {
lineType := pamDirectiveType(trimmed)
if lineType == "" {
return lockscreenPamIncludeDirective{}, false
}
return lockscreenPamIncludeDirective{
target: fields[2],
filterType: lineType,
}, true
}
return lockscreenPamIncludeDirective{}, false
}
func resolveLockscreenPamIncludePath(pamDir, target string) (string, error) {
if strings.TrimSpace(target) == "" {
return "", fmt.Errorf("empty PAM include target")
}
cleanPamDir := filepath.Clean(pamDir)
if filepath.IsAbs(target) {
cleanTarget := filepath.Clean(target)
if filepath.Dir(cleanTarget) != cleanPamDir {
return "", fmt.Errorf("unsupported PAM include outside %s: %s", cleanPamDir, target)
}
return cleanTarget, nil
}
cleanTarget := filepath.Clean(target)
if cleanTarget == "." || cleanTarget == ".." || strings.HasPrefix(cleanTarget, ".."+string(os.PathSeparator)) {
return "", fmt.Errorf("invalid PAM include target: %s", target)
}
return filepath.Join(cleanPamDir, cleanTarget), nil
}
func (r lockscreenPamResolver) resolveService(serviceName string, filterType string, stack []string) ([]string, error) {
path, err := resolveLockscreenPamIncludePath(r.pamDir, serviceName)
if err != nil {
return nil, err
}
for _, seen := range stack {
if seen == path {
chain := append(append([]string{}, stack...), path)
display := make([]string, 0, len(chain))
for _, item := range chain {
display = append(display, filepath.Base(item))
}
return nil, fmt.Errorf("cyclic PAM include detected: %s", strings.Join(display, " -> "))
}
}
data, err := r.readFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read PAM file %s: %w", path, err)
}
var resolved []string
for _, rawLine := range strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n") {
rawLine = strings.TrimRight(rawLine, "\r")
trimmed := strings.TrimSpace(rawLine)
if trimmed == "" || strings.HasPrefix(trimmed, "#") || trimmed == "#%PAM-1.0" {
continue
}
if include, ok := parseLockscreenPamIncludeDirective(trimmed, filterType); ok {
lineType := pamDirectiveType(trimmed)
if filterType != "" && lineType != "" && lineType != filterType {
continue
}
nested, err := r.resolveService(include.target, include.filterType, append(stack, path))
if err != nil {
return nil, err
}
resolved = append(resolved, nested...)
continue
}
lineType := pamDirectiveType(trimmed)
if lineType == "" {
return nil, fmt.Errorf("unsupported PAM directive in %s: %s", filepath.Base(path), trimmed)
}
if filterType != "" && lineType != filterType {
continue
}
if isExcludedLockscreenPamLine(trimmed) {
continue
}
resolved = append(resolved, rawLine)
}
return resolved, nil
}
func buildManagedLockscreenPamContent(pamDir string, readFile func(string) ([]byte, error)) (string, error) {
resolver := lockscreenPamResolver{
pamDir: pamDir,
readFile: readFile,
}
resolvedLines, err := resolver.resolveService("login", "", nil)
if err != nil {
return "", err
}
if len(resolvedLines) == 0 {
return "", fmt.Errorf("no auth directives remained after filtering %s", filepath.Join(pamDir, "login"))
}
hasAuth := false
for _, line := range resolvedLines {
if pamDirectiveType(strings.TrimSpace(line)) == "auth" {
hasAuth = true
break
}
}
if !hasAuth {
return "", fmt.Errorf("no auth directives remained after filtering %s", filepath.Join(pamDir, "login"))
}
var b strings.Builder
b.WriteString("#%PAM-1.0\n")
b.WriteString(LockscreenPamManagedBlockStart + "\n")
for _, line := range resolvedLines {
b.WriteString(line)
b.WriteByte('\n')
}
b.WriteString(LockscreenPamManagedBlockEnd + "\n")
return b.String(), nil
}
func buildManagedLockscreenU2FPamContent() string {
var b strings.Builder
b.WriteString("#%PAM-1.0\n")
b.WriteString(LockscreenU2FPamManagedBlockStart + "\n")
b.WriteString("auth required pam_u2f.so cue nouserok timeout=10\n")
b.WriteString(LockscreenU2FPamManagedBlockEnd + "\n")
return b.String()
}
func syncLockscreenPamConfigWithDeps(logFunc func(string), sudoPassword string, deps syncDeps) error {
if deps.isNixOS() {
logFunc(" NixOS detected. DMS continues to use /etc/pam.d/login for lock screen password auth on NixOS unless you declare security.pam.services.dankshell yourself. U2F and fingerprint are handled separately and should not be included in dankshell.")
return nil
}
existingData, err := deps.readFile(deps.dankshellPath)
if err == nil {
if !hasManagedLockscreenPamFile(string(existingData)) {
logFunc(" Custom /etc/pam.d/dankshell found (no DMS block). Skipping.")
return nil
}
} else if !os.IsNotExist(err) {
return fmt.Errorf("failed to read %s: %w", deps.dankshellPath, err)
}
content, err := buildManagedLockscreenPamContent(deps.pamDir, deps.readFile)
if err != nil {
return fmt.Errorf("failed to build %s from %s: %w", deps.dankshellPath, filepath.Join(deps.pamDir, "login"), err)
}
if err := writeManagedPamFile(content, deps.dankshellPath, sudoPassword, deps); err != nil {
return fmt.Errorf("failed to write %s: %w", deps.dankshellPath, err)
}
logFunc("✓ Created or updated /etc/pam.d/dankshell for lock screen authentication")
return nil
}
func syncLockscreenU2FPamConfigWithDeps(logFunc func(string), sudoPassword string, enabled bool, deps syncDeps) error {
if deps.isNixOS() {
logFunc(" NixOS detected. DMS does not manage /etc/pam.d/dankshell-u2f on NixOS. Keep using the bundled U2F helper or configure a custom PAM service yourself.")
return nil
}
existingData, err := deps.readFile(deps.dankshellU2fPath)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to read %s: %w", deps.dankshellU2fPath, err)
}
if enabled {
if err == nil && !hasManagedLockscreenU2FPamFile(string(existingData)) {
logFunc(" Custom /etc/pam.d/dankshell-u2f found (no DMS block). Skipping.")
return nil
}
if err := writeManagedPamFile(buildManagedLockscreenU2FPamContent(), deps.dankshellU2fPath, sudoPassword, deps); err != nil {
return fmt.Errorf("failed to write %s: %w", deps.dankshellU2fPath, err)
}
logFunc("✓ Created or updated /etc/pam.d/dankshell-u2f for lock screen security-key authentication")
return nil
}
if os.IsNotExist(err) {
return nil
}
if err == nil && !hasManagedLockscreenU2FPamFile(string(existingData)) {
logFunc(" Custom /etc/pam.d/dankshell-u2f found (no DMS block). Leaving it untouched.")
return nil
}
if err := deps.runSudoCmd(sudoPassword, "rm", "-f", deps.dankshellU2fPath); err != nil {
return fmt.Errorf("failed to remove %s: %w", deps.dankshellU2fPath, err)
}
logFunc("✓ Removed DMS-managed /etc/pam.d/dankshell-u2f")
return nil
}
func stripManagedGreeterPamBlock(content string) (string, bool) {
lines := strings.Split(content, "\n")
filtered := make([]string, 0, len(lines))
inManagedBlock := false
removed := false
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed == GreeterPamManagedBlockStart {
inManagedBlock = true
removed = true
continue
}
if trimmed == GreeterPamManagedBlockEnd {
inManagedBlock = false
removed = true
continue
}
if inManagedBlock {
removed = true
continue
}
filtered = append(filtered, line)
}
return strings.Join(filtered, "\n"), removed
}
func stripLegacyGreeterPamLines(content string) (string, bool) {
lines := strings.Split(content, "\n")
filtered := make([]string, 0, len(lines))
removed := false
for i := 0; i < len(lines); i++ {
trimmed := strings.TrimSpace(lines[i])
if strings.HasPrefix(trimmed, legacyGreeterPamFprintComment) || strings.HasPrefix(trimmed, legacyGreeterPamU2FComment) {
removed = true
if i+1 < len(lines) {
nextLine := strings.TrimSpace(lines[i+1])
if strings.HasPrefix(nextLine, "auth") &&
(strings.Contains(nextLine, "pam_fprintd") || strings.Contains(nextLine, "pam_u2f")) {
i++
}
}
continue
}
filtered = append(filtered, lines[i])
}
return strings.Join(filtered, "\n"), removed
}
func insertManagedGreeterPamBlock(content string, blockLines []string, greetdPamPath string) (string, error) {
lines := strings.Split(content, "\n")
for i, line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed != "" && !strings.HasPrefix(trimmed, "#") && strings.HasPrefix(trimmed, "auth") {
block := strings.Join(blockLines, "\n")
prefix := strings.Join(lines[:i], "\n")
suffix := strings.Join(lines[i:], "\n")
switch {
case prefix == "":
return block + "\n" + suffix, nil
case suffix == "":
return prefix + "\n" + block, nil
default:
return prefix + "\n" + block + "\n" + suffix, nil
}
}
}
return "", fmt.Errorf("no auth directive found in %s", greetdPamPath)
}
func syncGreeterPamConfigWithDeps(logFunc func(string), sudoPassword string, settings AuthSettings, forceAuth bool, deps syncDeps) error {
var wantFprint, wantU2f bool
fprintToggleEnabled := forceAuth
u2fToggleEnabled := forceAuth
if forceAuth {
wantFprint = deps.pamModuleExists("pam_fprintd.so")
wantU2f = deps.pamModuleExists("pam_u2f.so")
} else {
fprintToggleEnabled = settings.GreeterEnableFprint
u2fToggleEnabled = settings.GreeterEnableU2f
fprintModule := deps.pamModuleExists("pam_fprintd.so")
u2fModule := deps.pamModuleExists("pam_u2f.so")
wantFprint = settings.GreeterEnableFprint && fprintModule
wantU2f = settings.GreeterEnableU2f && u2fModule
if settings.GreeterEnableFprint && !fprintModule {
logFunc("⚠ Warning: greeter fingerprint toggle is enabled, but pam_fprintd.so was not found.")
}
if settings.GreeterEnableU2f && !u2fModule {
logFunc("⚠ Warning: greeter security key toggle is enabled, but pam_u2f.so was not found.")
}
}
if deps.isNixOS() {
logFunc(" NixOS detected: PAM config is managed by NixOS modules. Skipping DMS PAM block write.")
logFunc(" Configure fingerprint/U2F auth via your greetd NixOS module options (e.g. security.pam.services.greetd).")
return nil
}
pamData, err := deps.readFile(deps.greetdPath)
if err != nil {
return fmt.Errorf("failed to read %s: %w", deps.greetdPath, err)
}
originalContent := string(pamData)
content, _ := stripManagedGreeterPamBlock(originalContent)
content, _ = stripLegacyGreeterPamLines(content)
includedFprintFile := detectIncludedPamModule(content, "pam_fprintd.so", deps)
includedU2fFile := detectIncludedPamModule(content, "pam_u2f.so", deps)
fprintAvailableForCurrentUser := deps.fingerprintAvailableForCurrentUser()
if wantFprint && includedFprintFile != "" {
logFunc("⚠ pam_fprintd already present in included " + includedFprintFile + " (managed by authselect/pam-auth-update). Skipping DMS fprint block to avoid double-fingerprint auth.")
wantFprint = false
}
if wantU2f && includedU2fFile != "" {
logFunc("⚠ pam_u2f already present in included " + includedU2fFile + " (managed by authselect/pam-auth-update). Skipping DMS U2F block to avoid double security-key auth.")
wantU2f = false
}
if !wantFprint && includedFprintFile != "" {
if fprintToggleEnabled {
logFunc(" Fingerprint auth is still enabled via included " + includedFprintFile + ".")
if fprintAvailableForCurrentUser {
logFunc(" DMS toggle is enabled, and effective auth is provided by the included PAM stack.")
} else {
logFunc(" No enrolled fingerprints detected for the current user; password auth remains the effective path.")
}
} else {
if fprintAvailableForCurrentUser {
logFunc(" Fingerprint auth is active via included " + includedFprintFile + " while DMS fingerprint toggle is off.")
logFunc(" Password login will work but may be delayed while the fingerprint module runs first.")
logFunc(" To eliminate the delay, " + pamManagerHintForCurrentDistro())
} else {
logFunc(" pam_fprintd is present via included " + includedFprintFile + ", but no enrolled fingerprints were detected for the current user.")
logFunc(" Password auth remains the effective login path.")
}
}
}
if !wantU2f && includedU2fFile != "" {
if u2fToggleEnabled {
logFunc(" Security-key auth is still enabled via included " + includedU2fFile + ".")
logFunc(" DMS toggle is enabled, but effective auth is provided by the included PAM stack.")
} else {
logFunc("⚠ Security-key auth is active via included " + includedU2fFile + " while DMS security-key toggle is off.")
logFunc(" " + pamManagerHintForCurrentDistro())
}
}
if wantFprint || wantU2f {
blockLines := []string{GreeterPamManagedBlockStart}
if wantFprint {
blockLines = append(blockLines, "auth sufficient pam_fprintd.so max-tries=1 timeout=5")
}
if wantU2f {
blockLines = append(blockLines, "auth sufficient pam_u2f.so cue nouserok timeout=10")
}
blockLines = append(blockLines, GreeterPamManagedBlockEnd)
content, err = insertManagedGreeterPamBlock(content, blockLines, deps.greetdPath)
if err != nil {
return err
}
}
if content == originalContent {
return nil
}
if err := writeManagedPamFile(content, deps.greetdPath, sudoPassword, deps); err != nil {
return fmt.Errorf("failed to install updated PAM config at %s: %w", deps.greetdPath, err)
}
if wantFprint || wantU2f {
logFunc("✓ Configured greetd PAM for fingerprint/U2F")
} else {
logFunc("✓ Cleared DMS-managed greeter PAM auth block")
}
return nil
}
func writeManagedPamFile(content string, destPath string, sudoPassword string, deps syncDeps) error {
tmpFile, err := deps.createTemp("", "dms-pam-*.conf")
if err != nil {
return err
}
tmpPath := tmpFile.Name()
defer func() {
_ = deps.removeFile(tmpPath)
}()
if _, err := tmpFile.WriteString(content); err != nil {
tmpFile.Close()
return err
}
if err := tmpFile.Close(); err != nil {
return err
}
if err := deps.runSudoCmd(sudoPassword, "cp", tmpPath, destPath); err != nil {
return err
}
if err := deps.runSudoCmd(sudoPassword, "chmod", "644", destPath); err != nil {
return fmt.Errorf("failed to set permissions on %s: %w", destPath, err)
}
return nil
}
func pamManagerHintForCurrentDistro() string {
osInfo, err := distros.GetOSInfo()
if err != nil {
return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
}
config, exists := distros.Registry[osInfo.Distribution.ID]
if !exists {
return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
}
switch config.Family {
case distros.FamilyFedora:
return "Disable it in authselect to force password-only greeter login."
case distros.FamilyDebian, distros.FamilyUbuntu:
return "Disable it in pam-auth-update to force password-only greeter login."
default:
return "Disable it in your distro PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login."
}
}
func pamModuleExists(module string) bool {
for _, libDir := range []string{
"/usr/lib64/security",
"/usr/lib/security",
"/lib64/security",
"/lib/security",
"/lib/x86_64-linux-gnu/security",
"/usr/lib/x86_64-linux-gnu/security",
"/lib/aarch64-linux-gnu/security",
"/usr/lib/aarch64-linux-gnu/security",
"/run/current-system/sw/lib64/security",
"/run/current-system/sw/lib/security",
} {
if _, err := os.Stat(filepath.Join(libDir, module)); err == nil {
return true
}
}
return false
}
func hasEnrolledFingerprintOutput(output string) bool {
lower := strings.ToLower(output)
if strings.Contains(lower, "no fingers enrolled") ||
strings.Contains(lower, "no fingerprints enrolled") ||
strings.Contains(lower, "no prints enrolled") {
return false
}
if strings.Contains(lower, "has fingers enrolled") ||
strings.Contains(lower, "has fingerprints enrolled") {
return true
}
for _, line := range strings.Split(lower, "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "finger:") {
return true
}
if strings.HasPrefix(trimmed, "- ") && strings.Contains(trimmed, "finger") {
return true
}
}
return false
}
func FingerprintAuthAvailableForCurrentUser() bool {
username := strings.TrimSpace(os.Getenv("SUDO_USER"))
if username == "" {
username = strings.TrimSpace(os.Getenv("USER"))
}
if username == "" {
out, err := exec.Command("id", "-un").Output()
if err == nil {
username = strings.TrimSpace(string(out))
}
}
return fingerprintAuthAvailableForUser(username)
}
func fingerprintAuthAvailableForUser(username string) bool {
username = strings.TrimSpace(username)
if username == "" {
return false
}
if !pamModuleExists("pam_fprintd.so") {
return false
}
if _, err := exec.LookPath("fprintd-list"); err != nil {
return false
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
out, err := exec.CommandContext(ctx, "fprintd-list", username).CombinedOutput()
if err != nil {
return false
}
return hasEnrolledFingerprintOutput(string(out))
}
func runSudoCmd(sudoPassword string, command string, args ...string) error {
var cmd *exec.Cmd
if sudoPassword != "" {
fullArgs := append([]string{command}, args...)
quotedArgs := make([]string, len(fullArgs))
for i, arg := range fullArgs {
quotedArgs[i] = "'" + strings.ReplaceAll(arg, "'", "'\\''") + "'"
}
cmdStr := strings.Join(quotedArgs, " ")
cmd = distros.ExecSudoCommand(context.Background(), sudoPassword, cmdStr)
} else {
cmd = exec.Command("sudo", append([]string{command}, args...)...)
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
+671
View File
@@ -0,0 +1,671 @@
package pam
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
)
func writeTestFile(t *testing.T, path string, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("failed to create parent dir for %s: %v", path, err)
}
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatalf("failed to write %s: %v", path, err)
}
}
type pamTestEnv struct {
pamDir string
greetdPath string
dankshellPath string
dankshellU2fPath string
tmpDir string
homeDir string
availableModules map[string]bool
fingerprintAvailable bool
}
func newPamTestEnv(t *testing.T) *pamTestEnv {
t.Helper()
root := t.TempDir()
pamDir := filepath.Join(root, "pam.d")
tmpDir := filepath.Join(root, "tmp")
homeDir := filepath.Join(root, "home")
for _, dir := range []string{pamDir, tmpDir, homeDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("failed to create %s: %v", dir, err)
}
}
return &pamTestEnv{
pamDir: pamDir,
greetdPath: filepath.Join(pamDir, "greetd"),
dankshellPath: filepath.Join(pamDir, "dankshell"),
dankshellU2fPath: filepath.Join(pamDir, "dankshell-u2f"),
tmpDir: tmpDir,
homeDir: homeDir,
availableModules: map[string]bool{},
}
}
func (e *pamTestEnv) writePamFile(t *testing.T, name string, content string) {
t.Helper()
writeTestFile(t, filepath.Join(e.pamDir, name), content)
}
func (e *pamTestEnv) writeSettings(t *testing.T, content string) {
t.Helper()
writeTestFile(t, filepath.Join(e.homeDir, ".config", "DankMaterialShell", "settings.json"), content)
}
func (e *pamTestEnv) deps(isNixOS bool) syncDeps {
return syncDeps{
pamDir: e.pamDir,
greetdPath: e.greetdPath,
dankshellPath: e.dankshellPath,
dankshellU2fPath: e.dankshellU2fPath,
isNixOS: func() bool { return isNixOS },
readFile: os.ReadFile,
stat: os.Stat,
createTemp: func(_ string, pattern string) (*os.File, error) {
return os.CreateTemp(e.tmpDir, pattern)
},
removeFile: os.Remove,
runSudoCmd: func(_ string, command string, args ...string) error {
switch command {
case "cp":
if len(args) != 2 {
return fmt.Errorf("unexpected cp args: %v", args)
}
data, err := os.ReadFile(args[0])
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(args[1]), 0o755); err != nil {
return err
}
return os.WriteFile(args[1], data, 0o644)
case "chmod":
if len(args) != 2 {
return fmt.Errorf("unexpected chmod args: %v", args)
}
return nil
case "rm":
if len(args) != 2 || args[0] != "-f" {
return fmt.Errorf("unexpected rm args: %v", args)
}
if err := os.Remove(args[1]); err != nil && !os.IsNotExist(err) {
return err
}
return nil
default:
return fmt.Errorf("unexpected sudo command: %s %v", command, args)
}
},
pamModuleExists: func(module string) bool {
return e.availableModules[module]
},
fingerprintAvailableForCurrentUser: func() bool {
return e.fingerprintAvailable
},
}
}
func readFileString(t *testing.T, path string) string {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("failed to read %s: %v", path, err)
}
return string(data)
}
func TestHasManagedLockscreenPamFile(t *testing.T) {
t.Parallel()
tests := []struct {
name string
content string
want bool
}{
{
name: "both markers present",
content: "#%PAM-1.0\n" +
LockscreenPamManagedBlockStart + "\n" +
"auth sufficient pam_unix.so\n" +
LockscreenPamManagedBlockEnd + "\n",
want: true,
},
{
name: "missing end marker is not managed",
content: "#%PAM-1.0\n" +
LockscreenPamManagedBlockStart + "\n" +
"auth sufficient pam_unix.so\n",
want: false,
},
{
name: "custom file is not managed",
content: "#%PAM-1.0\nauth sufficient pam_unix.so\n",
want: false,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := hasManagedLockscreenPamFile(tt.content); got != tt.want {
t.Fatalf("hasManagedLockscreenPamFile() = %v, want %v", got, tt.want)
}
})
}
}
func TestBuildManagedLockscreenPamContent(t *testing.T) {
t.Parallel()
tests := []struct {
name string
files map[string]string
wantContains []string
wantNotContains []string
wantCounts map[string]int
wantErr string
}{
{
name: "preserves custom modules and strips direct u2f and fprint directives",
files: map[string]string{
"login": "#%PAM-1.0\n" +
"auth include system-auth\n" +
"account include system-auth\n" +
"session include system-auth\n",
"system-auth": "auth requisite pam_nologin.so\n" +
"auth sufficient pam_unix.so try_first_pass nullok\n" +
"auth sufficient pam_u2f.so cue\n" +
"auth sufficient pam_fprintd.so max-tries=1\n" +
"auth required pam_radius_auth.so conf=/etc/raddb/server\n" +
"account required pam_access.so\n" +
"session optional pam_lastlog.so silent\n",
},
wantContains: []string{
"#%PAM-1.0",
LockscreenPamManagedBlockStart,
LockscreenPamManagedBlockEnd,
"auth requisite pam_nologin.so",
"auth sufficient pam_unix.so try_first_pass nullok",
"auth required pam_radius_auth.so conf=/etc/raddb/server",
"account required pam_access.so",
"session optional pam_lastlog.so silent",
},
wantNotContains: []string{
"pam_u2f",
"pam_fprintd",
},
wantCounts: map[string]int{
"auth required pam_radius_auth.so conf=/etc/raddb/server": 1,
"account required pam_access.so": 1,
},
},
{
name: "resolves nested include substack and @include transitively",
files: map[string]string{
"login": "#%PAM-1.0\n" +
"auth include system-auth\n" +
"account include system-auth\n" +
"password include system-auth\n" +
"session include system-auth\n",
"system-auth": "auth substack custom-auth\n" +
"account include custom-auth\n" +
"password include custom-auth\n" +
"session @include common-session\n",
"custom-auth": "auth required pam_custom.so one=two\n" +
"account required pam_custom_account.so\n" +
"password required pam_custom_password.so\n",
"common-session": "session optional pam_fprintd.so max-tries=1\n" +
"session optional pam_lastlog.so silent\n",
},
wantContains: []string{
"auth required pam_custom.so one=two",
"account required pam_custom_account.so",
"password required pam_custom_password.so",
"session optional pam_lastlog.so silent",
},
wantNotContains: []string{
"pam_fprintd",
},
wantCounts: map[string]int{
"auth required pam_custom.so one=two": 1,
"account required pam_custom_account.so": 1,
"password required pam_custom_password.so": 1,
"session optional pam_lastlog.so silent": 1,
},
},
{
name: "missing include fails",
files: map[string]string{
"login": "#%PAM-1.0\nauth include missing-auth\n",
},
wantErr: "failed to read PAM file",
},
{
name: "cyclic include fails",
files: map[string]string{
"login": "#%PAM-1.0\nauth include system-auth\n",
"system-auth": "auth include login\n",
},
wantErr: "cyclic PAM include detected",
},
{
name: "no auth directives remain after filtering fails",
files: map[string]string{
"login": "#%PAM-1.0\nauth include system-auth\n",
"system-auth": "auth sufficient pam_u2f.so cue\n",
},
wantErr: "no auth directives remained after filtering",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
for name, content := range tt.files {
env.writePamFile(t, name, content)
}
content, err := buildManagedLockscreenPamContent(env.pamDir, os.ReadFile)
if tt.wantErr != "" {
if err == nil {
t.Fatalf("expected error containing %q, got nil", tt.wantErr)
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("error = %q, want substring %q", err.Error(), tt.wantErr)
}
return
}
if err != nil {
t.Fatalf("buildManagedLockscreenPamContent returned error: %v", err)
}
for _, want := range tt.wantContains {
if !strings.Contains(content, want) {
t.Errorf("missing expected string %q in output:\n%s", want, content)
}
}
for _, notWant := range tt.wantNotContains {
if strings.Contains(content, notWant) {
t.Errorf("unexpected string %q found in output:\n%s", notWant, content)
}
}
for want, wantCount := range tt.wantCounts {
if gotCount := strings.Count(content, want); gotCount != wantCount {
t.Errorf("count for %q = %d, want %d\noutput:\n%s", want, gotCount, wantCount, content)
}
}
})
}
}
func TestSyncLockscreenPamConfigWithDeps(t *testing.T) {
t.Parallel()
t.Run("custom dankshell file is skipped untouched", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
customContent := "#%PAM-1.0\nauth required pam_unix.so\n"
env.writePamFile(t, "dankshell", customContent)
var logs []string
err := syncLockscreenPamConfigWithDeps(func(msg string) {
logs = append(logs, msg)
}, "", env.deps(false))
if err != nil {
t.Fatalf("syncLockscreenPamConfigWithDeps returned error: %v", err)
}
if got := readFileString(t, env.dankshellPath); got != customContent {
t.Fatalf("custom dankshell content changed\ngot:\n%s\nwant:\n%s", got, customContent)
}
if len(logs) == 0 || !strings.Contains(logs[0], "Custom /etc/pam.d/dankshell found") {
t.Fatalf("expected custom-file skip log, got %v", logs)
}
})
t.Run("managed dankshell file is rewritten from resolved login stack", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\nauth sufficient pam_u2f.so cue\naccount required pam_access.so\n")
env.writePamFile(t, "dankshell", "#%PAM-1.0\n"+LockscreenPamManagedBlockStart+"\nauth required pam_env.so\n"+LockscreenPamManagedBlockEnd+"\n")
var logs []string
err := syncLockscreenPamConfigWithDeps(func(msg string) {
logs = append(logs, msg)
}, "", env.deps(false))
if err != nil {
t.Fatalf("syncLockscreenPamConfigWithDeps returned error: %v", err)
}
output := readFileString(t, env.dankshellPath)
for _, want := range []string{
LockscreenPamManagedBlockStart,
"auth sufficient pam_unix.so try_first_pass nullok",
"account required pam_access.so",
LockscreenPamManagedBlockEnd,
} {
if !strings.Contains(output, want) {
t.Errorf("missing expected string %q in rewritten dankshell:\n%s", want, output)
}
}
if strings.Contains(output, "pam_u2f") {
t.Errorf("rewritten dankshell still contains pam_u2f:\n%s", output)
}
if len(logs) == 0 || !strings.Contains(logs[len(logs)-1], "Created or updated /etc/pam.d/dankshell") {
t.Fatalf("expected success log, got %v", logs)
}
})
t.Run("mutable systems fail when login stack cannot be converted safely", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
err := syncLockscreenPamConfigWithDeps(func(string) {}, "", env.deps(false))
if err == nil {
t.Fatal("expected error when login PAM file is missing, got nil")
}
if !strings.Contains(err.Error(), "failed to build") {
t.Fatalf("error = %q, want substring %q", err.Error(), "failed to build")
}
})
t.Run("NixOS remains informational and does not write dankshell", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
var logs []string
err := syncLockscreenPamConfigWithDeps(func(msg string) {
logs = append(logs, msg)
}, "", env.deps(true))
if err != nil {
t.Fatalf("syncLockscreenPamConfigWithDeps returned error on NixOS path: %v", err)
}
if len(logs) == 0 || !strings.Contains(logs[0], "NixOS detected") || !strings.Contains(logs[0], "/etc/pam.d/login") {
t.Fatalf("expected NixOS informational log mentioning /etc/pam.d/login, got %v", logs)
}
if _, err := os.Stat(env.dankshellPath); !os.IsNotExist(err) {
t.Fatalf("expected no dankshell file to be written on NixOS path, stat err = %v", err)
}
})
}
func TestSyncLockscreenU2FPamConfigWithDeps(t *testing.T) {
t.Parallel()
t.Run("enabled creates managed file", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
var logs []string
err := syncLockscreenU2FPamConfigWithDeps(func(msg string) {
logs = append(logs, msg)
}, "", true, env.deps(false))
if err != nil {
t.Fatalf("syncLockscreenU2FPamConfigWithDeps returned error: %v", err)
}
got := readFileString(t, env.dankshellU2fPath)
if got != buildManagedLockscreenU2FPamContent() {
t.Fatalf("unexpected managed dankshell-u2f content:\n%s", got)
}
if len(logs) == 0 || !strings.Contains(logs[len(logs)-1], "Created or updated /etc/pam.d/dankshell-u2f") {
t.Fatalf("expected create log, got %v", logs)
}
})
t.Run("enabled rewrites existing managed file", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.writePamFile(t, "dankshell-u2f", "#%PAM-1.0\n"+LockscreenU2FPamManagedBlockStart+"\nauth required pam_u2f.so old\n"+LockscreenU2FPamManagedBlockEnd+"\n")
if err := syncLockscreenU2FPamConfigWithDeps(func(string) {}, "", true, env.deps(false)); err != nil {
t.Fatalf("syncLockscreenU2FPamConfigWithDeps returned error: %v", err)
}
if got := readFileString(t, env.dankshellU2fPath); got != buildManagedLockscreenU2FPamContent() {
t.Fatalf("managed dankshell-u2f was not rewritten:\n%s", got)
}
})
t.Run("disabled removes DMS-managed file", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.writePamFile(t, "dankshell-u2f", buildManagedLockscreenU2FPamContent())
var logs []string
err := syncLockscreenU2FPamConfigWithDeps(func(msg string) {
logs = append(logs, msg)
}, "", false, env.deps(false))
if err != nil {
t.Fatalf("syncLockscreenU2FPamConfigWithDeps returned error: %v", err)
}
if _, err := os.Stat(env.dankshellU2fPath); !os.IsNotExist(err) {
t.Fatalf("expected managed dankshell-u2f to be removed, stat err = %v", err)
}
if len(logs) == 0 || !strings.Contains(logs[len(logs)-1], "Removed DMS-managed /etc/pam.d/dankshell-u2f") {
t.Fatalf("expected removal log, got %v", logs)
}
})
t.Run("disabled preserves custom file", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
customContent := "#%PAM-1.0\nauth required pam_u2f.so cue\n"
env.writePamFile(t, "dankshell-u2f", customContent)
var logs []string
err := syncLockscreenU2FPamConfigWithDeps(func(msg string) {
logs = append(logs, msg)
}, "", false, env.deps(false))
if err != nil {
t.Fatalf("syncLockscreenU2FPamConfigWithDeps returned error: %v", err)
}
if got := readFileString(t, env.dankshellU2fPath); got != customContent {
t.Fatalf("custom dankshell-u2f content changed\ngot:\n%s\nwant:\n%s", got, customContent)
}
if len(logs) == 0 || !strings.Contains(logs[0], "Custom /etc/pam.d/dankshell-u2f found") {
t.Fatalf("expected custom-file log, got %v", logs)
}
})
}
func TestSyncGreeterPamConfigWithDeps(t *testing.T) {
t.Parallel()
t.Run("adds managed block for enabled auth modules", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_fprintd.so"] = true
env.availableModules["pam_u2f.so"] = true
env.writePamFile(t, "greetd", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so\naccount required pam_unix.so\n")
settings := AuthSettings{GreeterEnableFprint: true, GreeterEnableU2f: true}
if err := syncGreeterPamConfigWithDeps(func(string) {}, "", settings, false, env.deps(false)); err != nil {
t.Fatalf("syncGreeterPamConfigWithDeps returned error: %v", err)
}
got := readFileString(t, env.greetdPath)
for _, want := range []string{
GreeterPamManagedBlockStart,
"auth sufficient pam_fprintd.so max-tries=1 timeout=5",
"auth sufficient pam_u2f.so cue nouserok timeout=10",
GreeterPamManagedBlockEnd,
} {
if !strings.Contains(got, want) {
t.Errorf("missing expected string %q in greetd PAM:\n%s", want, got)
}
}
if strings.Index(got, GreeterPamManagedBlockStart) > strings.Index(got, "auth include system-auth") {
t.Fatalf("managed block was not inserted before first auth line:\n%s", got)
}
})
t.Run("avoids duplicate fingerprint when included stack already provides it", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_fprintd.so"] = true
env.fingerprintAvailable = true
original := "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n"
env.writePamFile(t, "greetd", original)
env.writePamFile(t, "system-auth", "auth sufficient pam_fprintd.so max-tries=1\nauth sufficient pam_unix.so\n")
settings := AuthSettings{GreeterEnableFprint: true}
if err := syncGreeterPamConfigWithDeps(func(string) {}, "", settings, false, env.deps(false)); err != nil {
t.Fatalf("syncGreeterPamConfigWithDeps returned error: %v", err)
}
got := readFileString(t, env.greetdPath)
if got != original {
t.Fatalf("greetd PAM changed despite included pam_fprintd stack\ngot:\n%s\nwant:\n%s", got, original)
}
if strings.Contains(got, GreeterPamManagedBlockStart) {
t.Fatalf("managed block should not be inserted when included stack already has pam_fprintd:\n%s", got)
}
})
}
func TestRemoveManagedGreeterPamBlockWithDeps(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.writePamFile(t, "greetd", "#%PAM-1.0\n"+
legacyGreeterPamFprintComment+"\n"+
"auth sufficient pam_fprintd.so max-tries=1\n"+
GreeterPamManagedBlockStart+"\n"+
"auth sufficient pam_u2f.so cue nouserok timeout=10\n"+
GreeterPamManagedBlockEnd+"\n"+
"auth include system-auth\n")
if err := removeManagedGreeterPamBlockWithDeps(func(string) {}, "", env.deps(false)); err != nil {
t.Fatalf("removeManagedGreeterPamBlockWithDeps returned error: %v", err)
}
got := readFileString(t, env.greetdPath)
if strings.Contains(got, GreeterPamManagedBlockStart) || strings.Contains(got, legacyGreeterPamFprintComment) {
t.Fatalf("managed or legacy DMS auth lines remained in greetd PAM:\n%s", got)
}
if !strings.Contains(got, "auth include system-auth") {
t.Fatalf("expected non-DMS greetd auth lines to remain:\n%s", got)
}
}
func TestSyncAuthConfigWithDeps(t *testing.T) {
t.Parallel()
t.Run("creates lockscreen targets and skips greetd when greeter is not installed", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.writeSettings(t, `{"enableU2f":true}`)
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\naccount required pam_access.so\n")
var logs []string
err := syncAuthConfigWithDeps(func(msg string) {
logs = append(logs, msg)
}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
if err != nil {
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
}
if _, err := os.Stat(env.dankshellPath); err != nil {
t.Fatalf("expected dankshell to be created: %v", err)
}
if got := readFileString(t, env.dankshellU2fPath); got != buildManagedLockscreenU2FPamContent() {
t.Fatalf("unexpected dankshell-u2f content:\n%s", got)
}
if len(logs) == 0 || !strings.Contains(logs[len(logs)-1], "greetd not found") {
t.Fatalf("expected greetd skip log, got %v", logs)
}
})
t.Run("separate greeter and lockscreen toggles are respected", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_fprintd.so"] = true
env.writeSettings(t, `{"enableU2f":false,"greeterEnableFprint":true,"greeterEnableU2f":false}`)
env.writePamFile(t, "login", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
env.writePamFile(t, "system-auth", "auth sufficient pam_unix.so try_first_pass nullok\naccount required pam_access.so\n")
env.writePamFile(t, "greetd", "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n")
err := syncAuthConfigWithDeps(func(string) {}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(false))
if err != nil {
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
}
dankshell := readFileString(t, env.dankshellPath)
if strings.Contains(dankshell, "pam_fprintd") || strings.Contains(dankshell, "pam_u2f") {
t.Fatalf("lockscreen PAM should strip fingerprint and U2F modules:\n%s", dankshell)
}
if _, err := os.Stat(env.dankshellU2fPath); !os.IsNotExist(err) {
t.Fatalf("expected dankshell-u2f to remain absent when enableU2f is false, stat err = %v", err)
}
greetd := readFileString(t, env.greetdPath)
if !strings.Contains(greetd, "auth sufficient pam_fprintd.so max-tries=1 timeout=5") {
t.Fatalf("expected greetd PAM to receive fingerprint auth block:\n%s", greetd)
}
if strings.Contains(greetd, "auth sufficient pam_u2f.so cue nouserok timeout=10") {
t.Fatalf("did not expect greetd PAM to receive U2F auth block:\n%s", greetd)
}
})
t.Run("NixOS remains informational and non-mutating", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_fprintd.so"] = true
env.availableModules["pam_u2f.so"] = true
env.writeSettings(t, `{"enableU2f":true,"greeterEnableFprint":true,"greeterEnableU2f":true}`)
originalGreetd := "#%PAM-1.0\nauth include system-auth\naccount include system-auth\n"
env.writePamFile(t, "greetd", originalGreetd)
var logs []string
err := syncAuthConfigWithDeps(func(msg string) {
logs = append(logs, msg)
}, "", SyncAuthOptions{HomeDir: env.homeDir}, env.deps(true))
if err != nil {
t.Fatalf("syncAuthConfigWithDeps returned error: %v", err)
}
if _, err := os.Stat(env.dankshellPath); !os.IsNotExist(err) {
t.Fatalf("expected dankshell to remain absent on NixOS path, stat err = %v", err)
}
if _, err := os.Stat(env.dankshellU2fPath); !os.IsNotExist(err) {
t.Fatalf("expected dankshell-u2f to remain absent on NixOS path, stat err = %v", err)
}
if got := readFileString(t, env.greetdPath); got != originalGreetd {
t.Fatalf("expected greetd PAM to remain unchanged on NixOS path\ngot:\n%s\nwant:\n%s", got, originalGreetd)
}
if len(logs) < 2 || !strings.Contains(strings.Join(logs, "\n"), "NixOS detected") {
t.Fatalf("expected informational NixOS logs, got %v", logs)
}
})
}
+5 -1
View File
@@ -113,7 +113,11 @@ func NewRegionSelector(s *Screenshoter) *RegionSelector {
} }
func (r *RegionSelector) Run() (*CaptureResult, bool, error) { func (r *RegionSelector) Run() (*CaptureResult, bool, error) {
r.preSelect = GetLastRegion() if r.screenshoter != nil && r.screenshoter.config.Reset {
r.preSelect = Region{}
} else {
r.preSelect = GetLastRegion()
}
if err := r.connect(); err != nil { if err := r.connect(); err != nil {
return nil, false, fmt.Errorf("wayland connect: %w", err) return nil, false, fmt.Errorf("wayland connect: %w", err)
+3
View File
@@ -114,6 +114,9 @@ func (r *RegionSelector) setupPointerHandlers() {
for _, os := range r.surfaces { for _, os := range r.surfaces {
r.redrawSurface(os) r.redrawSurface(os)
} }
if r.screenshoter != nil && r.screenshoter.config.NoConfirm && r.selection.hasSelection {
r.finishSelection()
}
} }
default: default:
r.cancelled = true r.cancelled = true
+5 -1
View File
@@ -138,9 +138,13 @@ func (r *RegionSelector) drawHUD(data []byte, stride, bufW, bufH int, format uin
if !r.showCapturedCursor { if !r.showCapturedCursor {
cursorLabel = "show" cursorLabel = "show"
} }
captureKey := "Space/Enter"
if r.screenshoter != nil && r.screenshoter.config.NoConfirm {
captureKey = "Drag+Release"
}
items := []struct{ key, desc string }{ items := []struct{ key, desc string }{
{"Space/Enter", "capture"}, {captureKey, "capture"},
{"P", cursorLabel + " cursor"}, {"P", cursorLabel + " cursor"},
{"Esc", "cancel"}, {"Esc", "cancel"},
} }
+6
View File
@@ -106,6 +106,12 @@ func (s *Screenshoter) captureLastRegion() (*CaptureResult, error) {
} }
func (s *Screenshoter) captureRegion() (*CaptureResult, error) { func (s *Screenshoter) captureRegion() (*CaptureResult, error) {
if s.config.Reset {
if err := SaveLastRegion(Region{}); err != nil {
log.Debug("failed to reset last region", "err", err)
}
}
selector := NewRegionSelector(s) selector := NewRegionSelector(s)
result, cancelled, err := selector.Run() result, cancelled, err := selector.Run()
if err != nil { if err != nil {
+4
View File
@@ -52,6 +52,8 @@ type Config struct {
Mode Mode Mode Mode
OutputName string OutputName string
Cursor CursorMode Cursor CursorMode
NoConfirm bool
Reset bool
Format Format Format Format
Quality int Quality int
OutputDir string OutputDir string
@@ -66,6 +68,8 @@ func DefaultConfig() Config {
return Config{ return Config{
Mode: ModeRegion, Mode: ModeRegion,
Cursor: CursorOff, Cursor: CursorOff,
NoConfirm: false,
Reset: false,
Format: FormatPNG, Format: FormatPNG,
Quality: 90, Quality: 90,
OutputDir: "", OutputDir: "",
+1 -1
View File
@@ -13,7 +13,7 @@ func NewManager(display wlclient.WaylandDisplay) (*Manager, error) {
m := &Manager{ m := &Manager{
display: display, display: display,
ctx: display.Context(), ctx: display.Context(),
cmdq: make(chan cmd, 128), cmdq: make(chan cmd, 512),
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
dirty: make(chan struct{}, 1), dirty: make(chan struct{}, 1),
fatalError: make(chan error, 1), fatalError: make(chan error, 1),
+7
View File
@@ -139,6 +139,13 @@ in
''; '';
} }
]; ];
# DMS currently relies on /etc/pam.d/login for lock screen password auth on NixOS.
# Declare security.pam.services.dankshell only if you want to override that runtime fallback.
# U2F and fingerprint are handled separately by DMS — do not add pam_u2f or pam_fprintd here.
# security.pam.services.dankshell = {
# # Example: add faillock
# faillock.enable = true;
# };
services.greetd = { services.greetd = {
enable = lib.mkDefault true; enable = lib.mkDefault true;
settings.default_session.command = lib.mkDefault (lib.getExe greeterScript); settings.default_session.command = lib.mkDefault (lib.getExe greeterScript);
+17 -9
View File
@@ -3,8 +3,10 @@
# Usage: ./create-source.sh <package-dir> [ubuntu-series] # Usage: ./create-source.sh <package-dir> [ubuntu-series]
# #
# Example: # Example:
# ./create-source.sh ../dms questing # ./create-source.sh ../dms questing # Ubuntu 25.10 (default series in ppa-upload)
# ./create-source.sh ../dms resolute # Ubuntu 26.04 LTS
# ./create-source.sh ../dms-git questing # ./create-source.sh ../dms-git questing
# ./create-source.sh ../dms-git resolute
set -e set -e
@@ -25,11 +27,13 @@ if [ $# -lt 1 ]; then
echo "Arguments:" echo "Arguments:"
echo " package-dir : Path to package directory (e.g., ../dms)" echo " package-dir : Path to package directory (e.g., ../dms)"
echo " ubuntu-series : Ubuntu series (optional, default: noble)" echo " ubuntu-series : Ubuntu series (optional, default: noble)"
echo " Options: noble, jammy, oracular, mantic" echo " Options: noble, jammy, oracular, mantic, questing, resolute"
echo echo
echo "Examples:" echo "Examples:"
echo " $0 ../dms questing" echo " $0 ../dms questing"
echo " $0 ../dms resolute"
echo " $0 ../dms-git questing" echo " $0 ../dms-git questing"
echo " $0 ../dms-git resolute"
exit 1 exit 1
fi fi
@@ -129,10 +133,14 @@ check_ppa_version_exists() {
local SOURCE_NAME="$2" local SOURCE_NAME="$2"
local VERSION="$3" local VERSION="$3"
local CHECK_MODE="${4:-commit}" local CHECK_MODE="${4:-commit}"
local DISTRO_SERIES="${5:-}"
# Query Launchpad API # Query Launchpad API (optionally scoped to one Ubuntu series so the same version can ship to questing and resolute)
PPA_VERSION=$(curl -s \ local API_URL="https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/$PPA_NAME?ws.op=getPublishedSources&source_name=$SOURCE_NAME&status=Published"
"https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/$PPA_NAME?ws.op=getPublishedSources&source_name=$SOURCE_NAME&status=Published" \ if [[ -n "$DISTRO_SERIES" ]]; then
API_URL+="&distro_series=https://api.launchpad.net/1.0/ubuntu/${DISTRO_SERIES}"
fi
PPA_VERSION=$(curl -s "$API_URL" \
| grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "") | grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
if [[ -n "$PPA_VERSION" ]]; then if [[ -n "$PPA_VERSION" ]]; then
@@ -259,14 +267,14 @@ if [ "$IS_GIT_PACKAGE" = false ] && [ -n "$GIT_REPO" ]; then
if [[ -n "$PPA_NAME" ]]; then if [[ -n "$PPA_NAME" ]]; then
info "Checking if version $NEW_VERSION already exists in PPA..." info "Checking if version $NEW_VERSION already exists in PPA..."
if [[ -z "${REBUILD_RELEASE:-}" ]]; then if [[ -z "${REBUILD_RELEASE:-}" ]]; then
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "${BASE_VERSION}ppa1" "exact"; then if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "${BASE_VERSION}ppa1" "exact" "$UBUNTU_SERIES"; then
error "==> Error: Version ${BASE_VERSION}ppa1 already exists in PPA $PPA_NAME" error "==> Error: Version ${BASE_VERSION}ppa1 already exists in PPA $PPA_NAME"
error " To rebuild with a different release number, use:" error " To rebuild with a different release number, use:"
error " ./distro/scripts/ppa-upload.sh $PACKAGE_NAME 2" error " ./distro/scripts/ppa-upload.sh $PACKAGE_NAME 2"
exit 1 exit 1
fi fi
else else
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "$NEW_VERSION" "exact"; then if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "$NEW_VERSION" "exact" "$UBUNTU_SERIES"; then
error "==> Error: Version $NEW_VERSION already exists in PPA $PPA_NAME" error "==> Error: Version $NEW_VERSION already exists in PPA $PPA_NAME"
NEXT_NUM=$((REBUILD_RELEASE + 1)) NEXT_NUM=$((REBUILD_RELEASE + 1))
error " To rebuild with a different release number, use:" error " To rebuild with a different release number, use:"
@@ -410,7 +418,7 @@ if [ "$IS_GIT_PACKAGE" = true ] && [ -n "$GIT_REPO" ]; then
if [[ -n "$PPA_NAME" ]]; then if [[ -n "$PPA_NAME" ]]; then
if [[ -z "${REBUILD_RELEASE:-}" ]]; then if [[ -z "${REBUILD_RELEASE:-}" ]]; then
info "Checking if commit $GIT_COMMIT_HASH already exists in PPA..." info "Checking if commit $GIT_COMMIT_HASH already exists in PPA..."
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "${BASE_VERSION}ppa1" "commit"; then if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "${BASE_VERSION}ppa1" "commit" "$UBUNTU_SERIES"; then
error "==> Error: This commit is already uploaded to PPA" error "==> Error: This commit is already uploaded to PPA"
error " The same git commit ($GIT_COMMIT_HASH) already exists in PPA." error " The same git commit ($GIT_COMMIT_HASH) already exists in PPA."
error " To rebuild the same commit, specify a rebuild number:" error " To rebuild the same commit, specify a rebuild number:"
@@ -429,7 +437,7 @@ if [ "$IS_GIT_PACKAGE" = true ] && [ -n "$GIT_REPO" ]; then
PPA_NUM=$REBUILD_RELEASE PPA_NUM=$REBUILD_RELEASE
NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}" NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}"
info "Checking if version $NEW_VERSION already exists in PPA..." info "Checking if version $NEW_VERSION already exists in PPA..."
if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "$NEW_VERSION" "exact"; then if check_ppa_version_exists "$PPA_NAME" "$SOURCE_NAME" "$NEW_VERSION" "exact" "$UBUNTU_SERIES"; then
error "==> Error: Version $NEW_VERSION already exists in PPA" error "==> Error: Version $NEW_VERSION already exists in PPA"
error " This exact version (including ppa${PPA_NUM}) is already uploaded." error " This exact version (including ppa${PPA_NUM}) is already uploaded."
NEXT_NUM=$((PPA_NUM + 1)) NEXT_NUM=$((PPA_NUM + 1))
+5 -3
View File
@@ -10,7 +10,8 @@
PPA_OWNER="avengemedia" PPA_OWNER="avengemedia"
LAUNCHPAD_API="https://api.launchpad.net/1.0" LAUNCHPAD_API="https://api.launchpad.net/1.0"
DISTRO_SERIES="questing" # Supported Ubuntu series for PPA builds (25.10 questing + 26.04 LTS resolute)
DISTRO_SERIES_LIST=(questing resolute)
# Define packages (sync with ppa-upload.sh) # Define packages (sync with ppa-upload.sh)
ALL_PACKAGES=(dms dms-git dms-greeter) ALL_PACKAGES=(dms dms-git dms-greeter)
@@ -106,10 +107,10 @@ get_status_display() {
for PPA_NAME in "${PPAS[@]}"; do for PPA_NAME in "${PPAS[@]}"; do
PPA_ARCHIVE="${LAUNCHPAD_API}/~${PPA_OWNER}/+archive/ubuntu/${PPA_NAME}" PPA_ARCHIVE="${LAUNCHPAD_API}/~${PPA_OWNER}/+archive/ubuntu/${PPA_NAME}"
for DISTRO_SERIES in "${DISTRO_SERIES_LIST[@]}"; do
echo "==========================================" echo "=========================================="
echo "=== PPA: ${PPA_OWNER}/${PPA_NAME} ===" echo "=== PPA: ${PPA_OWNER}/${PPA_NAME} (Ubuntu ${DISTRO_SERIES}) ==="
echo "==========================================" echo "=========================================="
echo "Distribution: Ubuntu $DISTRO_SERIES"
echo "" echo ""
for pkg in "${PACKAGES[@]}"; do for pkg in "${PACKAGES[@]}"; do
@@ -210,6 +211,7 @@ for PPA_NAME in "${PPAS[@]}"; do
echo "View full PPA at: https://launchpad.net/~${PPA_OWNER}/+archive/ubuntu/${PPA_NAME}" echo "View full PPA at: https://launchpad.net/~${PPA_OWNER}/+archive/ubuntu/${PPA_NAME}"
echo "" echo ""
done
done done
echo "==========================================" echo "=========================================="
+78 -10
View File
@@ -3,13 +3,15 @@
# Usage: ./ppa-upload.sh [package-name] [ppa-name] [ubuntu-series] [rebuild-number] [--keep-builds] [--rebuild=N] # Usage: ./ppa-upload.sh [package-name] [ppa-name] [ubuntu-series] [rebuild-number] [--keep-builds] [--rebuild=N]
# #
# Examples: # Examples:
# ./ppa-upload.sh dms # Single package (auto-detects PPA) # ./ppa-upload.sh dms # Upload to questing + resolute (default)
# ./ppa-upload.sh dms 2 # Rebuild with ppa2 (simple syntax) # ./ppa-upload.sh dms 2 # Native: questing ppa2, resolute ppa3 (auto +1 on second series)
# ./ppa-upload.sh dms --rebuild=2 # Rebuild with ppa2 (flag syntax) # ./ppa-upload.sh dms --rebuild=2 # Rebuild with ppa2 (flag syntax)
# ./ppa-upload.sh dms-git # Single package # ./ppa-upload.sh dms-git # Single package (both series)
# ./ppa-upload.sh all # All packages # ./ppa-upload.sh all # All packages (each to both series)
# ./ppa-upload.sh dms dms questing # Explicit PPA and series # ./ppa-upload.sh dms resolute # 26.04 LTS only (same as "dms dms resolute")
# ./ppa-upload.sh dms dms questing 2 # Explicit PPA, series, and rebuild number # ./ppa-upload.sh dms questing # 25.10 only
# ./ppa-upload.sh dms dms resolute # Explicit PPA name + one series (optional form)
# ./ppa-upload.sh dms dms resolute 2 # One series + rebuild number
# ./ppa-upload.sh distro/ubuntu/dms dms # Path-style (backward compatible) # ./ppa-upload.sh distro/ubuntu/dms dms # Path-style (backward compatible)
set -e set -e
@@ -52,7 +54,7 @@ done
PACKAGE_INPUT="${POSITIONAL_ARGS[0]:-}" PACKAGE_INPUT="${POSITIONAL_ARGS[0]:-}"
PPA_NAME_INPUT="${POSITIONAL_ARGS[1]:-}" PPA_NAME_INPUT="${POSITIONAL_ARGS[1]:-}"
UBUNTU_SERIES="${POSITIONAL_ARGS[2]:-questing}" UBUNTU_SERIES_RAW="${POSITIONAL_ARGS[2]:-}"
if [[ ${#POSITIONAL_ARGS[@]} -gt 0 ]]; then if [[ ${#POSITIONAL_ARGS[@]} -gt 0 ]]; then
LAST_INDEX=$((${#POSITIONAL_ARGS[@]} - 1)) LAST_INDEX=$((${#POSITIONAL_ARGS[@]} - 1))
@@ -64,10 +66,27 @@ if [[ ${#POSITIONAL_ARGS[@]} -gt 0 ]]; then
POSITIONAL_ARGS=("${POSITIONAL_ARGS[@]:0:$LAST_INDEX}") POSITIONAL_ARGS=("${POSITIONAL_ARGS[@]:0:$LAST_INDEX}")
PACKAGE_INPUT="${POSITIONAL_ARGS[0]:-}" PACKAGE_INPUT="${POSITIONAL_ARGS[0]:-}"
PPA_NAME_INPUT="${POSITIONAL_ARGS[1]:-}" PPA_NAME_INPUT="${POSITIONAL_ARGS[1]:-}"
UBUNTU_SERIES="${POSITIONAL_ARGS[2]:-questing}" UBUNTU_SERIES_RAW="${POSITIONAL_ARGS[2]:-}"
fi fi
fi fi
# Shorthand: "dms resolute" / "dms questing" (package + series; PPA inferred — no need for "dms dms resolute")
if [[ ${#POSITIONAL_ARGS[@]} -eq 2 ]] && [[ "${POSITIONAL_ARGS[1]}" == "questing" || "${POSITIONAL_ARGS[1]}" == "resolute" ]]; then
PACKAGE_INPUT="${POSITIONAL_ARGS[0]}"
PPA_NAME_INPUT=""
UBUNTU_SERIES_RAW="${POSITIONAL_ARGS[1]}"
fi
SERIES_LIST=()
if [[ -z "$UBUNTU_SERIES_RAW" ]]; then
SERIES_LIST=(questing resolute)
elif [[ "$UBUNTU_SERIES_RAW" == "questing" || "$UBUNTU_SERIES_RAW" == "resolute" ]]; then
SERIES_LIST=("$UBUNTU_SERIES_RAW")
else
error "Invalid Ubuntu series: $UBUNTU_SERIES_RAW (use questing, resolute, or omit for both)"
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
BUILD_SCRIPT="$SCRIPT_DIR/ppa-build.sh" BUILD_SCRIPT="$SCRIPT_DIR/ppa-build.sh"
@@ -119,7 +138,12 @@ elif [[ -n "$PACKAGE_INPUT" ]] && [[ "$PACKAGE_INPUT" == "all" ]]; then
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
info "Processing $pkg..." info "Processing $pkg..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
BUILD_ARGS=("$pkg" "$PPA_NAME_INPUT" "$UBUNTU_SERIES") BUILD_ARGS=("$pkg")
[[ -n "$PPA_NAME_INPUT" ]] && BUILD_ARGS+=("$PPA_NAME_INPUT")
if [[ ${#SERIES_LIST[@]} -eq 1 ]]; then
BUILD_ARGS+=("${SERIES_LIST[0]}")
fi
[[ -n "$REBUILD_RELEASE" ]] && BUILD_ARGS+=("$REBUILD_RELEASE")
[[ "$KEEP_BUILDS" == "true" ]] && BUILD_ARGS+=("--keep-builds") [[ "$KEEP_BUILDS" == "true" ]] && BUILD_ARGS+=("--keep-builds")
if ! "$0" "${BUILD_ARGS[@]}"; then if ! "$0" "${BUILD_ARGS[@]}"; then
FAILED_PACKAGES+=("$pkg") FAILED_PACKAGES+=("$pkg")
@@ -165,7 +189,9 @@ else
if [[ "$selection" == "a" ]] || [[ "$selection" == "all" ]]; then if [[ "$selection" == "a" ]] || [[ "$selection" == "all" ]]; then
PACKAGE_INPUT="all" PACKAGE_INPUT="all"
BUILD_ARGS=("all" "$PPA_NAME_INPUT" "$UBUNTU_SERIES") BUILD_ARGS=("all")
[[ -n "$PPA_NAME_INPUT" ]] && BUILD_ARGS+=("$PPA_NAME_INPUT")
[[ -n "$REBUILD_RELEASE" ]] && BUILD_ARGS+=("$REBUILD_RELEASE")
[[ "$KEEP_BUILDS" == "true" ]] && BUILD_ARGS+=("--keep-builds") [[ "$KEEP_BUILDS" == "true" ]] && BUILD_ARGS+=("--keep-builds")
exec "$0" "${BUILD_ARGS[@]}" exec "$0" "${BUILD_ARGS[@]}"
elif [[ "$selection" =~ ^[0-9]+$ ]] && [[ "$selection" -ge 1 ]] && [[ "$selection" -le ${#AVAILABLE_PACKAGES[@]} ]]; then elif [[ "$selection" =~ ^[0-9]+$ ]] && [[ "$selection" -ge 1 ]] && [[ "$selection" -le ${#AVAILABLE_PACKAGES[@]} ]]; then
@@ -191,6 +217,48 @@ fi
PACKAGE_DIR=$(cd "$PACKAGE_DIR" && pwd) PACKAGE_DIR=$(cd "$PACKAGE_DIR" && pwd)
PARENT_DIR=$(dirname "$PACKAGE_DIR") PARENT_DIR=$(dirname "$PACKAGE_DIR")
if [[ ${#SERIES_LIST[@]} -gt 1 ]]; then
SOURCE_FORMAT_LINE=$(head -1 "$PACKAGE_DIR/debian/source/format" 2>/dev/null || echo "")
IS_NATIVE_DUAL=false
if [[ "$SOURCE_FORMAT_LINE" == *"native"* ]]; then
IS_NATIVE_DUAL=true
info "Native source format: second series uses PPA suffix +1 (or ppa2 if unset) so both uploads succeed."
fi
export REBUILD_RELEASE
for idx in "${!SERIES_LIST[@]}"; do
SERIES="${SERIES_LIST[$idx]}"
if [[ -n "$PACKAGE_INPUT" ]] && [[ "$PACKAGE_INPUT" == *"/"* ]]; then
ARGS=("$PACKAGE_DIR" "$PPA_NAME" "$SERIES")
else
ARGS=("$PACKAGE_NAME" "$PPA_NAME" "$SERIES")
fi
if [[ "$IS_NATIVE_DUAL" == true ]]; then
if [[ "$idx" -eq 0 ]]; then
[[ -n "${REBUILD_RELEASE:-}" ]] && ARGS+=("$REBUILD_RELEASE")
else
if [[ -n "${REBUILD_RELEASE:-}" ]]; then
SECOND_PPA=$((REBUILD_RELEASE + 1))
ARGS+=("$SECOND_PPA")
info "Second series ${SERIES}: using ppa${SECOND_PPA} (native dual-series)"
else
ARGS+=("2")
info "Second series ${SERIES}: using ppa2 (native dual-series; first uses default ppa1)"
fi
fi
else
[[ -n "${REBUILD_RELEASE:-}" ]] && ARGS+=("$REBUILD_RELEASE")
fi
[[ "$KEEP_BUILDS" == "true" ]] && ARGS+=("--keep-builds")
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
info "Upload series: $SERIES (of ${SERIES_LIST[*]})"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
"$0" "${ARGS[@]}" || exit 1
done
exit 0
fi
UBUNTU_SERIES="${SERIES_LIST[0]}"
info "Building and uploading: $PACKAGE_NAME" info "Building and uploading: $PACKAGE_NAME"
info "Package directory: $PACKAGE_DIR" info "Package directory: $PACKAGE_DIR"
info "PPA: ppa:avengemedia/$PPA_NAME" info "PPA: ppa:avengemedia/$PPA_NAME"
+3
View File
@@ -150,6 +150,9 @@
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
substituteInPlace $out/share/quickshell/dms/assets/pam/u2f \
--replace-fail pam_u2f.so ${pkgs.pam_u2f}/lib/security/pam_u2f.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) \
+1
View File
@@ -10,6 +10,7 @@ Singleton {
readonly property url home: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0] readonly property url home: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
readonly property url pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0] readonly property url pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
readonly property url xdgCache: StandardPaths.standardLocations(StandardPaths.GenericCacheLocation)[0]
readonly property url data: `${StandardPaths.standardLocations(StandardPaths.GenericDataLocation)[0]}/DankMaterialShell` readonly property url data: `${StandardPaths.standardLocations(StandardPaths.GenericDataLocation)[0]}/DankMaterialShell`
readonly property url state: `${StandardPaths.standardLocations(StandardPaths.GenericStateLocation)[0]}/DankMaterialShell` readonly property url state: `${StandardPaths.standardLocations(StandardPaths.GenericStateLocation)[0]}/DankMaterialShell`
+20 -9
View File
@@ -1,4 +1,5 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import QtQuick import QtQuick
@@ -13,21 +14,31 @@ Singleton {
signal popoutChanged signal popoutChanged
function _closePopout(popout) { function _closePopout(popout) {
switch (true) { try {
case popout.dashVisible !== undefined: switch (true) {
popout.dashVisible = false; case popout.dashVisible !== undefined:
popout.dashVisible = false;
return;
case popout.notificationHistoryVisible !== undefined:
popout.notificationHistoryVisible = false;
return;
default:
if (typeof popout.close !== "function")
return;
popout.close();
}
} catch (e) {
return; return;
case popout.notificationHistoryVisible !== undefined:
popout.notificationHistoryVisible = false;
return;
default:
popout.close();
} }
} }
function _isStale(popout) { function _isStale(popout) {
try { try {
return !popout || !("shouldBeVisible" in popout); if (!popout || !("shouldBeVisible" in popout))
return true;
if (!popout.screen)
return true;
return false;
} catch (e) { } catch (e) {
return true; return true;
} }
+44 -4
View File
@@ -132,6 +132,8 @@ Singleton {
property string timeLocale: "" property string timeLocale: ""
property string launcherLastMode: "all" property string launcherLastMode: "all"
property string launcherLastQuery: ""
property var launcherQueryHistory: []
property string appDrawerLastMode: "apps" property string appDrawerLastMode: "apps"
property string niriOverviewLastMode: "apps" property string niriOverviewLastMode: "apps"
property string settingsSidebarExpandedIds: "," property string settingsSidebarExpandedIds: ","
@@ -345,8 +347,8 @@ Singleton {
function setLightMode(lightMode) { function setLightMode(lightMode) {
isSwitchingMode = true; isSwitchingMode = true;
syncWallpaperForCurrentMode(lightMode);
isLightMode = lightMode; isLightMode = lightMode;
syncWallpaperForCurrentMode();
saveSettings(); saveSettings();
Qt.callLater(() => { Qt.callLater(() => {
isSwitchingMode = false; isSwitchingMode = false;
@@ -1096,6 +1098,43 @@ Singleton {
saveSettings(); saveSettings();
} }
function setLauncherLastQuery(query) {
launcherLastQuery = query;
saveSettings();
}
function addLauncherHistory(query) {
let q = query.trim();
setLauncherLastQuery(q);
if (!q)
return;
if (launcherQueryHistory.length > 0 && launcherQueryHistory[0] === q) {
return;
}
let history = [...launcherQueryHistory];
let idx = history.indexOf(q);
if (idx !== -1)
history.splice(idx, 1);
history.unshift(q);
if (history.length > 50)
history = history.slice(0, 50);
launcherQueryHistory = history;
saveSettings();
}
function clearLauncherHistory() {
launcherLastQuery = "";
launcherSearchHistory = [];
saveSettings();
}
function setAppDrawerLastMode(mode) { function setAppDrawerLastMode(mode) {
appDrawerLastMode = mode; appDrawerLastMode = mode;
saveSettings(); saveSettings();
@@ -1112,15 +1151,16 @@ Singleton {
saveSettings(); saveSettings();
} }
function syncWallpaperForCurrentMode() { function syncWallpaperForCurrentMode(mode) {
if (!perModeWallpaper) if (!perModeWallpaper)
return; return;
var light = (mode !== undefined) ? mode : isLightMode;
if (perMonitorWallpaper) { if (perMonitorWallpaper) {
monitorWallpapers = isLightMode ? Object.assign({}, monitorWallpapersLight) : Object.assign({}, monitorWallpapersDark); monitorWallpapers = light ? Object.assign({}, monitorWallpapersLight) : Object.assign({}, monitorWallpapersDark);
return; return;
} }
wallpaperPath = isLightMode ? wallpaperPathLight : wallpaperPathDark; wallpaperPath = light ? wallpaperPathLight : wallpaperPathDark;
} }
function _findMonitorValue(map, screenName) { function _findMonitorValue(map, screenName) {
+21 -1
View File
@@ -186,6 +186,14 @@ Singleton {
onPopoutElevationEnabledChanged: saveSettings() onPopoutElevationEnabledChanged: saveSettings()
property bool barElevationEnabled: true property bool barElevationEnabled: true
onBarElevationEnabledChanged: saveSettings() onBarElevationEnabledChanged: saveSettings()
property bool blurEnabled: false
onBlurEnabledChanged: saveSettings()
property string blurBorderColor: "outline"
onBlurBorderColorChanged: saveSettings()
property string blurBorderCustomColor: "#ffffff"
onBlurBorderCustomColorChanged: saveSettings()
property real blurBorderOpacity: 1.0
onBlurBorderOpacityChanged: saveSettings()
property string wallpaperFillMode: "Fill" property string wallpaperFillMode: "Fill"
property bool blurredWallpaperLayer: false property bool blurredWallpaperLayer: false
property bool blurWallpaperOnOverview: false property bool blurWallpaperOnOverview: false
@@ -338,6 +346,7 @@ Singleton {
property bool sortAppsAlphabetically: false property bool sortAppsAlphabetically: false
property int appLauncherGridColumns: 4 property int appLauncherGridColumns: 4
property bool spotlightCloseNiriOverview: true property bool spotlightCloseNiriOverview: true
property bool rememberLastQuery: false
property var spotlightSectionViewModes: ({}) property var spotlightSectionViewModes: ({})
onSpotlightSectionViewModesChanged: saveSettings() onSpotlightSectionViewModesChanged: saveSettings()
property var appDrawerSectionViewModes: ({}) property var appDrawerSectionViewModes: ({})
@@ -494,6 +503,7 @@ Singleton {
"harmony": 0.5 "harmony": 0.5
} }
}) })
property bool matugenTemplateNeovimSetBackground: true
property bool showDock: false property bool showDock: false
property bool dockAutoHide: false property bool dockAutoHide: false
@@ -1202,13 +1212,23 @@ Singleton {
Quickshell.execDetached(["sh", "-lc", script]); Quickshell.execDetached(["sh", "-lc", script]);
} }
function scheduleAuthApply() {
if (isGreeterMode)
return;
Qt.callLater(() => {
Processes.settingsRoot = root;
Processes.scheduleAuthApply();
});
}
readonly property var _hooks: ({ readonly property var _hooks: ({
"applyStoredTheme": applyStoredTheme, "applyStoredTheme": applyStoredTheme,
"regenSystemThemes": regenSystemThemes, "regenSystemThemes": regenSystemThemes,
"updateCompositorLayout": updateCompositorLayout, "updateCompositorLayout": updateCompositorLayout,
"applyStoredIconTheme": applyStoredIconTheme, "applyStoredIconTheme": applyStoredIconTheme,
"updateBarConfigs": updateBarConfigs, "updateBarConfigs": updateBarConfigs,
"updateCompositorCursor": updateCompositorCursor "updateCompositorCursor": updateCompositorCursor,
"scheduleAuthApply": scheduleAuthApply
}) })
function set(key, value) { function set(key, value) {
+8 -7
View File
@@ -1249,7 +1249,7 @@ const defaultOpts = {
}; };
class Finder { class Finder {
constructor(list, ...optionsTuple) { constructor(list, ...optionsTuple) {
this.opts = Object.assign(defaultOpts, optionsTuple[0]); this.opts = Object.assign({}, defaultOpts, optionsTuple[0]);
this.items = list; this.items = list;
this.runesList = list.map((item) => strToRunes(this.opts.selector(item).normalize())); this.runesList = list.map((item) => strToRunes(this.opts.selector(item).normalize()));
this.algoFn = exactMatchNaive; this.algoFn = exactMatchNaive;
@@ -1283,12 +1283,13 @@ function postProcessResultItems(result, opts) {
if (opts.sort) { if (opts.sort) {
const { selector } = opts; const { selector } = opts;
result.sort((a, b) => { result.sort((a, b) => {
if (a.score === b.score) { if (a.score !== b.score) {
for (const tiebreaker of opts.tiebreakers) { return b.score - a.score;
const diff = tiebreaker(a, b, selector); }
if (diff !== 0) { for (const tiebreaker of opts.tiebreakers) {
return diff; const diff = tiebreaker(a, b, selector);
} if (diff !== 0) {
return diff;
} }
} }
return 0; return 0;
+145 -8
View File
@@ -4,6 +4,8 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Common
import qs.Services
Singleton { Singleton {
id: root id: root
@@ -52,6 +54,14 @@ Singleton {
readonly property var forcedFprintAvailable: envFlag("DMS_FORCE_FPRINT_AVAILABLE") readonly property var forcedFprintAvailable: envFlag("DMS_FORCE_FPRINT_AVAILABLE")
readonly property var forcedU2fAvailable: envFlag("DMS_FORCE_U2F_AVAILABLE") readonly property var forcedU2fAvailable: envFlag("DMS_FORCE_U2F_AVAILABLE")
property bool authApplyRunning: false
property bool authApplyQueued: false
property bool authApplyRerunRequested: false
property bool authApplyTerminalFallbackFromPrecheck: false
property string authApplyStdout: ""
property string authApplyStderr: ""
property string authApplySudoProbeStderr: ""
property string authApplyTerminalFallbackStderr: ""
function detectQtTools() { function detectQtTools() {
qtToolsDetectionProcess.running = true; qtToolsDetectionProcess.running = true;
@@ -70,14 +80,12 @@ Singleton {
fingerprintProbeState = forcedFprintAvailable ? "ready" : "probe_failed"; fingerprintProbeState = forcedFprintAvailable ? "ready" : "probe_failed";
} }
if (forcedFprintAvailable === null || forcedU2fAvailable === null) { pamFprintSupportDetected = false;
pamFprintSupportDetected = false; pamU2fSupportDetected = false;
pamU2fSupportDetected = false; pamSupportProbeOutput = "";
pamSupportProbeOutput = ""; pamSupportProbeStreamFinished = false;
pamSupportProbeStreamFinished = false; pamSupportProbeExited = false;
pamSupportProbeExited = false; pamSupportDetectionProcess.running = true;
pamSupportDetectionProcess.running = true;
}
recomputeAuthCapabilities(); recomputeAuthCapabilities();
} }
@@ -94,6 +102,50 @@ Singleton {
pluginSettingsCheckProcess.running = true; pluginSettingsCheckProcess.running = true;
} }
function scheduleAuthApply() {
if (!settingsRoot || settingsRoot.isGreeterMode)
return;
authApplyQueued = true;
if (authApplyRunning) {
authApplyRerunRequested = true;
return;
}
authApplyDebounce.restart();
}
function beginAuthApply() {
if (!authApplyQueued || authApplyRunning || !settingsRoot || settingsRoot.isGreeterMode)
return;
authApplyQueued = false;
authApplyRerunRequested = false;
authApplyStdout = "";
authApplyStderr = "";
authApplySudoProbeStderr = "";
authApplyTerminalFallbackStderr = "";
authApplyTerminalFallbackFromPrecheck = false;
authApplyRunning = true;
authApplySudoProbeProcess.running = true;
}
function launchAuthApplyTerminalFallback(fromPrecheck, details) {
authApplyTerminalFallbackFromPrecheck = fromPrecheck;
if (details && details !== "")
ToastService.showInfo(I18n.tr("Authentication changes need sudo. Opening terminal so you can use password or fingerprint."), details, "", "auth-sync");
authApplyTerminalFallbackStderr = "";
authApplyTerminalFallbackProcess.running = true;
}
function finishAuthApply() {
const shouldRerun = authApplyQueued || authApplyRerunRequested;
authApplyRunning = false;
authApplyRerunRequested = false;
if (shouldRerun)
authApplyDebounce.restart();
}
function stripPamComment(line) { function stripPamComment(line) {
if (!line) if (!line)
return ""; return "";
@@ -419,6 +471,91 @@ Singleton {
} }
} }
Timer {
id: authApplyDebounce
interval: 300
repeat: false
onTriggered: root.beginAuthApply()
}
property var authApplyProcess: Process {
command: ["dms", "auth", "sync", "--yes"]
running: false
stdout: StdioCollector {
onStreamFinished: root.authApplyStdout = text || ""
}
stderr: StdioCollector {
onStreamFinished: root.authApplyStderr = text || ""
}
onExited: exitCode => {
const out = (root.authApplyStdout || "").trim();
const err = (root.authApplyStderr || "").trim();
if (exitCode === 0) {
let details = out;
if (err !== "")
details = details !== "" ? details + "\n\nstderr:\n" + err : "stderr:\n" + err;
ToastService.showInfo(I18n.tr("Authentication changes applied."), details, "", "auth-sync");
root.detectAuthCapabilities();
root.finishAuthApply();
return;
}
let details = "";
if (out !== "")
details = out;
if (err !== "")
details = details !== "" ? details + "\n\nstderr:\n" + err : "stderr:\n" + err;
ToastService.showWarning(I18n.tr("Background authentication sync failed. Trying terminal mode."), details, "", "auth-sync");
root.launchAuthApplyTerminalFallback(false, "");
}
}
property var authApplySudoProbeProcess: Process {
command: ["sudo", "-n", "true"]
running: false
stderr: StdioCollector {
onStreamFinished: root.authApplySudoProbeStderr = text || ""
}
onExited: exitCode => {
const err = (root.authApplySudoProbeStderr || "").trim();
if (exitCode === 0) {
ToastService.showInfo(I18n.tr("Applying authentication changes…"), "", "", "auth-sync");
root.authApplyProcess.running = true;
return;
}
root.launchAuthApplyTerminalFallback(true, err);
}
}
property var authApplyTerminalFallbackProcess: Process {
command: ["dms", "auth", "sync", "--terminal", "--yes"]
running: false
stderr: StdioCollector {
onStreamFinished: root.authApplyTerminalFallbackStderr = text || ""
}
onExited: exitCode => {
if (exitCode === 0) {
const message = root.authApplyTerminalFallbackFromPrecheck
? I18n.tr("Terminal opened. Complete authentication setup there; it will close automatically when done.")
: I18n.tr("Terminal fallback opened. Complete authentication setup there; it will close automatically when done.");
ToastService.showInfo(message, "", "", "auth-sync");
} else {
let details = (root.authApplyTerminalFallbackStderr || "").trim();
ToastService.showError(I18n.tr("Terminal fallback failed. Install a supported terminal emulator or run 'dms auth sync' manually.") + " (exit " + exitCode + ")", details, "", "auth-sync");
}
root.finishAuthApply();
}
}
FileView { FileView {
id: greetdPamWatcher id: greetdPamWatcher
path: "/etc/pam.d/greetd" path: "/etc/pam.d/greetd"
@@ -83,6 +83,8 @@ var SPEC = {
timeLocale: { def: "" }, timeLocale: { def: "" },
launcherLastMode: { def: "all" }, launcherLastMode: { def: "all" },
launcherLastQuery: { def: "" },
launcherQueryHistory: { def: [] },
appDrawerLastMode: { def: "apps" }, appDrawerLastMode: { def: "apps" },
niriOverviewLastMode: { def: "apps" }, niriOverviewLastMode: { def: "apps" },
+10 -4
View File
@@ -58,6 +58,10 @@ var SPEC = {
modalElevationEnabled: { def: true }, modalElevationEnabled: { def: true },
popoutElevationEnabled: { def: true }, popoutElevationEnabled: { def: true },
barElevationEnabled: { def: true }, barElevationEnabled: { def: true },
blurEnabled: { def: false },
blurBorderColor: { def: "outline" },
blurBorderCustomColor: { def: "#ffffff" },
blurBorderOpacity: { def: 1.0, coerce: percentToUnit },
wallpaperFillMode: { def: "Fill" }, wallpaperFillMode: { def: "Fill" },
blurredWallpaperLayer: { def: false }, blurredWallpaperLayer: { def: false },
blurWallpaperOnOverview: { def: false }, blurWallpaperOnOverview: { def: false },
@@ -169,8 +173,8 @@ var SPEC = {
lockDateFormat: { def: "" }, lockDateFormat: { def: "" },
greeterRememberLastSession: { def: true }, greeterRememberLastSession: { def: true },
greeterRememberLastUser: { def: true }, greeterRememberLastUser: { def: true },
greeterEnableFprint: { def: false }, greeterEnableFprint: { def: false, onChange: "scheduleAuthApply" },
greeterEnableU2f: { def: false }, greeterEnableU2f: { def: false, onChange: "scheduleAuthApply" },
greeterWallpaperPath: { def: "" }, greeterWallpaperPath: { def: "" },
greeterUse24HourClock: { def: true }, greeterUse24HourClock: { def: true },
greeterShowSeconds: { def: false }, greeterShowSeconds: { def: false },
@@ -189,6 +193,7 @@ var SPEC = {
sortAppsAlphabetically: { def: false }, sortAppsAlphabetically: { def: false },
appLauncherGridColumns: { def: 4 }, appLauncherGridColumns: { def: 4 },
spotlightCloseNiriOverview: { def: true }, spotlightCloseNiriOverview: { def: true },
rememberLastQuery: { def: false },
spotlightSectionViewModes: { def: {} }, spotlightSectionViewModes: { def: {} },
appDrawerSectionViewModes: { def: {} }, appDrawerSectionViewModes: { def: {} },
niriOverviewOverlayEnabled: { def: true }, niriOverviewOverlayEnabled: { def: true },
@@ -305,6 +310,7 @@ var SPEC = {
light: { baseTheme: "github_light", harmony: 0.5 } light: { baseTheme: "github_light", harmony: 0.5 }
} }
}, },
matugenTemplateNeovimSetBackground: { def: true },
showDock: { def: false }, showDock: { def: false },
dockAutoHide: { def: false }, dockAutoHide: { def: false },
@@ -352,7 +358,7 @@ var SPEC = {
lockScreenShowMediaPlayer: { def: true }, lockScreenShowMediaPlayer: { def: true },
lockScreenPowerOffMonitorsOnLock: { def: false }, lockScreenPowerOffMonitorsOnLock: { def: false },
lockAtStartup: { def: false }, lockAtStartup: { def: false },
enableFprint: { def: false }, enableFprint: { def: false, onChange: "scheduleAuthApply" },
maxFprintTries: { def: 15 }, maxFprintTries: { def: 15 },
fprintdAvailable: { def: false, persist: false }, fprintdAvailable: { def: false, persist: false },
lockFingerprintCanEnable: { def: false, persist: false }, lockFingerprintCanEnable: { def: false, persist: false },
@@ -362,7 +368,7 @@ var SPEC = {
greeterFingerprintReady: { def: false, persist: false }, greeterFingerprintReady: { def: false, persist: false },
greeterFingerprintReason: { def: "probe_failed", persist: false }, greeterFingerprintReason: { def: "probe_failed", persist: false },
greeterFingerprintSource: { def: "none", persist: false }, greeterFingerprintSource: { def: "none", persist: false },
enableU2f: { def: false }, enableU2f: { def: false, onChange: "scheduleAuthApply" },
u2fMode: { def: "or" }, u2fMode: { def: "or" },
u2fAvailable: { def: false, persist: false }, u2fAvailable: { def: false, persist: false },
lockU2fCanEnable: { def: false, persist: false }, lockU2fCanEnable: { def: false, persist: false },
+35 -1
View File
@@ -3,6 +3,7 @@ import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets
Item { Item {
id: root id: root
@@ -59,11 +60,25 @@ Item {
function open() { function open() {
closeTimer.stop(); closeTimer.stop();
const focusedScreen = CompositorService.getFocusedScreen(); const focusedScreen = CompositorService.getFocusedScreen();
const screenChanged = focusedScreen && contentWindow.screen !== focusedScreen;
if (focusedScreen) { if (focusedScreen) {
if (screenChanged)
contentWindow.visible = false;
contentWindow.screen = focusedScreen; contentWindow.screen = focusedScreen;
if (!useSingleWindow) if (!useSingleWindow) {
if (screenChanged)
clickCatcher.visible = false;
clickCatcher.screen = focusedScreen; clickCatcher.screen = focusedScreen;
}
} }
if (screenChanged) {
Qt.callLater(() => root._finishOpen());
} else {
_finishOpen();
}
}
function _finishOpen() {
ModalManager.openModal(root); ModalManager.openModal(root);
shouldBeVisible = true; shouldBeVisible = true;
if (!useSingleWindow) if (!useSingleWindow)
@@ -215,6 +230,16 @@ Item {
visible: false visible: false
color: "transparent" color: "transparent"
WindowBlur {
targetWindow: contentWindow
readonly property real s: Math.min(1, modalContainer.scaleValue)
blurX: modalContainer.x + modalContainer.width * (1 - s) * 0.5 + Theme.snap(modalContainer.animX, root.dpr)
blurY: modalContainer.y + modalContainer.height * (1 - s) * 0.5 + Theme.snap(modalContainer.animY, root.dpr)
blurWidth: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.width * s : 0
blurHeight: (shouldBeVisible && animatedContent.opacity > 0) ? modalContainer.height * s : 0
blurRadius: root.cornerRadius
}
WlrLayershell.namespace: root.layerNamespace WlrLayershell.namespace: root.layerNamespace
WlrLayershell.layer: { WlrLayershell.layer: {
if (root.useOverlayLayer) if (root.useOverlayLayer)
@@ -393,6 +418,15 @@ Item {
shadowEnabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" shadowEnabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
} }
Rectangle {
anchors.fill: parent
radius: root.cornerRadius
color: "transparent"
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
z: 100
}
FocusScope { FocusScope {
anchors.fill: parent anchors.fill: parent
focus: root.shouldBeVisible focus: root.shouldBeVisible
@@ -39,11 +39,14 @@ Item {
signal itemExecuted signal itemExecuted
signal searchCompleted signal searchCompleted
signal modeChanged(string mode) signal modeChanged(string mode)
signal queryChanged(string query)
signal viewModeChanged(string sectionId, string mode) signal viewModeChanged(string sectionId, string mode)
signal searchQueryRequested(string query) signal searchQueryRequested(string query)
onActiveChanged: { onActiveChanged: {
if (!active) { if (!active) {
SessionData.addLauncherHistory(searchQuery);
sections = []; sections = [];
flatModel = []; flatModel = [];
selectedItem = null; selectedItem = null;
@@ -175,6 +178,33 @@ Item {
} }
] ]
property int historyIndex: -1
property string typingBackup: ""
function navigateHistory(direction) {
let history = SessionData.launcherQueryHistory;
if (history.length === 0)
return;
if (historyIndex === -1)
typingBackup = searchQuery;
let nextIndex = historyIndex + (direction === "up" ? 1 : -1);
if (nextIndex >= history.length)
nextIndex = history.length - 1;
if (nextIndex < -1)
nextIndex = -1;
if (nextIndex === historyIndex)
return;
historyIndex = nextIndex;
let targetText = (historyIndex === -1) ? typingBackup : history[historyIndex];
setSearchQuery(targetText);
searchQueryRequested(targetText);
}
property string fileSearchType: "all" property string fileSearchType: "all"
property string fileSearchExt: "" property string fileSearchExt: ""
property string fileSearchFolder: "" property string fileSearchFolder: ""
@@ -496,6 +526,8 @@ Item {
} }
function performSearch() { function performSearch() {
queryChanged(searchQuery);
var currentVersion = _searchVersion; var currentVersion = _searchVersion;
isSearching = true; isSearching = true;
var shouldResetSelection = _queryDrivenSearch; var shouldResetSelection = _queryDrivenSearch;
@@ -1654,6 +1686,9 @@ Item {
function executeItem(item) { function executeItem(item) {
if (!item) if (!item)
return; return;
SessionData.addLauncherHistory(searchQuery);
if (item.type === "plugin_browse") { if (item.type === "plugin_browse") {
var browsePluginId = item.data?.pluginId; var browsePluginId = item.data?.pluginId;
if (!browsePluginId) if (!browsePluginId)
@@ -4,6 +4,7 @@ import Quickshell.Wayland
import Quickshell.Hyprland import Quickshell.Hyprland
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets
Item { Item {
id: root id: root
@@ -97,8 +98,16 @@ Item {
contentVisible = true; contentVisible = true;
spotlightContent.searchField.forceActiveFocus(); spotlightContent.searchField.forceActiveFocus();
var targetQuery = "";
if (query) {
targetQuery = query;
} else if (SettingsData.rememberLastQuery) {
targetQuery = SessionData.launcherLastQuery || "";
}
if (spotlightContent.searchField) { if (spotlightContent.searchField) {
spotlightContent.searchField.text = query; spotlightContent.searchField.text = targetQuery;
} }
if (spotlightContent.controller) { if (spotlightContent.controller) {
var targetMode = mode || SessionData.launcherLastMode || "all"; var targetMode = mode || SessionData.launcherLastMode || "all";
@@ -113,12 +122,10 @@ Item {
spotlightContent.controller.collapsedSections = {}; spotlightContent.controller.collapsedSections = {};
spotlightContent.controller.selectedFlatIndex = 0; spotlightContent.controller.selectedFlatIndex = 0;
spotlightContent.controller.selectedItem = null; spotlightContent.controller.selectedItem = null;
if (query) { spotlightContent.controller.historyIndex = -1;
spotlightContent.controller.setSearchQuery(query); spotlightContent.controller.searchQuery = targetQuery;
} else {
spotlightContent.controller.searchQuery = ""; spotlightContent.controller.performSearch();
spotlightContent.controller.performSearch();
}
} }
if (spotlightContent.resetScroll) { if (spotlightContent.resetScroll) {
spotlightContent.resetScroll(); spotlightContent.resetScroll();
@@ -128,40 +135,47 @@ Item {
} }
} }
function show() { function _finishShow(query, mode) {
closeCleanupTimer.stop(); spotlightOpen = true;
isClosing = false; isClosing = false;
openedFromOverview = false; openedFromOverview = false;
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen)
launcherWindow.screen = focusedScreen;
spotlightOpen = true;
keyboardActive = true; keyboardActive = true;
ModalManager.openModal(root); ModalManager.openModal(root);
if (useHyprlandFocusGrab) if (useHyprlandFocusGrab)
focusGrab.active = true; focusGrab.active = true;
_ensureContentLoadedAndInitialize("", ""); _ensureContentLoadedAndInitialize(query || "", mode || "");
}
function show() {
closeCleanupTimer.stop();
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
spotlightOpen = false;
isClosing = false;
launcherWindow.screen = focusedScreen;
Qt.callLater(() => root._finishShow("", ""));
return;
}
_finishShow("", "");
} }
function showWithQuery(query) { function showWithQuery(query) {
closeCleanupTimer.stop(); closeCleanupTimer.stop();
isClosing = false;
openedFromOverview = false;
var focusedScreen = CompositorService.getFocusedScreen(); var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen) if (focusedScreen && launcherWindow.screen !== focusedScreen) {
spotlightOpen = false;
isClosing = false;
launcherWindow.screen = focusedScreen; launcherWindow.screen = focusedScreen;
Qt.callLater(() => root._finishShow(query, ""));
return;
}
spotlightOpen = true; _finishShow(query, "");
keyboardActive = true;
ModalManager.openModal(root);
if (useHyprlandFocusGrab)
focusGrab.active = true;
_ensureContentLoadedAndInitialize(query, "");
} }
function hide() { function hide() {
@@ -185,14 +199,20 @@ Item {
function showWithMode(mode) { function showWithMode(mode) {
closeCleanupTimer.stop(); closeCleanupTimer.stop();
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
spotlightOpen = false;
isClosing = false;
launcherWindow.screen = focusedScreen;
Qt.callLater(() => root._finishShow("", mode));
return;
}
spotlightOpen = true;
isClosing = false; isClosing = false;
openedFromOverview = false; openedFromOverview = false;
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen)
launcherWindow.screen = focusedScreen;
spotlightOpen = true;
keyboardActive = true; keyboardActive = true;
ModalManager.openModal(root); ModalManager.openModal(root);
if (useHyprlandFocusGrab) if (useHyprlandFocusGrab)
@@ -231,6 +251,7 @@ Item {
Connections { Connections {
target: spotlightContent?.controller ?? null target: spotlightContent?.controller ?? null
function onModeChanged(mode) { function onModeChanged(mode) {
if (spotlightContent.controller.autoSwitchedToFiles) if (spotlightContent.controller.autoSwitchedToFiles)
return; return;
@@ -288,6 +309,16 @@ Item {
color: "transparent" color: "transparent"
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
WindowBlur {
targetWindow: launcherWindow
readonly property real s: Math.min(1, modalContainer.scale)
blurX: root.modalX + root.modalWidth * (1 - s) * 0.5
blurY: root.modalY + root.modalHeight * (1 - s) * 0.5
blurWidth: (contentVisible && modalContainer.opacity > 0) ? root.modalWidth * s : 0
blurHeight: (contentVisible && modalContainer.opacity > 0) ? root.modalHeight * s : 0
blurRadius: root.cornerRadius
}
WlrLayershell.namespace: "dms:spotlight" WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: { WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) { switch (Quickshell.env("DMS_MODAL_LAYER")) {
@@ -421,6 +452,14 @@ Item {
event.accepted = true; event.accepted = true;
} }
} }
Rectangle {
anchors.fill: parent
radius: root.cornerRadius
color: "transparent"
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
}
} }
} }
} }
@@ -149,10 +149,18 @@ FocusScope {
event.accepted = false; event.accepted = false;
return; return;
case Qt.Key_Down: case Qt.Key_Down:
controller.selectNext(); if (hasCtrl) {
controller.navigateHistory("down");
} else {
controller.selectNext();
}
return; return;
case Qt.Key_Up: case Qt.Key_Up:
controller.selectPrevious(); if (hasCtrl) {
controller.navigateHistory("up");
} else {
controller.selectPrevious();
}
return; return;
case Qt.Key_PageDown: case Qt.Key_PageDown:
controller.selectPageDown(8); controller.selectPageDown(8);
@@ -763,6 +771,7 @@ FocusScope {
} }
function onSearchQueryRequested(query) { function onSearchQueryRequested(query) {
searchField.text = query; searchField.text = query;
searchField.cursorPosition = query.length;
} }
function onModeChanged() { function onModeChanged() {
extFilterField.text = ""; extFilterField.text = "";
+1 -1
View File
@@ -152,7 +152,7 @@ function scoreItems(items, query, getFrecencyFn) {
var item = items[i] var item = items[i]
var itemScore var itemScore
if (query && item._preScored !== undefined) { if (item._preScored !== undefined && (query || item._preScored > 900)) {
itemScore = item._preScored itemScore = item._preScored
} else { } else {
var frecencyData = getFrecencyFn ? getFrecencyFn(item) : null var frecencyData = getFrecencyFn ? getFrecencyFn(item) : null
@@ -75,6 +75,50 @@ StyledRect {
return determineFileType(fileName) === "image"; return determineFileType(fileName) === "image";
} }
function isVideoFile(fileName) {
if (!fileName) {
return false;
}
return determineFileType(fileName) === "video";
}
property bool isImage: isImageFile(delegateRoot.fileName)
property bool isVideo: isVideoFile(delegateRoot.fileName)
property string _xdgCacheHome: Paths.strip(Paths.xdgCache)
property string _thumbnailSize: iconSizeIndex >= 2 ? "x-large" : "large"
property int _thumbnailPx: iconSizeIndex >= 2 ? 512 : 256
property string videoThumbnailPath: {
if (!delegateRoot.fileIsDir && isVideo) {
const hash = Qt.md5("file://" + delegateRoot.filePath);
return _xdgCacheHome + "/thumbnails/" + _thumbnailSize + "/" + hash + ".png";
}
return "";
}
property string _videoThumb: ""
onVideoThumbnailPathChanged: {
_videoThumb = "";
if (!videoThumbnailPath)
return;
const thumbPath = videoThumbnailPath;
const thumbDir = _xdgCacheHome + "/thumbnails/" + _thumbnailSize;
const size = _thumbnailPx;
const fp = delegateRoot.filePath;
Paths.mkdir(thumbDir);
Proc.runCommand(null, ["test", "-f", thumbPath], function(output, exitCode) {
if (exitCode === 0) {
_videoThumb = thumbPath;
} else {
Proc.runCommand(null, ["ffmpegthumbnailer", "-i", fp, "-o", thumbPath, "-s", String(size), "-f"], function(output, exitCode) {
if (exitCode === 0)
_videoThumb = thumbPath;
});
}
});
}
function getIconForFile(fileName) { function getIconForFile(fileName) {
const lowerName = fileName.toLowerCase(); const lowerName = fileName.toLowerCase();
if (lowerName.startsWith("dockerfile")) { if (lowerName.startsWith("dockerfile")) {
@@ -124,7 +168,11 @@ StyledRect {
property string imagePath: { property string imagePath: {
if (weMode && delegateRoot.fileIsDir) if (weMode && delegateRoot.fileIsDir)
return delegateRoot.filePath + "/preview" + weExtensions[weExtIndex]; return delegateRoot.filePath + "/preview" + weExtensions[weExtIndex];
return (!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) ? delegateRoot.filePath : ""; if (!delegateRoot.fileIsDir && isImage)
return delegateRoot.filePath;
if (_videoThumb)
return _videoThumb;
return "";
} }
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : "" source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
onStatusChanged: { onStatusChanged: {
@@ -149,7 +197,7 @@ StyledRect {
source: gridPreviewImage source: gridPreviewImage
maskEnabled: true maskEnabled: true
maskSource: gridImageMask maskSource: gridImageMask
visible: gridPreviewImage.status === Image.Ready && ((!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) || (weMode && delegateRoot.fileIsDir)) visible: gridPreviewImage.status === Image.Ready && ((!delegateRoot.fileIsDir && (isImage || isVideo)) || (weMode && delegateRoot.fileIsDir))
maskThresholdMin: 0.5 maskThresholdMin: 0.5
maskSpreadAtMin: 1 maskSpreadAtMin: 1
} }
@@ -175,7 +223,7 @@ StyledRect {
name: delegateRoot.fileIsDir ? "folder" : getIconForFile(delegateRoot.fileName) name: delegateRoot.fileIsDir ? "folder" : getIconForFile(delegateRoot.fileName)
size: iconSizes[iconSizeIndex] * 0.45 size: iconSizes[iconSizeIndex] * 0.45
color: delegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText color: delegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText
visible: (!delegateRoot.fileIsDir && !isImageFile(delegateRoot.fileName)) || (delegateRoot.fileIsDir && !weMode) visible: (!delegateRoot.fileIsDir && !isImage && !(isVideo && gridPreviewImage.status === Image.Ready)) || (delegateRoot.fileIsDir && !weMode)
} }
} }
@@ -74,6 +74,46 @@ StyledRect {
return determineFileType(fileName) === "image"; return determineFileType(fileName) === "image";
} }
function isVideoFile(fileName) {
if (!fileName) {
return false;
}
return determineFileType(fileName) === "video";
}
property bool isImage: isImageFile(listDelegateRoot.fileName)
property bool isVideo: isVideoFile(listDelegateRoot.fileName)
property string _xdgCacheHome: Paths.strip(Paths.xdgCache)
property string videoThumbnailPath: {
if (!listDelegateRoot.fileIsDir && isVideo) {
const hash = Qt.md5("file://" + listDelegateRoot.filePath);
return _xdgCacheHome + "/thumbnails/normal/" + hash + ".png";
}
return "";
}
property string _videoThumb: ""
onVideoThumbnailPathChanged: {
_videoThumb = "";
if (!videoThumbnailPath)
return;
const thumbPath = videoThumbnailPath;
const fp = listDelegateRoot.filePath;
Paths.mkdir(_xdgCacheHome + "/thumbnails/normal");
Proc.runCommand(null, ["test", "-f", thumbPath], function(output, exitCode) {
if (exitCode === 0) {
_videoThumb = thumbPath;
} else {
Proc.runCommand(null, ["ffmpegthumbnailer", "-i", fp, "-o", thumbPath, "-s", "128", "-f"], function(output, exitCode) {
if (exitCode === 0)
_videoThumb = thumbPath;
});
}
});
}
function getIconForFile(fileName) { function getIconForFile(fileName) {
const lowerName = fileName.toLowerCase(); const lowerName = fileName.toLowerCase();
if (lowerName.startsWith("dockerfile")) { if (lowerName.startsWith("dockerfile")) {
@@ -127,7 +167,13 @@ StyledRect {
Image { Image {
id: listPreviewImage id: listPreviewImage
anchors.fill: parent anchors.fill: parent
property string imagePath: (!listDelegateRoot.fileIsDir && isImageFile(listDelegateRoot.fileName)) ? listDelegateRoot.filePath : "" property string imagePath: {
if (!listDelegateRoot.fileIsDir && isImage)
return listDelegateRoot.filePath;
if (_videoThumb)
return _videoThumb;
return "";
}
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : "" source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
sourceSize.width: 32 sourceSize.width: 32
@@ -141,7 +187,7 @@ StyledRect {
source: listPreviewImage source: listPreviewImage
maskEnabled: true maskEnabled: true
maskSource: listImageMask maskSource: listImageMask
visible: listPreviewImage.status === Image.Ready && !listDelegateRoot.fileIsDir && isImageFile(listDelegateRoot.fileName) visible: listPreviewImage.status === Image.Ready && !listDelegateRoot.fileIsDir && (isImage || isVideo)
maskThresholdMin: 0.5 maskThresholdMin: 0.5
maskSpreadAtMin: 1 maskSpreadAtMin: 1
} }
@@ -166,7 +212,7 @@ StyledRect {
name: listDelegateRoot.fileIsDir ? "folder" : getIconForFile(listDelegateRoot.fileName) name: listDelegateRoot.fileIsDir ? "folder" : getIconForFile(listDelegateRoot.fileName)
size: Theme.iconSize - 2 size: Theme.iconSize - 2
color: listDelegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText color: listDelegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText
visible: listDelegateRoot.fileIsDir || !isImageFile(listDelegateRoot.fileName) visible: listDelegateRoot.fileIsDir || (!isImage && !(isVideo && listPreviewImage.status === Image.Ready))
} }
} }
@@ -90,7 +90,7 @@ DankPopout {
if (!lc) if (!lc)
return; return;
const query = _pendingQuery; const query = _pendingQuery || (SettingsData.rememberLastQuery ? SessionData.launcherLastQuery : "") || "";
const mode = _pendingMode || SessionData.appDrawerLastMode || "apps"; const mode = _pendingMode || SessionData.appDrawerLastMode || "apps";
_pendingMode = ""; _pendingMode = "";
_pendingQuery = ""; _pendingQuery = "";
@@ -102,12 +102,9 @@ DankPopout {
if (lc.controller) { if (lc.controller) {
lc.controller.searchMode = mode; lc.controller.searchMode = mode;
lc.controller.pluginFilter = ""; lc.controller.pluginFilter = "";
lc.controller.searchQuery = ""; lc.controller.searchQuery = query;
if (query) {
lc.controller.setSearchQuery(query); lc.controller.performSearch();
} else {
lc.controller.performSearch();
}
} }
lc.resetScroll?.(); lc.resetScroll?.();
lc.actionPanel?.hide(); lc.actionPanel?.hide();
@@ -107,6 +107,26 @@ Variants {
} }
} }
Connections {
target: blurWallpaperWindow
function onWidthChanged() {
root._renderSettling = true;
renderSettleTimer.restart();
}
function onHeightChanged() {
root._renderSettling = true;
renderSettleTimer.restart();
}
}
Connections {
target: Quickshell
function onScreensChanged() {
root._renderSettling = true;
renderSettleTimer.restart();
}
}
Timer { Timer {
id: renderSettleTimer id: renderSettleTimer
interval: 1000 interval: 1000
@@ -14,6 +14,7 @@ Item {
property real barThickness: 48 property real barThickness: 48
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null
property bool overrideAxisLayout: false property bool overrideAxisLayout: false
property bool forceVerticalLayout: false property bool forceVerticalLayout: false
@@ -357,6 +358,7 @@ Item {
barThickness: root.barThickness barThickness: root.barThickness
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow
isFirst: index === 0 isFirst: index === 0
isLast: index === centerRepeater.count - 1 isLast: index === centerRepeater.count - 1
sectionSpacing: parent.itemSpacing sectionSpacing: parent.itemSpacing
@@ -14,6 +14,8 @@ Item {
required property var rootWindow required property var rootWindow
required property var barConfig required property var barConfig
readonly property var blurBarWindow: barWindow
property var leftWidgetsModel property var leftWidgetsModel
property var centerWidgetsModel property var centerWidgetsModel
property var rightWidgetsModel property var rightWidgetsModel
@@ -408,6 +410,12 @@ Item {
value: topBarContent.barConfig value: topBarContent.barConfig
restoreMode: Binding.RestoreNone restoreMode: Binding.RestoreNone
} }
Binding {
target: hLeftSection
property: "blurBarWindow"
value: topBarContent.blurBarWindow
restoreMode: Binding.RestoreNone
}
RightSection { RightSection {
id: hRightSection id: hRightSection
@@ -434,6 +442,12 @@ Item {
value: topBarContent.barConfig value: topBarContent.barConfig
restoreMode: Binding.RestoreNone restoreMode: Binding.RestoreNone
} }
Binding {
target: hRightSection
property: "blurBarWindow"
value: topBarContent.blurBarWindow
restoreMode: Binding.RestoreNone
}
CenterSection { CenterSection {
id: hCenterSection id: hCenterSection
@@ -460,6 +474,12 @@ Item {
value: topBarContent.barConfig value: topBarContent.barConfig
restoreMode: Binding.RestoreNone restoreMode: Binding.RestoreNone
} }
Binding {
target: hCenterSection
property: "blurBarWindow"
value: topBarContent.blurBarWindow
restoreMode: Binding.RestoreNone
}
} }
Item { Item {
@@ -493,6 +513,12 @@ Item {
value: topBarContent.barConfig value: topBarContent.barConfig
restoreMode: Binding.RestoreNone restoreMode: Binding.RestoreNone
} }
Binding {
target: vLeftSection
property: "blurBarWindow"
value: topBarContent.blurBarWindow
restoreMode: Binding.RestoreNone
}
CenterSection { CenterSection {
id: vCenterSection id: vCenterSection
@@ -520,6 +546,12 @@ Item {
value: topBarContent.barConfig value: topBarContent.barConfig
restoreMode: Binding.RestoreNone restoreMode: Binding.RestoreNone
} }
Binding {
target: vCenterSection
property: "blurBarWindow"
value: topBarContent.blurBarWindow
restoreMode: Binding.RestoreNone
}
RightSection { RightSection {
id: vRightSection id: vRightSection
@@ -548,6 +580,12 @@ Item {
value: topBarContent.barConfig value: topBarContent.barConfig
restoreMode: Binding.RestoreNone restoreMode: Binding.RestoreNone
} }
Binding {
target: vRightSection
property: "blurBarWindow"
value: topBarContent.blurBarWindow
restoreMode: Binding.RestoreNone
}
} }
} }
+133 -1
View File
@@ -97,6 +97,112 @@ PanelWindow {
} }
} }
property var blurRegion: null
property var _blurWidgetItems: []
function registerBlurWidget(item) {
if (_blurWidgetItems.indexOf(item) >= 0)
return;
_blurWidgetItems = _blurWidgetItems.concat([item]);
_blurRebuildTimer.restart();
}
function unregisterBlurWidget(item) {
const idx = _blurWidgetItems.indexOf(item);
if (idx < 0)
return;
const arr = _blurWidgetItems.slice();
arr.splice(idx, 1);
_blurWidgetItems = arr;
_blurRebuildTimer.restart();
}
Timer {
id: _blurRebuildTimer
interval: 1
onTriggered: barBlur.rebuild()
}
Item {
id: barBlur
visible: false
readonly property bool barHasTransparency: barWindow._backgroundAlpha > 0 && barWindow._backgroundAlpha < 1
function rebuild() {
teardown();
if (!BlurService.enabled || !BlurService.available)
return;
const widgets = barWindow._blurWidgetItems.filter(w => w && w.visible && w.width > 0 && w.height > 0);
const hasBar = barHasTransparency;
if (!hasBar && widgets.length === 0)
return;
const cr = Theme.cornerRadius;
let qml = 'import QtQuick; import Quickshell; Region {';
for (let i = 0; i < widgets.length; i++) {
qml += ` property Item w${i}; Region { item: w${i}; radius: ${cr} }`;
}
qml += '}';
try {
const region = Qt.createQmlObject(qml, barWindow, "BarBlurRegion");
if (hasBar) {
region.x = Qt.binding(() => topBarMouseArea.x + barUnitInset.x + topBarSlide.x);
region.y = Qt.binding(() => topBarMouseArea.y + barUnitInset.y + topBarSlide.y);
region.width = Qt.binding(() => barUnitInset.width);
region.height = Qt.binding(() => barUnitInset.height);
region.radius = Qt.binding(() => barBackground.rt);
}
for (let i = 0; i < widgets.length; i++) {
region[`w${i}`] = widgets[i];
}
barWindow.BackgroundEffect.blurRegion = region;
barWindow.blurRegion = region;
} catch (e) {
console.warn("BarBlur: Failed to create blur region:", e);
}
}
function teardown() {
if (!barWindow.blurRegion)
return;
try {
barWindow.BackgroundEffect.blurRegion = null;
} catch (e) {}
barWindow.blurRegion.destroy();
barWindow.blurRegion = null;
}
onBarHasTransparencyChanged: _blurRebuildTimer.restart()
Connections {
target: BlurService
function onEnabledChanged() {
barBlur.rebuild();
}
}
Connections {
target: topBarSlide
function onXChanged() {
if (barWindow.blurRegion)
barWindow.blurRegion.changed();
}
function onYChanged() {
if (barWindow.blurRegion)
barWindow.blurRegion.changed();
}
}
Component.onCompleted: rebuild()
Component.onDestruction: teardown()
}
WlrLayershell.layer: dBarLayer WlrLayershell.layer: dBarLayer
WlrLayershell.namespace: "dms:bar" WlrLayershell.namespace: "dms:bar"
@@ -157,6 +263,7 @@ PanelWindow {
property string screenName: modelData.name property string screenName: modelData.name
property bool hasMaximizedToplevel: false property bool hasMaximizedToplevel: false
property bool hasFullscreenToplevel: false
property bool shouldHideForWindows: false property bool shouldHideForWindows: false
function _updateHasMaximizedToplevel() { function _updateHasMaximizedToplevel() {
@@ -179,6 +286,25 @@ PanelWindow {
hasMaximizedToplevel = false; hasMaximizedToplevel = false;
} }
function _updateHasFullscreenToplevel() {
if (!CompositorService.isHyprland) {
hasFullscreenToplevel = false;
return;
}
const filtered = CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
for (let i = 0; i < filtered.length; i++) {
if (filtered[i]?.fullscreen) {
// On niri, fullscreen windows in inactive columns should not hide the bar
if (CompositorService.isNiri && !filtered[i]?.activated)
continue;
hasFullscreenToplevel = true;
return;
}
}
hasFullscreenToplevel = false;
}
function _updateShouldHideForWindows() { function _updateShouldHideForWindows() {
if (!(barConfig?.showOnWindowsOpen ?? false)) { if (!(barConfig?.showOnWindowsOpen ?? false)) {
shouldHideForWindows = false; shouldHideForWindows = false;
@@ -485,6 +611,7 @@ PanelWindow {
target: CompositorService target: CompositorService
function onToplevelsChanged() { function onToplevelsChanged() {
barWindow._updateHasMaximizedToplevel(); barWindow._updateHasMaximizedToplevel();
barWindow._updateHasFullscreenToplevel();
barWindow._updateShouldHideForWindows(); barWindow._updateShouldHideForWindows();
} }
} }
@@ -493,6 +620,7 @@ PanelWindow {
target: NiriService target: NiriService
function onAllWorkspacesChanged() { function onAllWorkspacesChanged() {
barWindow._updateHasMaximizedToplevel(); barWindow._updateHasMaximizedToplevel();
barWindow._updateHasFullscreenToplevel();
barWindow._updateShouldHideForWindows(); barWindow._updateShouldHideForWindows();
} }
} }
@@ -668,6 +796,9 @@ PanelWindow {
if (inOverviewWithShow) if (inOverviewWithShow)
return true; return true;
if (barWindow.hasFullscreenToplevel)
return false;
const showOnWindowsSetting = barConfig?.showOnWindowsOpen ?? false; const showOnWindowsSetting = barConfig?.showOnWindowsOpen ?? false;
if (showOnWindowsSetting && autoHide && (CompositorService.isNiri || CompositorService.isHyprland)) { if (showOnWindowsSetting && autoHide && (CompositorService.isNiri || CompositorService.isHyprland)) {
if (barWindow.shouldHideForWindows) if (barWindow.shouldHideForWindows)
@@ -686,7 +817,8 @@ PanelWindow {
onHasActivePopoutChanged: evaluateReveal() onHasActivePopoutChanged: evaluateReveal()
function updateActivePopoutState() { function updateActivePopoutState() {
if (!barWindow.screen) return; 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];
@@ -13,6 +13,7 @@ Item {
property real barThickness: 48 property real barThickness: 48
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null
property bool overrideAxisLayout: false property bool overrideAxisLayout: false
property bool forceVerticalLayout: false property bool forceVerticalLayout: false
@@ -59,6 +60,7 @@ Item {
barThickness: root.barThickness barThickness: root.barThickness
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow
isFirst: index === 0 isFirst: index === 0
isLast: index === rowRepeater.count - 1 isLast: index === rowRepeater.count - 1
sectionSpacing: parent.rowSpacing sectionSpacing: parent.rowSpacing
@@ -103,6 +105,7 @@ Item {
barThickness: root.barThickness barThickness: root.barThickness
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow
isFirst: index === 0 isFirst: index === 0
isLast: index === columnRepeater.count - 1 isLast: index === columnRepeater.count - 1
sectionSpacing: parent.columnSpacing sectionSpacing: parent.columnSpacing
@@ -13,6 +13,7 @@ Item {
property real barThickness: 48 property real barThickness: 48
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null
property bool overrideAxisLayout: false property bool overrideAxisLayout: false
property bool forceVerticalLayout: false property bool forceVerticalLayout: false
@@ -61,6 +62,7 @@ Item {
barThickness: root.barThickness barThickness: root.barThickness
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow
isFirst: index === 0 isFirst: index === 0
isLast: index === rowRepeater.count - 1 isLast: index === rowRepeater.count - 1
sectionSpacing: parent.rowSpacing sectionSpacing: parent.rowSpacing
@@ -105,6 +107,7 @@ Item {
barThickness: root.barThickness barThickness: root.barThickness
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow
isFirst: index === 0 isFirst: index === 0
isLast: index === columnRepeater.count - 1 isLast: index === columnRepeater.count - 1
sectionSpacing: parent.columnSpacing sectionSpacing: parent.columnSpacing
@@ -16,6 +16,7 @@ Loader {
property real barThickness: 48 property real barThickness: 48
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null
property bool isFirst: false property bool isFirst: false
property bool isLast: false property bool isLast: false
property real sectionSpacing: 0 property real sectionSpacing: 0
@@ -92,6 +93,14 @@ Loader {
restoreMode: Binding.RestoreNone restoreMode: Binding.RestoreNone
} }
Binding {
target: root.item
when: root.item && "blurBarWindow" in root.item
property: "blurBarWindow"
value: root.blurBarWindow
restoreMode: Binding.RestoreNone
}
Binding { Binding {
target: root.item target: root.item
when: root.item && "axis" in root.item when: root.item && "axis" in root.item
@@ -630,7 +630,7 @@ BasePill {
if (appItem.isFocused && colorizeEnabled) { if (appItem.isFocused && colorizeEnabled) {
return mouseArea.containsMouse ? Theme.withAlpha(Qt.lighter(appItem.activeOverlayColor, 1.3), 0.4) : Theme.withAlpha(appItem.activeOverlayColor, 0.3); return mouseArea.containsMouse ? Theme.withAlpha(Qt.lighter(appItem.activeOverlayColor, 1.3), 0.4) : Theme.withAlpha(appItem.activeOverlayColor, 0.3);
} }
return mouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"; return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
} }
border.width: dragHandler.dragging ? 2 : 0 border.width: dragHandler.dragging ? 2 : 0
@@ -3,6 +3,7 @@ import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Modules.Plugins import qs.Modules.Plugins
import qs.Services
import qs.Widgets import qs.Widgets
BasePill { BasePill {
@@ -93,6 +94,15 @@ BasePill {
PanelWindow { PanelWindow {
id: contextMenuWindow id: contextMenuWindow
WindowBlur {
targetWindow: contextMenuWindow
blurX: menuContainer.x
blurY: menuContainer.y
blurWidth: contextMenuWindow.visible ? menuContainer.width : 0
blurHeight: contextMenuWindow.visible ? menuContainer.height : 0
blurRadius: Theme.cornerRadius
}
WlrLayershell.namespace: "dms:clipboard-context-menu" WlrLayershell.namespace: "dms:clipboard-context-menu"
property bool isVertical: false property bool isVertical: false
@@ -187,8 +197,8 @@ BasePill {
height: Math.max(64, menuColumn.implicitHeight + Theme.spacingS * 2) height: Math.max(64, menuColumn.implicitHeight + Theme.spacingS * 2)
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1 border.width: BlurService.enabled ? BlurService.borderWidth : 1
opacity: contextMenuWindow.visible ? 1 : 0 opacity: contextMenuWindow.visible ? 1 : 0
visible: opacity > 0 visible: opacity > 0
@@ -224,7 +234,7 @@ BasePill {
width: parent.width width: parent.width
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: clearAllArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: clearAllArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
Row { Row {
anchors.fill: parent anchors.fill: parent
@@ -264,7 +274,7 @@ BasePill {
width: parent.width width: parent.width
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: savedItemsArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: savedItemsArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
Row { Row {
anchors.fill: parent anchors.fill: parent
+2 -2
View File
@@ -354,7 +354,7 @@ BasePill {
height: 20 height: 20
radius: 10 radius: 10
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: prevArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: prevArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
visible: root.playerAvailable visible: root.playerAvailable
opacity: (activePlayer && activePlayer.canGoPrevious) ? 1 : 0.3 opacity: (activePlayer && activePlayer.canGoPrevious) ? 1 : 0.3
@@ -411,7 +411,7 @@ BasePill {
height: 20 height: 20
radius: 10 radius: 10
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: nextArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: nextArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
visible: playerAvailable visible: playerAvailable
opacity: (activePlayer && activePlayer.canGoNext) ? 1 : 0.3 opacity: (activePlayer && activePlayer.canGoNext) ? 1 : 0.3
@@ -285,7 +285,7 @@ BasePill {
width: parent.width width: parent.width
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: tabArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: tabArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
Row { Row {
anchors.fill: parent anchors.fill: parent
@@ -327,7 +327,7 @@ BasePill {
width: parent.width width: parent.width
height: 30 height: 30
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: newNoteArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: newNoteArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
Row { Row {
anchors.fill: parent anchors.fill: parent
@@ -273,7 +273,7 @@ BasePill {
if (isFocused) { if (isFocused) {
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.2); return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.2);
} }
return mouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"; return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
} }
// App icon // App icon
@@ -528,7 +528,7 @@ BasePill {
if (isFocused) { if (isFocused) {
return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.2); return mouseArea.containsMouse ? Theme.primarySelected : Theme.withAlpha(Theme.primary, 0.2);
} }
return mouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent"; return mouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent";
} }
IconImage { IconImage {
@@ -738,6 +738,15 @@ BasePill {
sourceComponent: PanelWindow { sourceComponent: PanelWindow {
id: contextMenuWindow id: contextMenuWindow
WindowBlur {
targetWindow: contextMenuWindow
blurX: contextMenuRect.x
blurY: contextMenuRect.y
blurWidth: contextMenuWindow.isVisible ? contextMenuRect.width : 0
blurHeight: contextMenuWindow.isVisible ? contextMenuRect.height : 0
blurRadius: Theme.cornerRadius
}
property var currentWindow: null property var currentWindow: null
property bool isVisible: false property bool isVisible: false
property point anchorPos: Qt.point(0, 0) property point anchorPos: Qt.point(0, 0)
@@ -830,6 +839,7 @@ BasePill {
} }
Rectangle { Rectangle {
id: contextMenuRect
x: { x: {
if (contextMenuWindow.isVertical) { if (contextMenuWindow.isVertical) {
if (contextMenuWindow.edge === "left") { if (contextMenuWindow.edge === "left") {
@@ -858,13 +868,13 @@ BasePill {
height: 32 height: 32
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.width: 1 border.width: BlurService.enabled ? BlurService.borderWidth : 1
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12) border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: parent.radius radius: parent.radius
color: closeMouseArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: closeMouseArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
} }
StyledText { StyledText {
@@ -287,7 +287,7 @@ BasePill {
height: root.trayItemSize height: root.trayItemSize
anchors.centerIn: parent anchors.centerIn: parent
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: trayItemArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: trayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
border.width: dragHandler.dragging ? 2 : 0 border.width: dragHandler.dragging ? 2 : 0
border.color: Theme.primary border.color: Theme.primary
opacity: dragHandler.dragging ? 0.8 : 1.0 opacity: dragHandler.dragging ? 0.8 : 1.0
@@ -425,7 +425,7 @@ BasePill {
height: root.trayItemSize height: root.trayItemSize
anchors.centerIn: parent anchors.centerIn: parent
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: caretArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: caretArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
@@ -547,7 +547,7 @@ BasePill {
height: root.trayItemSize height: root.trayItemSize
anchors.centerIn: parent anchors.centerIn: parent
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: trayItemArea.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: trayItemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
border.width: dragHandler.dragging ? 2 : 0 border.width: dragHandler.dragging ? 2 : 0
border.color: Theme.primary border.color: Theme.primary
opacity: dragHandler.dragging ? 0.8 : 1.0 opacity: dragHandler.dragging ? 0.8 : 1.0
@@ -685,7 +685,7 @@ BasePill {
height: root.trayItemSize height: root.trayItemSize
anchors.centerIn: parent anchors.centerIn: parent
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: caretAreaVert.containsMouse ? Theme.widgetBaseHoverColor : "transparent" color: caretAreaVert.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : "transparent"
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
@@ -723,6 +723,16 @@ BasePill {
PanelWindow { PanelWindow {
id: overflowMenu id: overflowMenu
WindowBlur {
targetWindow: overflowMenu
blurX: menuContainer.x
blurY: menuContainer.y
blurWidth: root.menuOpen ? menuContainer.width : 0
blurHeight: root.menuOpen ? menuContainer.height : 0
blurRadius: Theme.cornerRadius
}
visible: root.menuOpen visible: root.menuOpen
screen: root.parentScreen screen: root.parentScreen
WlrLayershell.layer: WlrLayershell.Top WlrLayershell.layer: WlrLayershell.Top
@@ -990,6 +1000,15 @@ BasePill {
layer.samples: 4 layer.samples: 4
} }
Rectangle {
anchors.fill: parent
color: "transparent"
radius: Theme.cornerRadius
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
z: 100
}
Grid { Grid {
id: menuGrid id: menuGrid
anchors.centerIn: parent anchors.centerIn: parent
@@ -1030,7 +1049,7 @@ BasePill {
width: root.trayItemSize + 4 width: root.trayItemSize + 4
height: root.trayItemSize + 4 height: root.trayItemSize + 4
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: itemArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.withAlpha(Theme.surfaceContainer, 0) color: itemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0)
IconImage { IconImage {
id: menuIconImg id: menuIconImg
@@ -1191,6 +1210,15 @@ BasePill {
PanelWindow { PanelWindow {
id: menuWindow id: menuWindow
WindowBlur {
targetWindow: menuWindow
blurX: trayMenuContainer.x
blurY: trayMenuContainer.y
blurWidth: menuRoot.showMenu ? trayMenuContainer.width : 0
blurHeight: menuRoot.showMenu ? trayMenuContainer.height : 0
blurRadius: Theme.cornerRadius
}
WlrLayershell.namespace: "dms:tray-menu-window" WlrLayershell.namespace: "dms:tray-menu-window"
visible: menuRoot.showMenu && (menuRoot.trayItem?.hasMenu ?? false) visible: menuRoot.showMenu && (menuRoot.trayItem?.hasMenu ?? false)
WlrLayershell.layer: WlrLayershell.Top WlrLayershell.layer: WlrLayershell.Top
@@ -1302,7 +1330,7 @@ BasePill {
onClicked: mouse => { onClicked: mouse => {
const clickX = mouse.x + menuWindow.maskX; const clickX = mouse.x + menuWindow.maskX;
const clickY = mouse.y + menuWindow.maskY; const clickY = mouse.y + menuWindow.maskY;
const outsideContent = clickX < menuContainer.x || clickX > menuContainer.x + menuContainer.width || clickY < menuContainer.y || clickY > menuContainer.y + menuContainer.height; const outsideContent = clickX < trayMenuContainer.x || clickX > trayMenuContainer.x + trayMenuContainer.width || clickY < trayMenuContainer.y || clickY > trayMenuContainer.y + trayMenuContainer.height;
if (!outsideContent) if (!outsideContent)
return; return;
@@ -1360,7 +1388,7 @@ BasePill {
} }
Item { Item {
id: menuContainer id: trayMenuContainer
readonly property real rawWidth: Math.min(500, Math.max(250, menuColumn.implicitWidth + Theme.spacingS * 2)) readonly property real rawWidth: Math.min(500, Math.max(250, menuColumn.implicitWidth + Theme.spacingS * 2))
readonly property real rawHeight: Math.max(40, menuColumn.implicitHeight + Theme.spacingS * 2) readonly property real rawHeight: Math.max(40, menuColumn.implicitHeight + Theme.spacingS * 2)
@@ -1438,6 +1466,15 @@ BasePill {
layer.textureMirroring: ShaderEffectSource.MirrorVertically layer.textureMirroring: ShaderEffectSource.MirrorVertically
} }
Rectangle {
anchors.fill: parent
color: "transparent"
radius: Theme.cornerRadius
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
z: 100
}
QsMenuAnchor { QsMenuAnchor {
id: submenuHydrator id: submenuHydrator
anchor.window: menuWindow anchor.window: menuWindow
@@ -1470,7 +1507,7 @@ BasePill {
width: parent.width width: parent.width
height: 28 height: 28
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: visibilityToggleArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.withAlpha(Theme.surfaceContainer, 0) color: visibilityToggleArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0)
StyledText { StyledText {
anchors.left: parent.left anchors.left: parent.left
@@ -1523,7 +1560,7 @@ BasePill {
width: parent.width width: parent.width
height: 28 height: 28
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: backArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.withAlpha(Theme.surfaceContainer, 0) color: backArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0)
Row { Row {
anchors.left: parent.left anchors.left: parent.left
@@ -1574,7 +1611,7 @@ BasePill {
color: { color: {
if (menuEntry?.isSeparator) if (menuEntry?.isSeparator)
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2); return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2);
return itemArea.containsMouse ? Theme.widgetBaseHoverColor : Theme.withAlpha(Theme.surfaceContainer, 0); return itemArea.containsMouse ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.withAlpha(Theme.surfaceContainer, 0);
} }
MouseArea { MouseArea {
@@ -17,6 +17,7 @@ Item {
property real widgetHeight: 30 property real widgetHeight: 30
property real barThickness: 48 property real barThickness: 48
property var barConfig: null property var barConfig: null
property var blurBarWindow: null
property var hyprlandOverviewLoader: null property var hyprlandOverviewLoader: null
property var parentScreen: null property var parentScreen: null
property int _desktopEntriesUpdateTrigger: 0 property int _desktopEntriesUpdateTrigger: 0
@@ -1478,7 +1479,7 @@ Item {
readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active
readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected
readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary
readonly property real appOpacity: (modelData.active || isActive) ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6 readonly property real appOpacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
IconImage { IconImage {
id: rowAppIcon id: rowAppIcon
@@ -1647,7 +1648,7 @@ Item {
readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active readonly property bool appHighlightActive: SettingsData.workspaceActiveAppHighlightEnabled && modelData.active
readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected readonly property color appBorderColor: appHighlightActive ? focusedBorderColor : Theme.primarySelected
readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary readonly property color appGlyphColor: appHighlightActive ? focusedBorderColor : Theme.primary
readonly property real appOpacity: (modelData.active || isActive) ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6 readonly property real appOpacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
IconImage { IconImage {
id: colAppIcon id: colAppIcon
@@ -1845,5 +1846,27 @@ Item {
if (useExtWorkspace && !DMSService.activeSubscriptions.includes("extworkspace")) { if (useExtWorkspace && !DMSService.activeSubscriptions.includes("extworkspace")) {
DMSService.addSubscription("extworkspace"); DMSService.addSubscription("extworkspace");
} }
_updateBlurRegistration();
}
property bool _blurRegistered: false
readonly property bool _shouldBlur: BlurService.enabled && blurBarWindow && blurBarWindow.registerBlurWidget && !(barConfig?.noBackground ?? false) && root.visible && root.width > 0
on_ShouldBlurChanged: _updateBlurRegistration()
function _updateBlurRegistration() {
if (_shouldBlur && !_blurRegistered) {
blurBarWindow.registerBlurWidget(visualBackground);
_blurRegistered = true;
} else if (!_shouldBlur && _blurRegistered) {
if (blurBarWindow && blurBarWindow.unregisterBlurWidget)
blurBarWindow.unregisterBlurWidget(visualBackground);
_blurRegistered = false;
}
}
Component.onDestruction: {
if (_blurRegistered && blurBarWindow && blurBarWindow.unregisterBlurWidget)
blurBarWindow.unregisterBlurWidget(visualBackground);
} }
} }
+19 -1
View File
@@ -17,6 +17,15 @@ Variants {
delegate: PanelWindow { delegate: PanelWindow {
id: dock id: dock
WindowBlur {
targetWindow: dock
blurX: dockBackground.x + dockContainer.x + dockMouseArea.x + dockCore.x + dockSlide.x
blurY: dockBackground.y + dockContainer.y + dockMouseArea.y + dockCore.y + dockSlide.y
blurWidth: dock.hasApps && dock.reveal ? dockBackground.width : 0
blurHeight: dock.hasApps && dock.reveal ? dockBackground.height : 0
blurRadius: Theme.cornerRadius
}
WlrLayershell.namespace: "dms:dock" WlrLayershell.namespace: "dms:dock"
readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right readonly property bool isVertical: SettingsData.dockPosition === SettingsData.Position.Left || SettingsData.dockPosition === SettingsData.Position.Right
@@ -448,7 +457,7 @@ Variants {
height: { height: {
if (dock.isVertical) { if (dock.isVertical) {
// Keep the taller hit area regardless of the reveal state to prevent shrinking loop // Keep the taller hit area regardless of the reveal state to prevent shrinking loop
return Math.min(Math.max(dockBackground.height + 64, 200), screenHeight * 0.5); return Math.min(Math.max(dockBackground.height + 64, 200), maxDockHeight);
} }
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1; return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1;
} }
@@ -562,6 +571,15 @@ Variants {
color: Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency) color: Theme.withAlpha(Theme.surfaceContainer, backgroundTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
} }
Rectangle {
anchors.fill: parent
color: "transparent"
radius: Theme.cornerRadius
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
z: 100
}
} }
Shape { Shape {
+14
View File
@@ -73,6 +73,20 @@ Item {
id: appLayout id: appLayout
width: layoutFlow.width width: layoutFlow.width
height: layoutFlow.height height: layoutFlow.height
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Easing.OutCubic
}
}
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Easing.OutCubic
}
}
anchors.horizontalCenter: root.isVertical ? undefined : parent.horizontalCenter anchors.horizontalCenter: root.isVertical ? undefined : parent.horizontalCenter
anchors.verticalCenter: root.isVertical ? parent.verticalCenter : undefined anchors.verticalCenter: root.isVertical ? parent.verticalCenter : undefined
anchors.left: root.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined anchors.left: root.isVertical && SettingsData.dockPosition === SettingsData.Position.Left ? parent.left : undefined
+11 -2
View File
@@ -9,6 +9,15 @@ import qs.Widgets
PanelWindow { PanelWindow {
id: root id: root
WindowBlur {
targetWindow: root
blurX: menuContainer.x
blurY: menuContainer.y
blurWidth: root.visible ? menuContainer.width : 0
blurHeight: root.visible ? menuContainer.height : 0
blurRadius: Theme.cornerRadius
}
WlrLayershell.namespace: "dms:dock-context-menu" WlrLayershell.namespace: "dms:dock-context-menu"
property var appData: null property var appData: null
@@ -168,8 +177,8 @@ PanelWindow {
height: menuColumn.implicitHeight + Theme.spacingS * 2 height: menuColumn.implicitHeight + Theme.spacingS * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1 border.width: BlurService.enabled ? BlurService.borderWidth : 1
opacity: root.visible ? 1 : 0 opacity: root.visible ? 1 : 0
visible: opacity > 0 visible: opacity > 0
+41 -21
View File
@@ -39,6 +39,38 @@ Item {
lockerReadyArmed = true; lockerReadyArmed = true;
unlocking = false; unlocking = false;
pamState = ""; pamState = "";
if (pam)
pam.lockMessage = "";
}
function currentAuthFeedbackText() {
if (!pam)
return "";
if (pam.u2fState === "insert" && !pam.u2fPending)
return I18n.tr("Insert your security key...");
if (pam.u2fState === "waiting" && !pam.u2fPending)
return I18n.tr("Touch your security key...");
if (pam.lockMessage && pam.lockMessage.length > 0)
return pam.lockMessage;
if (root.pamState === "error")
return I18n.tr("Authentication error - try again");
if (root.pamState === "max")
return I18n.tr("Too many attempts - locked out");
if (root.pamState === "fail")
return I18n.tr("Incorrect password - try again");
if (pam.fprintState === "error") {
const detail = (pam.fprint.message || "").trim();
return detail.length > 0 ? I18n.tr("Fingerprint error: %1").arg(detail) : I18n.tr("Fingerprint error");
}
if (pam.fprintState === "max")
return I18n.tr("Maximum fingerprint attempts reached. Please use password.");
if (pam.fprintState === "fail")
return I18n.tr("Fingerprint not recognized (%1/%2). Please try again or use password.").arg(pam.fprint.tries).arg(SettingsData.maxFprintTries);
return "";
}
function authFeedbackIsHint() {
return pam && (pam.u2fState === "waiting" || pam.u2fState === "insert") && !pam.u2fPending;
} }
Component.onCompleted: { Component.onCompleted: {
@@ -1045,30 +1077,18 @@ Item {
} }
StyledText { StyledText {
id: authFeedbackText
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 20 Layout.preferredHeight: text.length > 0 ? Math.min(implicitHeight, Math.ceil(Theme.fontSizeSmall * 4.5)) : 0
text: { text: root.currentAuthFeedbackText()
if (pam.u2fState === "insert" && !pam.u2fPending) { color: root.authFeedbackIsHint() ? Theme.outline : Theme.error
return "Insert your security key...";
}
if (pam.u2fState === "waiting" && !pam.u2fPending) {
return "Touch your security key...";
}
if (root.pamState === "error") {
return "Authentication error - try again";
}
if (root.pamState === "max") {
return "Too many attempts - locked out";
}
if (root.pamState === "fail") {
return "Incorrect password - try again";
}
return "";
}
color: (pam.u2fState === "waiting" || pam.u2fState === "insert") ? Theme.outline : Theme.error
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
opacity: (root.pamState !== "" || ((pam.u2fState === "waiting" || pam.u2fState === "insert") && !pam.u2fPending)) ? 1 : 0 wrapMode: Text.WordWrap
maximumLineCount: 3
elide: Text.ElideRight
opacity: text.length > 0 ? 1 : 0
Behavior on opacity { Behavior on opacity {
NumberAnimation { NumberAnimation {
+23 -17
View File
@@ -34,14 +34,14 @@ Scope {
u2fPendingTimeout.running = false; u2fPendingTimeout.running = false;
passwdActiveTimeout.running = false; passwdActiveTimeout.running = false;
unlockRequestTimeout.running = false; unlockRequestTimeout.running = false;
u2fPending = false; root.u2fPending = false;
u2fState = ""; root.u2fState = "";
unlockInProgress = false; root.unlockInProgress = false;
} }
function recoverFromAuthStall(newState: string): void { function recoverFromAuthStall(newState: string): void {
resetAuthFlows(); resetAuthFlows();
state = newState; root.state = newState;
flashMsg(); flashMsg();
stateReset.restart(); stateReset.restart();
fprint.checkAvail(); fprint.checkAvail();
@@ -49,16 +49,16 @@ Scope {
} }
function completeUnlock(): void { function completeUnlock(): void {
if (!unlockInProgress) { if (!root.unlockInProgress) {
unlockInProgress = true; root.unlockInProgress = true;
passwd.abort(); passwd.abort();
fprint.abort(); fprint.abort();
u2f.abort(); u2f.abort();
errorRetry.running = false; errorRetry.running = false;
u2fErrorRetry.running = false; u2fErrorRetry.running = false;
u2fPendingTimeout.running = false; u2fPendingTimeout.running = false;
u2fPending = false; root.u2fPending = false;
u2fState = ""; root.u2fState = "";
unlockRequestTimeout.restart(); unlockRequestTimeout.restart();
unlockRequested(); unlockRequested();
} }
@@ -73,13 +73,13 @@ Scope {
} }
function cancelU2fPending(): void { function cancelU2fPending(): void {
if (!u2fPending) if (!root.u2fPending)
return; return;
u2f.abort(); u2f.abort();
u2fErrorRetry.running = false; u2fErrorRetry.running = false;
u2fPendingTimeout.running = false; u2fPendingTimeout.running = false;
u2fPending = false; root.u2fPending = false;
u2fState = ""; root.u2fState = "";
fprint.checkAvail(); fprint.checkAvail();
} }
@@ -91,9 +91,9 @@ Scope {
} }
FileView { FileView {
id: loginConfigWatcher id: nixosMarker
path: "/etc/pam.d/login" path: "/etc/NIXOS"
printErrors: false printErrors: false
} }
@@ -104,17 +104,23 @@ Scope {
printErrors: false printErrors: false
} }
// Detects Nix-installed DMS on non-NixOS systems
readonly property bool runningFromNixStore: Quickshell.shellDir.startsWith("/nix/store/")
PamContext { PamContext {
id: passwd id: passwd
config: dankshellConfigWatcher.loaded ? "dankshell" : "login" config: dankshellConfigWatcher.loaded ? "dankshell" : "login"
configDirectory: dankshellConfigWatcher.loaded || loginConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam" configDirectory: (dankshellConfigWatcher.loaded || nixosMarker.loaded || root.runningFromNixStore) ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
onMessageChanged: { onMessageChanged: {
if (message.startsWith("The account is locked")) if (message.startsWith("The account is locked")) {
root.lockMessage = message; root.lockMessage = message;
else if (root.lockMessage && message.endsWith(" left to unlock)")) } else if (root.lockMessage && message.endsWith(" left to unlock)")) {
root.lockMessage += "\n" + message; root.lockMessage += "\n" + message;
} else if (root.lockMessage && message && message.length > 0) {
root.lockMessage = "";
}
} }
onResponseRequiredChanged: { onResponseRequiredChanged: {
@@ -252,7 +258,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 !== "") if (message.toLowerCase().includes("touch"))
root.u2fState = "waiting"; root.u2fState = "waiting";
} }
@@ -173,12 +173,18 @@ DankPopout {
property var externalKeyboardController: null property var externalKeyboardController: null
property real cachedHeaderHeight: 32 property real cachedHeaderHeight: 32
readonly property real settingsMaxHeight: {
const screenH = root.screen ? root.screen.height : 1080;
const maxPopupH = screenH * 0.8;
const overhead = cachedHeaderHeight + Theme.spacingL * 2 + Theme.spacingM * 2;
return Math.max(200, maxPopupH - overhead - 150);
}
implicitHeight: { implicitHeight: {
let baseHeight = Theme.spacingL * 2; let baseHeight = Theme.spacingL * 2;
baseHeight += cachedHeaderHeight; baseHeight += cachedHeaderHeight;
baseHeight += Theme.spacingM * 2; baseHeight += Theme.spacingM * 2;
const settingsHeight = notificationSettings.expanded ? notificationSettings.contentHeight : 0; const settingsHeight = notificationSettings.expanded ? Math.min(notificationSettings.naturalContentHeight, settingsMaxHeight) : 0;
const currentListHeight = root.shouldBeVisible ? notificationList.stableContentHeight : notificationList.listContentHeight; const currentListHeight = root.shouldBeVisible ? notificationList.stableContentHeight : notificationList.listContentHeight;
let listHeight = notificationHeader.currentTab === 0 ? currentListHeight : Math.max(200, NotificationService.historyList.length * 80); let listHeight = notificationHeader.currentTab === 0 ? currentListHeight : Math.max(200, NotificationService.historyList.length * 80);
if (notificationHeader.currentTab === 0 && NotificationService.groupedNotifications.length === 0) { if (notificationHeader.currentTab === 0 && NotificationService.groupedNotifications.length === 0) {
@@ -272,6 +278,7 @@ DankPopout {
NotificationSettings { NotificationSettings {
id: notificationSettings id: notificationSettings
expanded: notificationHeader.showSettings expanded: notificationHeader.showSettings
maxAllowedHeight: notificationContent.settingsMaxHeight
} }
Item { Item {
@@ -6,10 +6,11 @@ Rectangle {
id: root id: root
property bool expanded: false property bool expanded: false
readonly property real contentHeight: contentColumn.height + Theme.spacingL * 2 property real maxAllowedHeight: 0
readonly property real naturalContentHeight: contentColumn.height + Theme.spacingL * 2
width: parent.width width: parent.width
height: expanded ? contentHeight : 0 height: expanded ? (maxAllowedHeight > 0 ? Math.min(naturalContentHeight, maxAllowedHeight) : naturalContentHeight) : 0
visible: expanded visible: expanded
clip: true clip: true
radius: Theme.cornerRadius radius: Theme.cornerRadius
@@ -105,13 +106,22 @@ Rectangle {
return Math.round(value / 60000) + " " + I18n.tr("minutes"); return Math.round(value / 60000) + " " + I18n.tr("minutes");
} }
Column { Flickable {
id: contentColumn id: settingsFlickable
anchors.top: parent.top anchors.fill: parent
anchors.left: parent.left contentHeight: contentColumn.height + Theme.spacingL * 2
anchors.right: parent.right clip: true
anchors.margins: Theme.spacingL flickableDirection: Flickable.VerticalFlick
spacing: Theme.spacingM boundsBehavior: Flickable.DragAndOvershootBounds
interactive: root.naturalContentHeight > root.height
Column {
id: contentColumn
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingL
spacing: Theme.spacingM
StyledText { StyledText {
text: I18n.tr("Notification Settings") text: I18n.tr("Notification Settings")
@@ -435,4 +445,5 @@ Rectangle {
} }
} }
} }
}
} }
@@ -1,6 +1,5 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Effects
import Quickshell import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Services.Notifications import Quickshell.Services.Notifications
@@ -11,6 +10,15 @@ import qs.Widgets
PanelWindow { PanelWindow {
id: win id: win
WindowBlur {
targetWindow: win
blurX: content.x + content.cardInset + swipeTx.x + tx.x
blurY: content.y + content.cardInset + swipeTx.y + tx.y
blurWidth: !win._finalized ? Math.max(0, content.width - content.cardInset * 2) : 0
blurHeight: !win._finalized ? Math.max(0, content.height - content.cardInset * 2) : 0
blurRadius: Theme.cornerRadius
}
WlrLayershell.namespace: "dms:notification-popup" WlrLayershell.namespace: "dms:notification-popup"
required property var notificationData required property var notificationData
@@ -436,6 +444,16 @@ PanelWindow {
} }
} }
Rectangle {
anchors.fill: parent
anchors.margins: content.cardInset
radius: Theme.cornerRadius
color: "transparent"
border.color: BlurService.borderColor
border.width: BlurService.borderWidth
z: 100
}
Item { Item {
id: backgroundContainer id: backgroundContainer
anchors.fill: parent anchors.fill: parent
+2 -2
View File
@@ -95,7 +95,7 @@ DankOSD {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
minimum: 0 minimum: 0
maximum: AudioService.sinkMaxVolume maximum: AudioService.sinkMaxVolume
enabled: AudioService.sink?.audio enabled: AudioService.sink?.audio ?? false
showValue: true showValue: true
unit: "%" unit: "%"
thumbOutlineColor: Theme.surfaceContainer thumbOutlineColor: Theme.surfaceContainer
@@ -207,7 +207,7 @@ DankOSD {
id: vertSliderArea id: vertSliderArea
anchors.fill: parent anchors.fill: parent
anchors.margins: -12 anchors.margins: -12
enabled: AudioService.sink?.audio enabled: AudioService.sink?.audio ?? false
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
+24 -1
View File
@@ -14,6 +14,7 @@ Item {
property real barThickness: 48 property real barThickness: 48
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null
property alias content: contentLoader.sourceComponent property alias content: contentLoader.sourceComponent
property bool isVerticalOrientation: axis?.isVertical ?? false property bool isVerticalOrientation: axis?.isVertical ?? false
property bool isFirst: false property bool isFirst: false
@@ -106,7 +107,7 @@ Item {
const rawTransparency = (root.barConfig && root.barConfig.widgetTransparency !== undefined) ? root.barConfig.widgetTransparency : 1.0; const rawTransparency = (root.barConfig && root.barConfig.widgetTransparency !== undefined) ? root.barConfig.widgetTransparency : 1.0;
const isHovered = root.enableBackgroundHover && (mouseArea.containsMouse || (root.isHovered || false)); const isHovered = root.enableBackgroundHover && (mouseArea.containsMouse || (root.isHovered || false));
const transparency = isHovered ? Math.max(0.3, rawTransparency) : rawTransparency; const transparency = isHovered ? Math.max(0.3, rawTransparency) : rawTransparency;
const baseColor = isHovered ? Theme.widgetBaseHoverColor : Theme.widgetBaseBackgroundColor; const baseColor = isHovered ? BlurService.hoverColor(Theme.widgetBaseHoverColor) : Theme.widgetBaseBackgroundColor;
if (Theme.widgetBackgroundHasAlpha) { if (Theme.widgetBackgroundHasAlpha) {
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * transparency); return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, baseColor.a * transparency);
@@ -169,4 +170,26 @@ Item {
root.wheel(wheelEvent); root.wheel(wheelEvent);
} }
} }
property bool _blurRegistered: false
readonly property bool _shouldBlur: BlurService.enabled && blurBarWindow && blurBarWindow.registerBlurWidget && !(barConfig?.noBackground ?? false) && root.visible && root.width > 0
on_ShouldBlurChanged: _updateBlurRegistration()
function _updateBlurRegistration() {
if (_shouldBlur && !_blurRegistered) {
blurBarWindow.registerBlurWidget(visualContent);
_blurRegistered = true;
} else if (!_shouldBlur && _blurRegistered) {
if (blurBarWindow && blurBarWindow.unregisterBlurWidget)
blurBarWindow.unregisterBlurWidget(visualContent);
_blurRegistered = false;
}
}
Component.onCompleted: _updateBlurRegistration()
Component.onDestruction: {
if (_blurRegistered && blurBarWindow && blurBarWindow.unregisterBlurWidget)
blurBarWindow.unregisterBlurWidget(visualContent);
}
} }
@@ -14,6 +14,7 @@ Item {
property real barThickness: 48 property real barThickness: 48
property real barSpacing: 4 property real barSpacing: 4
property var barConfig: null property var barConfig: null
property var blurBarWindow: null
property string pluginId: "" property string pluginId: ""
property var pluginService: null property var pluginService: null
@@ -182,6 +183,7 @@ Item {
barThickness: root.barThickness barThickness: root.barThickness
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow
content: root.horizontalBarPill content: root.horizontalBarPill
states: State { states: State {
@@ -241,6 +243,7 @@ Item {
barThickness: root.barThickness barThickness: root.barThickness
barSpacing: root.barSpacing barSpacing: root.barSpacing
barConfig: root.barConfig barConfig: root.barConfig
blurBarWindow: root.blurBarWindow
content: root.verticalBarPill content: root.verticalBarPill
isVerticalOrientation: true isVerticalOrientation: true
@@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import Quickshell import Quickshell
import qs.Common import qs.Common
import qs.Services
import qs.Widgets import qs.Widgets
Popup { Popup {
@@ -186,8 +187,8 @@ Popup {
contentItem: Rectangle { contentItem: Rectangle {
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08) border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1 border.width: BlurService.enabled ? BlurService.borderWidth : 1
Item { Item {
id: keyboardHandler id: keyboardHandler
+5 -5
View File
@@ -36,7 +36,7 @@ Item {
switch (reason) { switch (reason) {
case "ready": case "ready":
return SettingsData.greeterEnableFprint ? I18n.tr("Run Sync to apply. Fingerprint-only login may not unlock GNOME Keyring.") : I18n.tr("Only affects DMS-managed PAM. If greetd already includes pam_fprintd, fingerprint stays enabled."); return SettingsData.greeterEnableFprint ? I18n.tr("Authentication changes apply automatically. Fingerprint-only login may not unlock Keyring.") : I18n.tr("Only affects DMS-managed PAM. If greetd already includes pam_fprintd, fingerprint stays enabled.");
case "missing_enrollment": case "missing_enrollment":
if (SettingsData.greeterEnableFprint) if (SettingsData.greeterEnableFprint)
return I18n.tr("Enabled, but no prints are enrolled yet. Enroll fingerprints and run Sync."); return I18n.tr("Enabled, but no prints are enrolled yet. Enroll fingerprints and run Sync.");
@@ -60,7 +60,7 @@ Item {
switch (reason) { switch (reason) {
case "ready": case "ready":
return SettingsData.greeterEnableU2f ? I18n.tr("Run Sync to apply.") : I18n.tr("Available."); return SettingsData.greeterEnableU2f ? I18n.tr("Authentication changes apply automatically.") : I18n.tr("Available.");
case "missing_key_registration": case "missing_key_registration":
if (SettingsData.greeterEnableU2f) if (SettingsData.greeterEnableU2f)
return I18n.tr("Enabled, but no registered security key was found yet. Register a key and run Sync."); return I18n.tr("Enabled, but no registered security key was found yet. Register a key and run Sync.");
@@ -448,7 +448,7 @@ Item {
settingKey: "greeterStatus" settingKey: "greeterStatus"
StyledText { StyledText {
text: I18n.tr("Check sync status on demand. Sync copies your theme, settings, PAM config, and wallpaper to the login screen in one step. Must run Sync to apply changes.") text: I18n.tr("Check sync status on demand. Sync copies your theme, settings, and wallpaper configuration to the login screen. Authentication changes apply automatically.")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
width: parent.width width: parent.width
@@ -525,7 +525,7 @@ Item {
settingKey: "greeterAuth" settingKey: "greeterAuth"
StyledText { StyledText {
text: I18n.tr("Enable fingerprint or security key for DMS Greeter. Run Sync to apply and configure PAM.") text: I18n.tr("Enable fingerprint or security key for DMS Greeter. Authentication changes apply automatically.")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
width: parent.width width: parent.width
@@ -754,7 +754,7 @@ Item {
settingKey: "greeterDeps" settingKey: "greeterDeps"
StyledText { StyledText {
text: I18n.tr("DMS greeter needs: greetd, dms-greeter. Fingerprint: fprintd, pam_fprintd. Security keys: pam_u2f. Add your user to the greeter group. Sync checks sudo first and opens a terminal when interactive authentication is required.") text: I18n.tr("DMS greeter needs: greetd, dms-greeter. Fingerprint: fprintd, pam_fprintd. Security keys: pam_u2f. Add your user to the greeter group. Authentication changes apply automatically and may open a terminal when sudo authentication is required.")
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
width: parent.width width: parent.width
+12 -9
View File
@@ -831,6 +831,15 @@ Item {
checked: SessionData.searchAppActions checked: SessionData.searchAppActions
onToggled: checked => SessionData.setSearchAppActions(checked) onToggled: checked => SessionData.setSearchAppActions(checked)
} }
SettingsToggleRow {
settingKey: "rememberLastQuery"
tags: ["launcher", "remember", "last", "search", "query"]
text: I18n.tr("Remember Last Query")
description: I18n.tr("Autofill last remembered query when opened")
checked: SettingsData.rememberLastQuery
onToggled: checked => SettingsData.set("rememberLastQuery", checked)
}
} }
SettingsCard { SettingsCard {
@@ -1189,17 +1198,11 @@ Item {
if (diffMins < 1) if (diffMins < 1)
return I18n.tr("Last launched just now"); return I18n.tr("Last launched just now");
if (diffMins < 60) if (diffMins < 60)
return diffMins === 1 return diffMins === 1 ? I18n.tr("Last launched %1 minute ago").arg(diffMins) : I18n.tr("Last launched %1 minutes ago").arg(diffMins);
? I18n.tr("Last launched %1 minute ago").arg(diffMins)
: I18n.tr("Last launched %1 minutes ago").arg(diffMins);
if (diffHours < 24) if (diffHours < 24)
return diffHours === 1 return diffHours === 1 ? I18n.tr("Last launched %1 hour ago").arg(diffHours) : I18n.tr("Last launched %1 hours ago").arg(diffHours);
? I18n.tr("Last launched %1 hour ago").arg(diffHours)
: I18n.tr("Last launched %1 hours ago").arg(diffHours);
if (diffDays < 7) if (diffDays < 7)
return diffDays === 1 return diffDays === 1 ? I18n.tr("Last launched %1 day ago").arg(diffDays) : I18n.tr("Last launched %1 days ago").arg(diffDays);
? I18n.tr("Last launched %1 day ago").arg(diffDays)
: I18n.tr("Last launched %1 days ago").arg(diffDays);
return I18n.tr("Last launched %1").arg(date.toLocaleDateString()); return I18n.tr("Last launched %1").arg(date.toLocaleDateString());
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
+13 -4
View File
@@ -15,10 +15,10 @@ Item {
function lockFingerprintDescription() { function lockFingerprintDescription() {
switch (SettingsData.lockFingerprintReason) { switch (SettingsData.lockFingerprintReason) {
case "ready": case "ready":
return I18n.tr("Use fingerprint authentication for the lock screen."); return SettingsData.enableFprint ? I18n.tr("Authentication changes apply automatically.") : I18n.tr("Use fingerprint authentication for the lock screen.");
case "missing_enrollment": case "missing_enrollment":
if (SettingsData.enableFprint) if (SettingsData.enableFprint)
return I18n.tr("Enabled, but no prints are enrolled yet. Enroll fingerprints to use it."); return I18n.tr("Enabled, but no prints are enrolled yet. Authentication changes apply automatically once you enroll fingerprints.");
return I18n.tr("Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and enroll later."); return I18n.tr("Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and enroll later.");
case "missing_reader": case "missing_reader":
return SettingsData.enableFprint ? I18n.tr("Enabled, but no fingerprint reader was detected.") : I18n.tr("No fingerprint reader detected."); return SettingsData.enableFprint ? I18n.tr("Enabled, but no fingerprint reader was detected.") : I18n.tr("No fingerprint reader detected.");
@@ -32,10 +32,10 @@ Item {
function lockU2fDescription() { function lockU2fDescription() {
switch (SettingsData.lockU2fReason) { switch (SettingsData.lockU2fReason) {
case "ready": case "ready":
return I18n.tr("Use a security key for lock screen authentication.", "lock screen U2F security key setting"); return SettingsData.enableU2f ? I18n.tr("Authentication changes apply automatically.") : I18n.tr("Use a security key for lock screen authentication.", "lock screen U2F security key setting");
case "missing_key_registration": case "missing_key_registration":
if (SettingsData.enableU2f) if (SettingsData.enableU2f)
return I18n.tr("Enabled, but no registered security key was found yet. Register a key or update your U2F config."); return I18n.tr("Enabled, but no registered security key was found yet. Authentication changes apply automatically once your key is registered or your U2F config is updated.");
return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later."); return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.");
case "missing_pam_support": case "missing_pam_support":
return I18n.tr("Not available — install or configure pam_u2f."); return I18n.tr("Not available — install or configure pam_u2f.");
@@ -213,6 +213,15 @@ Item {
onToggled: checked => SettingsData.set("lockAtStartup", checked) onToggled: checked => SettingsData.set("lockAtStartup", checked)
} }
StyledText {
text: I18n.tr("Lock screen authentication changes apply automatically and may open a terminal when sudo authentication is required.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.Wrap
topPadding: Theme.spacingS
}
SettingsToggleRow { SettingsToggleRow {
settingKey: "enableFprint" settingKey: "enableFprint"
tags: ["lock", "screen", "fingerprint", "authentication", "biometric", "fprint"] tags: ["lock", "screen", "fingerprint", "authentication", "biometric", "fprint"]
+89 -1
View File
@@ -125,6 +125,15 @@ Item {
return Theme.warning; return Theme.warning;
} }
function openBlurBorderColorPicker() {
PopoutService.colorPickerModal.selectedColor = SettingsData.blurBorderCustomColor ?? "#ffffff";
PopoutService.colorPickerModal.pickerTitle = I18n.tr("Blur Border Color");
PopoutService.colorPickerModal.onColorSelectedCallback = function (color) {
SettingsData.set("blurBorderCustomColor", color.toString());
};
PopoutService.colorPickerModal.open();
}
function openM3ShadowColorPicker() { function openM3ShadowColorPicker() {
PopoutService.colorPickerModal.selectedColor = SettingsData.m3ElevationCustomColor ?? "#000000"; PopoutService.colorPickerModal.selectedColor = SettingsData.m3ElevationCustomColor ?? "#000000";
PopoutService.colorPickerModal.pickerTitle = I18n.tr("Shadow Color"); PopoutService.colorPickerModal.pickerTitle = I18n.tr("Shadow Color");
@@ -1816,6 +1825,77 @@ Item {
} }
} }
SettingsCard {
tab: "theme"
tags: ["blur", "background", "transparency", "glass", "frosted"]
title: I18n.tr("Background Blur")
settingKey: "blurEnabled"
iconName: "blur_on"
SettingsToggleRow {
tab: "theme"
tags: ["blur", "background", "transparency", "glass", "frosted"]
settingKey: "blurEnabled"
text: I18n.tr("Background Blur")
description: BlurService.available ? I18n.tr("Blur the background behind bars, popouts, modals, and notifications. Requires compositor support and configuration.") : I18n.tr("Requires a newer version of Quickshell")
checked: SettingsData.blurEnabled ?? false
enabled: BlurService.available
onToggled: checked => SettingsData.set("blurEnabled", checked)
}
SettingsDropdownRow {
tab: "theme"
tags: ["blur", "border", "outline", "edge"]
settingKey: "blurBorderColor"
text: I18n.tr("Blur Border Color")
description: I18n.tr("Border color around blurred surfaces")
visible: SettingsData.blurEnabled
options: [I18n.tr("Outline", "blur border color"), I18n.tr("Primary", "blur border color"), I18n.tr("Secondary", "blur border color"), I18n.tr("Text Color", "blur border color"), I18n.tr("Custom", "blur border color")]
currentValue: {
switch (SettingsData.blurBorderColor) {
case "primary":
return I18n.tr("Primary", "blur border color");
case "secondary":
return I18n.tr("Secondary", "blur border color");
case "surfaceText":
return I18n.tr("Text Color", "blur border color");
case "custom":
return I18n.tr("Custom", "blur border color");
default:
return I18n.tr("Outline", "blur border color");
}
}
onValueChanged: value => {
if (value === I18n.tr("Primary", "blur border color")) {
SettingsData.set("blurBorderColor", "primary");
} else if (value === I18n.tr("Secondary", "blur border color")) {
SettingsData.set("blurBorderColor", "secondary");
} else if (value === I18n.tr("Text Color", "blur border color")) {
SettingsData.set("blurBorderColor", "surfaceText");
} else if (value === I18n.tr("Custom", "blur border color")) {
SettingsData.set("blurBorderColor", "custom");
openBlurBorderColorPicker();
} else {
SettingsData.set("blurBorderColor", "outline");
}
}
}
SettingsSliderRow {
tab: "theme"
tags: ["blur", "border", "opacity"]
settingKey: "blurBorderOpacity"
text: I18n.tr("Blur Border Opacity")
visible: SettingsData.blurEnabled
value: Math.round((SettingsData.blurBorderOpacity ?? 1.0) * 100)
minimum: 0
maximum: 100
unit: "%"
defaultValue: 100
onSliderValueChanged: newValue => SettingsData.set("blurBorderOpacity", newValue / 100)
}
}
SettingsCard { SettingsCard {
tab: "theme" tab: "theme"
tags: ["niri", "layout", "gaps", "radius", "window", "border"] tags: ["niri", "layout", "gaps", "radius", "window", "border"]
@@ -2602,7 +2682,6 @@ Item {
onToggled: checked => SettingsData.set("matugenTemplateNeovim", checked) onToggled: checked => SettingsData.set("matugenTemplateNeovim", checked)
} }
SettingsDropdownRow { SettingsDropdownRow {
text: I18n.tr("Dark mode base") text: I18n.tr("Dark mode base")
tab: "theme" tab: "theme"
@@ -2671,6 +2750,15 @@ Item {
} }
} }
SettingsToggleRow {
text: I18n.tr("Follow DMS background color")
tags: ["matugen", "neovim", "terminal", "template"]
settingKey: "matugenTemplateNeovimSetBackground"
visible: neovimThemeToggle.visible && neovimThemeToggle.checked
checked: SettingsData.matugenTemplateNeovimSetBackground ?? true
onToggled: checked => SettingsData.set("matugenTemplateNeovimSetBackground", checked)
}
SettingsDivider { SettingsDivider {
visible: neovimThemeToggle.visible && neovimThemeToggle.checked visible: neovimThemeToggle.visible && neovimThemeToggle.checked
} }
@@ -108,10 +108,32 @@ Variants {
} }
} }
Connections {
target: wallpaperWindow
function onWidthChanged() {
root._renderSettling = true;
renderSettleTimer.restart();
}
function onHeightChanged() {
root._renderSettling = true;
renderSettleTimer.restart();
}
}
Connections {
target: Quickshell
function onScreensChanged() {
root._renderSettling = true;
renderSettleTimer.restart();
}
}
Connections { Connections {
target: NiriService target: NiriService
function onDisplayScalesChanged() { function onDisplayScalesChanged() {
root._recheckScreenScale(); root._recheckScreenScale();
root._renderSettling = true;
renderSettleTimer.restart();
} }
} }
@@ -119,6 +141,8 @@ Variants {
target: WlrOutputService target: WlrOutputService
function onWlrOutputAvailableChanged() { function onWlrOutputAvailableChanged() {
root._recheckScreenScale(); root._recheckScreenScale();
root._renderSettling = true;
renderSettleTimer.restart();
} }
} }
+88
View File
@@ -0,0 +1,88 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland // ! Import is needed despite what qmlls says
import qs.Common
Singleton {
id: root
property bool available: false
readonly property bool enabled: available && (SettingsData.blurEnabled ?? false)
readonly property color borderColor: {
if (!enabled)
return "transparent";
const opacity = SettingsData.blurBorderOpacity ?? 0.5;
switch (SettingsData.blurBorderColor ?? "outline") {
case "primary":
return Theme.withAlpha(Theme.primary, opacity);
case "secondary":
return Theme.withAlpha(Theme.secondary, opacity);
case "surfaceText":
return Theme.withAlpha(Theme.surfaceText, opacity);
case "custom":
return Theme.withAlpha(SettingsData.blurBorderCustomColor ?? "#ffffff", opacity);
default:
return Theme.withAlpha(Theme.outline, opacity);
}
}
readonly property int borderWidth: enabled ? 1 : 0
function hoverColor(baseColor, hoverAlpha) {
if (!enabled)
return baseColor;
return Theme.withAlpha(baseColor, hoverAlpha ?? 0.15);
}
function createBlurRegion(targetWindow) {
if (!available)
return null;
try {
const region = Qt.createQmlObject(`
import Quickshell
Region {}
`, targetWindow, "BlurRegion");
targetWindow.BackgroundEffect.blurRegion = region;
return region;
} catch (e) {
console.warn("BlurService: Failed to create blur region:", e);
return null;
}
}
function reapplyBlurRegion(targetWindow, region) {
if (!region || !available)
return;
try {
targetWindow.BackgroundEffect.blurRegion = region;
region.changed();
} catch (e) {}
}
function destroyBlurRegion(targetWindow, region) {
if (!region)
return;
try {
targetWindow.BackgroundEffect.blurRegion = null;
} catch (e) {}
region.destroy();
}
Component.onCompleted: {
try {
const test = Qt.createQmlObject(`
import Quickshell
Region { radius: 0 }
`, root, "BlurAvailabilityTest");
test.destroy();
available = true;
console.info("BlurService: Initialized with blur support");
} catch (e) {
console.info("BlurService: BackgroundEffect not available - blur disabled. Requires a newer version of Quickshell.");
}
}
}
+13 -5
View File
@@ -99,15 +99,14 @@ Singleton {
const lines = text.split('\n'); const lines = text.split('\n');
const trimmedLines = lines.map(line => line.replace(/\s+$/, '')).filter(line => line.length > 0); const trimmedLines = lines.map(line => line.replace(/\s+$/, '')).filter(line => line.length > 0);
configValidationOutput = trimmedLines.join('\n').trim(); configValidationOutput = trimmedLines.join('\n').trim();
if (hasInitialConnection) {
ToastService.showError("niri: failed to load config", configValidationOutput, "", "niri-config");
}
} }
} }
onExited: exitCode => { onExited: exitCode => {
if (exitCode === 0) { if (exitCode === 0) {
configValidationOutput = ""; configValidationOutput = "";
} else if (hasInitialConnection && configValidationOutput.length > 0) {
ToastService.showError("niri: failed to load config", configValidationOutput, "", "niri-config");
} }
} }
} }
@@ -629,9 +628,9 @@ Singleton {
if (pendingScreenshotPath && data.path === pendingScreenshotPath) { if (pendingScreenshotPath && data.path === pendingScreenshotPath) {
const editor = Quickshell.env("DMS_SCREENSHOT_EDITOR"); const editor = Quickshell.env("DMS_SCREENSHOT_EDITOR");
let command; let command;
if (editor === "satty") { if (editor === "satty" || !editor) {
command = ["satty", "-f", data.path]; command = ["satty", "-f", data.path];
} else if (editor === "swappy" || !editor) { } else if (editor === "swappy") {
command = ["swappy", "-f", data.path]; command = ["swappy", "-f", data.path];
} else { } else {
// Custom command with %path% placeholder // Custom command with %path% placeholder
@@ -1427,6 +1426,15 @@ Singleton {
} }
function renameWorkspace(name) { function renameWorkspace(name) {
if (!name || name.trim() === "") {
return send({
"Action": {
"UnsetWorkspaceName": {
"workspace": null
}
}
});
}
return send({ return send({
"Action": { "Action": {
"SetWorkspaceName": { "SetWorkspaceName": {
@@ -194,10 +194,11 @@ Singleton {
var timer = monitorTimers[screenName]; var timer = monitorTimers[screenName];
if (!timer && monitorTimerComponent && monitorTimerComponent.status === Component.Ready) { if (!timer && monitorTimerComponent && monitorTimerComponent.status === Component.Ready) {
var newTimers = Object.assign({}, monitorTimers); var newTimers = Object.assign({}, monitorTimers);
newTimers[screenName] = monitorTimerComponent.createObject(root); var newTimer = monitorTimerComponent.createObject(root);
newTimers[screenName].targetScreen = screenName; newTimer.targetScreen = screenName;
newTimers[screenName] = newTimer;
monitorTimers = newTimers; monitorTimers = newTimers;
timer = monitorTimers[screenName]; timer = newTimer;
} }
if (timer) { if (timer) {
timer.interval = settings.interval * 1000; timer.interval = settings.interval * 1000;
@@ -258,9 +259,10 @@ Singleton {
var process = monitorProcesses[screenName]; var process = monitorProcesses[screenName];
if (!process) { if (!process) {
var newProcesses = Object.assign({}, monitorProcesses); var newProcesses = Object.assign({}, monitorProcesses);
newProcesses[screenName] = monitorProcessComponent.createObject(root); var newProcess = monitorProcessComponent.createObject(root);
newProcesses[screenName] = newProcess;
monitorProcesses = newProcesses; monitorProcesses = newProcesses;
process = monitorProcesses[screenName]; process = newProcess;
} }
if (process) { if (process) {
@@ -290,9 +292,10 @@ Singleton {
var process = monitorProcesses[screenName]; var process = monitorProcesses[screenName];
if (!process) { if (!process) {
var newProcesses = Object.assign({}, monitorProcesses); var newProcesses = Object.assign({}, monitorProcesses);
newProcesses[screenName] = monitorProcessComponent.createObject(root); var newProcess = monitorProcessComponent.createObject(root);
newProcesses[screenName] = newProcess;
monitorProcesses = newProcesses; monitorProcesses = newProcesses;
process = monitorProcesses[screenName]; process = newProcess;
} }
if (process) { if (process) {
+1 -1
View File
@@ -3,7 +3,7 @@ import Quickshell
import qs.Common import qs.Common
import qs.Widgets import qs.Widgets
Flow { Row {
id: root id: root
property var model: [] property var model: []
+17 -3
View File
@@ -58,6 +58,13 @@ Item {
dropdownMenu.close(); dropdownMenu.close();
} }
function resetSearch() {
searchField.text = "";
dropdownMenu.fzfFinder = null;
dropdownMenu.searchQuery = "";
dropdownMenu.selectedIndex = -1;
}
width: compactMode ? dropdownWidth : parent.width width: compactMode ? dropdownWidth : parent.width
implicitHeight: compactMode ? 40 : Math.max(60, labelColumn.implicitHeight + Theme.spacingM) implicitHeight: compactMode ? 40 : Math.max(60, labelColumn.implicitHeight + Theme.spacingM)
@@ -206,7 +213,9 @@ Item {
fzfFinder = new Fzf.Finder(root.options, { fzfFinder = new Fzf.Finder(root.options, {
"selector": option => option, "selector": option => option,
"limit": 50, "limit": 50,
"casing": "case-insensitive" "casing": "case-insensitive",
"sort": true,
"tiebreakers": [(a, b, selector) => selector(a.item).length - selector(b.item).length]
}); });
} }
@@ -233,9 +242,14 @@ Item {
} }
onOpened: { onOpened: {
fzfFinder = null;
searchQuery = "";
selectedIndex = -1; selectedIndex = -1;
if (searchField.text.length > 0) {
initFinder();
searchQuery = searchField.text;
} else {
fzfFinder = null;
searchQuery = "";
}
} }
parent: Overlay.overlay parent: Overlay.overlay
+12 -2
View File
@@ -107,6 +107,16 @@ PanelWindow {
} }
WlrLayershell.exclusiveZone: -1 WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WindowBlur {
targetWindow: root
blurX: shadowBuffer
blurY: shadowBuffer
blurWidth: shouldBeVisible ? alignedWidth : 0
blurHeight: shouldBeVisible ? alignedHeight : 0
blurRadius: Theme.cornerRadius
}
color: "transparent" color: "transparent"
readonly property real dpr: CompositorService.getScreenScale(screen) readonly property real dpr: CompositorService.getScreenScale(screen)
@@ -263,8 +273,8 @@ PanelWindow {
anchors.fill: parent anchors.fill: parent
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, osdContainer.popupSurfaceAlpha) color: Theme.withAlpha(Theme.surfaceContainer, osdContainer.popupSurfaceAlpha)
border.color: Theme.outlineMedium border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium
border.width: 1 border.width: BlurService.enabled ? BlurService.borderWidth : 1
z: -1 z: -1
} }
+13 -2
View File
@@ -398,6 +398,17 @@ Item {
visible: false visible: false
color: "transparent" color: "transparent"
WindowBlur {
id: popoutBlur
targetWindow: contentWindow
readonly property real s: Math.min(1, contentContainer.scaleValue)
blurX: contentContainer.x + contentContainer.width * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr)
blurY: contentContainer.y + contentContainer.height * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr)
blurWidth: (shouldBeVisible && contentWrapper.opacity > 0) ? contentContainer.width * s : 0
blurHeight: (shouldBeVisible && contentWrapper.opacity > 0) ? contentContainer.height * s : 0
blurRadius: Theme.cornerRadius
}
WlrLayershell.namespace: root.layerNamespace WlrLayershell.namespace: root.layerNamespace
WlrLayershell.layer: { WlrLayershell.layer: {
switch (Quickshell.env("DMS_POPOUT_LAYER")) { switch (Quickshell.env("DMS_POPOUT_LAYER")) {
@@ -569,8 +580,8 @@ Item {
anchors.fill: parent anchors.fill: parent
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
border.color: Theme.outlineMedium border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium
border.width: 0 border.width: BlurService.borderWidth
} }
Loader { Loader {
+65
View File
@@ -0,0 +1,65 @@
import QtQuick
import qs.Services
Item {
id: root
visible: false
required property var targetWindow
property var blurItem: null
property real blurX: 0
property real blurY: 0
property real blurWidth: 0
property real blurHeight: 0
property real blurRadius: 0
property var _region: null
function _apply() {
if (!BlurService.enabled || !targetWindow) {
_cleanup();
return;
}
if (!_region)
_region = BlurService.createBlurRegion(targetWindow);
if (!_region)
return;
_region.item = Qt.binding(() => root.blurItem);
_region.x = Qt.binding(() => root.blurX);
_region.y = Qt.binding(() => root.blurY);
_region.width = Qt.binding(() => root.blurWidth);
_region.height = Qt.binding(() => root.blurHeight);
_region.radius = Qt.binding(() => root.blurRadius);
}
function _cleanup() {
if (!_region)
return;
BlurService.destroyBlurRegion(targetWindow, _region);
_region = null;
}
Connections {
target: BlurService
function onEnabledChanged() {
root._apply();
}
}
Connections {
target: root.targetWindow
function onVisibleChanged() {
if (root.targetWindow && root.targetWindow.visible) {
root._region = null;
root._apply();
}
}
}
Component.onCompleted: _apply()
Component.onDestruction: _cleanup()
}
+6 -2
View File
@@ -1,3 +1,7 @@
[templates.dmsneovim] [templates.dmsneovim-colors]
input_path = 'SHELL_DIR/matugen/templates/neovim.lua' input_path = 'SHELL_DIR/matugen/templates/neovim-colors.lua'
output_path = 'CONFIG_DIR/nvim/colors/dms.lua' output_path = 'CONFIG_DIR/nvim/colors/dms.lua'
[templates.dmsneovim-lualine]
input_path = 'SHELL_DIR/matugen/templates/neovim-lualine.lua'
output_path = 'CONFIG_DIR/nvim/lua/lualine/themes/dms.lua'
@@ -35,6 +35,15 @@ local function deepGet(t, k)
return t return t
end end
local mode = vim.system({ "dms", "ipc", "call", "theme", "getMode" }, { text = true }):wait().stdout
if mode ~= nil then
if mode:match("light") then
vim.o.background = "light"
elseif mode:match("dark") then
vim.o.background = "dark"
end
end
local current_file_path = debug.getinfo(1, "S").source:sub(2) local current_file_path = debug.getinfo(1, "S").source:sub(2)
local theme_base = deepGet(settings, { "matugenTemplateNeovimSettings", vim.o.background, "baseTheme" }) local theme_base = deepGet(settings, { "matugenTemplateNeovimSettings", vim.o.background, "baseTheme" })
or ("github_" .. vim.o.background) or ("github_" .. vim.o.background)
@@ -74,7 +83,9 @@ end
if not base46.theme_tables[theme_name] or base46.theme_tables[theme_name].type ~= vim.o.background then if not base46.theme_tables[theme_name] or base46.theme_tables[theme_name].type ~= vim.o.background then
local builtin = vim.deepcopy(assert(base46.get_builtin_theme(theme_base))) local builtin = vim.deepcopy(assert(base46.get_builtin_theme(theme_base)))
local harmonized = base46.theme_harmonize(builtin, "{{colors.source_color.default.hex}}", harmony) local harmonized = base46.theme_harmonize(builtin, "{{colors.source_color.default.hex}}", harmony)
harmonized = base46.theme_set_bg(harmonized, "{{colors.background.default.hex}}") if settings.matugenTemplateNeovimSetBackground ~= false then
harmonized = base46.theme_set_bg(harmonized, "{{colors.background.default.hex}}")
end
base46.theme_tables[theme_name] = harmonized base46.theme_tables[theme_name] = harmonized
end end
@@ -0,0 +1,3 @@
-- NOTE: this file should never be accessed if AvengeMedia/base46 is not installed,
-- the neovim-colors.lua template will fail first.
return require("lualine.themes._base46")("dms")
@@ -2,21 +2,21 @@
"wallpaper": "{{image}}", "wallpaper": "{{image}}",
"alpha": "100", "alpha": "100",
"colors": { "colors": {
"color0": "{{colors.background.default.hex}}", "color0": "{{colors.background.dark.hex}}",
"color1": "", "color1": "{{colors.surface_container_highest.light.hex}}",
"color2": "", "color2": "{{colors.surface_container_lowest.light.hex}}",
"color3": "", "color3": "{{colors.primary.light.hex}}",
"color4": "", "color4": "{{colors.tertiary_fixed.default.hex}}",
"color5": "", "color5": "{{colors.secondary.light.hex}}",
"color6": "", "color6": "{{colors.on_tertiary_fixed.default.hex}}",
"color7": "", "color7": "{{colors.surface_dim.light.hex}}",
"color8": "", "color8": "{{colors.on_secondary.light.hex}}",
"color9": "", "color9": "{{colors.error_container.default.hex}}",
"color10": "{{colors.primary.default.hex}}", "color10": "{{colors.primary.dark.hex}}",
"color11": "", "color11": "{{colors.on_error_container.default.hex}}",
"color12": "", "color12": "{{colors.on_primary_fixed_variant.default.hex}}",
"color13": "{{colors.surface_bright.default.hex}}", "color13": "{{colors.secondary.dark.hex}}",
"color14": "", "color14": "{{colors.inverse_primary.default.hex}}",
"color15": "{{colors.on_surface.default.hex}}" "color15": "{{colors.on_surface.dark.hex}}"
} }
} }
+3 -1
View File
@@ -27,7 +27,9 @@ LANGUAGES = {
"fa": "fa.json", "fa": "fa.json",
"fr": "fr.json", "fr": "fr.json",
"nl": "nl.json", "nl": "nl.json",
"ru": "ru.json" "ru": "ru.json",
"de": "de.json",
"sv": "sv.json"
} }
def error(msg): def error(msg):
+1 -1
View File
@@ -5,7 +5,7 @@
//@ pragma Env QT_WAYLAND_DISABLE_WINDOWDECORATION=1 //@ pragma Env QT_WAYLAND_DISABLE_WINDOWDECORATION=1
//@ pragma Env QT_QUICK_CONTROLS_STYLE=Material //@ pragma Env QT_QUICK_CONTROLS_STYLE=Material
//@ pragma UseQApplication //@ pragma UseQApplication
//@ pragma AppId com.danklinux.dms // ! TODO - replace pragma AppId when next QS releases, remove from GO launch injection.
import QtQuick import QtQuick
import Quickshell import Quickshell
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -329,6 +329,9 @@
"Adjust volume per scroll indent": { "Adjust volume per scroll indent": {
"Adjust volume per scroll indent": "" "Adjust volume per scroll indent": ""
}, },
"Adjusts contrast of generated colors (-100 = minimum, 0 = standard, 100 = maximum)": {
"Adjusts contrast of generated colors (-100 = minimum, 0 = standard, 100 = maximum)": ""
},
"Advanced": { "Advanced": {
"Advanced": "Avanzado" "Advanced": "Avanzado"
}, },
@@ -3160,6 +3163,9 @@
"Launch on dGPU": { "Launch on dGPU": {
"Launch on dGPU": "Iniciar con la tarjeta discreta" "Launch on dGPU": "Iniciar con la tarjeta discreta"
}, },
"Launch on dGPU by default": {
"Launch on dGPU by default": ""
},
"Launcher": { "Launcher": {
"Launcher": "Lanzador" "Launcher": "Lanzador"
}, },
@@ -3367,6 +3373,9 @@
"Material inspired shadows and elevation on modals, popouts, and dialogs": { "Material inspired shadows and elevation on modals, popouts, and dialogs": {
"Material inspired shadows and elevation on modals, popouts, and dialogs": "" "Material inspired shadows and elevation on modals, popouts, and dialogs": ""
}, },
"Matugen Contrast": {
"Matugen Contrast": ""
},
"Matugen Palette": { "Matugen Palette": {
"Matugen Palette": "Paleta de Matugen" "Matugen Palette": "Paleta de Matugen"
}, },
@@ -329,6 +329,9 @@
"Adjust volume per scroll indent": { "Adjust volume per scroll indent": {
"Adjust volume per scroll indent": "تنظیم حجم صدا به‌ازای هر پله اسکرول" "Adjust volume per scroll indent": "تنظیم حجم صدا به‌ازای هر پله اسکرول"
}, },
"Adjusts contrast of generated colors (-100 = minimum, 0 = standard, 100 = maximum)": {
"Adjusts contrast of generated colors (-100 = minimum, 0 = standard, 100 = maximum)": ""
},
"Advanced": { "Advanced": {
"Advanced": "پیشرفته" "Advanced": "پیشرفته"
}, },
@@ -3160,6 +3163,9 @@
"Launch on dGPU": { "Launch on dGPU": {
"Launch on dGPU": "اجرا با dGPU" "Launch on dGPU": "اجرا با dGPU"
}, },
"Launch on dGPU by default": {
"Launch on dGPU by default": ""
},
"Launcher": { "Launcher": {
"Launcher": "لانچر" "Launcher": "لانچر"
}, },
@@ -3367,6 +3373,9 @@
"Material inspired shadows and elevation on modals, popouts, and dialogs": { "Material inspired shadows and elevation on modals, popouts, and dialogs": {
"Material inspired shadows and elevation on modals, popouts, and dialogs": "" "Material inspired shadows and elevation on modals, popouts, and dialogs": ""
}, },
"Matugen Contrast": {
"Matugen Contrast": ""
},
"Matugen Palette": { "Matugen Palette": {
"Matugen Palette": "پالت رنگی Matugen" "Matugen Palette": "پالت رنگی Matugen"
}, },
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -329,6 +329,9 @@
"Adjust volume per scroll indent": { "Adjust volume per scroll indent": {
"Adjust volume per scroll indent": "" "Adjust volume per scroll indent": ""
}, },
"Adjusts contrast of generated colors (-100 = minimum, 0 = standard, 100 = maximum)": {
"Adjusts contrast of generated colors (-100 = minimum, 0 = standard, 100 = maximum)": ""
},
"Advanced": { "Advanced": {
"Advanced": "" "Advanced": ""
}, },
@@ -3160,6 +3163,9 @@
"Launch on dGPU": { "Launch on dGPU": {
"Launch on dGPU": "dGPUで起動" "Launch on dGPU": "dGPUで起動"
}, },
"Launch on dGPU by default": {
"Launch on dGPU by default": ""
},
"Launcher": { "Launcher": {
"Launcher": "ランチャー" "Launcher": "ランチャー"
}, },
@@ -3367,6 +3373,9 @@
"Material inspired shadows and elevation on modals, popouts, and dialogs": { "Material inspired shadows and elevation on modals, popouts, and dialogs": {
"Material inspired shadows and elevation on modals, popouts, and dialogs": "" "Material inspired shadows and elevation on modals, popouts, and dialogs": ""
}, },
"Matugen Contrast": {
"Matugen Contrast": ""
},
"Matugen Palette": { "Matugen Palette": {
"Matugen Palette": "Matugen Palette" "Matugen Palette": "Matugen Palette"
}, },
+136 -127
View File
@@ -6,13 +6,13 @@
"%1 DMS bind(s) may be overridden by config binds that come after the include.": "%1 DMS-bind(s) worden mogelijk overschreven door config-binds die na de include komen." "%1 DMS bind(s) may be overridden by config binds that come after the include.": "%1 DMS-bind(s) worden mogelijk overschreven door config-binds die na de include komen."
}, },
"%1 Sessions": { "%1 Sessions": {
"%1 Sessions": "" "%1 Sessions": "%1 sessies"
}, },
"%1 active session": { "%1 active session": {
"%1 active session": "" "%1 active session": "%1 actieve sessie"
}, },
"%1 active sessions": { "%1 active sessions": {
"%1 active sessions": "" "%1 active sessions": "%1 actieve sessies"
}, },
"%1 adapter(s), none connected": { "%1 adapter(s), none connected": {
"%1 adapter(s), none connected": "%1 adapter(s), geen verbonden" "%1 adapter(s), none connected": "%1 adapter(s), geen verbonden"
@@ -24,7 +24,7 @@
"%1 adapters, none connected": "%1 adapters, geen verbonden" "%1 adapters, none connected": "%1 adapters, geen verbonden"
}, },
"%1 character": { "%1 character": {
"%1 character": "" "%1 character": "%1 teken"
}, },
"%1 characters": { "%1 characters": {
"%1 characters": "%1 tekens" "%1 characters": "%1 tekens"
@@ -63,7 +63,7 @@
"%1 exists but is not included. Window rules won't apply.": "%1 bestaat maar is niet opgenomen. Vensterregels zijn niet van toepassing." "%1 exists but is not included. Window rules won't apply.": "%1 bestaat maar is niet opgenomen. Vensterregels zijn niet van toepassing."
}, },
"%1 filtered": { "%1 filtered": {
"%1 filtered": "" "%1 filtered": "%1 gefilterd"
}, },
"%1 is now included in config": { "%1 is now included in config": {
"%1 is now included in config": "%1 is nu opgenomen in config" "%1 is now included in config": "%1 is nu opgenomen in config"
@@ -78,28 +78,28 @@
"%1 printer(s)": "%1 printer(s)" "%1 printer(s)": "%1 printer(s)"
}, },
"%1 update": { "%1 update": {
"%1 update": "" "%1 update": "%1 update"
}, },
"%1 updates": { "%1 updates": {
"%1 updates": "" "%1 updates": "%1 updates"
}, },
"%1 variants": { "%1 variants": {
"%1 variants": "%1 varianten" "%1 variants": "%1 varianten"
}, },
"%1 wallpaper • %2 / %3": { "%1 wallpaper • %2 / %3": {
"%1 wallpaper • %2 / %3": "" "%1 wallpaper • %2 / %3": "%1 achtergrond • %2 / %3"
}, },
"%1 wallpapers • %2 / %3": { "%1 wallpapers • %2 / %3": {
"%1 wallpapers • %2 / %3": "" "%1 wallpapers • %2 / %3": "%1 achtergronden • %2 / %3"
}, },
"%1 widgets": { "%1 widgets": {
"%1 widgets": "%1 widgets" "%1 widgets": "%1 widgets"
}, },
"%1 window": { "%1 window": {
"%1 window": "" "%1 window": "%1 venster"
}, },
"%1 windows": { "%1 windows": {
"%1 windows": "" "%1 windows": "%1 vensters"
}, },
"%1m ago": { "%1m ago": {
"%1m ago": "%1m geleden" "%1m ago": "%1m geleden"
@@ -219,7 +219,7 @@
"90°": "90°" "90°": "90°"
}, },
"<a href=\"https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE\" style=\"text-decoration:none; color:%1;\">MIT License</a>": { "<a href=\"https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE\" style=\"text-decoration:none; color:%1;\">MIT License</a>": {
"<a href=\"https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE\" style=\"text-decoration:none; color:%1;\">MIT License</a>": "" "<a href=\"https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE\" style=\"text-decoration:none; color:%1;\">MIT License</a>": "<a href=\"https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE\" style=\"text-decoration:none; color:%1;\">MIT-licentie</a>"
}, },
"A file with this name already exists. Do you want to overwrite it?": { "A file with this name already exists. Do you want to overwrite it?": {
"A file with this name already exists. Do you want to overwrite it?": "Er bestaat al een bestand met deze naam. Wilt u het overschrijven?" "A file with this name already exists. Do you want to overwrite it?": "Er bestaat al een bestand met deze naam. Wilt u het overschrijven?"
@@ -329,6 +329,9 @@
"Adjust volume per scroll indent": { "Adjust volume per scroll indent": {
"Adjust volume per scroll indent": "Volume per scrollstap aanpassen" "Adjust volume per scroll indent": "Volume per scrollstap aanpassen"
}, },
"Adjusts contrast of generated colors (-100 = minimum, 0 = standard, 100 = maximum)": {
"Adjusts contrast of generated colors (-100 = minimum, 0 = standard, 100 = maximum)": ""
},
"Advanced": { "Advanced": {
"Advanced": "Geavanceerd" "Advanced": "Geavanceerd"
}, },
@@ -444,13 +447,13 @@
"Apps with notification popups muted. Unmute or delete to remove.": "Apps met gedempte meldingspop-ups. Dempen opheffen of verwijderen om te wissen." "Apps with notification popups muted. Unmute or delete to remove.": "Apps met gedempte meldingspop-ups. Dempen opheffen of verwijderen om te wissen."
}, },
"Are you sure you want to kill session \"%1\"?": { "Are you sure you want to kill session \"%1\"?": {
"Are you sure you want to kill session \"%1\"?": "" "Are you sure you want to kill session \"%1\"?": "Weet u zeker dat u sessie \"%1\" wilt beëindigen?"
}, },
"Arrange displays and configure resolution, refresh rate, and VRR": { "Arrange displays and configure resolution, refresh rate, and VRR": {
"Arrange displays and configure resolution, refresh rate, and VRR": "Beeldschermen rangschikken en resolutie, verversingssnelheid en VRR configureren" "Arrange displays and configure resolution, refresh rate, and VRR": "Beeldschermen rangschikken en resolutie, verversingssnelheid en VRR configureren"
}, },
"Attach": { "Attach": {
"Attach": "" "Attach": "Koppelen"
}, },
"Audio": { "Audio": {
"Audio": "Audio" "Audio": "Audio"
@@ -521,7 +524,7 @@
"Authenticate": "Authenticeren" "Authenticate": "Authenticeren"
}, },
"Authenticating...": { "Authenticating...": {
"Authenticating...": "" "Authenticating...": "Controleren..."
}, },
"Authentication": { "Authentication": {
"Authentication": "Authenticatie" "Authentication": "Authenticatie"
@@ -530,7 +533,7 @@
"Authentication Required": "Authenticatie vereist" "Authentication Required": "Authenticatie vereist"
}, },
"Authentication error - try again": { "Authentication error - try again": {
"Authentication error - try again": "" "Authentication error - try again": "Authenticatiefout - probeer het opnieuw"
}, },
"Authentication failed, please try again": { "Authentication failed, please try again": {
"Authentication failed, please try again": "Authenticatie mislukt, probeer het opnieuw" "Authentication failed, please try again": "Authenticatie mislukt, probeer het opnieuw"
@@ -635,7 +638,7 @@
"Available in Detailed and Forecast view modes": "Beschikbaar in weergavemodi Gedetailleerd en Voorspelling" "Available in Detailed and Forecast view modes": "Beschikbaar in weergavemodi Gedetailleerd en Voorspelling"
}, },
"Available.": { "Available.": {
"Available.": "" "Available.": "Beschikbaar."
}, },
"BSSID": { "BSSID": {
"BSSID": "BSSID" "BSSID": "BSSID"
@@ -677,7 +680,7 @@
"Battery": "Batterij" "Battery": "Batterij"
}, },
"Battery %1": { "Battery %1": {
"Battery %1": "" "Battery %1": "Accu %1"
}, },
"Battery Charge Limit": { "Battery Charge Limit": {
"Battery Charge Limit": "Batterijlaadlimiet" "Battery Charge Limit": "Batterijlaadlimiet"
@@ -899,13 +902,13 @@
"Check sync status on demand. Sync copies your theme, settings, PAM config, and wallpaper to the login screen in one step. Must run Sync to apply changes.": "Controleer de synchronisatiestatus op aanvraag. Sync kopieert je thema, instellingen, PAM-configuratie en achtergrond in één stap naar het aanmeldscherm. Je moet Sync uitvoeren om wijzigingen toe te passen." "Check sync status on demand. Sync copies your theme, settings, PAM config, and wallpaper to the login screen in one step. Must run Sync to apply changes.": "Controleer de synchronisatiestatus op aanvraag. Sync kopieert je thema, instellingen, PAM-configuratie en achtergrond in één stap naar het aanmeldscherm. Je moet Sync uitvoeren om wijzigingen toe te passen."
}, },
"Checking for updates...": { "Checking for updates...": {
"Checking for updates...": "" "Checking for updates...": "Zoeken naar updates..."
}, },
"Checking whether sudo authentication is needed…": { "Checking whether sudo authentication is needed…": {
"Checking whether sudo authentication is needed…": "Controleren of sudo-authenticatie vereist is…" "Checking whether sudo authentication is needed…": "Controleren of sudo-authenticatie vereist is…"
}, },
"Checking...": { "Checking...": {
"Checking...": "" "Checking...": "Controleren..."
}, },
"Choose Color": { "Choose Color": {
"Choose Color": "Kleur kiezen" "Choose Color": "Kleur kiezen"
@@ -1115,7 +1118,7 @@
"Column Width": "Kolombreedte" "Column Width": "Kolombreedte"
}, },
"Comma-separated list of session names to hide. Wrap in slashes for regex (e.g., /^_.*/).": { "Comma-separated list of session names to hide. Wrap in slashes for regex (e.g., /^_.*/).": {
"Comma-separated list of session names to hide. Wrap in slashes for regex (e.g., /^_.*/).": "" "Comma-separated list of session names to hide. Wrap in slashes for regex (e.g., /^_.*/).": "Komma-gescheiden lijst van te verbergen sessienamen. Gebruik slashes voor regex (bijv. /^_.*/)."
}, },
"Command": { "Command": {
"Command": "Opdracht" "Command": "Opdracht"
@@ -1328,7 +1331,7 @@
"Create Window Rule": "Vensterregel aanmaken" "Create Window Rule": "Vensterregel aanmaken"
}, },
"Create a new %1 session (n)": { "Create a new %1 session (n)": {
"Create a new %1 session (n)": "" "Create a new %1 session (n)": "Maak een nieuwe %1-sessie aan (n)"
}, },
"Create rule for:": { "Create rule for:": {
"Create rule for:": "Regel aanmaken voor:" "Create rule for:": "Regel aanmaken voor:"
@@ -1490,10 +1493,10 @@
"Dark Mode": "Donkere modus" "Dark Mode": "Donkere modus"
}, },
"Dark mode base": { "Dark mode base": {
"Dark mode base": "" "Dark mode base": "Donkere modus basis"
}, },
"Dark mode harmony": { "Dark mode harmony": {
"Dark mode harmony": "" "Dark mode harmony": "Donkere modus harmonie"
}, },
"Darken Modal Background": { "Darken Modal Background": {
"Darken Modal Background": "Modale achtergrond verduisteren" "Darken Modal Background": "Modale achtergrond verduisteren"
@@ -1601,7 +1604,7 @@
"Detailed": "Gedetailleerd" "Detailed": "Gedetailleerd"
}, },
"Details for \"%1": { "Details for \"%1": {
"Details for \"%1\"": "" "Details for \"%1\"": "Details voor \"%1\""
}, },
"Development": { "Development": {
"Development": "Ontwikkeling" "Development": "Ontwikkeling"
@@ -1916,34 +1919,34 @@
"Enabled": "Ingeschakeld" "Enabled": "Ingeschakeld"
}, },
"Enabled, but fingerprint availability could not be confirmed.": { "Enabled, but fingerprint availability could not be confirmed.": {
"Enabled, but fingerprint availability could not be confirmed.": "" "Enabled, but fingerprint availability could not be confirmed.": "Ingeschakeld, maar de beschikbaarheid van de vingerafdrukscanner kon niet worden bevestigd."
}, },
"Enabled, but no fingerprint reader was detected.": { "Enabled, but no fingerprint reader was detected.": {
"Enabled, but no fingerprint reader was detected.": "" "Enabled, but no fingerprint reader was detected.": "Ingeschakeld, maar er is geen vingerafdrukscanner gedetecteerd."
}, },
"Enabled, but no prints are enrolled yet. Enroll fingerprints and run Sync.": { "Enabled, but no prints are enrolled yet. Enroll fingerprints and run Sync.": {
"Enabled, but no prints are enrolled yet. Enroll fingerprints and run Sync.": "" "Enabled, but no prints are enrolled yet. Enroll fingerprints and run Sync.": "Ingeschakeld, maar er zijn nog geen afdrukken geregistreerd. Registreer vingerafdrukken en voer Sync uit."
}, },
"Enabled, but no prints are enrolled yet. Enroll fingerprints to use it.": { "Enabled, but no prints are enrolled yet. Enroll fingerprints to use it.": {
"Enabled, but no prints are enrolled yet. Enroll fingerprints to use it.": "" "Enabled, but no prints are enrolled yet. Enroll fingerprints to use it.": "Ingeschakeld, maar er zijn nog geen afdrukken geregistreerd. Registreer vingerafdrukken om dit te gebruiken."
}, },
"Enabled, but no registered security key was found yet. Register a key and run Sync.": { "Enabled, but no registered security key was found yet. Register a key and run Sync.": {
"Enabled, but no registered security key was found yet. Register a key and run Sync.": "" "Enabled, but no registered security key was found yet. Register a key and run Sync.": "Ingeschakeld, maar er is nog geen geregistreerde beveiligingssleutel gevonden. Registreer een sleutel en voer Sync uit."
}, },
"Enabled, but no registered security key was found yet. Register a key or update your U2F config.": { "Enabled, but no registered security key was found yet. Register a key or update your U2F config.": {
"Enabled, but no registered security key was found yet. Register a key or update your U2F config.": "" "Enabled, but no registered security key was found yet. Register a key or update your U2F config.": "Ingeschakeld, maar er is nog geen geregistreerde beveiligingssleutel gevonden. Registreer een sleutel of werk uw U2F-configuratie bij."
}, },
"Enabled, but security-key availability could not be confirmed.": { "Enabled, but security-key availability could not be confirmed.": {
"Enabled, but security-key availability could not be confirmed.": "" "Enabled, but security-key availability could not be confirmed.": "Ingeschakeld, maar de beschikbaarheid van de beveiligingssleutel kon niet worden bevestigd."
}, },
"Enabled. PAM already provides fingerprint auth.": { "Enabled. PAM already provides fingerprint auth.": {
"Enabled. PAM already provides fingerprint auth.": "" "Enabled. PAM already provides fingerprint auth.": "Ingeschakeld. PAM biedt al vingerafdrukauthenticatie."
}, },
"Enabled. PAM already provides security-key auth.": { "Enabled. PAM already provides security-key auth.": {
"Enabled. PAM already provides security-key auth.": "" "Enabled. PAM already provides security-key auth.": "Ingeschakeld. PAM biedt al authenticatie via beveiligingssleutel."
}, },
"Enabled. PAM provides fingerprint auth, but no prints are enrolled yet.": { "Enabled. PAM provides fingerprint auth, but no prints are enrolled yet.": {
"Enabled. PAM provides fingerprint auth, but no prints are enrolled yet.": "" "Enabled. PAM provides fingerprint auth, but no prints are enrolled yet.": "Ingeschakeld. PAM biedt vingerafdrukauthenticatie, maar er zijn nog geen afdrukken geregistreerd."
}, },
"Enabling WiFi...": { "Enabling WiFi...": {
"Enabling WiFi...": "Wifi inschakelen..." "Enabling WiFi...": "Wifi inschakelen..."
@@ -1967,7 +1970,7 @@
"Enter PIN for ": "Voer pincode in voor " "Enter PIN for ": "Voer pincode in voor "
}, },
"Enter a new name for session \"%1": { "Enter a new name for session \"%1": {
"Enter a new name for session \"%1\"": "" "Enter a new name for session \"%1\"": "Voer een nieuwe naam in voor sessie \"%1\""
}, },
"Enter a new name for this workspace": { "Enter a new name for this workspace": {
"Enter a new name for this workspace": "Voer een nieuwe naam in voor dit werkblad" "Enter a new name for this workspace": "Voer een nieuwe naam in voor dit werkblad"
@@ -1976,7 +1979,7 @@
"Enter a search query": "Voer een zoekopdracht in" "Enter a search query": "Voer een zoekopdracht in"
}, },
"Enter command or script path": { "Enter command or script path": {
"Enter command or script path": "" "Enter command or script path": "Voer opdracht of scriptpad in"
}, },
"Enter credentials for ": { "Enter credentials for ": {
"Enter credentials for ": "Voer inloggegevens in voor " "Enter credentials for ": "Voer inloggegevens in voor "
@@ -2069,7 +2072,7 @@
"Failed to cancel selected job": "Annuleren van geselecteerde taak mislukt" "Failed to cancel selected job": "Annuleren van geselecteerde taak mislukt"
}, },
"Failed to check for updates:\n%1": { "Failed to check for updates:\n%1": {
"Failed to check for updates:\n%1": "" "Failed to check for updates:\n%1": "Controleren op updates mislukt:\n%1"
}, },
"Failed to check pin limit": { "Failed to check pin limit": {
"Failed to check pin limit": "Controleren van vastmaaklimiet mislukt" "Failed to check pin limit": "Controleren van vastmaaklimiet mislukt"
@@ -2201,7 +2204,7 @@
"Failed to set night mode temperature": "Instellen van nachtmodus-temperatuur mislukt" "Failed to set night mode temperature": "Instellen van nachtmodus-temperatuur mislukt"
}, },
"Failed to set power profile": { "Failed to set power profile": {
"Failed to set power profile": "" "Failed to set power profile": "Instellen van energieprofiel mislukt"
}, },
"Failed to set profile image": { "Failed to set profile image": {
"Failed to set profile image": "Instellen van profielafbeelding mislukt" "Failed to set profile image": "Instellen van profielafbeelding mislukt"
@@ -2273,13 +2276,13 @@
"Fine-tune animation timing in milliseconds": "Animatietiming afstellen in milliseconden" "Fine-tune animation timing in milliseconds": "Animatietiming afstellen in milliseconden"
}, },
"Fingerprint availability could not be confirmed.": { "Fingerprint availability could not be confirmed.": {
"Fingerprint availability could not be confirmed.": "" "Fingerprint availability could not be confirmed.": "Beschikbaarheid van vingerafdrukscanner kon niet worden bevestigd."
}, },
"Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and enroll later.": { "Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and enroll later.": {
"Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and enroll later.": "" "Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and enroll later.": "Vingerafdrukscanner gedetecteerd, maar er zijn nog geen afdrukken geregistreerd. U kunt dit nu inschakelen en later registreren."
}, },
"Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and run Sync later.": { "Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and run Sync later.": {
"Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and run Sync later.": "" "Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and run Sync later.": "Vingerafdrukscanner gedetecteerd, maar er zijn nog geen afdrukken geregistreerd. U kunt dit nu inschakelen en later Sync uitvoeren."
}, },
"First Day of Week": { "First Day of Week": {
"First Day of Week": "Eerste dag van de week" "First Day of Week": "Eerste dag van de week"
@@ -2324,13 +2327,13 @@
"Focused Color": "Gefocuste kleur" "Focused Color": "Gefocuste kleur"
}, },
"Focused Monitor Only": { "Focused Monitor Only": {
"Focused Monitor Only": "" "Focused Monitor Only": "Alleen op actieve monitor"
}, },
"Focused Window": { "Focused Window": {
"Focused Window": "Actieve venster" "Focused Window": "Actieve venster"
}, },
"Focused monitor only": { "Focused monitor only": {
"Focused monitor only": "" "Focused monitor only": "Alleen op actieve monitor"
}, },
"Fog": { "Fog": {
"Fog": "Mist" "Fog": "Mist"
@@ -2408,10 +2411,10 @@
"Format Legend": "Legenda voor notatie" "Format Legend": "Legenda voor notatie"
}, },
"Found %1 package to update:": { "Found %1 package to update:": {
"Found %1 package to update:": "" "Found %1 package to update:": "%1 pakket gevonden om bij te werken:"
}, },
"Found %1 packages to update:": { "Found %1 packages to update:": {
"Found %1 packages to update:": "" "Found %1 packages to update:": "%1 pakketten gevonden om bij te werken:"
}, },
"Free VRAM/memory when the launcher is closed. May cause a slight delay when reopening.": { "Free VRAM/memory when the launcher is closed. May cause a slight delay when reopening.": {
"Free VRAM/memory when the launcher is closed. May cause a slight delay when reopening.": "VRAM/geheugen vrijmaken wanneer het startmenu gesloten is. Dit kan een lichte vertraging veroorzaken bij het heropenen." "Free VRAM/memory when the launcher is closed. May cause a slight delay when reopening.": "VRAM/geheugen vrijmaken wanneer het startmenu gesloten is. Dit kan een lichte vertraging veroorzaken bij het heropenen."
@@ -2438,7 +2441,7 @@
"GPU Temperature": "GPU-temperatuur" "GPU Temperature": "GPU-temperatuur"
}, },
"GPU temperature display": { "GPU temperature display": {
"GPU temperature display": "" "GPU temperature display": "GPU-temperatuurweergave"
}, },
"Games": { "Games": {
"Games": "Spellen" "Games": "Spellen"
@@ -2450,7 +2453,7 @@
"Gamma control not available. Requires DMS API v6+.": "Gammaregeling niet beschikbaar. Vereist DMS API v6+." "Gamma control not available. Requires DMS API v6+.": "Gammaregeling niet beschikbaar. Vereist DMS API v6+."
}, },
"Generate baseline GTK3/4 or QT5/QT6 (requires qt6ct-kde) configurations to follow DMS colors. Only needed once.<br /><br />It is recommended to configure <a href=\"https://github.com/AvengeMedia/DankMaterialShell/blob/master/README.md#Theming\" style=\"text-decoration:none; color:%1;\">adw-gtk3</a> prior to applying GTK themes.": { "Generate baseline GTK3/4 or QT5/QT6 (requires qt6ct-kde) configurations to follow DMS colors. Only needed once.<br /><br />It is recommended to configure <a href=\"https://github.com/AvengeMedia/DankMaterialShell/blob/master/README.md#Theming\" style=\"text-decoration:none; color:%1;\">adw-gtk3</a> prior to applying GTK themes.": {
"Generate baseline GTK3/4 or QT5/QT6 (requires qt6ct-kde) configurations to follow DMS colors. Only needed once.<br /><br />It is recommended to configure <a href=\"https://github.com/AvengeMedia/DankMaterialShell/blob/master/README.md#Theming\" style=\"text-decoration:none; color:%1;\">adw-gtk3</a> prior to applying GTK themes.": "" "Generate baseline GTK3/4 or QT5/QT6 (requires qt6ct-kde) configurations to follow DMS colors. Only needed once.<br /><br />It is recommended to configure <a href=\"https://github.com/AvengeMedia/DankMaterialShell/blob/master/README.md#Theming\" style=\"text-decoration:none; color:%1;\">adw-gtk3</a> prior to applying GTK themes.": "Genereer basisconfiguraties voor GTK3/4 of QT5/QT6 (vereist qt6ct-kde) om de DMS-kleuren te volgen. Slechts eenmalig nodig.<br /><br />Het wordt aanbevolen om <a href=\"https://github.com/AvengeMedia/DankMaterialShell/blob/master/README.md#Theming\" style=\"text-decoration:none; color:%1;\">adw-gtk3</a> te configureren voordat u GTK-thema's toepast."
}, },
"Generic device name | Generic device name fallback": { "Generic device name | Generic device name fallback": {
"device": "apparaat" "device": "apparaat"
@@ -2642,10 +2645,10 @@
"High-fidelity palette that preserves source hues.": "Natuurgetrouw palet dat de oorspronkelijke tinten behoudt." "High-fidelity palette that preserves source hues.": "Natuurgetrouw palet dat de oorspronkelijke tinten behoudt."
}, },
"Highlight Active Workspace App": { "Highlight Active Workspace App": {
"Highlight Active Workspace App": "" "Highlight Active Workspace App": "Actieve app in werkruimte markeren"
}, },
"Highlight the currently focused app inside workspace indicators": { "Highlight the currently focused app inside workspace indicators": {
"Highlight the currently focused app inside workspace indicators": "" "Highlight the currently focused app inside workspace indicators": "Markeer de momenteel actieve app in de werkruimte-indicatoren"
}, },
"History Settings": { "History Settings": {
"History Settings": "Geschiedenisinstellingen" "History Settings": "Geschiedenisinstellingen"
@@ -2699,7 +2702,7 @@
"Humidity": "Luchtvochtigheid" "Humidity": "Luchtvochtigheid"
}, },
"Hyprland Discord Server": { "Hyprland Discord Server": {
"Hyprland Discord Server": "" "Hyprland Discord Server": "Hyprland Discord-server"
}, },
"Hyprland Layout Overrides": { "Hyprland Layout Overrides": {
"Hyprland Layout Overrides": "Hyprland-indelingsoverschrijvingen" "Hyprland Layout Overrides": "Hyprland-indelingsoverschrijvingen"
@@ -2708,7 +2711,7 @@
"Hyprland Options": "Hyprland-opties" "Hyprland Options": "Hyprland-opties"
}, },
"Hyprland Website": { "Hyprland Website": {
"Hyprland Website": "" "Hyprland Website": "Hyprland-website"
}, },
"I Understand": { "I Understand": {
"I Understand": "Ik begrijp het" "I Understand": "Ik begrijp het"
@@ -2777,10 +2780,10 @@
"Incorrect password": "Onjuist wachtwoord" "Incorrect password": "Onjuist wachtwoord"
}, },
"Incorrect password - attempt %1 of %2 (lockout may follow)": { "Incorrect password - attempt %1 of %2 (lockout may follow)": {
"Incorrect password - attempt %1 of %2 (lockout may follow)": "" "Incorrect password - attempt %1 of %2 (lockout may follow)": "Onjuist wachtwoord - poging %1 van %2 (blokkering kan volgen)"
}, },
"Incorrect password - next failures may trigger account lockout": { "Incorrect password - next failures may trigger account lockout": {
"Incorrect password - next failures may trigger account lockout": "" "Incorrect password - next failures may trigger account lockout": "Onjuist wachtwoord - volgende fouten kunnen leiden tot blokkering van account"
}, },
"Indicator Style": { "Indicator Style": {
"Indicator Style": "Indicatorstijl" "Indicator Style": "Indicatorstijl"
@@ -3080,13 +3083,13 @@
"Keys": "Toetsen" "Keys": "Toetsen"
}, },
"Kill": { "Kill": {
"Kill": "" "Kill": "Beëindigen"
}, },
"Kill Process": { "Kill Process": {
"Kill Process": "Proces beëindigen" "Kill Process": "Proces beëindigen"
}, },
"Kill Session": { "Kill Session": {
"Kill Session": "" "Kill Session": "Sessie beëindigen"
}, },
"Ko-fi": { "Ko-fi": {
"Ko-fi": "Ko-fi" "Ko-fi": "Ko-fi"
@@ -3095,10 +3098,10 @@
"LED device": "LED-apparaat" "LED device": "LED-apparaat"
}, },
"LabWC IRC Channel": { "LabWC IRC Channel": {
"LabWC IRC Channel": "" "LabWC IRC Channel": "LabWC IRC-kanaal"
}, },
"LabWC Website": { "LabWC Website": {
"LabWC Website": "" "LabWC Website": "LabWC-website"
}, },
"Label for printer IP address or hostname input field": { "Label for printer IP address or hostname input field": {
"Host": "Host" "Host": "Host"
@@ -3160,6 +3163,9 @@
"Launch on dGPU": { "Launch on dGPU": {
"Launch on dGPU": "Starten op dGPU" "Launch on dGPU": "Starten op dGPU"
}, },
"Launch on dGPU by default": {
"Launch on dGPU by default": ""
},
"Launcher": { "Launcher": {
"Launcher": "Starter" "Launcher": "Starter"
}, },
@@ -3200,13 +3206,13 @@
"Light Snow Showers": "Lichte sneeuwbuien" "Light Snow Showers": "Lichte sneeuwbuien"
}, },
"Light mode base": { "Light mode base": {
"Light mode base": "" "Light mode base": "Lichte modus basis"
}, },
"Light mode harmony": { "Light mode harmony": {
"Light mode harmony": "" "Light mode harmony": "Lichte modus harmonie"
}, },
"Line: %1": { "Line: %1": {
"Line: %1": "" "Line: %1": "Regel: %1"
}, },
"Linear": { "Linear": {
"Linear": "Lineair" "Linear": "Lineair"
@@ -3290,7 +3296,7 @@
"Log Out": "Afmelden" "Log Out": "Afmelden"
}, },
"Logging in...": { "Logging in...": {
"Logging in...": "" "Logging in...": "Inloggen..."
}, },
"Login Authentication": { "Login Authentication": {
"Login Authentication": "Aanmeld-authenticatie" "Login Authentication": "Aanmeld-authenticatie"
@@ -3367,6 +3373,9 @@
"Material inspired shadows and elevation on modals, popouts, and dialogs": { "Material inspired shadows and elevation on modals, popouts, and dialogs": {
"Material inspired shadows and elevation on modals, popouts, and dialogs": "Op Material Design geïnspireerde schaduwen en diepte (elevation) op modale vensters, pop-outs en dialoogvensters" "Material inspired shadows and elevation on modals, popouts, and dialogs": "Op Material Design geïnspireerde schaduwen en diepte (elevation) op modale vensters, pop-outs en dialoogvensters"
}, },
"Matugen Contrast": {
"Matugen Contrast": ""
},
"Matugen Palette": { "Matugen Palette": {
"Matugen Palette": "Matugen-palet" "Matugen Palette": "Matugen-palet"
}, },
@@ -3464,7 +3473,7 @@
"Media Player Settings": "Instellingen mediaspeler" "Media Player Settings": "Instellingen mediaspeler"
}, },
"Media Players": { "Media Players": {
"Media Players": "" "Media Players": "Mediaspelers"
}, },
"Media Players (": { "Media Players (": {
"Media Players (": "Mediaspelers (" "Media Players (": "Mediaspelers ("
@@ -3581,13 +3590,13 @@
"Moving to Paused": "Overschakelen naar gepauzeerd" "Moving to Paused": "Overschakelen naar gepauzeerd"
}, },
"Multiplexer": { "Multiplexer": {
"Multiplexer": "" "Multiplexer": "Multiplexer"
}, },
"Multiplexer Type": { "Multiplexer Type": {
"Multiplexer Type": "" "Multiplexer Type": "Multiplexer-type"
}, },
"Multiplexers": { "Multiplexers": {
"Multiplexers": "" "Multiplexers": "Multiplexers"
}, },
"Music": { "Music": {
"Music": "Muziek" "Music": "Muziek"
@@ -3611,7 +3620,7 @@
"Named Workspace Icons": "Pictogrammen voor benoemde werkbladen" "Named Workspace Icons": "Pictogrammen voor benoemde werkbladen"
}, },
"Navigate": { "Navigate": {
"Navigate": "" "Navigate": "Navigeren"
}, },
"Navigation": { "Navigation": {
"Navigation": "Navigatie" "Navigation": "Navigatie"
@@ -3659,7 +3668,7 @@
"New Notification": "Nieuwe melding" "New Notification": "Nieuwe melding"
}, },
"New Session": { "New Session": {
"New Session": "" "New Session": "Nieuwe sessie"
}, },
"New Window Rule": { "New Window Rule": {
"New Window Rule": "Nieuwe vensterregel" "New Window Rule": "Nieuwe vensterregel"
@@ -3737,7 +3746,7 @@
"No VPN profiles": "Geen VPN-profielen" "No VPN profiles": "Geen VPN-profielen"
}, },
"No Weather": { "No Weather": {
"No Weather": "" "No Weather": "Geen weergegevens"
}, },
"No Weather Data": { "No Weather Data": {
"No Weather Data": "Geen weergegevens" "No Weather Data": "Geen weergegevens"
@@ -3749,7 +3758,7 @@
"No action": "Geen actie" "No action": "Geen actie"
}, },
"No active %1 sessions": { "No active %1 sessions": {
"No active %1 sessions": "" "No active %1 sessions": "Geen actieve %1-sessies"
}, },
"No adapters": { "No adapters": {
"No adapters": "Geen adapters" "No adapters": "Geen adapters"
@@ -3797,7 +3806,7 @@
"No files found": "Geen bestanden gevonden" "No files found": "Geen bestanden gevonden"
}, },
"No fingerprint reader detected.": { "No fingerprint reader detected.": {
"No fingerprint reader detected.": "" "No fingerprint reader detected.": "Geen vingerafdrukscanner gedetecteerd."
}, },
"No folders found": { "No folders found": {
"No folders found": "Geen mappen gevonden" "No folders found": "Geen mappen gevonden"
@@ -3806,7 +3815,7 @@
"No hidden apps.": "Geen verborgen apps." "No hidden apps.": "Geen verborgen apps."
}, },
"No information available": { "No information available": {
"No information available": "" "No information available": "Geen informatie beschikbaar"
}, },
"No items added yet": { "No items added yet": {
"No items added yet": "Nog geen items toegevoegd" "No items added yet": "Nog geen items toegevoegd"
@@ -3824,7 +3833,7 @@
"No matches": "Geen overeenkomsten" "No matches": "Geen overeenkomsten"
}, },
"No package manager found. Please install 'paru' or 'yay' on Arch-based systems to check for updates.": { "No package manager found. Please install 'paru' or 'yay' on Arch-based systems to check for updates.": {
"No package manager found. Please install 'paru' or 'yay' on Arch-based systems to check for updates.": "" "No package manager found. Please install 'paru' or 'yay' on Arch-based systems to check for updates.": "Geen pakketbeheerder gevonden. Installeer 'paru' of 'yay' op Arch-gebaseerde systemen om op updates te controleren."
}, },
"No plugin results": { "No plugin results": {
"No plugin results": "Geen plug-in resultaten" "No plugin results": "Geen plug-in resultaten"
@@ -3857,7 +3866,7 @@
"No saved clipboard entries": "Geen opgeslagen klemborditems" "No saved clipboard entries": "Geen opgeslagen klemborditems"
}, },
"No sessions found": { "No sessions found": {
"No sessions found": "" "No sessions found": "Geen sessies gevonden"
}, },
"No status output.": { "No status output.": {
"No status output.": "Geen statusuitvoer." "No status output.": "Geen statusuitvoer."
@@ -3872,7 +3881,7 @@
"No video found in folder": "Geen video gevonden in map" "No video found in folder": "Geen video gevonden in map"
}, },
"No wallpapers": { "No wallpapers": {
"No wallpapers": "" "No wallpapers": "Geen achtergronden"
}, },
"No wallpapers found\n\nClick the folder icon below to browse": { "No wallpapers found\n\nClick the folder icon below to browse": {
"No wallpapers found\n\nClick the folder icon below to browse": "Geen achtergronden gevonden\n\nKlik op het map-pictogram hieronder om te bladeren" "No wallpapers found\n\nClick the folder icon below to browse": "Geen achtergronden gevonden\n\nKlik op het map-pictogram hieronder om te bladeren"
@@ -3908,16 +3917,16 @@
"Not available — install fprintd and enroll fingerprints.": "Niet beschikbaar — installeer fprintd en registreer vingerafdrukken." "Not available — install fprintd and enroll fingerprints.": "Niet beschikbaar — installeer fprintd en registreer vingerafdrukken."
}, },
"Not available — install fprintd and pam_fprintd, or configure greetd PAM.": { "Not available — install fprintd and pam_fprintd, or configure greetd PAM.": {
"Not available — install fprintd and pam_fprintd, or configure greetd PAM.": "" "Not available — install fprintd and pam_fprintd, or configure greetd PAM.": "Niet beschikbaar — installeer fprintd en pam_fprintd, of configureer greetd PAM."
}, },
"Not available — install fprintd and pam_fprintd.": { "Not available — install fprintd and pam_fprintd.": {
"Not available — install fprintd and pam_fprintd.": "" "Not available — install fprintd and pam_fprintd.": "Niet beschikbaar — installeer fprintd en pam_fprintd."
}, },
"Not available — install or configure pam_u2f, or configure greetd PAM.": { "Not available — install or configure pam_u2f, or configure greetd PAM.": {
"Not available — install or configure pam_u2f, or configure greetd PAM.": "" "Not available — install or configure pam_u2f, or configure greetd PAM.": "Niet beschikbaar — installeer of configureer pam_u2f, of configureer greetd PAM."
}, },
"Not available — install or configure pam_u2f.": { "Not available — install or configure pam_u2f.": {
"Not available — install or configure pam_u2f.": "" "Not available — install or configure pam_u2f.": "Niet beschikbaar — installeer of configureer pam_u2f."
}, },
"Not available — install pam_u2f and enroll keys.": { "Not available — install pam_u2f and enroll keys.": {
"Not available — install pam_u2f and enroll keys.": "Niet beschikbaar — installeer pam_u2f en registreer sleutels." "Not available — install pam_u2f and enroll keys.": "Niet beschikbaar — installeer pam_u2f en registreer sleutels."
@@ -4004,7 +4013,7 @@
"Only adjust gamma based on time or location rules.": "Gamma alleen aanpassen op basis van tijd- of locatieregels." "Only adjust gamma based on time or location rules.": "Gamma alleen aanpassen op basis van tijd- of locatieregels."
}, },
"Only affects DMS-managed PAM. If greetd already includes pam_fprintd, fingerprint stays enabled.": { "Only affects DMS-managed PAM. If greetd already includes pam_fprintd, fingerprint stays enabled.": {
"Only affects DMS-managed PAM. If greetd already includes pam_fprintd, fingerprint stays enabled.": "" "Only affects DMS-managed PAM. If greetd already includes pam_fprintd, fingerprint stays enabled.": "Heeft alleen invloed op door DMS beheerde PAM. Als greetd al pam_fprintd bevat, blijft de vingerafdruk ingeschakeld."
}, },
"Only off for DMS-managed PAM lines. If greetd includes system-auth/common-auth/password-auth with pam_fprintd, fingerprint still stays enabled.": { "Only off for DMS-managed PAM lines. If greetd includes system-auth/common-auth/password-auth with pam_fprintd, fingerprint still stays enabled.": {
"Only off for DMS-managed PAM lines. If greetd includes system-auth/common-auth/password-auth with pam_fprintd, fingerprint still stays enabled.": "Alleen uitgeschakeld voor door DMS beheerde PAM-regels. Als greetd system-auth/common-auth/password-auth met pam_fprintd bevat, blijft de vingerafdruk alsnog ingeschakeld." "Only off for DMS-managed PAM lines. If greetd includes system-auth/common-auth/password-auth with pam_fprintd, fingerprint still stays enabled.": "Alleen uitgeschakeld voor door DMS beheerde PAM-regels. Als greetd system-auth/common-auth/password-auth met pam_fprintd bevat, blijft de vingerafdruk alsnog ingeschakeld."
@@ -4073,7 +4082,7 @@
"Output Area Full": "Uitvoergebied vol" "Output Area Full": "Uitvoergebied vol"
}, },
"Output Device": { "Output Device": {
"Output Device": "" "Output Device": "Uitvoerapparaat"
}, },
"Output Tray Missing": { "Output Tray Missing": {
"Output Tray Missing": "Uitvoerlade ontbreekt" "Output Tray Missing": "Uitvoerlade ontbreekt"
@@ -4106,7 +4115,7 @@
"Override global layout settings for this output": "Globale indelingsinstellingen overschrijven voor deze uitvoer" "Override global layout settings for this output": "Globale indelingsinstellingen overschrijven voor deze uitvoer"
}, },
"Override terminal with a custom command or script": { "Override terminal with a custom command or script": {
"Override terminal with a custom command or script": "" "Override terminal with a custom command or script": "Terminal overschrijven met een aangepaste opdracht of script"
}, },
"Override the global shadow with per-bar settings": { "Override the global shadow with per-bar settings": {
"Override the global shadow with per-bar settings": "Overschrijf de globale schaduw met instellingen per balk" "Override the global shadow with per-bar settings": "Overschrijf de globale schaduw met instellingen per balk"
@@ -4124,19 +4133,19 @@
"Overwrite": "Overschrijven" "Overwrite": "Overschrijven"
}, },
"PAM already provides fingerprint auth. Enable this to show it at login.": { "PAM already provides fingerprint auth. Enable this to show it at login.": {
"PAM already provides fingerprint auth. Enable this to show it at login.": "" "PAM already provides fingerprint auth. Enable this to show it at login.": "PAM biedt al vingerafdrukauthenticatie. Schakel dit in om het te tonen bij het inloggen."
}, },
"PAM already provides security-key auth. Enable this to show it at login.": { "PAM already provides security-key auth. Enable this to show it at login.": {
"PAM already provides security-key auth. Enable this to show it at login.": "" "PAM already provides security-key auth. Enable this to show it at login.": "PAM biedt al authenticatie via beveiligingssleutel. Schakel dit in om het te tonen bij het inloggen."
}, },
"PAM provides fingerprint auth, but availability could not be confirmed.": { "PAM provides fingerprint auth, but availability could not be confirmed.": {
"PAM provides fingerprint auth, but availability could not be confirmed.": "" "PAM provides fingerprint auth, but availability could not be confirmed.": "PAM biedt vingerafdrukauthenticatie, maar de beschikbaarheid kon niet worden bevestigd."
}, },
"PAM provides fingerprint auth, but no prints are enrolled yet.": { "PAM provides fingerprint auth, but no prints are enrolled yet.": {
"PAM provides fingerprint auth, but no prints are enrolled yet.": "" "PAM provides fingerprint auth, but no prints are enrolled yet.": "PAM biedt vingerafdrukauthenticatie, maar er zijn nog geen afdrukken geregistreerd."
}, },
"PAM provides fingerprint auth, but no reader was detected.": { "PAM provides fingerprint auth, but no reader was detected.": {
"PAM provides fingerprint auth, but no reader was detected.": "" "PAM provides fingerprint auth, but no reader was detected.": "PAM biedt vingerafdrukauthenticatie, maar er is geen scanner gedetecteerd."
}, },
"PIN": { "PIN": {
"PIN": "PIN" "PIN": "PIN"
@@ -4175,7 +4184,7 @@
"Password": "Wachtwoord" "Password": "Wachtwoord"
}, },
"Password...": { "Password...": {
"Password...": "" "Password...": "Wachtwoord..."
}, },
"Paste": { "Paste": {
"Paste": "Plakken" "Paste": "Plakken"
@@ -4330,7 +4339,7 @@
"Playback error: ": "Afspeelfout: " "Playback error: ": "Afspeelfout: "
}, },
"Please write a name for your new %1 session": { "Please write a name for your new %1 session": {
"Please write a name for your new %1 session": "" "Please write a name for your new %1 session": "Voer een naam in voor uw nieuwe %1-sessie"
}, },
"Plugged In": { "Plugged In": {
"Plugged In": "Aangesloten" "Plugged In": "Aangesloten"
@@ -4441,7 +4450,7 @@
"Preset Widths (%)": "Voor ingestelde breedtes (%)" "Preset Widths (%)": "Voor ingestelde breedtes (%)"
}, },
"Press 'n' or click 'New Session' to create one": { "Press 'n' or click 'New Session' to create one": {
"Press 'n' or click 'New Session' to create one": "" "Press 'n' or click 'New Session' to create one": "Druk op 'n' of klik op 'Nieuwe sessie' om er een aan te maken"
}, },
"Press key...": { "Press key...": {
"Press key...": "Druk op toets..." "Press key...": "Druk op toets..."
@@ -4627,7 +4636,7 @@
"Rename": "Hernoemen" "Rename": "Hernoemen"
}, },
"Rename Session": { "Rename Session": {
"Rename Session": "" "Rename Session": "Sessie hernoemen"
}, },
"Rename Workspace": { "Rename Workspace": {
"Rename Workspace": "Werkblad hernoemen" "Rename Workspace": "Werkblad hernoemen"
@@ -4645,7 +4654,7 @@
"Require holding button/key to confirm power off, restart, suspend, hibernate and logout": "Vereis het ingedrukt houden van knop/toets om uitschakelen, opnieuw opstarten, slaapstand, sluimerstand en afmelden te bevestigen" "Require holding button/key to confirm power off, restart, suspend, hibernate and logout": "Vereis het ingedrukt houden van knop/toets om uitschakelen, opnieuw opstarten, slaapstand, sluimerstand en afmelden te bevestigen"
}, },
"Required plugin: ": { "Required plugin: ": {
"Required plugin: ": "" "Required plugin: ": "Vereiste plug-in: "
}, },
"Requires 'dgop' tool": { "Requires 'dgop' tool": {
"Requires 'dgop' tool": "Vereist 'dgop'-tool" "Requires 'dgop' tool": "Vereist 'dgop'-tool"
@@ -4684,7 +4693,7 @@
"Restart the DankMaterialShell": "Herstart de DankMaterialShell" "Restart the DankMaterialShell": "Herstart de DankMaterialShell"
}, },
"Restore Special Workspace Windows": { "Restore Special Workspace Windows": {
"Restore Special Workspace Windows": "" "Restore Special Workspace Windows": "Speciale werkruimtevensters herstellen"
}, },
"Resume": { "Resume": {
"Resume": "Hervatten" "Resume": "Hervatten"
@@ -4852,7 +4861,7 @@
"Scroll Factor": "Scrollfactor" "Scroll Factor": "Scrollfactor"
}, },
"Scroll GitHub": { "Scroll GitHub": {
"Scroll GitHub": "" "Scroll GitHub": "Scroll GitHub"
}, },
"Scroll Wheel": { "Scroll Wheel": {
"Scroll Wheel": "Scrollwiel" "Scroll Wheel": "Scrollwiel"
@@ -4903,7 +4912,7 @@
"Search plugins...": "Plug-ins zoeken..." "Search plugins...": "Plug-ins zoeken..."
}, },
"Search sessions...": { "Search sessions...": {
"Search sessions...": "" "Search sessions...": "Sessies zoeken..."
}, },
"Search widgets...": { "Search widgets...": {
"Search widgets...": "Widgets zoeken..." "Search widgets...": "Widgets zoeken..."
@@ -4924,10 +4933,10 @@
"Security": "Beveiliging" "Security": "Beveiliging"
}, },
"Security-key availability could not be confirmed.": { "Security-key availability could not be confirmed.": {
"Security-key availability could not be confirmed.": "" "Security-key availability could not be confirmed.": "Beschikbaarheid van beveiligingssleutel kon niet worden bevestigd."
}, },
"Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.": { "Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.": {
"Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.": "" "Security-key support was detected, but no registered key was found yet. You can enable this now and register one later.": "Ondersteuning voor beveiligingssleutels gedetecteerd, maar er is nog geen geregistreerde sleutel gevonden. U kunt dit nu inschakelen en later een sleutel registreren."
}, },
"Select": { "Select": {
"Select": "Selecteren" "Select": "Selecteren"
@@ -5011,7 +5020,7 @@
"Server": "Server" "Server": "Server"
}, },
"Session Filter": { "Session Filter": {
"Session Filter": "" "Session Filter": "Sessiefilter"
}, },
"Set custom name": { "Set custom name": {
"Set custom name": "Aangepaste naam instellen" "Set custom name": "Aangepaste naam instellen"
@@ -5071,7 +5080,7 @@
"Short": "Kort" "Short": "Kort"
}, },
"Shortcut (%1)": { "Shortcut (%1)": {
"Shortcut (%1)": "" "Shortcut (%1)": "Sneltoets (%1)"
}, },
"Shortcuts": { "Shortcuts": {
"Shortcuts": "Sneltoetsen" "Shortcuts": "Sneltoetsen"
@@ -5209,7 +5218,7 @@
"Show Weather Condition": "Toon weersomstandigheid" "Show Weather Condition": "Toon weersomstandigheid"
}, },
"Show Week Number": { "Show Week Number": {
"Show Week Number": "" "Show Week Number": "Weeknummer tonen"
}, },
"Show Welcome": { "Show Welcome": {
"Show Welcome": "Toon welkom" "Show Welcome": "Toon welkom"
@@ -5251,10 +5260,10 @@
"Show launcher overlay when typing in Niri overview. Disable to use another launcher.": "Starter-overlay tonen bij typen in Niri-overzicht. Schakel uit om een andere starter te gebruiken." "Show launcher overlay when typing in Niri overview. Disable to use another launcher.": "Starter-overlay tonen bij typen in Niri-overzicht. Schakel uit om een andere starter te gebruiken."
}, },
"Show notification popups only on the currently focused monitor": { "Show notification popups only on the currently focused monitor": {
"Show notification popups only on the currently focused monitor": "" "Show notification popups only on the currently focused monitor": "Meldingspop-ups alleen op de momenteel actieve monitor tonen"
}, },
"Show notifications only on the currently focused monitor": { "Show notifications only on the currently focused monitor": {
"Show notifications only on the currently focused monitor": "" "Show notifications only on the currently focused monitor": "Meldingen alleen op de momenteel actieve monitor tonen"
}, },
"Show on Last Display": { "Show on Last Display": {
"Show on Last Display": "Tonen op laatste beeldscherm" "Show on Last Display": "Tonen op laatste beeldscherm"
@@ -5317,7 +5326,7 @@
"Show weather information in top bar and control center": "Weerinformatie tonen in bovenbalk en bedieningspaneel" "Show weather information in top bar and control center": "Weerinformatie tonen in bovenbalk en bedieningspaneel"
}, },
"Show week number in the calendar": { "Show week number in the calendar": {
"Show week number in the calendar": "" "Show week number in the calendar": "Toon weeknummer in de kalender"
}, },
"Show workspace index numbers in the top bar workspace switcher": { "Show workspace index numbers in the top bar workspace switcher": {
"Show workspace index numbers in the top bar workspace switcher": "Werkbladindexnummers tonen in de werkbladwisselaar in de bovenbalk" "Show workspace index numbers in the top bar workspace switcher": "Werkbladindexnummers tonen in de werkbladwisselaar in de bovenbalk"
@@ -5491,7 +5500,7 @@
"Swap": "Swap" "Swap": "Swap"
}, },
"Sway Website": { "Sway Website": {
"Sway Website": "" "Sway Website": "Sway-website"
}, },
"Switch User": { "Switch User": {
"Switch User": "Ander account" "Switch User": "Ander account"
@@ -5578,7 +5587,7 @@
"Tab/Shift+Tab: Nav • ←→↑↓: Grid Nav • Enter/Space: Select": "Tab/Shift+Tab: Nav • ←→↑↓: Raster Nav • Enter/Spatie: Selecteren" "Tab/Shift+Tab: Nav • ←→↑↓: Grid Nav • Enter/Space: Select": "Tab/Shift+Tab: Nav • ←→↑↓: Raster Nav • Enter/Spatie: Selecteren"
}, },
"Terminal": { "Terminal": {
"Terminal": "" "Terminal": "Terminal"
}, },
"Terminal custom additional parameters": { "Terminal custom additional parameters": {
"Terminal custom additional parameters": "Aangepaste extra parameters terminal" "Terminal custom additional parameters": "Aangepaste extra parameters terminal"
@@ -5590,7 +5599,7 @@
"Terminal fallback opened. Complete sync there; it will close automatically when done.": "Terminal fallback geopend. Voltooi de synchronisatie daar; de terminal sluit automatisch wanneer dit is voltooid." "Terminal fallback opened. Complete sync there; it will close automatically when done.": "Terminal fallback geopend. Voltooi de synchronisatie daar; de terminal sluit automatisch wanneer dit is voltooid."
}, },
"Terminal multiplexer backend to use": { "Terminal multiplexer backend to use": {
"Terminal multiplexer backend to use": "" "Terminal multiplexer backend to use": "Te gebruiken terminal-multiplexer-backend"
}, },
"Terminal opened. Complete sync authentication there; it will close automatically when done.": { "Terminal opened. Complete sync authentication there; it will close automatically when done.": {
"Terminal opened. Complete sync authentication there; it will close automatically when done.": "Terminal geopend. Voltooi de sync-authenticatie daar; de terminal sluit automatisch wanneer dit is voltooid." "Terminal opened. Complete sync authentication there; it will close automatically when done.": "Terminal geopend. Voltooi de sync-authenticatie daar; de terminal sluit automatisch wanneer dit is voltooid."
@@ -5617,7 +5626,7 @@
"The below settings will modify your GTK and Qt settings. If you wish to preserve your current configurations, please back them up (qt5ct.conf|qt6ct.conf and ~/.config/gtk-3.0|gtk-4.0).": "De onderstaande instellingen wijzigen uw GTK- en Qt-instellingen. Als u uw huidige configuraties wilt behouden, maak er dan een back-up van (qt5ct.conf|qt6ct.conf en ~/.config/gtk-3.0|gtk-4.0)." "The below settings will modify your GTK and Qt settings. If you wish to preserve your current configurations, please back them up (qt5ct.conf|qt6ct.conf and ~/.config/gtk-3.0|gtk-4.0).": "De onderstaande instellingen wijzigen uw GTK- en Qt-instellingen. Als u uw huidige configuraties wilt behouden, maak er dan een back-up van (qt5ct.conf|qt6ct.conf en ~/.config/gtk-3.0|gtk-4.0)."
}, },
"The custom command used when attaching to sessions (receives the session name as the first argument)": { "The custom command used when attaching to sessions (receives the session name as the first argument)": {
"The custom command used when attaching to sessions (receives the session name as the first argument)": "" "The custom command used when attaching to sessions (receives the session name as the first argument)": "De aangepaste opdracht die wordt gebruikt bij het koppelen aan sessies (ontvangt de sessienaam als eerste argument)"
}, },
"The job queue of this printer is empty": { "The job queue of this printer is empty": {
"The job queue of this printer is empty": "De wachtrij van deze printer is leeg" "The job queue of this printer is empty": "De wachtrij van deze printer is leeg"
@@ -5746,7 +5755,7 @@
"Toner Low": "Toner laag" "Toner Low": "Toner laag"
}, },
"Too many failed attempts - account may be locked": { "Too many failed attempts - account may be locked": {
"Too many failed attempts - account may be locked": "" "Too many failed attempts - account may be locked": "Te veel mislukte pogingen - account is mogelijk geblokkeerd"
}, },
"Tools": { "Tools": {
"Tools": "Hulpmiddelen" "Tools": "Hulpmiddelen"
@@ -5884,7 +5893,7 @@
"Unknown Title": "Onbekende titel" "Unknown Title": "Onbekende titel"
}, },
"Unknown Track": { "Unknown Track": {
"Unknown Track": "" "Unknown Track": "Onbekend nummer"
}, },
"Unload on Close": { "Unload on Close": {
"Unload on Close": "Ontladen bij sluiten" "Unload on Close": "Ontladen bij sluiten"
@@ -5920,7 +5929,7 @@
"Untrust": "Niet vertrouwen" "Untrust": "Niet vertrouwen"
}, },
"Up to date": { "Up to date": {
"Up to date": "" "Up to date": "Bijgewerkt"
}, },
"Update": { "Update": {
"Update": "Bijwerken" "Update": "Bijwerken"
@@ -5995,7 +6004,7 @@
"Use desktop wallpaper": "Bureaubladachtergrond gebruiken" "Use desktop wallpaper": "Bureaubladachtergrond gebruiken"
}, },
"Use fingerprint authentication for the lock screen.": { "Use fingerprint authentication for the lock screen.": {
"Use fingerprint authentication for the lock screen.": "" "Use fingerprint authentication for the lock screen.": "Gebruik vingerafdrukauthenticatie voor het vergrendelscherm."
}, },
"Use fingerprint reader for lock screen authentication (requires enrolled fingerprints)": { "Use fingerprint reader for lock screen authentication (requires enrolled fingerprints)": {
"Use fingerprint reader for lock screen authentication (requires enrolled fingerprints)": "Vingerafdruklezer gebruiken voor vergrendelschermverificatie (vereist geregistreerde vingerafdrukken)" "Use fingerprint reader for lock screen authentication (requires enrolled fingerprints)": "Vingerafdruklezer gebruiken voor vergrendelschermverificatie (vereist geregistreerde vingerafdrukken)"
@@ -6034,7 +6043,7 @@
"Username": "Gebruikersnaam" "Username": "Gebruikersnaam"
}, },
"Username...": { "Username...": {
"Username...": "" "Username...": "Gebruikersnaam..."
}, },
"Uses sunrise/sunset times based on your location.": { "Uses sunrise/sunset times based on your location.": {
"Uses sunrise/sunset times based on your location.": "Gebruikt tijden voor zonsopgang/zonsondergang op basis van uw locatie." "Uses sunrise/sunset times based on your location.": "Gebruikt tijden voor zonsopgang/zonsondergang op basis van uw locatie."
@@ -6181,7 +6190,7 @@
"Weather Widget": "Weer-widget" "Weather Widget": "Weer-widget"
}, },
"When clicking a dock window in a Hyprland special workspace, bring that special workspace back before focusing the window": { "When clicking a dock window in a Hyprland special workspace, bring that special workspace back before focusing the window": {
"When clicking a dock window in a Hyprland special workspace, bring that special workspace back before focusing the window": "" "When clicking a dock window in a Hyprland special workspace, bring that special workspace back before focusing the window": "Breng bij het klikken op een dock-venster in een Hyprland-speciale-werkruimte die werkruimte terug voordat het venster wordt gefocust"
}, },
"When enabled, apps are sorted alphabetically. When disabled, apps are sorted by usage frequency.": { "When enabled, apps are sorted alphabetically. When disabled, apps are sorted by usage frequency.": {
"When enabled, apps are sorted alphabetically. When disabled, apps are sorted by usage frequency.": "Indien ingeschakeld, worden apps alfabetisch gesorteerd. Indien uitgeschakeld, worden apps gesorteerd op gebruiksfrequentie." "When enabled, apps are sorted alphabetically. When disabled, apps are sorted by usage frequency.": "Indien ingeschakeld, worden apps alfabetisch gesorteerd. Indien uitgeschakeld, worden apps gesorteerd op gebruiksfrequentie."
@@ -6357,7 +6366,7 @@
"You have unsaved changes. Save before opening a file?": "U hebt niet-opgeslagen wijzigingen. Opslaan voordat u een bestand opent?" "You have unsaved changes. Save before opening a file?": "U hebt niet-opgeslagen wijzigingen. Opslaan voordat u een bestand opent?"
}, },
"Your system is up to date!": { "Your system is up to date!": {
"Your system is up to date!": "" "Your system is up to date!": "Uw systeem is bijgewerkt!"
}, },
"actions": { "actions": {
"actions": "acties" "actions": "acties"
@@ -6366,7 +6375,7 @@
"apps": "apps" "apps": "apps"
}, },
"attached": { "attached": {
"attached": "" "attached": "gekoppeld"
}, },
"audio status": { "audio status": {
"Muted": "Gedempt", "Muted": "Gedempt",
@@ -6498,7 +6507,7 @@
"(Default)": "(Standaard)" "(Default)": "(Standaard)"
}, },
"detached": { "detached": {
"detached": "" "detached": "ontkoppeld"
}, },
"dgop not available": { "dgop not available": {
"dgop not available": "dgop niet beschikbaar" "dgop not available": "dgop niet beschikbaar"
@@ -6517,7 +6526,7 @@
"Write:": "Schrijven:" "Write:": "Schrijven:"
}, },
"dms is a highly customizable, modern desktop shell with a <a href=\"https://m3.material.io/\" style=\"text-decoration:none; color:%1;\">material 3 inspired</a> design.<br /><br/>It is built with <a href=\"https://quickshell.org\" style=\"text-decoration:none; color:%1;\">Quickshell</a>, a QT6 framework for building desktop shells, and <a href=\"https://go.dev\" style=\"text-decoration:none; color:%1;\">Go</a>, a statically typed, compiled programming language.": { "dms is a highly customizable, modern desktop shell with a <a href=\"https://m3.material.io/\" style=\"text-decoration:none; color:%1;\">material 3 inspired</a> design.<br /><br/>It is built with <a href=\"https://quickshell.org\" style=\"text-decoration:none; color:%1;\">Quickshell</a>, a QT6 framework for building desktop shells, and <a href=\"https://go.dev\" style=\"text-decoration:none; color:%1;\">Go</a>, a statically typed, compiled programming language.": {
"dms is a highly customizable, modern desktop shell with a <a href=\"https://m3.material.io/\" style=\"text-decoration:none; color:%1;\">material 3 inspired</a> design.<br /><br/>It is built with <a href=\"https://quickshell.org\" style=\"text-decoration:none; color:%1;\">Quickshell</a>, a QT6 framework for building desktop shells, and <a href=\"https://go.dev\" style=\"text-decoration:none; color:%1;\">Go</a>, a statically typed, compiled programming language.": "" "dms is a highly customizable, modern desktop shell with a <a href=\"https://m3.material.io/\" style=\"text-decoration:none; color:%1;\">material 3 inspired</a> design.<br /><br/>It is built with <a href=\"https://quickshell.org\" style=\"text-decoration:none; color:%1;\">Quickshell</a>, a QT6 framework for building desktop shells, and <a href=\"https://go.dev\" style=\"text-decoration:none; color:%1;\">Go</a>, a statically typed, compiled programming language.": "dms is een zeer aanpasbare, moderne desktop-shell met een op <a href=\"https://m3.material.io/\" style=\"text-decoration:none; color:%1;\">Material 3 geïnspireerd</a> ontwerp.<br /><br/>Het is gebouwd met <a href=\"https://quickshell.org\" style=\"text-decoration:none; color:%1;\">Quickshell</a>, een QT6-framework voor het bouwen van desktop-shells, en <a href=\"https://go.dev\" style=\"text-decoration:none; color:%1;\">Go</a>, een statisch getypeerde, gecompileerde programmeertaal."
}, },
"dms/binds.kdl exists but is not included in config.kdl. Custom keybinds will not work until this is fixed.": { "dms/binds.kdl exists but is not included in config.kdl. Custom keybinds will not work until this is fixed.": {
"dms/binds.kdl exists but is not included in config.kdl. Custom keybinds will not work until this is fixed.": "dms/binds.kdl bestaat maar is niet opgenomen in config.kdl. Aangepaste sneltoetsen werken pas als dit is opgelost." "dms/binds.kdl exists but is not included in config.kdl. Custom keybinds will not work until this is fixed.": "dms/binds.kdl bestaat maar is niet opgenomen in config.kdl. Aangepaste sneltoetsen werken pas als dit is opgelost."
@@ -6560,7 +6569,7 @@
"e.g., notify-send 'Hello' && sleep 1": "bijv. notify-send 'Hallo' && sleep 1" "e.g., notify-send 'Hello' && sleep 1": "bijv. notify-send 'Hallo' && sleep 1"
}, },
"e.g., scratch, /^tmp_.*/, build": { "e.g., scratch, /^tmp_.*/, build": {
"e.g., scratch, /^tmp_.*/, build": "" "e.g., scratch, /^tmp_.*/, build": "bijv. scratch, /^tmp_.*/, build"
}, },
"empty plugin list": { "empty plugin list": {
"No plugins found": "Geen plug-ins gevonden" "No plugins found": "Geen plug-ins gevonden"
@@ -6805,7 +6814,7 @@
}, },
"lock screen U2F security key setting": { "lock screen U2F security key setting": {
"Use a FIDO2/U2F security key (e.g. YubiKey) for lock screen authentication (requires enrolled keys)": "Gebruik een FIDO2/U2F-beveiligingssleutel (bijv. YubiKey) voor verificatie op het vergrendelscherm (vereist geregistreerde sleutels)", "Use a FIDO2/U2F security key (e.g. YubiKey) for lock screen authentication (requires enrolled keys)": "Gebruik een FIDO2/U2F-beveiligingssleutel (bijv. YubiKey) voor verificatie op het vergrendelscherm (vereist geregistreerde sleutels)",
"Use a security key for lock screen authentication.": "" "Use a security key for lock screen authentication.": "Gebruik een beveiligingssleutel voor vergrendelscherm-authenticatie."
}, },
"lock screen notification mode option": { "lock screen notification mode option": {
"App Names": "App-namen", "App Names": "App-namen",
@@ -6827,10 +6836,10 @@
"loginctl not available - lock integration requires DMS socket connection": "loginctl niet beschikbaar - vergrendelintegratie vereist DMS-socketverbinding" "loginctl not available - lock integration requires DMS socket connection": "loginctl niet beschikbaar - vergrendelintegratie vereist DMS-socketverbinding"
}, },
"mangowc Discord Server": { "mangowc Discord Server": {
"mangowc Discord Server": "" "mangowc Discord Server": "mangowc Discord-server"
}, },
"mangowc GitHub": { "mangowc GitHub": {
"mangowc GitHub": "" "mangowc GitHub": "mangowc GitHub"
}, },
"matugen color scheme option": { "matugen color scheme option": {
"Content": "Inhoud", "Content": "Inhoud",
@@ -6883,13 +6892,13 @@
"WiFi off": "Wifi uit" "WiFi off": "Wifi uit"
}, },
"niri GitHub": { "niri GitHub": {
"niri GitHub": "" "niri GitHub": "niri GitHub"
}, },
"niri Matrix Chat": { "niri Matrix Chat": {
"niri Matrix Chat": "" "niri Matrix Chat": "niri Matrix-chat"
}, },
"niri/dms Discord": { "niri/dms Discord": {
"niri/dms Discord": "" "niri/dms Discord": "niri/dms Discord"
}, },
"no custom theme file status": { "no custom theme file status": {
"No custom theme file": "Geen aangepast themabestand" "No custom theme file": "Geen aangepast themabestand"
@@ -7031,7 +7040,7 @@
"Power Saver": "Energiebesparing" "Power Saver": "Energiebesparing"
}, },
"power-profiles-daemon not available": { "power-profiles-daemon not available": {
"power-profiles-daemon not available": "" "power-profiles-daemon not available": "power-profiles-daemon niet beschikbaar"
}, },
"primary color": { "primary color": {
"Primary": "Primair" "Primary": "Primair"
@@ -7055,7 +7064,7 @@
"Missing Environment Variables": "Ontbrekende omgevingsvariabelen" "Missing Environment Variables": "Ontbrekende omgevingsvariabelen"
}, },
"r/niri Subreddit": { "r/niri Subreddit": {
"r/niri Subreddit": "" "r/niri Subreddit": "r/niri Subreddit"
}, },
"read-only settings warning for NixOS home-manager users": { "read-only settings warning for NixOS home-manager users": {
"Settings are read-only. Changes will not persist.": "Instellingen zijn alleen-lezen. Wijzigingen worden niet opgeslagen." "Settings are read-only. Changes will not persist.": "Instellingen zijn alleen-lezen. Wijzigingen worden niet opgeslagen."
@@ -7323,6 +7332,6 @@
"↑/↓: Nav • Space: Expand • Enter: Action/Expand • E: Text": "↑/↓: Nav • Spatie: Uitvouwen • Enter: Actie/Uitvouwen • E: Tekst" "↑/↓: Nav • Space: Expand • Enter: Action/Expand • E: Text": "↑/↓: Nav • Spatie: Uitvouwen • Enter: Actie/Uitvouwen • E: Tekst"
}, },
"↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help": { "↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help": {
"↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help": "" "↑/↓: Navigate • Enter/Ctrl+C: Copy • Del: Delete • F10: Help": "↑/↓: Navigeer • Enter/Ctrl+C: Kopieer • Del: Verwijder • F10: Help"
} }
} }
@@ -329,6 +329,9 @@
"Adjust volume per scroll indent": { "Adjust volume per scroll indent": {
"Adjust volume per scroll indent": "" "Adjust volume per scroll indent": ""
}, },
"Adjusts contrast of generated colors (-100 = minimum, 0 = standard, 100 = maximum)": {
"Adjusts contrast of generated colors (-100 = minimum, 0 = standard, 100 = maximum)": ""
},
"Advanced": { "Advanced": {
"Advanced": "Zaawansowane" "Advanced": "Zaawansowane"
}, },
@@ -3160,6 +3163,9 @@
"Launch on dGPU": { "Launch on dGPU": {
"Launch on dGPU": "Uruchom na dGPU" "Launch on dGPU": "Uruchom na dGPU"
}, },
"Launch on dGPU by default": {
"Launch on dGPU by default": ""
},
"Launcher": { "Launcher": {
"Launcher": "Program uruchamiający" "Launcher": "Program uruchamiający"
}, },
@@ -3367,6 +3373,9 @@
"Material inspired shadows and elevation on modals, popouts, and dialogs": { "Material inspired shadows and elevation on modals, popouts, and dialogs": {
"Material inspired shadows and elevation on modals, popouts, and dialogs": "" "Material inspired shadows and elevation on modals, popouts, and dialogs": ""
}, },
"Matugen Contrast": {
"Matugen Contrast": ""
},
"Matugen Palette": { "Matugen Palette": {
"Matugen Palette": "Paleta Matugen" "Matugen Palette": "Paleta Matugen"
}, },

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