mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-13 07:42:46 -04:00
Compare commits
18 Commits
cbfb9f6dd0
...
blur
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e6416c8ba | |||
| a0b2debd7e | |||
| c471cff456 | |||
| f83bb10e0c | |||
| 74ad58b1e1 | |||
| 577863b969 | |||
| 03d2a3fd39 | |||
| 802b23ed60 | |||
| 2b9f3a9eef | |||
| 62c60900eb | |||
| b381e1e54c | |||
| e7ee26ce74 | |||
| 521a3fa6e8 | |||
| 5ee93a67fe | |||
| 5d0a03c822 | |||
| 293c2a0035 | |||
| 9a5fa50541 | |||
| d5ceea8a56 |
@@ -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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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) \
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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: ({})
|
||||||
@@ -1203,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) {
|
||||||
|
|||||||
@@ -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" },
|
||||||
|
|
||||||
|
|||||||
@@ -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 },
|
||||||
@@ -353,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 },
|
||||||
@@ -363,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 },
|
||||||
|
|||||||
@@ -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 = "";
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
@@ -181,7 +287,7 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _updateHasFullscreenToplevel() {
|
function _updateHasFullscreenToplevel() {
|
||||||
if (!CompositorService.isHyprland && !CompositorService.isNiri) {
|
if (!CompositorService.isHyprland) {
|
||||||
hasFullscreenToplevel = false;
|
hasFullscreenToplevel = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -189,6 +295,9 @@ PanelWindow {
|
|||||||
const filtered = CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
|
const filtered = CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
|
||||||
for (let i = 0; i < filtered.length; i++) {
|
for (let i = 0; i < filtered.length; i++) {
|
||||||
if (filtered[i]?.fullscreen) {
|
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;
|
hasFullscreenToplevel = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -708,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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +90,13 @@ Scope {
|
|||||||
printErrors: false
|
printErrors: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: nixosMarker
|
||||||
|
|
||||||
|
path: "/etc/NIXOS"
|
||||||
|
printErrors: false
|
||||||
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
id: u2fConfigWatcher
|
id: u2fConfigWatcher
|
||||||
|
|
||||||
@@ -97,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 ? "/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: {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user