mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-13 07:42:46 -04:00
Compare commits
32 Commits
9b44bc3259
...
frame
| Author | SHA1 | Date | |
|---|---|---|---|
| 1217b25de5 | |||
| e913630f90 | |||
| 220bb2708b | |||
| e57ab3e1f3 | |||
| 952ab9b753 | |||
| 28f9aabcd9 | |||
| 3d9bd73336 | |||
| 3497d5f523 | |||
| 8ef1d95e65 | |||
| e9aeb9ac60 | |||
| fb02f7294d | |||
| f15d49d80a | |||
| c471cff456 | |||
| f83bb10e0c | |||
| 74ad58b1e1 | |||
| 577863b969 | |||
| 03d2a3fd39 | |||
| 802b23ed60 | |||
| 2b9f3a9eef | |||
| 62c60900eb | |||
| b381e1e54c | |||
| e7ee26ce74 | |||
| 521a3fa6e8 | |||
| 5ee93a67fe | |||
| 5d0a03c822 | |||
| 293c2a0035 | |||
| 9a5fa50541 | |||
| d5ceea8a56 | |||
| faa5e7e02d | |||
| 516c478f3d | |||
| 906c6a2501 | |||
| 86d8fe4fa4 |
@@ -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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/blur"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var blurCmd = &cobra.Command{
|
||||||
|
Use: "blur",
|
||||||
|
Short: "Background blur utilities",
|
||||||
|
}
|
||||||
|
|
||||||
|
var blurCheckCmd = &cobra.Command{
|
||||||
|
Use: "check",
|
||||||
|
Short: "Check if the compositor supports background blur (ext-background-effect-v1)",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
Run: runBlurCheck,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
blurCmd.AddCommand(blurCheckCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runBlurCheck(cmd *cobra.Command, args []string) {
|
||||||
|
supported, err := blur.ProbeSupport()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch supported {
|
||||||
|
case true:
|
||||||
|
fmt.Println("supported")
|
||||||
|
default:
|
||||||
|
fmt.Println("unsupported")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -525,5 +525,6 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
configCmd,
|
configCmd,
|
||||||
dlCmd,
|
dlCmd,
|
||||||
randrCmd,
|
randrCmd,
|
||||||
|
blurCmd,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package blur
|
||||||
|
|
||||||
|
import (
|
||||||
|
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
||||||
|
client "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
const extBackgroundEffectInterface = "ext_background_effect_manager_v1"
|
||||||
|
|
||||||
|
func ProbeSupport() (bool, error) {
|
||||||
|
display, err := client.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer display.Context().Close()
|
||||||
|
|
||||||
|
registry, err := display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
||||||
|
switch e.Interface {
|
||||||
|
case extBackgroundEffectInterface:
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := wlhelpers.Roundtrip(display, display.Context()); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return found, nil
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -444,20 +444,21 @@ func GetFocusedMonitor() string {
|
|||||||
|
|
||||||
type outputInfo struct {
|
type outputInfo struct {
|
||||||
x, y int32
|
x, y int32
|
||||||
|
scale float64
|
||||||
transform int32
|
transform int32
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOutputInfo(outputName string) (*outputInfo, bool) {
|
func getAllOutputInfos() map[string]*outputInfo {
|
||||||
display, err := client.Connect("")
|
display, err := client.Connect("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
ctx := display.Context()
|
ctx := display.Context()
|
||||||
defer ctx.Close()
|
defer ctx.Close()
|
||||||
|
|
||||||
registry, err := display.GetRegistry()
|
registry, err := display.GetRegistry()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var outputManager *wlr_output_management.ZwlrOutputManagerV1
|
var outputManager *wlr_output_management.ZwlrOutputManagerV1
|
||||||
@@ -476,16 +477,17 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if outputManager == nil {
|
if outputManager == nil {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type headState struct {
|
type headState struct {
|
||||||
name string
|
name string
|
||||||
x, y int32
|
x, y int32
|
||||||
|
scale float64
|
||||||
transform int32
|
transform int32
|
||||||
}
|
}
|
||||||
heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState)
|
heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState)
|
||||||
@@ -501,6 +503,9 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
|
|||||||
state.x = pe.X
|
state.x = pe.X
|
||||||
state.y = pe.Y
|
state.y = pe.Y
|
||||||
})
|
})
|
||||||
|
e.Head.SetScaleHandler(func(se wlr_output_management.ZwlrOutputHeadV1ScaleEvent) {
|
||||||
|
state.scale = se.Scale
|
||||||
|
})
|
||||||
e.Head.SetTransformHandler(func(te wlr_output_management.ZwlrOutputHeadV1TransformEvent) {
|
e.Head.SetTransformHandler(func(te wlr_output_management.ZwlrOutputHeadV1TransformEvent) {
|
||||||
state.transform = te.Transform
|
state.transform = te.Transform
|
||||||
})
|
})
|
||||||
@@ -511,21 +516,32 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
|
|||||||
|
|
||||||
for !done {
|
for !done {
|
||||||
if err := ctx.Dispatch(); err != nil {
|
if err := ctx.Dispatch(); err != nil {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result := make(map[string]*outputInfo, len(heads))
|
||||||
for _, state := range heads {
|
for _, state := range heads {
|
||||||
if state.name == outputName {
|
if state.name == "" {
|
||||||
return &outputInfo{
|
continue
|
||||||
x: state.x,
|
}
|
||||||
y: state.y,
|
result[state.name] = &outputInfo{
|
||||||
transform: state.transform,
|
x: state.x,
|
||||||
}, true
|
y: state.y,
|
||||||
|
scale: state.scale,
|
||||||
|
transform: state.transform,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
return nil, false
|
func getOutputInfo(outputName string) (*outputInfo, bool) {
|
||||||
|
infos := getAllOutputInfos()
|
||||||
|
if infos == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
info, ok := infos[outputName]
|
||||||
|
return info, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDWLActiveWindow() (*WindowGeometry, error) {
|
func getDWLActiveWindow() (*WindowGeometry, error) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package screenshot
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
@@ -304,22 +305,20 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
|||||||
if len(outputs) == 0 {
|
if len(outputs) == 0 {
|
||||||
return nil, fmt.Errorf("no outputs available")
|
return nil, fmt.Errorf("no outputs available")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(outputs) == 1 {
|
if len(outputs) == 1 {
|
||||||
return s.captureWholeOutput(outputs[0])
|
return s.captureWholeOutput(outputs[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture all outputs first to get actual buffer sizes
|
wlrInfos := getAllOutputInfos()
|
||||||
type capturedOutput struct {
|
|
||||||
output *WaylandOutput
|
|
||||||
result *CaptureResult
|
|
||||||
physX int
|
|
||||||
physY int
|
|
||||||
}
|
|
||||||
captured := make([]capturedOutput, 0, len(outputs))
|
|
||||||
|
|
||||||
var minX, minY, maxX, maxY int
|
type pendingOutput struct {
|
||||||
first := true
|
result *CaptureResult
|
||||||
|
logX float64
|
||||||
|
logY float64
|
||||||
|
scale float64
|
||||||
|
}
|
||||||
|
var pending []pendingOutput
|
||||||
|
maxScale := 1.0
|
||||||
|
|
||||||
for _, output := range outputs {
|
for _, output := range outputs {
|
||||||
result, err := s.captureWholeOutput(output)
|
result, err := s.captureWholeOutput(output)
|
||||||
@@ -328,50 +327,74 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
outX, outY := output.x, output.y
|
logX, logY := float64(output.x), float64(output.y)
|
||||||
scale := float64(output.scale)
|
scale := float64(output.scale)
|
||||||
|
|
||||||
switch DetectCompositor() {
|
switch DetectCompositor() {
|
||||||
case CompositorHyprland:
|
case CompositorHyprland:
|
||||||
if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
|
if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
|
||||||
outX, outY = hx, hy
|
logX, logY = float64(hx), float64(hy)
|
||||||
}
|
}
|
||||||
if s := GetHyprlandMonitorScale(output.name); s > 0 {
|
if hs := GetHyprlandMonitorScale(output.name); hs > 0 {
|
||||||
scale = s
|
scale = hs
|
||||||
}
|
}
|
||||||
case CompositorDWL:
|
default:
|
||||||
if info, ok := getOutputInfo(output.name); ok {
|
if wlrInfos != nil {
|
||||||
outX, outY = info.x, info.y
|
if info, ok := wlrInfos[output.name]; ok {
|
||||||
|
logX, logY = float64(info.x), float64(info.y)
|
||||||
|
if info.scale > 0 {
|
||||||
|
scale = info.scale
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if scale <= 0 {
|
if scale <= 0 {
|
||||||
scale = 1.0
|
scale = 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
physX := int(float64(outX) * scale)
|
pending = append(pending, pendingOutput{result: result, logX: logX, logY: logY, scale: scale})
|
||||||
physY := int(float64(outY) * scale)
|
if scale > maxScale {
|
||||||
|
maxScale = scale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
captured = append(captured, capturedOutput{
|
if len(pending) == 0 {
|
||||||
output: output,
|
return nil, fmt.Errorf("failed to capture any outputs")
|
||||||
result: result,
|
}
|
||||||
physX: physX,
|
if len(pending) == 1 {
|
||||||
physY: physY,
|
return pending[0].result, nil
|
||||||
})
|
}
|
||||||
|
|
||||||
right := physX + result.Buffer.Width
|
type layoutEntry struct {
|
||||||
bottom := physY + result.Buffer.Height
|
result *CaptureResult
|
||||||
|
canvasX int
|
||||||
|
canvasY int
|
||||||
|
canvasW int
|
||||||
|
canvasH int
|
||||||
|
}
|
||||||
|
entries := make([]layoutEntry, len(pending))
|
||||||
|
var minX, minY, maxX, maxY int
|
||||||
|
|
||||||
if first {
|
for i, p := range pending {
|
||||||
minX, minY = physX, physY
|
cx := int(math.Round(p.logX * maxScale))
|
||||||
maxX, maxY = right, bottom
|
cy := int(math.Round(p.logY * maxScale))
|
||||||
first = false
|
cw := int(math.Round(float64(p.result.Buffer.Width) * maxScale / p.scale))
|
||||||
|
ch := int(math.Round(float64(p.result.Buffer.Height) * maxScale / p.scale))
|
||||||
|
|
||||||
|
entries[i] = layoutEntry{result: p.result, canvasX: cx, canvasY: cy, canvasW: cw, canvasH: ch}
|
||||||
|
|
||||||
|
right := cx + cw
|
||||||
|
bottom := cy + ch
|
||||||
|
if i == 0 {
|
||||||
|
minX, minY, maxX, maxY = cx, cy, right, bottom
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if cx < minX {
|
||||||
if physX < minX {
|
minX = cx
|
||||||
minX = physX
|
|
||||||
}
|
}
|
||||||
if physY < minY {
|
if cy < minY {
|
||||||
minY = physY
|
minY = cy
|
||||||
}
|
}
|
||||||
if right > maxX {
|
if right > maxX {
|
||||||
maxX = right
|
maxX = right
|
||||||
@@ -381,35 +404,26 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(captured) == 0 {
|
|
||||||
return nil, fmt.Errorf("failed to capture any outputs")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(captured) == 1 {
|
|
||||||
return captured[0].result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
totalW := maxX - minX
|
totalW := maxX - minX
|
||||||
totalH := maxY - minY
|
totalH := maxY - minY
|
||||||
|
composite, err := CreateShmBuffer(totalW, totalH, totalW*4)
|
||||||
compositeStride := totalW * 4
|
|
||||||
composite, err := CreateShmBuffer(totalW, totalH, compositeStride)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
for _, c := range captured {
|
for _, e := range entries {
|
||||||
c.result.Buffer.Close()
|
e.result.Buffer.Close()
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("create composite buffer: %w", err)
|
return nil, fmt.Errorf("create composite buffer: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
composite.Clear()
|
composite.Clear()
|
||||||
|
|
||||||
var format uint32
|
var format uint32
|
||||||
for _, c := range captured {
|
for _, e := range entries {
|
||||||
if format == 0 {
|
if format == 0 {
|
||||||
format = c.result.Format
|
format = e.result.Format
|
||||||
}
|
}
|
||||||
s.blitBuffer(composite, c.result.Buffer, c.physX-minX, c.physY-minY, c.result.YInverted)
|
s.blitBufferScaled(composite, e.result.Buffer,
|
||||||
c.result.Buffer.Close()
|
e.canvasX-minX, e.canvasY-minY, e.canvasW, e.canvasH,
|
||||||
|
e.result.YInverted)
|
||||||
|
e.result.Buffer.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
return &CaptureResult{
|
return &CaptureResult{
|
||||||
@@ -419,32 +433,44 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Screenshoter) blitBuffer(dst, src *ShmBuffer, dstX, dstY int, yInverted bool) {
|
func (s *Screenshoter) blitBufferScaled(dst, src *ShmBuffer, dstX, dstY, dstW, dstH int, yInverted bool) {
|
||||||
|
if dstW <= 0 || dstH <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
srcData := src.Data()
|
srcData := src.Data()
|
||||||
dstData := dst.Data()
|
dstData := dst.Data()
|
||||||
|
|
||||||
for srcY := 0; srcY < src.Height; srcY++ {
|
for dy := 0; dy < dstH; dy++ {
|
||||||
actualSrcY := srcY
|
canvasY := dstY + dy
|
||||||
if yInverted {
|
if canvasY < 0 || canvasY >= dst.Height {
|
||||||
actualSrcY = src.Height - 1 - srcY
|
|
||||||
}
|
|
||||||
|
|
||||||
dy := dstY + srcY
|
|
||||||
if dy < 0 || dy >= dst.Height {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
srcRowOff := actualSrcY * src.Stride
|
srcY := dy * src.Height / dstH
|
||||||
dstRowOff := dy * dst.Stride
|
if yInverted {
|
||||||
|
srcY = src.Height - 1 - srcY
|
||||||
|
}
|
||||||
|
if srcY < 0 || srcY >= src.Height {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
for srcX := 0; srcX < src.Width; srcX++ {
|
srcRowOff := srcY * src.Stride
|
||||||
dx := dstX + srcX
|
dstRowOff := canvasY * dst.Stride
|
||||||
if dx < 0 || dx >= dst.Width {
|
|
||||||
|
for dx := 0; dx < dstW; dx++ {
|
||||||
|
canvasX := dstX + dx
|
||||||
|
if canvasX < 0 || canvasX >= dst.Width {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
srcX := dx * src.Width / dstW
|
||||||
|
if srcX >= src.Width {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
si := srcRowOff + srcX*4
|
si := srcRowOff + srcX*4
|
||||||
di := dstRowOff + dx*4
|
di := dstRowOff + canvasX*4
|
||||||
|
|
||||||
if si+3 >= len(srcData) || di+3 >= len(dstData) {
|
if si+3 >= len(srcData) || di+3 >= len(dstData) {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -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) \
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ Singleton {
|
|||||||
|
|
||||||
readonly property url home: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
readonly property url home: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
||||||
readonly property url pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
|
readonly property url pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
|
||||||
|
readonly property url xdgCache: StandardPaths.standardLocations(StandardPaths.GenericCacheLocation)[0]
|
||||||
|
|
||||||
readonly property url data: `${StandardPaths.standardLocations(StandardPaths.GenericDataLocation)[0]}/DankMaterialShell`
|
readonly property url data: `${StandardPaths.standardLocations(StandardPaths.GenericDataLocation)[0]}/DankMaterialShell`
|
||||||
readonly property url state: `${StandardPaths.standardLocations(StandardPaths.GenericStateLocation)[0]}/DankMaterialShell`
|
readonly property url state: `${StandardPaths.standardLocations(StandardPaths.GenericStateLocation)[0]}/DankMaterialShell`
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import "settings/SettingsStore.js" as Store
|
|||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property int settingsConfigVersion: 5
|
readonly property int settingsConfigVersion: 11
|
||||||
|
|
||||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
||||||
|
|
||||||
@@ -186,10 +186,46 @@ 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
|
||||||
|
|
||||||
|
property bool frameEnabled: false
|
||||||
|
onFrameEnabledChanged: saveSettings()
|
||||||
|
property real frameThickness: 16
|
||||||
|
onFrameThicknessChanged: saveSettings()
|
||||||
|
property real frameRounding: 23
|
||||||
|
onFrameRoundingChanged: saveSettings()
|
||||||
|
property string frameColor: ""
|
||||||
|
onFrameColorChanged: saveSettings()
|
||||||
|
property real frameOpacity: 1.0
|
||||||
|
onFrameOpacityChanged: saveSettings()
|
||||||
|
property var frameScreenPreferences: ["all"]
|
||||||
|
onFrameScreenPreferencesChanged: saveSettings()
|
||||||
|
property real frameBarSize: 40
|
||||||
|
onFrameBarSizeChanged: saveSettings()
|
||||||
|
property bool frameShowOnOverview: false
|
||||||
|
onFrameShowOnOverviewChanged: saveSettings()
|
||||||
|
property bool frameBlurEnabled: true
|
||||||
|
onFrameBlurEnabledChanged: saveSettings()
|
||||||
|
|
||||||
|
readonly property color effectiveFrameColor: {
|
||||||
|
const fc = frameColor;
|
||||||
|
if (!fc || fc === "default") return Theme.surfaceContainer;
|
||||||
|
if (fc === "primary") return Theme.primary;
|
||||||
|
if (fc === "surface") return Theme.surface;
|
||||||
|
return fc;
|
||||||
|
}
|
||||||
|
|
||||||
property bool showLauncherButton: true
|
property bool showLauncherButton: true
|
||||||
property bool showWorkspaceSwitcher: true
|
property bool showWorkspaceSwitcher: true
|
||||||
property bool showFocusedWindow: true
|
property bool showFocusedWindow: true
|
||||||
@@ -338,6 +374,7 @@ Singleton {
|
|||||||
property bool sortAppsAlphabetically: false
|
property bool sortAppsAlphabetically: false
|
||||||
property int appLauncherGridColumns: 4
|
property int appLauncherGridColumns: 4
|
||||||
property bool spotlightCloseNiriOverview: true
|
property bool spotlightCloseNiriOverview: true
|
||||||
|
property bool rememberLastQuery: false
|
||||||
property var spotlightSectionViewModes: ({})
|
property var spotlightSectionViewModes: ({})
|
||||||
onSpotlightSectionViewModesChanged: saveSettings()
|
onSpotlightSectionViewModesChanged: saveSettings()
|
||||||
property var appDrawerSectionViewModes: ({})
|
property var appDrawerSectionViewModes: ({})
|
||||||
@@ -494,6 +531,7 @@ Singleton {
|
|||||||
"harmony": 0.5
|
"harmony": 0.5
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
property bool matugenTemplateNeovimSetBackground: true
|
||||||
|
|
||||||
property bool showDock: false
|
property bool showDock: false
|
||||||
property bool dockAutoHide: false
|
property bool dockAutoHide: false
|
||||||
@@ -1202,13 +1240,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) {
|
||||||
@@ -1918,6 +1966,66 @@ Singleton {
|
|||||||
return filtered;
|
return filtered;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getFrameFilteredScreens() {
|
||||||
|
var prefs = frameScreenPreferences || ["all"];
|
||||||
|
if (!prefs || prefs.length === 0 || prefs.includes("all")) {
|
||||||
|
return Quickshell.screens;
|
||||||
|
}
|
||||||
|
return Quickshell.screens.filter(screen => isScreenInPreferences(screen, prefs));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveBarEdgeForScreen(screen) {
|
||||||
|
if (!screen) return "";
|
||||||
|
for (var i = 0; i < barConfigs.length; i++) {
|
||||||
|
var bc = barConfigs[i];
|
||||||
|
if (!bc.enabled) continue;
|
||||||
|
var prefs = bc.screenPreferences || ["all"];
|
||||||
|
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
|
||||||
|
switch (bc.position ?? 0) {
|
||||||
|
case SettingsData.Position.Top: return "top";
|
||||||
|
case SettingsData.Position.Bottom: return "bottom";
|
||||||
|
case SettingsData.Position.Left: return "left";
|
||||||
|
case SettingsData.Position.Right: return "right";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveBarEdgesForScreen(screen) {
|
||||||
|
if (!screen) return [];
|
||||||
|
var edges = [];
|
||||||
|
for (var i = 0; i < barConfigs.length; i++) {
|
||||||
|
var bc = barConfigs[i];
|
||||||
|
if (!bc.enabled) continue;
|
||||||
|
var prefs = bc.screenPreferences || ["all"];
|
||||||
|
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
|
||||||
|
switch (bc.position ?? 0) {
|
||||||
|
case SettingsData.Position.Top: edges.push("top"); break;
|
||||||
|
case SettingsData.Position.Bottom: edges.push("bottom"); break;
|
||||||
|
case SettingsData.Position.Left: edges.push("left"); break;
|
||||||
|
case SettingsData.Position.Right: edges.push("right"); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveBarThicknessForScreen(screen) {
|
||||||
|
if (frameEnabled) return frameBarSize;
|
||||||
|
if (!screen) return frameThickness;
|
||||||
|
for (var i = 0; i < barConfigs.length; i++) {
|
||||||
|
var bc = barConfigs[i];
|
||||||
|
if (!bc.enabled) continue;
|
||||||
|
var prefs = bc.screenPreferences || ["all"];
|
||||||
|
if (!prefs.includes("all") && !isScreenInPreferences(screen, prefs)) continue;
|
||||||
|
const innerPadding = bc.innerPadding ?? 4;
|
||||||
|
const barT = Math.max(26 + innerPadding * 0.6, Theme.barHeight - 4 - (8 - innerPadding));
|
||||||
|
const spacing = bc.spacing ?? 4;
|
||||||
|
const bottomGap = bc.bottomGap ?? 0;
|
||||||
|
return barT + spacing + bottomGap;
|
||||||
|
}
|
||||||
|
return frameThickness;
|
||||||
|
}
|
||||||
|
|
||||||
function sendTestNotifications() {
|
function sendTestNotifications() {
|
||||||
NotificationService.dismissAllPopups();
|
NotificationService.dismissAllPopups();
|
||||||
sendTestNotification(0);
|
sendTestNotification(0);
|
||||||
|
|||||||
@@ -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 },
|
||||||
@@ -305,6 +310,7 @@ var SPEC = {
|
|||||||
light: { baseTheme: "github_light", harmony: 0.5 }
|
light: { baseTheme: "github_light", harmony: 0.5 }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
matugenTemplateNeovimSetBackground: { def: true },
|
||||||
|
|
||||||
showDock: { def: false },
|
showDock: { def: false },
|
||||||
dockAutoHide: { def: false },
|
dockAutoHide: { def: false },
|
||||||
@@ -352,7 +358,7 @@ var SPEC = {
|
|||||||
lockScreenShowMediaPlayer: { def: true },
|
lockScreenShowMediaPlayer: { def: true },
|
||||||
lockScreenPowerOffMonitorsOnLock: { def: false },
|
lockScreenPowerOffMonitorsOnLock: { def: false },
|
||||||
lockAtStartup: { def: false },
|
lockAtStartup: { def: false },
|
||||||
enableFprint: { def: false },
|
enableFprint: { def: false, onChange: "scheduleAuthApply" },
|
||||||
maxFprintTries: { def: 15 },
|
maxFprintTries: { def: 15 },
|
||||||
fprintdAvailable: { def: false, persist: false },
|
fprintdAvailable: { def: false, persist: false },
|
||||||
lockFingerprintCanEnable: { def: false, persist: false },
|
lockFingerprintCanEnable: { def: false, persist: false },
|
||||||
@@ -362,7 +368,7 @@ var SPEC = {
|
|||||||
greeterFingerprintReady: { def: false, persist: false },
|
greeterFingerprintReady: { def: false, persist: false },
|
||||||
greeterFingerprintReason: { def: "probe_failed", persist: false },
|
greeterFingerprintReason: { def: "probe_failed", persist: false },
|
||||||
greeterFingerprintSource: { def: "none", persist: false },
|
greeterFingerprintSource: { def: "none", persist: false },
|
||||||
enableU2f: { def: false },
|
enableU2f: { def: false, onChange: "scheduleAuthApply" },
|
||||||
u2fMode: { def: "or" },
|
u2fMode: { def: "or" },
|
||||||
u2fAvailable: { def: false, persist: false },
|
u2fAvailable: { def: false, persist: false },
|
||||||
lockU2fCanEnable: { def: false, persist: false },
|
lockU2fCanEnable: { def: false, persist: false },
|
||||||
@@ -541,7 +547,17 @@ var SPEC = {
|
|||||||
clipboardEnterToPaste: { def: false },
|
clipboardEnterToPaste: { def: false },
|
||||||
|
|
||||||
launcherPluginVisibility: { def: {} },
|
launcherPluginVisibility: { def: {} },
|
||||||
launcherPluginOrder: { def: [] }
|
launcherPluginOrder: { def: [] },
|
||||||
|
|
||||||
|
frameEnabled: { def: false },
|
||||||
|
frameThickness: { def: 16 },
|
||||||
|
frameRounding: { def: 23 },
|
||||||
|
frameColor: { def: "" },
|
||||||
|
frameOpacity: { def: 1.0 },
|
||||||
|
frameScreenPreferences: { def: ["all"] },
|
||||||
|
frameBarSize: { def: 40 },
|
||||||
|
frameShowOnOverview: { def: false },
|
||||||
|
frameBlurEnabled: { def: true }
|
||||||
};
|
};
|
||||||
|
|
||||||
function getValidKeys() {
|
function getValidKeys() {
|
||||||
|
|||||||
@@ -248,6 +248,10 @@ function migrateToVersion(obj, targetVersion) {
|
|||||||
settings.configVersion = 6;
|
settings.configVersion = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentVersion < 11) {
|
||||||
|
settings.configVersion = 11;
|
||||||
|
}
|
||||||
|
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import qs.Modules.OSD
|
|||||||
import qs.Modules.ProcessList
|
import qs.Modules.ProcessList
|
||||||
import qs.Modules.DankBar
|
import qs.Modules.DankBar
|
||||||
import qs.Modules.DankBar.Popouts
|
import qs.Modules.DankBar.Popouts
|
||||||
|
import qs.Modules.Frame
|
||||||
import qs.Modules.WorkspaceOverlays
|
import qs.Modules.WorkspaceOverlays
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
@@ -176,6 +177,8 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Frame {}
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
id: dankBarRepeater
|
id: dankBarRepeater
|
||||||
model: ScriptModel {
|
model: ScriptModel {
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -30,7 +31,7 @@ Item {
|
|||||||
property real animationOffset: Theme.spacingL
|
property real animationOffset: Theme.spacingL
|
||||||
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||||
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
||||||
property color backgroundColor: Theme.surfaceContainer
|
property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
property color borderColor: Theme.outlineMedium
|
property color borderColor: Theme.outlineMedium
|
||||||
property real borderWidth: 0
|
property real borderWidth: 0
|
||||||
property real cornerRadius: Theme.cornerRadius
|
property real cornerRadius: Theme.cornerRadius
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ DankModal {
|
|||||||
|
|
||||||
modalWidth: 680
|
modalWidth: 680
|
||||||
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 680
|
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 680
|
||||||
backgroundColor: Theme.surfaceContainer
|
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
cornerRadius: Theme.cornerRadius
|
cornerRadius: Theme.cornerRadius
|
||||||
borderColor: Theme.outlineMedium
|
borderColor: Theme.outlineMedium
|
||||||
borderWidth: 1
|
borderWidth: 1
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -303,7 +311,7 @@ FocusScope {
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
visible: !editMode
|
visible: !editMode && !(root.parentModal?.isClosing ?? false)
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: footerBar
|
id: footerBar
|
||||||
@@ -729,8 +737,6 @@ FocusScope {
|
|||||||
Item {
|
Item {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2)
|
height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2)
|
||||||
opacity: root.parentModal?.isClosing ? 0 : 1
|
|
||||||
|
|
||||||
ResultsList {
|
ResultsList {
|
||||||
id: resultsList
|
id: resultsList
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -763,6 +769,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 = "";
|
||||||
|
|||||||
@@ -324,6 +324,8 @@ Item {
|
|||||||
height: 24
|
height: 24
|
||||||
z: 100
|
z: 100
|
||||||
visible: {
|
visible: {
|
||||||
|
if (BlurService.enabled)
|
||||||
|
return false;
|
||||||
if (mainListView.contentHeight <= mainListView.height)
|
if (mainListView.contentHeight <= mainListView.height)
|
||||||
return false;
|
return false;
|
||||||
var atBottom = mainListView.contentY >= mainListView.contentHeight - mainListView.height + mainListView.originY - 5;
|
var atBottom = mainListView.contentY >= mainListView.contentHeight - mainListView.height + mainListView.originY - 5;
|
||||||
@@ -449,7 +451,7 @@ Item {
|
|||||||
case "apps":
|
case "apps":
|
||||||
return "apps";
|
return "apps";
|
||||||
default:
|
default:
|
||||||
return root.controller?.searchQuery?.length > 0 ? "search_off" : "search";
|
return "search_off";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -485,9 +487,9 @@ Item {
|
|||||||
case "plugins":
|
case "plugins":
|
||||||
return hasQuery ? I18n.tr("No plugin results") : I18n.tr("Browse or search plugins");
|
return hasQuery ? I18n.tr("No plugin results") : I18n.tr("Browse or search plugins");
|
||||||
case "apps":
|
case "apps":
|
||||||
return hasQuery ? I18n.tr("No apps found") : I18n.tr("Type to search apps");
|
return I18n.tr("No apps found");
|
||||||
default:
|
default:
|
||||||
return hasQuery ? I18n.tr("No results found") : I18n.tr("Type to search");
|
return I18n.tr("No results found");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,50 @@ StyledRect {
|
|||||||
return determineFileType(fileName) === "image";
|
return determineFileType(fileName) === "image";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isVideoFile(fileName) {
|
||||||
|
if (!fileName) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return determineFileType(fileName) === "video";
|
||||||
|
}
|
||||||
|
|
||||||
|
property bool isImage: isImageFile(delegateRoot.fileName)
|
||||||
|
property bool isVideo: isVideoFile(delegateRoot.fileName)
|
||||||
|
|
||||||
|
property string _xdgCacheHome: Paths.strip(Paths.xdgCache)
|
||||||
|
property string _thumbnailSize: iconSizeIndex >= 2 ? "x-large" : "large"
|
||||||
|
property int _thumbnailPx: iconSizeIndex >= 2 ? 512 : 256
|
||||||
|
property string videoThumbnailPath: {
|
||||||
|
if (!delegateRoot.fileIsDir && isVideo) {
|
||||||
|
const hash = Qt.md5("file://" + delegateRoot.filePath);
|
||||||
|
return _xdgCacheHome + "/thumbnails/" + _thumbnailSize + "/" + hash + ".png";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
property string _videoThumb: ""
|
||||||
|
|
||||||
|
onVideoThumbnailPathChanged: {
|
||||||
|
_videoThumb = "";
|
||||||
|
if (!videoThumbnailPath)
|
||||||
|
return;
|
||||||
|
const thumbPath = videoThumbnailPath;
|
||||||
|
const thumbDir = _xdgCacheHome + "/thumbnails/" + _thumbnailSize;
|
||||||
|
const size = _thumbnailPx;
|
||||||
|
const fp = delegateRoot.filePath;
|
||||||
|
Paths.mkdir(thumbDir);
|
||||||
|
Proc.runCommand(null, ["test", "-f", thumbPath], function(output, exitCode) {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
_videoThumb = thumbPath;
|
||||||
|
} else {
|
||||||
|
Proc.runCommand(null, ["ffmpegthumbnailer", "-i", fp, "-o", thumbPath, "-s", String(size), "-f"], function(output, exitCode) {
|
||||||
|
if (exitCode === 0)
|
||||||
|
_videoThumb = thumbPath;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getIconForFile(fileName) {
|
function getIconForFile(fileName) {
|
||||||
const lowerName = fileName.toLowerCase();
|
const lowerName = fileName.toLowerCase();
|
||||||
if (lowerName.startsWith("dockerfile")) {
|
if (lowerName.startsWith("dockerfile")) {
|
||||||
@@ -124,7 +168,11 @@ StyledRect {
|
|||||||
property string imagePath: {
|
property string imagePath: {
|
||||||
if (weMode && delegateRoot.fileIsDir)
|
if (weMode && delegateRoot.fileIsDir)
|
||||||
return delegateRoot.filePath + "/preview" + weExtensions[weExtIndex];
|
return delegateRoot.filePath + "/preview" + weExtensions[weExtIndex];
|
||||||
return (!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) ? delegateRoot.filePath : "";
|
if (!delegateRoot.fileIsDir && isImage)
|
||||||
|
return delegateRoot.filePath;
|
||||||
|
if (_videoThumb)
|
||||||
|
return _videoThumb;
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
|
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
|
||||||
onStatusChanged: {
|
onStatusChanged: {
|
||||||
@@ -149,7 +197,7 @@ StyledRect {
|
|||||||
source: gridPreviewImage
|
source: gridPreviewImage
|
||||||
maskEnabled: true
|
maskEnabled: true
|
||||||
maskSource: gridImageMask
|
maskSource: gridImageMask
|
||||||
visible: gridPreviewImage.status === Image.Ready && ((!delegateRoot.fileIsDir && isImageFile(delegateRoot.fileName)) || (weMode && delegateRoot.fileIsDir))
|
visible: gridPreviewImage.status === Image.Ready && ((!delegateRoot.fileIsDir && (isImage || isVideo)) || (weMode && delegateRoot.fileIsDir))
|
||||||
maskThresholdMin: 0.5
|
maskThresholdMin: 0.5
|
||||||
maskSpreadAtMin: 1
|
maskSpreadAtMin: 1
|
||||||
}
|
}
|
||||||
@@ -175,7 +223,7 @@ StyledRect {
|
|||||||
name: delegateRoot.fileIsDir ? "folder" : getIconForFile(delegateRoot.fileName)
|
name: delegateRoot.fileIsDir ? "folder" : getIconForFile(delegateRoot.fileName)
|
||||||
size: iconSizes[iconSizeIndex] * 0.45
|
size: iconSizes[iconSizeIndex] * 0.45
|
||||||
color: delegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText
|
color: delegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText
|
||||||
visible: (!delegateRoot.fileIsDir && !isImageFile(delegateRoot.fileName)) || (delegateRoot.fileIsDir && !weMode)
|
visible: (!delegateRoot.fileIsDir && !isImage && !(isVideo && gridPreviewImage.status === Image.Ready)) || (delegateRoot.fileIsDir && !weMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,46 @@ StyledRect {
|
|||||||
return determineFileType(fileName) === "image";
|
return determineFileType(fileName) === "image";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isVideoFile(fileName) {
|
||||||
|
if (!fileName) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return determineFileType(fileName) === "video";
|
||||||
|
}
|
||||||
|
|
||||||
|
property bool isImage: isImageFile(listDelegateRoot.fileName)
|
||||||
|
property bool isVideo: isVideoFile(listDelegateRoot.fileName)
|
||||||
|
|
||||||
|
property string _xdgCacheHome: Paths.strip(Paths.xdgCache)
|
||||||
|
property string videoThumbnailPath: {
|
||||||
|
if (!listDelegateRoot.fileIsDir && isVideo) {
|
||||||
|
const hash = Qt.md5("file://" + listDelegateRoot.filePath);
|
||||||
|
return _xdgCacheHome + "/thumbnails/normal/" + hash + ".png";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
property string _videoThumb: ""
|
||||||
|
|
||||||
|
onVideoThumbnailPathChanged: {
|
||||||
|
_videoThumb = "";
|
||||||
|
if (!videoThumbnailPath)
|
||||||
|
return;
|
||||||
|
const thumbPath = videoThumbnailPath;
|
||||||
|
const fp = listDelegateRoot.filePath;
|
||||||
|
Paths.mkdir(_xdgCacheHome + "/thumbnails/normal");
|
||||||
|
Proc.runCommand(null, ["test", "-f", thumbPath], function(output, exitCode) {
|
||||||
|
if (exitCode === 0) {
|
||||||
|
_videoThumb = thumbPath;
|
||||||
|
} else {
|
||||||
|
Proc.runCommand(null, ["ffmpegthumbnailer", "-i", fp, "-o", thumbPath, "-s", "128", "-f"], function(output, exitCode) {
|
||||||
|
if (exitCode === 0)
|
||||||
|
_videoThumb = thumbPath;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getIconForFile(fileName) {
|
function getIconForFile(fileName) {
|
||||||
const lowerName = fileName.toLowerCase();
|
const lowerName = fileName.toLowerCase();
|
||||||
if (lowerName.startsWith("dockerfile")) {
|
if (lowerName.startsWith("dockerfile")) {
|
||||||
@@ -127,7 +167,13 @@ StyledRect {
|
|||||||
Image {
|
Image {
|
||||||
id: listPreviewImage
|
id: listPreviewImage
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
property string imagePath: (!listDelegateRoot.fileIsDir && isImageFile(listDelegateRoot.fileName)) ? listDelegateRoot.filePath : ""
|
property string imagePath: {
|
||||||
|
if (!listDelegateRoot.fileIsDir && isImage)
|
||||||
|
return listDelegateRoot.filePath;
|
||||||
|
if (_videoThumb)
|
||||||
|
return _videoThumb;
|
||||||
|
return "";
|
||||||
|
}
|
||||||
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
|
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
sourceSize.width: 32
|
sourceSize.width: 32
|
||||||
@@ -141,7 +187,7 @@ StyledRect {
|
|||||||
source: listPreviewImage
|
source: listPreviewImage
|
||||||
maskEnabled: true
|
maskEnabled: true
|
||||||
maskSource: listImageMask
|
maskSource: listImageMask
|
||||||
visible: listPreviewImage.status === Image.Ready && !listDelegateRoot.fileIsDir && isImageFile(listDelegateRoot.fileName)
|
visible: listPreviewImage.status === Image.Ready && !listDelegateRoot.fileIsDir && (isImage || isVideo)
|
||||||
maskThresholdMin: 0.5
|
maskThresholdMin: 0.5
|
||||||
maskSpreadAtMin: 1
|
maskSpreadAtMin: 1
|
||||||
}
|
}
|
||||||
@@ -166,7 +212,7 @@ StyledRect {
|
|||||||
name: listDelegateRoot.fileIsDir ? "folder" : getIconForFile(listDelegateRoot.fileName)
|
name: listDelegateRoot.fileIsDir ? "folder" : getIconForFile(listDelegateRoot.fileName)
|
||||||
size: Theme.iconSize - 2
|
size: Theme.iconSize - 2
|
||||||
color: listDelegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText
|
color: listDelegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText
|
||||||
visible: listDelegateRoot.fileIsDir || !isImageFile(listDelegateRoot.fileName)
|
visible: listDelegateRoot.fileIsDir || (!isImage && !(isVideo && listPreviewImage.status === Image.Ready))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -518,5 +518,20 @@ FocusScope {
|
|||||||
Qt.callLater(() => item.forceActiveFocus());
|
Qt.callLater(() => item.forceActiveFocus());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: frameLoader
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 33
|
||||||
|
visible: active
|
||||||
|
focus: active
|
||||||
|
|
||||||
|
sourceComponent: FrameTab {}
|
||||||
|
|
||||||
|
onActiveChanged: {
|
||||||
|
if (active && item)
|
||||||
|
Qt.callLater(() => item.forceActiveFocus());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,12 @@ Rectangle {
|
|||||||
"text": I18n.tr("Widgets"),
|
"text": I18n.tr("Widgets"),
|
||||||
"icon": "widgets",
|
"icon": "widgets",
|
||||||
"tabIndex": 22
|
"tabIndex": 22
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "frame",
|
||||||
|
"text": I18n.tr("Frame"),
|
||||||
|
"icon": "frame_source",
|
||||||
|
"tabIndex": 33
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -136,7 +133,7 @@ DankPopout {
|
|||||||
QtObject {
|
QtObject {
|
||||||
id: modalAdapter
|
id: modalAdapter
|
||||||
property bool spotlightOpen: appDrawerPopout.shouldBeVisible
|
property bool spotlightOpen: appDrawerPopout.shouldBeVisible
|
||||||
property bool isClosing: false
|
readonly property bool isClosing: !appDrawerPopout.shouldBeVisible
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
appDrawerPopout.close();
|
appDrawerPopout.close();
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ PluginComponent {
|
|||||||
id: detailRoot
|
id: detailRoot
|
||||||
implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2
|
implicitHeight: detailColumn.implicitHeight + Theme.spacingM * 2
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.surfaceContainerHigh
|
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
|
||||||
|
|
||||||
DankActionButton {
|
DankActionButton {
|
||||||
anchors.top: parent.top
|
anchors.top: parent.top
|
||||||
@@ -252,7 +252,7 @@ PluginComponent {
|
|||||||
width: parent ? parent.width : 300
|
width: parent ? parent.width : 300
|
||||||
height: 50
|
height: 50
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.surfaceContainerHighest
|
color: Theme.surfaceLight
|
||||||
border.width: 1
|
border.width: 1
|
||||||
border.color: Theme.outlineLight
|
border.color: Theme.outlineLight
|
||||||
opacity: 1.0
|
opacity: 1.0
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ Row {
|
|||||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
color: Theme.surfaceContainer
|
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
border.color: Theme.primarySelected
|
border.color: Theme.primarySelected
|
||||||
border.width: 0
|
border.width: 0
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
|
|||||||
@@ -207,9 +207,9 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 50
|
height: 50
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: deviceMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
|
||||||
border.color: modelData === AudioService.source ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: modelData === AudioService.source ? Theme.primary : Theme.outlineLight
|
||||||
border.width: 0
|
border.width: modelData === AudioService.source ? 2 : 1
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|||||||
@@ -218,9 +218,9 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 50
|
height: 50
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: deviceMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
|
||||||
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: modelData === AudioService.sink ? Theme.primary : Theme.outlineLight
|
||||||
border.width: 0
|
border.width: modelData === AudioService.sink ? 2 : 1
|
||||||
|
|
||||||
DankRipple {
|
DankRipple {
|
||||||
id: deviceRipple
|
id: deviceRipple
|
||||||
@@ -397,9 +397,9 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 50
|
height: 50
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: Theme.surfaceLight
|
||||||
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: modelData === AudioService.sink ? Theme.primary : Theme.outlineLight
|
||||||
border.width: 0
|
border.width: modelData === AudioService.sink ? 2 : 1
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|||||||
@@ -129,8 +129,9 @@ Rectangle {
|
|||||||
width: (parent.width - Theme.spacingM) / 2
|
width: (parent.width - Theme.spacingM) / 2
|
||||||
height: 64
|
height: 64
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: Theme.surfaceLight
|
||||||
border.width: 0
|
border.color: Theme.outlineLight
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
@@ -164,8 +165,9 @@ Rectangle {
|
|||||||
width: (parent.width - Theme.spacingM) / 2
|
width: (parent.width - Theme.spacingM) / 2
|
||||||
height: 64
|
height: 64
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: Theme.surfaceLight
|
||||||
border.width: 0
|
border.color: Theme.outlineLight
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ Item {
|
|||||||
width: 320
|
width: 320
|
||||||
height: contentColumn.implicitHeight + Theme.spacingL * 2
|
height: contentColumn.implicitHeight + Theme.spacingL * 2
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.surfaceContainer
|
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
border.width: 0
|
border.width: 0
|
||||||
opacity: modalVisible ? 1 : 0
|
opacity: modalVisible ? 1 : 0
|
||||||
|
|||||||
@@ -229,7 +229,6 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 50
|
height: 50
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
border.width: 0
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (!isConnected)
|
if (!isConnected)
|
||||||
@@ -243,8 +242,8 @@ Rectangle {
|
|||||||
if (isConnecting)
|
if (isConnecting)
|
||||||
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
|
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
|
||||||
if (deviceMouseArea.containsMouse)
|
if (deviceMouseArea.containsMouse)
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
return Theme.primaryHoverLight;
|
||||||
return Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency);
|
return Theme.surfaceLight;
|
||||||
}
|
}
|
||||||
|
|
||||||
border.color: {
|
border.color: {
|
||||||
@@ -252,8 +251,9 @@ Rectangle {
|
|||||||
return Theme.warning;
|
return Theme.warning;
|
||||||
if (isConnected)
|
if (isConnected)
|
||||||
return Theme.primary;
|
return Theme.primary;
|
||||||
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12);
|
return Theme.outlineLight;
|
||||||
}
|
}
|
||||||
|
border.width: (isConnecting || isConnected) ? 2 : 1
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -490,9 +490,9 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 50
|
height: 50
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: availableMouseArea.containsMouse && isInteractive ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: availableMouseArea.containsMouse && isInteractive ? Theme.primaryHoverLight : Theme.surfaceLight
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: Theme.outlineLight
|
||||||
border.width: 0
|
border.width: 1
|
||||||
opacity: isInteractive ? 1 : 0.6
|
opacity: isInteractive ? 1 : 0.6
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
|
|||||||
@@ -79,9 +79,9 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 80
|
height: 80
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: Theme.surfaceLight
|
||||||
border.color: modelData.mount === currentMountPath ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: modelData.mount === currentMountPath ? Theme.primary : Theme.outlineLight
|
||||||
border.width: modelData.mount === currentMountPath ? 2 : 0
|
border.width: modelData.mount === currentMountPath ? 2 : 1
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
|
|||||||
@@ -308,9 +308,9 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: wiredContentRow.implicitHeight + Theme.spacingM * 2
|
height: wiredContentRow.implicitHeight + Theme.spacingM * 2
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: wiredNetworkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: wiredNetworkMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
|
||||||
border.color: Theme.primary
|
border.color: isActive ? Theme.primary : Theme.outlineLight
|
||||||
border.width: 0
|
border.width: isActive ? 2 : 1
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: wiredContentRow
|
id: wiredContentRow
|
||||||
@@ -565,9 +565,9 @@ Rectangle {
|
|||||||
width: wifiContent.width
|
width: wifiContent.width
|
||||||
height: wifiContentRow.implicitHeight + Theme.spacingM * 2
|
height: wifiContentRow.implicitHeight + Theme.spacingM * 2
|
||||||
radius: Theme.cornerRadius
|
radius: Theme.cornerRadius
|
||||||
color: networkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Theme.withAlpha(Theme.surfaceContainerHighest, Theme.popupTransparency)
|
color: networkMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
|
||||||
border.color: wifiDelegate.isConnected ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
border.color: wifiDelegate.isConnected ? Theme.primary : Theme.outlineLight
|
||||||
border.width: 0
|
border.width: wifiDelegate.isConnected ? 2 : 1
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: wifiContentRow
|
id: wifiContentRow
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ Item {
|
|||||||
required property var axis
|
required property var axis
|
||||||
required property var barConfig
|
required property var barConfig
|
||||||
|
|
||||||
|
visible: !SettingsData.frameEnabled
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
@@ -37,6 +39,8 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
property real rt: {
|
property real rt: {
|
||||||
|
if (SettingsData.frameEnabled)
|
||||||
|
return SettingsData.frameRounding;
|
||||||
if (barConfig?.squareCorners ?? false)
|
if (barConfig?.squareCorners ?? false)
|
||||||
return 0;
|
return 0;
|
||||||
if (barWindow.hasMaximizedToplevel)
|
if (barWindow.hasMaximizedToplevel)
|
||||||
@@ -255,11 +259,12 @@ Item {
|
|||||||
h = h - wing;
|
h = h - wing;
|
||||||
const r = wing;
|
const r = wing;
|
||||||
const cr = rt;
|
const cr = rt;
|
||||||
|
const crE = SettingsData.frameEnabled ? 0 : cr;
|
||||||
|
|
||||||
let d = `M ${cr} 0`;
|
let d = `M ${crE} 0`;
|
||||||
d += ` L ${w - cr} 0`;
|
d += ` L ${w - crE} 0`;
|
||||||
if (cr > 0)
|
if (crE > 0)
|
||||||
d += ` A ${cr} ${cr} 0 0 1 ${w} ${cr}`;
|
d += ` A ${crE} ${crE} 0 0 1 ${w} ${crE}`;
|
||||||
if (r > 0) {
|
if (r > 0) {
|
||||||
d += ` L ${w} ${h + r}`;
|
d += ` L ${w} ${h + r}`;
|
||||||
d += ` A ${r} ${r} 0 0 0 ${w - r} ${h}`;
|
d += ` A ${r} ${r} 0 0 0 ${w - r} ${h}`;
|
||||||
@@ -273,9 +278,9 @@ Item {
|
|||||||
if (cr > 0)
|
if (cr > 0)
|
||||||
d += ` A ${cr} ${cr} 0 0 1 0 ${h - cr}`;
|
d += ` A ${cr} ${cr} 0 0 1 0 ${h - cr}`;
|
||||||
}
|
}
|
||||||
d += ` L 0 ${cr}`;
|
d += ` L 0 ${crE}`;
|
||||||
if (cr > 0)
|
if (crE > 0)
|
||||||
d += ` A ${cr} ${cr} 0 0 1 ${cr} 0`;
|
d += ` A ${crE} ${crE} 0 0 1 ${crE} 0`;
|
||||||
d += " Z";
|
d += " Z";
|
||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
@@ -285,11 +290,12 @@ Item {
|
|||||||
h = h - wing;
|
h = h - wing;
|
||||||
const r = wing;
|
const r = wing;
|
||||||
const cr = rt;
|
const cr = rt;
|
||||||
|
const crE = SettingsData.frameEnabled ? 0 : cr;
|
||||||
|
|
||||||
let d = `M ${cr} ${fullH}`;
|
let d = `M ${crE} ${fullH}`;
|
||||||
d += ` L ${w - cr} ${fullH}`;
|
d += ` L ${w - crE} ${fullH}`;
|
||||||
if (cr > 0)
|
if (crE > 0)
|
||||||
d += ` A ${cr} ${cr} 0 0 0 ${w} ${fullH - cr}`;
|
d += ` A ${crE} ${crE} 0 0 0 ${w} ${fullH - crE}`;
|
||||||
if (r > 0) {
|
if (r > 0) {
|
||||||
d += ` L ${w} 0`;
|
d += ` L ${w} 0`;
|
||||||
d += ` A ${r} ${r} 0 0 1 ${w - r} ${r}`;
|
d += ` A ${r} ${r} 0 0 1 ${w - r} ${r}`;
|
||||||
@@ -303,9 +309,9 @@ Item {
|
|||||||
if (cr > 0)
|
if (cr > 0)
|
||||||
d += ` A ${cr} ${cr} 0 0 0 0 ${cr}`;
|
d += ` A ${cr} ${cr} 0 0 0 0 ${cr}`;
|
||||||
}
|
}
|
||||||
d += ` L 0 ${fullH - cr}`;
|
d += ` L 0 ${fullH - crE}`;
|
||||||
if (cr > 0)
|
if (crE > 0)
|
||||||
d += ` A ${cr} ${cr} 0 0 0 ${cr} ${fullH}`;
|
d += ` A ${crE} ${crE} 0 0 0 ${crE} ${fullH}`;
|
||||||
d += " Z";
|
d += " Z";
|
||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
@@ -314,11 +320,12 @@ Item {
|
|||||||
w = w - wing;
|
w = w - wing;
|
||||||
const r = wing;
|
const r = wing;
|
||||||
const cr = rt;
|
const cr = rt;
|
||||||
|
const crE = SettingsData.frameEnabled ? 0 : cr;
|
||||||
|
|
||||||
let d = `M 0 ${cr}`;
|
let d = `M 0 ${crE}`;
|
||||||
d += ` L 0 ${h - cr}`;
|
d += ` L 0 ${h - crE}`;
|
||||||
if (cr > 0)
|
if (crE > 0)
|
||||||
d += ` A ${cr} ${cr} 0 0 0 ${cr} ${h}`;
|
d += ` A ${crE} ${crE} 0 0 0 ${crE} ${h}`;
|
||||||
if (r > 0) {
|
if (r > 0) {
|
||||||
d += ` L ${w + r} ${h}`;
|
d += ` L ${w + r} ${h}`;
|
||||||
d += ` A ${r} ${r} 0 0 1 ${w} ${h - r}`;
|
d += ` A ${r} ${r} 0 0 1 ${w} ${h - r}`;
|
||||||
@@ -332,9 +339,9 @@ Item {
|
|||||||
if (cr > 0)
|
if (cr > 0)
|
||||||
d += ` A ${cr} ${cr} 0 0 0 ${w - cr} 0`;
|
d += ` A ${cr} ${cr} 0 0 0 ${w - cr} 0`;
|
||||||
}
|
}
|
||||||
d += ` L ${cr} 0`;
|
d += ` L ${crE} 0`;
|
||||||
if (cr > 0)
|
if (crE > 0)
|
||||||
d += ` A ${cr} ${cr} 0 0 0 0 ${cr}`;
|
d += ` A ${crE} ${crE} 0 0 0 0 ${crE}`;
|
||||||
d += " Z";
|
d += " Z";
|
||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
@@ -344,11 +351,12 @@ Item {
|
|||||||
w = w - wing;
|
w = w - wing;
|
||||||
const r = wing;
|
const r = wing;
|
||||||
const cr = rt;
|
const cr = rt;
|
||||||
|
const crE = SettingsData.frameEnabled ? 0 : cr;
|
||||||
|
|
||||||
let d = `M ${fullW} ${cr}`;
|
let d = `M ${fullW} ${crE}`;
|
||||||
d += ` L ${fullW} ${h - cr}`;
|
d += ` L ${fullW} ${h - crE}`;
|
||||||
if (cr > 0)
|
if (crE > 0)
|
||||||
d += ` A ${cr} ${cr} 0 0 1 ${fullW - cr} ${h}`;
|
d += ` A ${crE} ${crE} 0 0 1 ${fullW - crE} ${h}`;
|
||||||
if (r > 0) {
|
if (r > 0) {
|
||||||
d += ` L 0 ${h}`;
|
d += ` L 0 ${h}`;
|
||||||
d += ` A ${r} ${r} 0 0 0 ${r} ${h - r}`;
|
d += ` A ${r} ${r} 0 0 0 ${r} ${h - r}`;
|
||||||
@@ -362,9 +370,9 @@ Item {
|
|||||||
if (cr > 0)
|
if (cr > 0)
|
||||||
d += ` A ${cr} ${cr} 0 0 1 ${cr} 0`;
|
d += ` A ${cr} ${cr} 0 0 1 ${cr} 0`;
|
||||||
}
|
}
|
||||||
d += ` L ${fullW - cr} 0`;
|
d += ` L ${fullW - crE} 0`;
|
||||||
if (cr > 0)
|
if (crE > 0)
|
||||||
d += ` A ${cr} ${cr} 0 0 1 ${fullW} ${cr}`;
|
d += ` A ${crE} ${crE} 0 0 1 ${fullW} ${crE}`;
|
||||||
d += " Z";
|
d += " Z";
|
||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -21,6 +23,31 @@ Item {
|
|||||||
readonly property real innerPadding: barConfig?.innerPadding ?? 4
|
readonly property real innerPadding: barConfig?.innerPadding ?? 4
|
||||||
readonly property real outlineThickness: (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0
|
readonly property real outlineThickness: (barConfig?.widgetOutlineEnabled ?? false) ? (barConfig?.widgetOutlineThickness ?? 1) : 0
|
||||||
|
|
||||||
|
readonly property real _frameLeftInset: {
|
||||||
|
if (!SettingsData.frameEnabled || barWindow.isVertical) return 0
|
||||||
|
return barWindow.hasAdjacentLeftBar
|
||||||
|
? SettingsData.frameBarSize
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
readonly property real _frameRightInset: {
|
||||||
|
if (!SettingsData.frameEnabled || barWindow.isVertical) return 0
|
||||||
|
return barWindow.hasAdjacentRightBar
|
||||||
|
? SettingsData.frameBarSize
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
readonly property real _frameTopInset: {
|
||||||
|
if (!SettingsData.frameEnabled || !barWindow.isVertical) return 0
|
||||||
|
return barWindow.hasAdjacentTopBar
|
||||||
|
? SettingsData.frameThickness
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
readonly property real _frameBottomInset: {
|
||||||
|
if (!SettingsData.frameEnabled || !barWindow.isVertical) return 0
|
||||||
|
return barWindow.hasAdjacentBottomBar
|
||||||
|
? SettingsData.frameThickness
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
|
||||||
property alias hLeftSection: hLeftSection
|
property alias hLeftSection: hLeftSection
|
||||||
property alias hCenterSection: hCenterSection
|
property alias hCenterSection: hCenterSection
|
||||||
property alias hRightSection: hRightSection
|
property alias hRightSection: hRightSection
|
||||||
@@ -29,10 +56,14 @@ Item {
|
|||||||
property alias vRightSection: vRightSection
|
property alias vRightSection: vRightSection
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.leftMargin: Math.max(Theme.spacingXS, innerPadding * 0.8)
|
anchors.leftMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) + _frameLeftInset
|
||||||
anchors.rightMargin: Math.max(Theme.spacingXS, innerPadding * 0.8)
|
anchors.rightMargin: Math.max(Theme.spacingXS, innerPadding * 0.8) + _frameRightInset
|
||||||
anchors.topMargin: barWindow.isVertical ? (barWindow.hasAdjacentTopBar ? outlineThickness : Theme.spacingXS) : 0
|
anchors.topMargin: (barWindow.isVertical
|
||||||
anchors.bottomMargin: barWindow.isVertical ? (barWindow.hasAdjacentBottomBar ? outlineThickness : Theme.spacingXS) : 0
|
? (barWindow.hasAdjacentTopBar ? outlineThickness : Theme.spacingXS)
|
||||||
|
: 0) + _frameTopInset
|
||||||
|
anchors.bottomMargin: (barWindow.isVertical
|
||||||
|
? (barWindow.hasAdjacentBottomBar ? outlineThickness : Theme.spacingXS)
|
||||||
|
: 0) + _frameBottomInset
|
||||||
clip: false
|
clip: false
|
||||||
|
|
||||||
property int componentMapRevision: 0
|
property int componentMapRevision: 0
|
||||||
@@ -408,6 +439,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 +471,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 +503,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 +542,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 +575,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 +609,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,122 @@ 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;
|
||||||
|
// In frame mode, FrameWindow owns the blur region for the entire screen edge
|
||||||
|
// (including the bar area). The bar must not set its own competing blur region
|
||||||
|
// so that frameBlurEnabled acts as the single control for all blur in frame mode.
|
||||||
|
if (SettingsData.frameEnabled)
|
||||||
|
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: SettingsData
|
||||||
|
function onFrameEnabledChanged() { 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"
|
||||||
|
|
||||||
@@ -132,7 +248,9 @@ PanelWindow {
|
|||||||
readonly property color _surfaceContainer: Theme.surfaceContainer
|
readonly property color _surfaceContainer: Theme.surfaceContainer
|
||||||
readonly property string _barId: barConfig?.id ?? "default"
|
readonly property string _barId: barConfig?.id ?? "default"
|
||||||
property real _backgroundAlpha: barConfig?.transparency ?? 1.0
|
property real _backgroundAlpha: barConfig?.transparency ?? 1.0
|
||||||
readonly property color _bgColor: Theme.withAlpha(_surfaceContainer, _backgroundAlpha)
|
readonly property color _bgColor: SettingsData.frameEnabled
|
||||||
|
? Qt.rgba(SettingsData.effectiveFrameColor.r, SettingsData.effectiveFrameColor.g, SettingsData.effectiveFrameColor.b, SettingsData.frameOpacity)
|
||||||
|
: Theme.withAlpha(_surfaceContainer, _backgroundAlpha)
|
||||||
|
|
||||||
function _updateBackgroundAlpha() {
|
function _updateBackgroundAlpha() {
|
||||||
const live = SettingsData.barConfigs.find(c => c.id === _barId);
|
const live = SettingsData.barConfigs.find(c => c.id === _barId);
|
||||||
@@ -181,7 +299,7 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _updateHasFullscreenToplevel() {
|
function _updateHasFullscreenToplevel() {
|
||||||
if (!CompositorService.isHyprland && !CompositorService.isNiri) {
|
if (!CompositorService.isHyprland) {
|
||||||
hasFullscreenToplevel = false;
|
hasFullscreenToplevel = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -189,6 +307,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;
|
||||||
}
|
}
|
||||||
@@ -275,7 +396,7 @@ PanelWindow {
|
|||||||
shouldHideForWindows = filtered.length > 0;
|
shouldHideForWindows = filtered.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
property real effectiveSpacing: hasMaximizedToplevel ? 0 : (barConfig?.spacing ?? 4)
|
property real effectiveSpacing: SettingsData.frameEnabled ? 0 : (hasMaximizedToplevel ? 0 : (barConfig?.spacing ?? 4))
|
||||||
|
|
||||||
Behavior on effectiveSpacing {
|
Behavior on effectiveSpacing {
|
||||||
enabled: barWindow.visible
|
enabled: barWindow.visible
|
||||||
@@ -286,7 +407,12 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readonly property int notificationCount: NotificationService.notifications.length
|
readonly property int notificationCount: NotificationService.notifications.length
|
||||||
readonly property real effectiveBarThickness: Theme.snap(Math.max(barWindow.widgetThickness + (barConfig?.innerPadding ?? 4) + 4, Theme.barHeight - 4 - (8 - (barConfig?.innerPadding ?? 4))), _dpr)
|
readonly property real effectiveBarThickness: SettingsData.frameEnabled
|
||||||
|
? SettingsData.frameBarSize
|
||||||
|
: Theme.snap(Math.max(barWindow.widgetThickness + (barConfig?.innerPadding ?? 4) + 4, Theme.barHeight - 4 - (8 - (barConfig?.innerPadding ?? 4))), _dpr)
|
||||||
|
readonly property bool effectiveOpenOnOverview: SettingsData.frameEnabled
|
||||||
|
? SettingsData.frameShowOnOverview
|
||||||
|
: (barConfig?.openOnOverview ?? false)
|
||||||
readonly property real widgetThickness: Theme.snap(Math.max(20, 26 + (barConfig?.innerPadding ?? 4) * 0.6), _dpr)
|
readonly property real widgetThickness: Theme.snap(Math.max(20, 26 + (barConfig?.innerPadding ?? 4) * 0.6), _dpr)
|
||||||
|
|
||||||
readonly property bool hasAdjacentTopBar: {
|
readonly property bool hasAdjacentTopBar: {
|
||||||
@@ -542,7 +668,7 @@ PanelWindow {
|
|||||||
|
|
||||||
readonly property int barThickness: Theme.px(barWindow.effectiveBarThickness + barWindow.effectiveSpacing, barWindow._dpr)
|
readonly property int barThickness: Theme.px(barWindow.effectiveBarThickness + barWindow.effectiveSpacing, barWindow._dpr)
|
||||||
|
|
||||||
readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false)
|
readonly property bool inOverviewWithShow: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview
|
||||||
readonly property bool effectiveVisible: (barConfig?.visible ?? true) || inOverviewWithShow
|
readonly property bool effectiveVisible: (barConfig?.visible ?? true) || inOverviewWithShow
|
||||||
readonly property bool showing: effectiveVisible && (topBarCore.reveal || inOverviewWithShow || !topBarCore.autoHide)
|
readonly property bool showing: effectiveVisible && (topBarCore.reveal || inOverviewWithShow || !topBarCore.autoHide)
|
||||||
|
|
||||||
@@ -683,7 +809,7 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
property bool reveal: {
|
property bool reveal: {
|
||||||
const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false);
|
const inOverviewWithShow = CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview;
|
||||||
if (inOverviewWithShow)
|
if (inOverviewWithShow)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
@@ -708,7 +834,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];
|
||||||
@@ -779,7 +906,7 @@ PanelWindow {
|
|||||||
top: barWindow.isVertical ? parent.top : undefined
|
top: barWindow.isVertical ? parent.top : undefined
|
||||||
bottom: barWindow.isVertical ? parent.bottom : undefined
|
bottom: barWindow.isVertical ? parent.bottom : undefined
|
||||||
}
|
}
|
||||||
readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && (barConfig?.openOnOverview ?? false)
|
readonly property bool inOverview: CompositorService.isNiri && NiriService.inOverview && barWindow.effectiveOpenOnOverview
|
||||||
hoverEnabled: (barConfig?.autoHide ?? false) && !inOverview && !topBarCore.hasActivePopout
|
hoverEnabled: (barConfig?.autoHide ?? false) && !inOverview && !topBarCore.hasActivePopout
|
||||||
acceptedButtons: Qt.NoButton
|
acceptedButtons: Qt.NoButton
|
||||||
enabled: (barConfig?.autoHide ?? false) && !inOverview
|
enabled: (barConfig?.autoHide ?? false) && !inOverview
|
||||||
|
|||||||
@@ -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,8 +17,49 @@ 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
|
||||||
|
|
||||||
|
readonly property real _leftMargin: {
|
||||||
|
if (isVertical)
|
||||||
|
return 0;
|
||||||
|
root.x;
|
||||||
|
if (!root.parent)
|
||||||
|
return 0;
|
||||||
|
const gap = root.mapToItem(null, 0, 0).x;
|
||||||
|
return (gap > 0 && gap < 30) ? gap + 5 : 0;
|
||||||
|
}
|
||||||
|
readonly property real _rightMargin: {
|
||||||
|
if (isVertical)
|
||||||
|
return 0;
|
||||||
|
root.x;
|
||||||
|
root.width;
|
||||||
|
if (!root.parent || !blurBarWindow)
|
||||||
|
return 0;
|
||||||
|
const gap = blurBarWindow.width - root.mapToItem(null, root.width, 0).x;
|
||||||
|
return (gap > 0 && gap < 30) ? gap + 5 : 0;
|
||||||
|
}
|
||||||
|
readonly property real _topMargin: {
|
||||||
|
if (!isVertical)
|
||||||
|
return 0;
|
||||||
|
root.y;
|
||||||
|
if (!root.parent)
|
||||||
|
return 0;
|
||||||
|
const gap = root.mapToItem(null, 0, 0).y;
|
||||||
|
return (gap > 0 && gap < 30) ? gap + 5 : 0;
|
||||||
|
}
|
||||||
|
readonly property real _bottomMargin: {
|
||||||
|
if (!isVertical)
|
||||||
|
return 0;
|
||||||
|
root.y;
|
||||||
|
root.height;
|
||||||
|
if (!root.parent || !blurBarWindow)
|
||||||
|
return 0;
|
||||||
|
const gap = blurBarWindow.height - root.mapToItem(null, 0, root.height).y;
|
||||||
|
return (gap > 0 && gap < 30) ? gap + 5 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
property int _desktopEntriesUpdateTrigger: 0
|
property int _desktopEntriesUpdateTrigger: 0
|
||||||
readonly property var sortedToplevels: {
|
readonly property var sortedToplevels: {
|
||||||
return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
|
return CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
|
||||||
@@ -538,6 +579,60 @@ Item {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function switchToWorkspaceByModelData(data) {
|
||||||
|
if (!data)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (root.useExtWorkspace && (data.id || data.name)) {
|
||||||
|
ExtWorkspaceService.activateWorkspace(data.id || data.name, data.groupID || "");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (CompositorService.compositor) {
|
||||||
|
case "niri":
|
||||||
|
if (data.idx !== undefined)
|
||||||
|
NiriService.switchToWorkspace(data.idx);
|
||||||
|
break;
|
||||||
|
case "hyprland":
|
||||||
|
if (data.id)
|
||||||
|
Hyprland.dispatch(`workspace ${data.id}`);
|
||||||
|
break;
|
||||||
|
case "dwl":
|
||||||
|
if (data.tag !== undefined)
|
||||||
|
DwlService.switchToTag(root.screenName, data.tag);
|
||||||
|
break;
|
||||||
|
case "sway":
|
||||||
|
case "scroll":
|
||||||
|
case "miracle":
|
||||||
|
if (data.num)
|
||||||
|
try {
|
||||||
|
I3.dispatch(`workspace number ${data.num}`);
|
||||||
|
} catch (_) {}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findClosestWorkspaceIndex(localX, localY) {
|
||||||
|
if (workspaceRepeater.count === 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
let closestIdx = -1;
|
||||||
|
let closestDist = Infinity;
|
||||||
|
|
||||||
|
for (let i = 0; i < workspaceRepeater.count; i++) {
|
||||||
|
const item = workspaceRepeater.itemAt(i);
|
||||||
|
if (!item)
|
||||||
|
continue;
|
||||||
|
const center = item.mapToItem(root, item.width / 2, item.height / 2);
|
||||||
|
const dist = isVertical ? Math.abs(localY - center.y) : Math.abs(localX - center.x);
|
||||||
|
if (dist < closestDist) {
|
||||||
|
closestDist = dist;
|
||||||
|
closestIdx = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return closestIdx;
|
||||||
|
}
|
||||||
|
|
||||||
function switchWorkspace(direction) {
|
function switchWorkspace(direction) {
|
||||||
if (useExtWorkspace) {
|
if (useExtWorkspace) {
|
||||||
const realWorkspaces = getRealWorkspaces();
|
const realWorkspaces = getRealWorkspaces();
|
||||||
@@ -751,8 +846,15 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
id: edgeMouseArea
|
||||||
acceptedButtons: Qt.RightButton
|
z: -1
|
||||||
|
x: -root._leftMargin
|
||||||
|
y: -root._topMargin
|
||||||
|
width: root.width + root._leftMargin + root._rightMargin
|
||||||
|
height: root.height + root._topMargin + root._bottomMargin
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
property real touchpadAccumulator: 0
|
property real touchpadAccumulator: 0
|
||||||
property real mouseAccumulator: 0
|
property real mouseAccumulator: 0
|
||||||
@@ -765,12 +867,20 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onClicked: mouse => {
|
onClicked: mouse => {
|
||||||
if (mouse.button === Qt.RightButton) {
|
const rootPos = edgeMouseArea.mapToItem(root, mouse.x, mouse.y);
|
||||||
|
switch (mouse.button) {
|
||||||
|
case Qt.RightButton:
|
||||||
if (CompositorService.isNiri) {
|
if (CompositorService.isNiri) {
|
||||||
NiriService.toggleOverview();
|
NiriService.toggleOverview();
|
||||||
} else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
|
} else if (CompositorService.isHyprland && root.hyprlandOverviewLoader?.item) {
|
||||||
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen;
|
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen;
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
case Qt.LeftButton:
|
||||||
|
const idx = root.findClosestWorkspaceIndex(rootPos.x, rootPos.y);
|
||||||
|
if (idx >= 0)
|
||||||
|
root.switchToWorkspaceByModelData(root.workspaceList[idx]);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1845,5 +1955,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
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
|
Variants {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
model: Quickshell.screens
|
||||||
|
|
||||||
|
FrameInstance {
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
screen: modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Effects
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
required property real cutoutTopInset
|
||||||
|
required property real cutoutBottomInset
|
||||||
|
required property real cutoutLeftInset
|
||||||
|
required property real cutoutRightInset
|
||||||
|
required property real cutoutRadius
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: borderRect
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
// Bake frameOpacity into the color alpha rather than using the `opacity` property.
|
||||||
|
// Qt Quick can skip layer.effect processing on items with opacity < 1 as an
|
||||||
|
// optimization, causing the MultiEffect inverted mask to stop working and the
|
||||||
|
// Rectangle to render as a plain square at low opacity values.
|
||||||
|
color: Qt.rgba(SettingsData.effectiveFrameColor.r,
|
||||||
|
SettingsData.effectiveFrameColor.g,
|
||||||
|
SettingsData.effectiveFrameColor.b,
|
||||||
|
SettingsData.frameOpacity)
|
||||||
|
|
||||||
|
layer.enabled: true
|
||||||
|
layer.effect: MultiEffect {
|
||||||
|
maskSource: cutoutMask
|
||||||
|
maskEnabled: true
|
||||||
|
maskInverted: true
|
||||||
|
maskThresholdMin: 0.5
|
||||||
|
maskSpreadAtMin: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: cutoutMask
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
layer.enabled: true
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors {
|
||||||
|
fill: parent
|
||||||
|
topMargin: root.cutoutTopInset
|
||||||
|
bottomMargin: root.cutoutBottomInset
|
||||||
|
leftMargin: root.cutoutLeftInset
|
||||||
|
rightMargin: root.cutoutRightInset
|
||||||
|
}
|
||||||
|
radius: root.cutoutRadius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
|
Scope {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var screen
|
||||||
|
|
||||||
|
readonly property var barEdges: {
|
||||||
|
SettingsData.barConfigs; // force re-eval when bar configs change
|
||||||
|
return SettingsData.getActiveBarEdgesForScreen(screen);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One thin invisible PanelWindow per edge.
|
||||||
|
// Skips any edge where a bar already provides its own exclusiveZone.
|
||||||
|
|
||||||
|
readonly property bool screenEnabled: SettingsData.frameEnabled && SettingsData.isScreenInPreferences(root.screen, SettingsData.frameScreenPreferences)
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
active: root.screenEnabled && !root.barEdges.includes("top")
|
||||||
|
sourceComponent: EdgeExclusion {
|
||||||
|
targetScreen: root.screen
|
||||||
|
anchorTop: true
|
||||||
|
anchorLeft: true
|
||||||
|
anchorRight: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
active: root.screenEnabled && !root.barEdges.includes("bottom")
|
||||||
|
sourceComponent: EdgeExclusion {
|
||||||
|
targetScreen: root.screen
|
||||||
|
anchorBottom: true
|
||||||
|
anchorLeft: true
|
||||||
|
anchorRight: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
active: root.screenEnabled && !root.barEdges.includes("left")
|
||||||
|
sourceComponent: EdgeExclusion {
|
||||||
|
targetScreen: root.screen
|
||||||
|
anchorLeft: true
|
||||||
|
anchorTop: true
|
||||||
|
anchorBottom: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
active: root.screenEnabled && !root.barEdges.includes("right")
|
||||||
|
sourceComponent: EdgeExclusion {
|
||||||
|
targetScreen: root.screen
|
||||||
|
anchorRight: true
|
||||||
|
anchorTop: true
|
||||||
|
anchorBottom: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component EdgeExclusion: PanelWindow {
|
||||||
|
required property var targetScreen
|
||||||
|
|
||||||
|
screen: targetScreen
|
||||||
|
property bool anchorTop: false
|
||||||
|
property bool anchorBottom: false
|
||||||
|
property bool anchorLeft: false
|
||||||
|
property bool anchorRight: false
|
||||||
|
|
||||||
|
WlrLayershell.namespace: "dms:frame-exclusion"
|
||||||
|
WlrLayershell.layer: WlrLayer.Top
|
||||||
|
exclusiveZone: SettingsData.frameThickness
|
||||||
|
color: "transparent"
|
||||||
|
mask: Region {}
|
||||||
|
implicitWidth: 1
|
||||||
|
implicitHeight: 1
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: anchorTop
|
||||||
|
bottom: anchorBottom
|
||||||
|
left: anchorLeft
|
||||||
|
right: anchorRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property var screen
|
||||||
|
|
||||||
|
FrameWindow {
|
||||||
|
targetScreen: root.screen
|
||||||
|
}
|
||||||
|
|
||||||
|
FrameExclusions {
|
||||||
|
screen: root.screen
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: win
|
||||||
|
|
||||||
|
required property var targetScreen
|
||||||
|
|
||||||
|
screen: targetScreen
|
||||||
|
visible: true
|
||||||
|
|
||||||
|
WlrLayershell.namespace: "dms:frame"
|
||||||
|
WlrLayershell.layer: WlrLayer.Top
|
||||||
|
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: true
|
||||||
|
bottom: true
|
||||||
|
left: true
|
||||||
|
right: true
|
||||||
|
}
|
||||||
|
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
// No input — pass everything through to apps and bar
|
||||||
|
mask: Region {}
|
||||||
|
|
||||||
|
readonly property var barEdges: {
|
||||||
|
SettingsData.barConfigs;
|
||||||
|
return SettingsData.getActiveBarEdgesForScreen(win.screen);
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property real _dpr: CompositorService.getScreenScale(win.screen)
|
||||||
|
readonly property bool _frameActive: SettingsData.frameEnabled
|
||||||
|
&& SettingsData.isScreenInPreferences(win.screen, SettingsData.frameScreenPreferences)
|
||||||
|
readonly property int _windowRegionWidth: win._regionInt(win.width)
|
||||||
|
readonly property int _windowRegionHeight: win._regionInt(win.height)
|
||||||
|
|
||||||
|
function _regionInt(value) {
|
||||||
|
return Math.max(0, Math.round(Theme.px(value, win._dpr)));
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property int cutoutTopInset: win._regionInt(barEdges.includes("top") ? SettingsData.frameBarSize : SettingsData.frameThickness)
|
||||||
|
readonly property int cutoutBottomInset: win._regionInt(barEdges.includes("bottom") ? SettingsData.frameBarSize : SettingsData.frameThickness)
|
||||||
|
readonly property int cutoutLeftInset: win._regionInt(barEdges.includes("left") ? SettingsData.frameBarSize : SettingsData.frameThickness)
|
||||||
|
readonly property int cutoutRightInset: win._regionInt(barEdges.includes("right") ? SettingsData.frameBarSize : SettingsData.frameThickness)
|
||||||
|
readonly property int cutoutWidth: Math.max(0, win._windowRegionWidth - win.cutoutLeftInset - win.cutoutRightInset)
|
||||||
|
readonly property int cutoutHeight: Math.max(0, win._windowRegionHeight - win.cutoutTopInset - win.cutoutBottomInset)
|
||||||
|
readonly property int cutoutRadius: {
|
||||||
|
const requested = win._regionInt(SettingsData.frameRounding);
|
||||||
|
const maxRadius = Math.floor(Math.min(win.cutoutWidth, win.cutoutHeight) / 2);
|
||||||
|
return Math.max(0, Math.min(requested, maxRadius));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slightly expand the subtractive blur cutout at very low opacity levels
|
||||||
|
readonly property int _blurCutoutCompensation: SettingsData.frameOpacity <= 0.2 ? 1 : 0
|
||||||
|
readonly property int _blurCutoutLeft: Math.max(0, win.cutoutLeftInset - win._blurCutoutCompensation)
|
||||||
|
readonly property int _blurCutoutTop: Math.max(0, win.cutoutTopInset - win._blurCutoutCompensation)
|
||||||
|
readonly property int _blurCutoutRight: Math.min(win._windowRegionWidth, win._windowRegionWidth - win.cutoutRightInset + win._blurCutoutCompensation)
|
||||||
|
readonly property int _blurCutoutBottom: Math.min(win._windowRegionHeight, win._windowRegionHeight - win.cutoutBottomInset + win._blurCutoutCompensation)
|
||||||
|
readonly property int _blurCutoutRadius: {
|
||||||
|
const requested = win.cutoutRadius + win._blurCutoutCompensation;
|
||||||
|
const maxRadius = Math.floor(Math.min(_blurCutout.width, _blurCutout.height) / 2);
|
||||||
|
return Math.max(0, Math.min(requested, maxRadius));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must stay visible so Region.item can resolve scene coordinates.
|
||||||
|
Item {
|
||||||
|
id: _blurCutout
|
||||||
|
x: win._blurCutoutLeft
|
||||||
|
y: win._blurCutoutTop
|
||||||
|
width: Math.max(0, win._blurCutoutRight - win._blurCutoutLeft)
|
||||||
|
height: Math.max(0, win._blurCutoutBottom - win._blurCutoutTop)
|
||||||
|
}
|
||||||
|
|
||||||
|
property var _frameBlurRegion: null
|
||||||
|
|
||||||
|
function _buildBlur() {
|
||||||
|
_teardownBlur();
|
||||||
|
// Follow the global blur toggle
|
||||||
|
if (!BlurService.enabled || !SettingsData.frameBlurEnabled || !win._frameActive || !win.visible)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
const region = Qt.createQmlObject(
|
||||||
|
'import QtQuick; import Quickshell; Region {' +
|
||||||
|
' property Item cutoutItem;' +
|
||||||
|
' property int cutoutRadius: 0;' +
|
||||||
|
' Region {' +
|
||||||
|
' item: cutoutItem;' +
|
||||||
|
' intersection: Intersection.Subtract;' +
|
||||||
|
' radius: cutoutRadius;' +
|
||||||
|
' }' +
|
||||||
|
'}',
|
||||||
|
win, "FrameBlurRegion");
|
||||||
|
|
||||||
|
region.x = Qt.binding(() => 0);
|
||||||
|
region.y = Qt.binding(() => 0);
|
||||||
|
region.width = Qt.binding(() => win._windowRegionWidth);
|
||||||
|
region.height = Qt.binding(() => win._windowRegionHeight);
|
||||||
|
region.cutoutItem = _blurCutout;
|
||||||
|
region.cutoutRadius = Qt.binding(() => win._blurCutoutRadius);
|
||||||
|
|
||||||
|
win.BackgroundEffect.blurRegion = region;
|
||||||
|
win._frameBlurRegion = region;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("FrameWindow: Failed to create blur region:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _teardownBlur() {
|
||||||
|
if (!win._frameBlurRegion)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
win.BackgroundEffect.blurRegion = null;
|
||||||
|
} catch (e) {}
|
||||||
|
win._frameBlurRegion.destroy();
|
||||||
|
win._frameBlurRegion = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: _blurRebuildTimer
|
||||||
|
interval: 1
|
||||||
|
onTriggered: win._buildBlur()
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: SettingsData
|
||||||
|
function onFrameBlurEnabledChanged() { _blurRebuildTimer.restart(); }
|
||||||
|
function onFrameEnabledChanged() { _blurRebuildTimer.restart(); }
|
||||||
|
function onFrameThicknessChanged() { _blurRebuildTimer.restart(); }
|
||||||
|
function onFrameBarSizeChanged() { _blurRebuildTimer.restart(); }
|
||||||
|
function onFrameOpacityChanged() { _blurRebuildTimer.restart(); }
|
||||||
|
function onFrameRoundingChanged() { _blurRebuildTimer.restart(); }
|
||||||
|
function onFrameScreenPreferencesChanged() { _blurRebuildTimer.restart(); }
|
||||||
|
function onBarConfigsChanged() { _blurRebuildTimer.restart(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: BlurService
|
||||||
|
function onEnabledChanged() { _blurRebuildTimer.restart(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible) {
|
||||||
|
win._frameBlurRegion = null;
|
||||||
|
_blurRebuildTimer.restart();
|
||||||
|
} else {
|
||||||
|
_teardownBlur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: Qt.callLater(() => win._buildBlur())
|
||||||
|
Component.onDestruction: win._teardownBlur()
|
||||||
|
|
||||||
|
FrameBorder {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: win._frameActive
|
||||||
|
cutoutTopInset: win.cutoutTopInset
|
||||||
|
cutoutBottomInset: win.cutoutBottomInset
|
||||||
|
cutoutLeftInset: win.cutoutLeftInset
|
||||||
|
cutoutRightInset: win.cutoutRightInset
|
||||||
|
cutoutRadius: win.cutoutRadius
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -693,6 +693,8 @@ Item {
|
|||||||
|
|
||||||
SettingsToggleRow {
|
SettingsToggleRow {
|
||||||
visible: CompositorService.isNiri
|
visible: CompositorService.isNiri
|
||||||
|
enabled: !SettingsData.frameEnabled
|
||||||
|
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
|
||||||
text: I18n.tr("Show on Overview")
|
text: I18n.tr("Show on Overview")
|
||||||
checked: selectedBarConfig?.openOnOverview ?? false
|
checked: selectedBarConfig?.openOnOverview ?? false
|
||||||
onToggled: toggled => {
|
onToggled: toggled => {
|
||||||
@@ -798,11 +800,42 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
visible: SettingsData.frameEnabled
|
||||||
|
width: parent.width
|
||||||
|
implicitHeight: frameNote.implicitHeight + Theme.spacingS * 2
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: frameNote
|
||||||
|
x: Theme.spacingM
|
||||||
|
width: parent.width - Theme.spacingM * 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "frame_source"
|
||||||
|
size: Theme.fontSizeMedium
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Spacing and size are managed by Frame mode")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
width: parent.width - Theme.fontSizeMedium - Theme.spacingS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
iconName: "space_bar"
|
iconName: "space_bar"
|
||||||
title: I18n.tr("Spacing")
|
title: I18n.tr("Spacing")
|
||||||
settingKey: "barSpacing"
|
settingKey: "barSpacing"
|
||||||
visible: selectedBarConfig?.enabled
|
visible: selectedBarConfig?.enabled
|
||||||
|
enabled: !SettingsData.frameEnabled
|
||||||
|
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
|
||||||
|
|
||||||
SettingsSliderRow {
|
SettingsSliderRow {
|
||||||
id: edgeSpacingSlider
|
id: edgeSpacingSlider
|
||||||
@@ -1003,6 +1036,8 @@ Item {
|
|||||||
|
|
||||||
SettingsSliderRow {
|
SettingsSliderRow {
|
||||||
id: barTransparencySlider
|
id: barTransparencySlider
|
||||||
|
enabled: !SettingsData.frameEnabled
|
||||||
|
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
|
||||||
text: I18n.tr("Bar Transparency")
|
text: I18n.tr("Bar Transparency")
|
||||||
value: (selectedBarConfig?.transparency ?? 1.0) * 100
|
value: (selectedBarConfig?.transparency ?? 1.0) * 100
|
||||||
minimum: 0
|
minimum: 0
|
||||||
@@ -1044,6 +1079,35 @@ Item {
|
|||||||
restoreMode: Binding.RestoreBinding
|
restoreMode: Binding.RestoreBinding
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
visible: SettingsData.frameEnabled
|
||||||
|
width: parent.width
|
||||||
|
implicitHeight: transparencyFrameNote.implicitHeight + Theme.spacingS * 2
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: transparencyFrameNote
|
||||||
|
x: Theme.spacingM
|
||||||
|
width: parent.width - Theme.spacingM * 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "frame_source"
|
||||||
|
size: Theme.fontSizeMedium
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Opacity is controlled by Frame Border Opacity in Frame settings")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
width: parent.width - Theme.fontSizeMedium - Theme.spacingS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
@@ -1287,6 +1351,8 @@ Item {
|
|||||||
|
|
||||||
SettingsToggleRow {
|
SettingsToggleRow {
|
||||||
text: I18n.tr("Square Corners")
|
text: I18n.tr("Square Corners")
|
||||||
|
enabled: !SettingsData.frameEnabled
|
||||||
|
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
|
||||||
checked: selectedBarConfig?.squareCorners ?? false
|
checked: selectedBarConfig?.squareCorners ?? false
|
||||||
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
||||||
squareCorners: checked
|
squareCorners: checked
|
||||||
@@ -1334,6 +1400,8 @@ Item {
|
|||||||
|
|
||||||
SettingsToggleRow {
|
SettingsToggleRow {
|
||||||
text: I18n.tr("Goth Corners")
|
text: I18n.tr("Goth Corners")
|
||||||
|
enabled: !SettingsData.frameEnabled
|
||||||
|
opacity: SettingsData.frameEnabled ? 0.5 : 1.0
|
||||||
checked: selectedBarConfig?.gothCornersEnabled ?? false
|
checked: selectedBarConfig?.gothCornersEnabled ?? false
|
||||||
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
||||||
gothCornersEnabled: checked
|
gothCornersEnabled: checked
|
||||||
|
|||||||
@@ -0,0 +1,295 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.Settings.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
LayoutMirroring.enabled: I18n.isRtl
|
||||||
|
LayoutMirroring.childrenInherit: true
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
anchors.fill: parent
|
||||||
|
clip: true
|
||||||
|
contentHeight: mainColumn.height + Theme.spacingXL
|
||||||
|
contentWidth: width
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: mainColumn
|
||||||
|
topPadding: 4
|
||||||
|
width: Math.min(550, parent.width - Theme.spacingL * 2)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: Theme.spacingXL
|
||||||
|
|
||||||
|
// ── Enable Frame ──────────────────────────────────────────────────
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "frame_source"
|
||||||
|
title: I18n.tr("Frame")
|
||||||
|
settingKey: "frameEnabled"
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "frameEnable"
|
||||||
|
tags: ["frame", "border", "outline", "display"]
|
||||||
|
text: I18n.tr("Enable Frame")
|
||||||
|
description: I18n.tr("Draw a connected picture-frame border around the entire display")
|
||||||
|
checked: SettingsData.frameEnabled
|
||||||
|
onToggled: checked => SettingsData.set("frameEnabled", checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Border ────────────────────────────────────────────────────────
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "border_outer"
|
||||||
|
title: I18n.tr("Border")
|
||||||
|
settingKey: "frameBorder"
|
||||||
|
collapsible: true
|
||||||
|
visible: SettingsData.frameEnabled
|
||||||
|
|
||||||
|
SettingsSliderRow {
|
||||||
|
id: roundingSlider
|
||||||
|
settingKey: "frameRounding"
|
||||||
|
tags: ["frame", "border", "rounding", "radius", "corner"]
|
||||||
|
text: I18n.tr("Border Radius")
|
||||||
|
unit: "px"
|
||||||
|
minimum: 0
|
||||||
|
maximum: 100
|
||||||
|
step: 1
|
||||||
|
defaultValue: 23
|
||||||
|
value: SettingsData.frameRounding
|
||||||
|
onSliderDragFinished: v => SettingsData.set("frameRounding", v)
|
||||||
|
|
||||||
|
Binding {
|
||||||
|
target: roundingSlider
|
||||||
|
property: "value"
|
||||||
|
value: SettingsData.frameRounding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsSliderRow {
|
||||||
|
id: thicknessSlider
|
||||||
|
settingKey: "frameThickness"
|
||||||
|
tags: ["frame", "border", "thickness", "size", "width"]
|
||||||
|
text: I18n.tr("Border Width")
|
||||||
|
unit: "px"
|
||||||
|
minimum: 2
|
||||||
|
maximum: 100
|
||||||
|
step: 1
|
||||||
|
defaultValue: 16
|
||||||
|
value: SettingsData.frameThickness
|
||||||
|
onSliderDragFinished: v => SettingsData.set("frameThickness", v)
|
||||||
|
|
||||||
|
Binding {
|
||||||
|
target: thicknessSlider
|
||||||
|
property: "value"
|
||||||
|
value: SettingsData.frameThickness
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsSliderRow {
|
||||||
|
id: barThicknessSlider
|
||||||
|
settingKey: "frameBarSize"
|
||||||
|
tags: ["frame", "bar", "thickness", "size", "height", "width"]
|
||||||
|
text: I18n.tr("Size")
|
||||||
|
description: I18n.tr("Height of horizontal bars / width of vertical bars in frame mode")
|
||||||
|
unit: "px"
|
||||||
|
minimum: 24
|
||||||
|
maximum: 100
|
||||||
|
step: 1
|
||||||
|
defaultValue: 40
|
||||||
|
value: SettingsData.frameBarSize
|
||||||
|
onSliderDragFinished: v => SettingsData.set("frameBarSize", v)
|
||||||
|
|
||||||
|
Binding {
|
||||||
|
target: barThicknessSlider
|
||||||
|
property: "value"
|
||||||
|
value: SettingsData.frameBarSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsSliderRow {
|
||||||
|
id: opacitySlider
|
||||||
|
settingKey: "frameOpacity"
|
||||||
|
tags: ["frame", "border", "opacity", "transparency"]
|
||||||
|
text: I18n.tr("Frame Opacity")
|
||||||
|
unit: "%"
|
||||||
|
minimum: 0
|
||||||
|
maximum: 100
|
||||||
|
defaultValue: 100
|
||||||
|
value: SettingsData.frameOpacity * 100
|
||||||
|
onSliderDragFinished: v => SettingsData.set("frameOpacity", v / 100)
|
||||||
|
|
||||||
|
Binding {
|
||||||
|
target: opacitySlider
|
||||||
|
property: "value"
|
||||||
|
value: SettingsData.frameOpacity * 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
id: frameBlurToggle
|
||||||
|
settingKey: "frameBlurEnabled"
|
||||||
|
tags: ["frame", "blur", "background", "glass", "transparency", "frosted"]
|
||||||
|
text: I18n.tr("Frame Blur")
|
||||||
|
description: !BlurService.available
|
||||||
|
? I18n.tr("Requires a newer version of Quickshell")
|
||||||
|
: I18n.tr("Apply compositor blur behind the frame border")
|
||||||
|
checked: SettingsData.frameBlurEnabled
|
||||||
|
onToggled: checked => SettingsData.set("frameBlurEnabled", checked)
|
||||||
|
enabled: BlurService.available && SettingsData.blurEnabled
|
||||||
|
opacity: enabled ? 1.0 : 0.5
|
||||||
|
visible: BlurService.available
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
visible: BlurService.available && !SettingsData.blurEnabled
|
||||||
|
width: parent.width
|
||||||
|
height: blurToggleNote.height + Theme.spacingM * 2
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: blurToggleNote
|
||||||
|
x: Theme.spacingM
|
||||||
|
width: parent.width - Theme.spacingM * 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: "blur_on"
|
||||||
|
size: Theme.fontSizeMedium
|
||||||
|
color: Theme.primary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Frame Blur is controlled by Background Blur in Theme & Colors")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
width: parent.width - Theme.fontSizeMedium - Theme.spacingS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color mode buttons
|
||||||
|
SettingsButtonGroupRow {
|
||||||
|
settingKey: "frameColor"
|
||||||
|
tags: ["frame", "border", "color", "theme", "primary", "surface", "default"]
|
||||||
|
text: I18n.tr("Border color")
|
||||||
|
model: [I18n.tr("Default"), I18n.tr("Primary"), I18n.tr("Surface"), I18n.tr("Custom")]
|
||||||
|
currentIndex: {
|
||||||
|
const fc = SettingsData.frameColor;
|
||||||
|
if (!fc || fc === "default") return 0;
|
||||||
|
if (fc === "primary") return 1;
|
||||||
|
if (fc === "surface") return 2;
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
onSelectionChanged: (index, selected) => {
|
||||||
|
if (!selected) return;
|
||||||
|
switch (index) {
|
||||||
|
case 0: SettingsData.set("frameColor", ""); break;
|
||||||
|
case 1: SettingsData.set("frameColor", "primary"); break;
|
||||||
|
case 2: SettingsData.set("frameColor", "surface"); break;
|
||||||
|
case 3:
|
||||||
|
const cur = SettingsData.frameColor;
|
||||||
|
const isPreset = !cur || cur === "primary" || cur === "surface";
|
||||||
|
if (isPreset) SettingsData.set("frameColor", "#2a2a2a");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom color swatch — only visible when a hex color is stored (Custom mode)
|
||||||
|
Item {
|
||||||
|
visible: {
|
||||||
|
const fc = SettingsData.frameColor;
|
||||||
|
return !!(fc && fc !== "primary" && fc !== "surface");
|
||||||
|
}
|
||||||
|
width: parent.width
|
||||||
|
height: customColorRow.height + Theme.spacingM * 2
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: customColorRow
|
||||||
|
width: parent.width - Theme.spacingM * 2
|
||||||
|
x: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: I18n.tr("Custom color")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: colorSwatch
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 32
|
||||||
|
height: 32
|
||||||
|
radius: 16
|
||||||
|
color: SettingsData.effectiveFrameColor
|
||||||
|
border.color: Theme.outline
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
PopoutService.colorPickerModal.selectedColor = SettingsData.effectiveFrameColor;
|
||||||
|
PopoutService.colorPickerModal.pickerTitle = I18n.tr("Frame Border Color");
|
||||||
|
PopoutService.colorPickerModal.onColorSelectedCallback = function (color) {
|
||||||
|
SettingsData.set("frameColor", color.toString());
|
||||||
|
};
|
||||||
|
PopoutService.colorPickerModal.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Bar Integration ───────────────────────────────────────────────
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "toolbar"
|
||||||
|
title: I18n.tr("Bar Integration")
|
||||||
|
settingKey: "frameBarIntegration"
|
||||||
|
collapsible: true
|
||||||
|
expanded: false
|
||||||
|
visible: SettingsData.frameEnabled
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
visible: CompositorService.isNiri
|
||||||
|
settingKey: "frameShowOnOverview"
|
||||||
|
tags: ["frame", "overview", "show", "hide", "niri"]
|
||||||
|
text: I18n.tr("Show on Overview")
|
||||||
|
description: I18n.tr("Show the bar and frame during Niri overview mode")
|
||||||
|
checked: SettingsData.frameShowOnOverview
|
||||||
|
onToggled: checked => SettingsData.set("frameShowOnOverview", checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Display Assignment ────────────────────────────────────────────
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "monitor"
|
||||||
|
title: I18n.tr("Display Assignment")
|
||||||
|
settingKey: "frameDisplays"
|
||||||
|
collapsible: true
|
||||||
|
expanded: false
|
||||||
|
visible: SettingsData.frameEnabled
|
||||||
|
|
||||||
|
SettingsDisplayPicker {
|
||||||
|
displayPreferences: SettingsData.frameScreenPreferences
|
||||||
|
onPreferencesChanged: prefs => SettingsData.set("frameScreenPreferences", prefs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
@@ -2671,6 +2750,15 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
text: I18n.tr("Follow DMS background color")
|
||||||
|
tags: ["matugen", "neovim", "terminal", "template"]
|
||||||
|
settingKey: "matugenTemplateNeovimSetBackground"
|
||||||
|
visible: neovimThemeToggle.visible && neovimThemeToggle.checked
|
||||||
|
checked: SettingsData.matugenTemplateNeovimSetBackground ?? true
|
||||||
|
onToggled: checked => SettingsData.set("matugenTemplateNeovimSetBackground", checked)
|
||||||
|
}
|
||||||
|
|
||||||
SettingsDivider {
|
SettingsDivider {
|
||||||
visible: neovimThemeToggle.visible && neovimThemeToggle.checked
|
visible: neovimThemeToggle.visible && neovimThemeToggle.checked
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ Item {
|
|||||||
description: modelData.width + "×" + modelData.height
|
description: modelData.width + "×" + modelData.height
|
||||||
checked: localChecked
|
checked: localChecked
|
||||||
onToggled: isChecked => {
|
onToggled: isChecked => {
|
||||||
localChecked = isChecked;
|
|
||||||
var prefs = JSON.parse(JSON.stringify(root.displayPreferences));
|
var prefs = JSON.parse(JSON.stringify(root.displayPreferences));
|
||||||
if (!Array.isArray(prefs) || prefs.includes("all"))
|
if (!Array.isArray(prefs) || prefs.includes("all"))
|
||||||
prefs = [];
|
prefs = [];
|
||||||
@@ -94,6 +93,11 @@ Item {
|
|||||||
model: modelData.model || ""
|
model: modelData.model || ""
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (prefs.length === 0) {
|
||||||
|
localChecked = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localChecked = isChecked;
|
||||||
root.preferencesChanged(prefs);
|
root.preferencesChanged(prefs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
import qs.Services
|
import qs.Services
|
||||||
@@ -52,15 +51,14 @@ Column {
|
|||||||
height: implicitHeight
|
height: implicitHeight
|
||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
RowLayout {
|
Row {
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
name: root.titleIcon
|
name: root.titleIcon
|
||||||
size: Theme.iconSize
|
size: Theme.iconSize
|
||||||
color: Theme.primary
|
color: Theme.primary
|
||||||
Layout.alignment: Qt.AlignVCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
@@ -68,7 +66,7 @@ Column {
|
|||||||
font.pixelSize: Theme.fontSizeLarge
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
Layout.alignment: Qt.AlignVCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -981,10 +979,26 @@ Column {
|
|||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: [
|
model: [
|
||||||
{ label: I18n.tr("Percentage"), mode: 0, icon: "percent" },
|
{
|
||||||
{ label: I18n.tr("Total"), mode: 1, icon: "storage" },
|
label: I18n.tr("Percentage"),
|
||||||
{ label: I18n.tr("Remaining"), mode: 2, icon: "hourglass_empty" },
|
mode: 0,
|
||||||
{ label: I18n.tr("Remaining / Total"), mode: 3, icon: "pie_chart" }
|
icon: "percent"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: I18n.tr("Total"),
|
||||||
|
mode: 1,
|
||||||
|
icon: "storage"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: I18n.tr("Remaining"),
|
||||||
|
mode: 2,
|
||||||
|
icon: "hourglass_empty"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: I18n.tr("Remaining / Total"),
|
||||||
|
mode: 3,
|
||||||
|
icon: "pie_chart"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
@@ -1316,20 +1330,7 @@ Column {
|
|||||||
id: longestControlCenterLabelMetrics
|
id: longestControlCenterLabelMetrics
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
text: {
|
text: {
|
||||||
const labels = [
|
const labels = [I18n.tr("Network"), I18n.tr("VPN"), I18n.tr("Bluetooth"), I18n.tr("Audio"), I18n.tr("Volume"), I18n.tr("Microphone"), I18n.tr("Microphone Volume"), I18n.tr("Brightness"), I18n.tr("Brightness Value"), I18n.tr("Battery"), I18n.tr("Printer"), I18n.tr("Screen Sharing")];
|
||||||
I18n.tr("Network"),
|
|
||||||
I18n.tr("VPN"),
|
|
||||||
I18n.tr("Bluetooth"),
|
|
||||||
I18n.tr("Audio"),
|
|
||||||
I18n.tr("Volume"),
|
|
||||||
I18n.tr("Microphone"),
|
|
||||||
I18n.tr("Microphone Volume"),
|
|
||||||
I18n.tr("Brightness"),
|
|
||||||
I18n.tr("Brightness Value"),
|
|
||||||
I18n.tr("Battery"),
|
|
||||||
I18n.tr("Printer"),
|
|
||||||
I18n.tr("Screen Sharing")
|
|
||||||
];
|
|
||||||
let longest = "";
|
let longest = "";
|
||||||
for (let i = 0; i < labels.length; i++) {
|
for (let i = 0; i < labels.length; i++) {
|
||||||
if (labels[i].length > longest.length)
|
if (labels[i].length > longest.length)
|
||||||
@@ -1340,6 +1341,7 @@ Column {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
|
id: groupRepeater
|
||||||
model: controlCenterContextMenu.controlCenterGroups
|
model: controlCenterContextMenu.controlCenterGroups
|
||||||
|
|
||||||
delegate: Item {
|
delegate: Item {
|
||||||
@@ -1569,8 +1571,6 @@ Column {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
id: groupRepeater
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
pragma Singleton
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Wayland // ! Import is needed despite what qmlls says
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool quickshellSupported: false
|
||||||
|
property bool compositorSupported: false
|
||||||
|
property bool available: quickshellSupported && compositorSupported
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: blurProbe
|
||||||
|
running: false
|
||||||
|
command: ["dms", "blur", "check"]
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
root.compositorSupported = text.trim() === "supported";
|
||||||
|
if (root.compositorSupported)
|
||||||
|
console.info("BlurService: Compositor supports ext-background-effect-v1");
|
||||||
|
else
|
||||||
|
console.info("BlurService: Compositor does not support ext-background-effect-v1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
if (exitCode !== 0)
|
||||||
|
console.warn("BlurService: blur probe failed with code:", exitCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
try {
|
||||||
|
const test = Qt.createQmlObject(`
|
||||||
|
import Quickshell
|
||||||
|
Region { radius: 0 }
|
||||||
|
`, root, "BlurAvailabilityTest");
|
||||||
|
test.destroy();
|
||||||
|
quickshellSupported = true;
|
||||||
|
console.info("BlurService: Quickshell blur support available");
|
||||||
|
blurProbe.running = true;
|
||||||
|
} 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": {
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ Row {
|
|||||||
width: Math.max(contentItem.implicitWidth + root.buttonPadding * 2, root.minButtonWidth) + (selected ? 4 : 0)
|
width: Math.max(contentItem.implicitWidth + root.buttonPadding * 2, root.minButtonWidth) + (selected ? 4 : 0)
|
||||||
height: root.buttonHeight
|
height: root.buttonHeight
|
||||||
|
|
||||||
color: selected ? Theme.buttonBg : Theme.surfaceVariant
|
color: selected ? Theme.buttonBg : Theme.withAlpha(Theme.surfaceVariant, Theme.popupTransparency)
|
||||||
border.color: "transparent"
|
border.color: "transparent"
|
||||||
border.width: 0
|
border.width: 0
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -256,15 +266,15 @@ PanelWindow {
|
|||||||
scale: shouldBeVisible ? 1 : 0.9
|
scale: shouldBeVisible ? 1 : 0.9
|
||||||
|
|
||||||
property bool childHovered: false
|
property bool childHovered: false
|
||||||
readonly property real popupSurfaceAlpha: SettingsData.popupTransparency
|
readonly property real popupSurfaceAlpha: Theme.popupTransparency
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: background
|
id: background
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,7 +286,7 @@ PanelWindow {
|
|||||||
level: Theme.elevationLevel3
|
level: Theme.elevationLevel3
|
||||||
fallbackOffset: 6
|
fallbackOffset: 6
|
||||||
targetRadius: Theme.cornerRadius
|
targetRadius: Theme.cornerRadius
|
||||||
targetColor: Theme.surfaceContainer
|
targetColor: Theme.withAlpha(Theme.surfaceContainer, osdContainer.popupSurfaceAlpha)
|
||||||
borderColor: Theme.outlineMedium
|
borderColor: Theme.outlineMedium
|
||||||
borderWidth: 1
|
borderWidth: 1
|
||||||
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "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")) {
|
||||||
@@ -565,14 +576,6 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
border.width: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: contentLoader
|
id: contentLoader
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -580,6 +583,21 @@ Item {
|
|||||||
asynchronous: false
|
asynchronous: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height
|
||||||
|
x: contentWrapper.x
|
||||||
|
y: contentWrapper.y
|
||||||
|
opacity: contentWrapper.opacity
|
||||||
|
scale: contentWrapper.scale
|
||||||
|
visible: contentWrapper.visible
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: "transparent"
|
||||||
|
border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium
|
||||||
|
border.width: BlurService.borderWidth
|
||||||
|
z: 100
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ Rectangle {
|
|||||||
width: fieldContent.width + Theme.spacingM * 2
|
width: fieldContent.width + Theme.spacingM * 2
|
||||||
height: 32
|
height: 32
|
||||||
radius: Theme.cornerRadius - 2
|
radius: Theme.cornerRadius - 2
|
||||||
color: Theme.surfaceContainerHigh
|
color: Theme.surfaceLight
|
||||||
border.width: 1
|
border.width: 1
|
||||||
border.color: Theme.outlineLight
|
border.color: Theme.outlineLight
|
||||||
|
|
||||||
@@ -272,7 +272,9 @@ Rectangle {
|
|||||||
checked: configData ? (configData.autoconnect || false) : false
|
checked: configData ? (configData.autoconnect || false) : false
|
||||||
visible: !VPNService.configLoading && configData !== null
|
visible: !VPNService.configLoading && configData !== null
|
||||||
onToggled: checked => {
|
onToggled: checked => {
|
||||||
VPNService.updateConfig(profile.uuid, {autoconnect: checked});
|
VPNService.updateConfig(profile.uuid, {
|
||||||
|
autoconnect: checked
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
[templates.dmsneovim]
|
[templates.dmsneovim-colors]
|
||||||
input_path = 'SHELL_DIR/matugen/templates/neovim.lua'
|
input_path = 'SHELL_DIR/matugen/templates/neovim-colors.lua'
|
||||||
output_path = 'CONFIG_DIR/nvim/colors/dms.lua'
|
output_path = 'CONFIG_DIR/nvim/colors/dms.lua'
|
||||||
|
|
||||||
|
[templates.dmsneovim-lualine]
|
||||||
|
input_path = 'SHELL_DIR/matugen/templates/neovim-lualine.lua'
|
||||||
|
output_path = 'CONFIG_DIR/nvim/lua/lualine/themes/dms.lua'
|
||||||
|
|||||||
+12
-1
@@ -35,6 +35,15 @@ local function deepGet(t, k)
|
|||||||
return t
|
return t
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local mode = vim.system({ "dms", "ipc", "call", "theme", "getMode" }, { text = true }):wait().stdout
|
||||||
|
if mode ~= nil then
|
||||||
|
if mode:match("light") then
|
||||||
|
vim.o.background = "light"
|
||||||
|
elseif mode:match("dark") then
|
||||||
|
vim.o.background = "dark"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local current_file_path = debug.getinfo(1, "S").source:sub(2)
|
local current_file_path = debug.getinfo(1, "S").source:sub(2)
|
||||||
local theme_base = deepGet(settings, { "matugenTemplateNeovimSettings", vim.o.background, "baseTheme" })
|
local theme_base = deepGet(settings, { "matugenTemplateNeovimSettings", vim.o.background, "baseTheme" })
|
||||||
or ("github_" .. vim.o.background)
|
or ("github_" .. vim.o.background)
|
||||||
@@ -74,7 +83,9 @@ end
|
|||||||
if not base46.theme_tables[theme_name] or base46.theme_tables[theme_name].type ~= vim.o.background then
|
if not base46.theme_tables[theme_name] or base46.theme_tables[theme_name].type ~= vim.o.background then
|
||||||
local builtin = vim.deepcopy(assert(base46.get_builtin_theme(theme_base)))
|
local builtin = vim.deepcopy(assert(base46.get_builtin_theme(theme_base)))
|
||||||
local harmonized = base46.theme_harmonize(builtin, "{{colors.source_color.default.hex}}", harmony)
|
local harmonized = base46.theme_harmonize(builtin, "{{colors.source_color.default.hex}}", harmony)
|
||||||
harmonized = base46.theme_set_bg(harmonized, "{{colors.background.default.hex}}")
|
if settings.matugenTemplateNeovimSetBackground ~= false then
|
||||||
|
harmonized = base46.theme_set_bg(harmonized, "{{colors.background.default.hex}}")
|
||||||
|
end
|
||||||
|
|
||||||
base46.theme_tables[theme_name] = harmonized
|
base46.theme_tables[theme_name] = harmonized
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- NOTE: this file should never be accessed if AvengeMedia/base46 is not installed,
|
||||||
|
-- the neovim-colors.lua template will fail first.
|
||||||
|
return require("lualine.themes._base46")("dms")
|
||||||
@@ -2,21 +2,21 @@
|
|||||||
"wallpaper": "{{image}}",
|
"wallpaper": "{{image}}",
|
||||||
"alpha": "100",
|
"alpha": "100",
|
||||||
"colors": {
|
"colors": {
|
||||||
"color0": "{{colors.background.default.hex}}",
|
"color0": "{{colors.background.dark.hex}}",
|
||||||
"color1": "",
|
"color1": "{{colors.surface_container_highest.light.hex}}",
|
||||||
"color2": "",
|
"color2": "{{colors.surface_container_lowest.light.hex}}",
|
||||||
"color3": "",
|
"color3": "{{colors.primary.light.hex}}",
|
||||||
"color4": "",
|
"color4": "{{colors.tertiary_fixed.default.hex}}",
|
||||||
"color5": "",
|
"color5": "{{colors.secondary.light.hex}}",
|
||||||
"color6": "",
|
"color6": "{{colors.on_tertiary_fixed.default.hex}}",
|
||||||
"color7": "",
|
"color7": "{{colors.surface_dim.light.hex}}",
|
||||||
"color8": "",
|
"color8": "{{colors.on_secondary.light.hex}}",
|
||||||
"color9": "",
|
"color9": "{{colors.error_container.default.hex}}",
|
||||||
"color10": "{{colors.primary.default.hex}}",
|
"color10": "{{colors.primary.dark.hex}}",
|
||||||
"color11": "",
|
"color11": "{{colors.on_error_container.default.hex}}",
|
||||||
"color12": "",
|
"color12": "{{colors.on_primary_fixed_variant.default.hex}}",
|
||||||
"color13": "{{colors.surface_bright.default.hex}}",
|
"color13": "{{colors.secondary.dark.hex}}",
|
||||||
"color14": "",
|
"color14": "{{colors.inverse_primary.default.hex}}",
|
||||||
"color15": "{{colors.on_surface.default.hex}}"
|
"color15": "{{colors.on_surface.dark.hex}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+185
-179
File diff suppressed because it is too large
Load Diff
@@ -2696,6 +2696,31 @@
|
|||||||
"theme"
|
"theme"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"section": "matugenTemplateNeovimSetBackground",
|
||||||
|
"label": "Follow DMS background color",
|
||||||
|
"tabIndex": 10,
|
||||||
|
"category": "Theme & Colors",
|
||||||
|
"keywords": [
|
||||||
|
"appearance",
|
||||||
|
"background",
|
||||||
|
"color",
|
||||||
|
"colors",
|
||||||
|
"colour",
|
||||||
|
"dms",
|
||||||
|
"follow",
|
||||||
|
"hue",
|
||||||
|
"look",
|
||||||
|
"matugen",
|
||||||
|
"neovim",
|
||||||
|
"scheme",
|
||||||
|
"style",
|
||||||
|
"template",
|
||||||
|
"terminal",
|
||||||
|
"theme",
|
||||||
|
"tint"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": "matugenTemplateGtk",
|
"section": "matugenTemplateGtk",
|
||||||
"label": "GTK",
|
"label": "GTK",
|
||||||
@@ -4359,6 +4384,27 @@
|
|||||||
],
|
],
|
||||||
"description": "Automatically lock the screen when DMS starts"
|
"description": "Automatically lock the screen when DMS starts"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"section": "lockBeforeSuspend",
|
||||||
|
"label": "Lock before suspend",
|
||||||
|
"tabIndex": 11,
|
||||||
|
"category": "Lock Screen",
|
||||||
|
"keywords": [
|
||||||
|
"automatic",
|
||||||
|
"automatically",
|
||||||
|
"before",
|
||||||
|
"lock",
|
||||||
|
"login",
|
||||||
|
"password",
|
||||||
|
"prepares",
|
||||||
|
"screen",
|
||||||
|
"security",
|
||||||
|
"sleep",
|
||||||
|
"suspend",
|
||||||
|
"system"
|
||||||
|
],
|
||||||
|
"description": "Automatically lock the screen when the system prepares to suspend"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": "lockScreenNotificationMode",
|
"section": "lockScreenNotificationMode",
|
||||||
"label": "Notification Display",
|
"label": "Notification Display",
|
||||||
@@ -6361,27 +6407,6 @@
|
|||||||
"icon": "schedule",
|
"icon": "schedule",
|
||||||
"description": "Gradually fade the screen before locking with a configurable grace period"
|
"description": "Gradually fade the screen before locking with a configurable grace period"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"section": "lockBeforeSuspend",
|
|
||||||
"label": "Lock before suspend",
|
|
||||||
"tabIndex": 21,
|
|
||||||
"category": "Power & Sleep",
|
|
||||||
"keywords": [
|
|
||||||
"automatically",
|
|
||||||
"before",
|
|
||||||
"energy",
|
|
||||||
"lock",
|
|
||||||
"power",
|
|
||||||
"prepares",
|
|
||||||
"screen",
|
|
||||||
"security",
|
|
||||||
"shutdown",
|
|
||||||
"sleep",
|
|
||||||
"suspend",
|
|
||||||
"system"
|
|
||||||
],
|
|
||||||
"description": "Automatically lock the screen when the system prepares to suspend"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"section": "fadeToLockGracePeriod",
|
"section": "fadeToLockGracePeriod",
|
||||||
"label": "Lock fade grace period",
|
"label": "Lock fade grace period",
|
||||||
|
|||||||
@@ -5977,6 +5977,13 @@
|
|||||||
"reference": "",
|
"reference": "",
|
||||||
"comment": ""
|
"comment": ""
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"term": "Follow DMS background color",
|
||||||
|
"translation": "",
|
||||||
|
"context": "",
|
||||||
|
"reference": "",
|
||||||
|
"comment": ""
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"term": "Follow Monitor Focus",
|
"term": "Follow Monitor Focus",
|
||||||
"translation": "",
|
"translation": "",
|
||||||
|
|||||||
Reference in New Issue
Block a user