mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-03 19:12:11 -04:00
Compare commits
6 Commits
anims
...
caaee88654
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
caaee88654 | ||
|
|
e872ddc1e7 | ||
|
|
1eca9b4c2c | ||
|
|
fe5bd42e25 | ||
|
|
32d16d0673 | ||
|
|
27c26d35ab |
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/golangci/golangci-lint
|
- repo: https://github.com/golangci/golangci-lint
|
||||||
rev: v2.10.1
|
rev: v2.9.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: golangci-lint-fmt
|
- id: golangci-lint-fmt
|
||||||
require_serial: true
|
require_serial: true
|
||||||
|
|||||||
10
core/cmd/dms/assets/cli-policy.default.json
Normal file
10
core/cmd/dms/assets/cli-policy.default.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"policy_version": 1,
|
||||||
|
"blocked_commands": [
|
||||||
|
"greeter install",
|
||||||
|
"greeter enable",
|
||||||
|
"greeter sync",
|
||||||
|
"setup"
|
||||||
|
],
|
||||||
|
"message": "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes."
|
||||||
|
}
|
||||||
@@ -24,9 +24,10 @@ var greeterCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var greeterInstallCmd = &cobra.Command{
|
var greeterInstallCmd = &cobra.Command{
|
||||||
Use: "install",
|
Use: "install",
|
||||||
Short: "Install and configure DMS greeter",
|
Short: "Install and configure DMS greeter",
|
||||||
Long: "Install greetd and configure it to use DMS as the greeter interface",
|
Long: "Install greetd and configure it to use DMS as the greeter interface",
|
||||||
|
PreRunE: requireMutableSystemCommand,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if err := installGreeter(); err != nil {
|
if err := installGreeter(); err != nil {
|
||||||
log.Fatalf("Error installing greeter: %v", err)
|
log.Fatalf("Error installing greeter: %v", err)
|
||||||
@@ -35,20 +36,39 @@ var greeterInstallCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var greeterSyncCmd = &cobra.Command{
|
var greeterSyncCmd = &cobra.Command{
|
||||||
Use: "sync",
|
Use: "sync",
|
||||||
Short: "Sync DMS theme and settings with greeter",
|
Short: "Sync DMS theme and settings with greeter",
|
||||||
Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen",
|
Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen",
|
||||||
|
PreRunE: requireMutableSystemCommand,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if err := syncGreeter(); err != nil {
|
yes, _ := cmd.Flags().GetBool("yes")
|
||||||
|
auth, _ := cmd.Flags().GetBool("auth")
|
||||||
|
local, _ := cmd.Flags().GetBool("local")
|
||||||
|
term, _ := cmd.Flags().GetBool("terminal")
|
||||||
|
if term {
|
||||||
|
if err := syncInTerminal(yes, auth, local); err != nil {
|
||||||
|
log.Fatalf("Error launching sync in terminal: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := syncGreeter(yes, auth, local); err != nil {
|
||||||
log.Fatalf("Error syncing greeter: %v", err)
|
log.Fatalf("Error syncing greeter: %v", err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
greeterSyncCmd.Flags().BoolP("yes", "y", false, "Non-interactive mode: skip prompts, use defaults (for UI)")
|
||||||
|
greeterSyncCmd.Flags().BoolP("terminal", "t", false, "Run sync in a new terminal (for entering sudo password); terminal auto-closes when done")
|
||||||
|
greeterSyncCmd.Flags().BoolP("auth", "a", false, "Configure PAM for fingerprint and U2F (adds both if modules exist); overrides UI toggles")
|
||||||
|
greeterSyncCmd.Flags().BoolP("local", "l", false, "Developer mode: force greetd config to use a local DMS checkout path")
|
||||||
|
}
|
||||||
|
|
||||||
var greeterEnableCmd = &cobra.Command{
|
var greeterEnableCmd = &cobra.Command{
|
||||||
Use: "enable",
|
Use: "enable",
|
||||||
Short: "Enable DMS greeter in greetd config",
|
Short: "Enable DMS greeter in greetd config",
|
||||||
Long: "Configure greetd to use DMS as the greeter",
|
Long: "Configure greetd to use DMS as the greeter",
|
||||||
|
PreRunE: requireMutableSystemCommand,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if err := enableGreeter(); err != nil {
|
if err := enableGreeter(); err != nil {
|
||||||
log.Fatalf("Error enabling greeter: %v", err)
|
log.Fatalf("Error enabling greeter: %v", err)
|
||||||
@@ -147,7 +167,7 @@ func installGreeter() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\nSynchronizing DMS configurations...")
|
fmt.Println("\nSynchronizing DMS configurations...")
|
||||||
if err := greeter.SyncDMSConfigs(dmsPath, selectedCompositor, logFunc, ""); err != nil {
|
if err := greeter.SyncDMSConfigs(dmsPath, selectedCompositor, logFunc, "", false); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,22 +191,88 @@ func installGreeter() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncGreeter() error {
|
func syncInTerminal(nonInteractive bool, forceAuth bool, local bool) error {
|
||||||
fmt.Println("=== DMS Greeter Theme Sync ===")
|
syncFlags := make([]string, 0, 3)
|
||||||
fmt.Println()
|
if nonInteractive {
|
||||||
|
syncFlags = append(syncFlags, "--yes")
|
||||||
|
}
|
||||||
|
if forceAuth {
|
||||||
|
syncFlags = append(syncFlags, "--auth")
|
||||||
|
}
|
||||||
|
if local {
|
||||||
|
syncFlags = append(syncFlags, "--local")
|
||||||
|
}
|
||||||
|
shellSyncCmd := "dms greeter sync"
|
||||||
|
if len(syncFlags) > 0 {
|
||||||
|
shellSyncCmd += " " + strings.Join(syncFlags, " ")
|
||||||
|
}
|
||||||
|
shellCmd := shellSyncCmd + `; echo; echo "Sync finished. Closing in 3 seconds..."; sleep 3`
|
||||||
|
terminals := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
}{
|
||||||
|
{"gnome-terminal", []string{"--", "bash", "-c", shellCmd}},
|
||||||
|
{"konsole", []string{"-e", "bash", "-c", shellCmd}},
|
||||||
|
{"xfce4-terminal", []string{"-e", "bash -c \"" + strings.ReplaceAll(shellCmd, `"`, `\"`) + "\""}},
|
||||||
|
{"ghostty", []string{"-e", "bash", "-c", shellCmd}},
|
||||||
|
{"wezterm", []string{"start", "--", "bash", "-c", shellCmd}},
|
||||||
|
{"alacritty", []string{"-e", "bash", "-c", shellCmd}},
|
||||||
|
{"kitty", []string{"bash", "-c", shellCmd}},
|
||||||
|
{"xterm", []string{"-e", "bash -c \"" + strings.ReplaceAll(shellCmd, `"`, `\"`) + "\""}},
|
||||||
|
}
|
||||||
|
for _, t := range terminals {
|
||||||
|
if _, err := exec.LookPath(t.name); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cmd := exec.Command(t.name, t.args...)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_ = cmd.Process.Release()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("no terminal emulator found (tried: gnome-terminal, konsole, xfce4-terminal, ghostty, wezterm, alacritty, kitty, xterm)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
||||||
|
if !nonInteractive {
|
||||||
|
fmt.Println("=== DMS Greeter Theme Sync ===")
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
logFunc := func(msg string) {
|
logFunc := func(msg string) {
|
||||||
fmt.Println(msg)
|
fmt.Println(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Detecting DMS installation...")
|
if !nonInteractive {
|
||||||
dmsPath, err := greeter.DetectDMSPath()
|
fmt.Println("Detecting DMS installation...")
|
||||||
if err != nil {
|
}
|
||||||
return err
|
var dmsPath string
|
||||||
|
var err error
|
||||||
|
if local {
|
||||||
|
dmsPath, err = resolveLocalDMSPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !nonInteractive {
|
||||||
|
fmt.Printf("✓ Using local DMS path: %s\n", dmsPath)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dmsPath, err = greeter.DetectDMSPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !nonInteractive {
|
||||||
|
fmt.Printf("✓ Found DMS at: %s\n", dmsPath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fmt.Printf("✓ Found DMS at: %s\n", dmsPath)
|
|
||||||
|
|
||||||
if !isGreeterEnabled() {
|
if !isGreeterEnabled() {
|
||||||
|
if nonInteractive {
|
||||||
|
return fmt.Errorf("greeter is not enabled; run 'dms greeter install' or 'dms greeter enable' first")
|
||||||
|
}
|
||||||
fmt.Println("\n⚠ DMS greeter is not enabled in greetd config.")
|
fmt.Println("\n⚠ DMS greeter is not enabled in greetd config.")
|
||||||
fmt.Print("Would you like to enable it now? (Y/n): ")
|
fmt.Print("Would you like to enable it now? (Y/n): ")
|
||||||
|
|
||||||
@@ -203,9 +289,12 @@ func syncGreeter() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheDir := "/var/cache/dms-greeter"
|
cacheDir := greeter.GreeterCacheDir
|
||||||
if _, err := os.Stat(cacheDir); os.IsNotExist(err) {
|
if _, err := os.Stat(cacheDir); os.IsNotExist(err) {
|
||||||
return fmt.Errorf("greeter cache directory not found at %s\nPlease install the greeter first", cacheDir)
|
logFunc("Cache directory not found — attempting to create it...")
|
||||||
|
if createErr := greeter.EnsureGreeterCacheDir(logFunc, ""); createErr != nil {
|
||||||
|
return fmt.Errorf("greeter cache directory not found at %s and could not be created: %w\nRun: sudo mkdir -p %s && sudo chown greeter:greeter %s", cacheDir, createErr, cacheDir, cacheDir)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
greeterGroup := greeter.DetectGreeterGroup()
|
greeterGroup := greeter.DetectGreeterGroup()
|
||||||
@@ -224,6 +313,9 @@ func syncGreeter() error {
|
|||||||
|
|
||||||
inGreeterGroup := strings.Contains(string(groupsOutput), greeterGroup)
|
inGreeterGroup := strings.Contains(string(groupsOutput), greeterGroup)
|
||||||
if !inGreeterGroup {
|
if !inGreeterGroup {
|
||||||
|
if nonInteractive {
|
||||||
|
return fmt.Errorf("user must be in the %s group; run 'dms greeter sync' from a terminal to add", greeterGroup)
|
||||||
|
}
|
||||||
fmt.Printf("\n⚠ Warning: You are not in the %s group.\n", greeterGroup)
|
fmt.Printf("\n⚠ Warning: You are not in the %s group.\n", greeterGroup)
|
||||||
fmt.Printf("Would you like to add your user to the %s group? (Y/n): ", greeterGroup)
|
fmt.Printf("Would you like to add your user to the %s group? (Y/n): ", greeterGroup)
|
||||||
|
|
||||||
@@ -255,8 +347,14 @@ func syncGreeter() error {
|
|||||||
return fmt.Errorf("no supported compositors found")
|
return fmt.Errorf("no supported compositors found")
|
||||||
case 1:
|
case 1:
|
||||||
compositor = compositors[0]
|
compositor = compositors[0]
|
||||||
fmt.Printf("✓ Using compositor: %s\n", compositor)
|
if !nonInteractive {
|
||||||
|
fmt.Printf("✓ Using compositor: %s\n", compositor)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
|
if nonInteractive {
|
||||||
|
compositor = compositors[0]
|
||||||
|
break
|
||||||
|
}
|
||||||
var err error
|
var err error
|
||||||
compositor, err = promptCompositorChoice(compositors)
|
compositor, err = promptCompositorChoice(compositors)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -264,27 +362,159 @@ func syncGreeter() error {
|
|||||||
}
|
}
|
||||||
fmt.Printf("✓ Selected compositor: %s\n", compositor)
|
fmt.Printf("✓ Selected compositor: %s\n", compositor)
|
||||||
}
|
}
|
||||||
} else {
|
} else if !nonInteractive {
|
||||||
fmt.Printf("✓ Detected compositor from config: %s\n", compositor)
|
fmt.Printf("✓ Detected compositor from config: %s\n", compositor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if local {
|
||||||
|
localWrapperScript := filepath.Join(dmsPath, "Modules", "Greetd", "assets", "dms-greeter")
|
||||||
|
restoreWrapperOverride := func() {}
|
||||||
|
if info, statErr := os.Stat(localWrapperScript); statErr == nil && !info.IsDir() {
|
||||||
|
previousWrapperOverride, hadWrapperOverride := os.LookupEnv("DMS_GREETER_WRAPPER_CMD")
|
||||||
|
wrapperCmdOverride := "/usr/bin/bash " + localWrapperScript
|
||||||
|
_ = os.Setenv("DMS_GREETER_WRAPPER_CMD", wrapperCmdOverride)
|
||||||
|
restoreWrapperOverride = func() {
|
||||||
|
if hadWrapperOverride {
|
||||||
|
_ = os.Setenv("DMS_GREETER_WRAPPER_CMD", previousWrapperOverride)
|
||||||
|
} else {
|
||||||
|
_ = os.Unsetenv("DMS_GREETER_WRAPPER_CMD")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !nonInteractive {
|
||||||
|
fmt.Printf("✓ Using local greeter wrapper script: %s\n", localWrapperScript)
|
||||||
|
}
|
||||||
|
} else if !nonInteractive {
|
||||||
|
fmt.Printf("ℹ Local wrapper script not found at %s; using system wrapper.\n", localWrapperScript)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nUpdating greetd command to use local DMS path...")
|
||||||
|
err := greeter.ConfigureGreetd(dmsPath, compositor, logFunc, "")
|
||||||
|
restoreWrapperOverride()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to apply local greeter path: %w", err)
|
||||||
|
}
|
||||||
|
if !nonInteractive {
|
||||||
|
fmt.Println("ℹ Local mode applies both DMS path override (-p) and local wrapper behavior when available.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
greeterPathForConfig := ""
|
||||||
|
if !greeter.IsGreeterPackaged() {
|
||||||
|
greeterPathForConfig = dmsPath
|
||||||
|
}
|
||||||
|
fmt.Println("\nUpdating greetd command...")
|
||||||
|
if err := greeter.ConfigureGreetd(greeterPathForConfig, compositor, logFunc, ""); err != nil {
|
||||||
|
return fmt.Errorf("failed to update greetd command: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("\nSetting up permissions and ACLs...")
|
fmt.Println("\nSetting up permissions and ACLs...")
|
||||||
if err := greeter.SetupDMSGroup(logFunc, ""); err != nil {
|
if err := greeter.SetupDMSGroup(logFunc, ""); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\nSynchronizing DMS configurations...")
|
fmt.Println("\nSynchronizing DMS configurations...")
|
||||||
if err := greeter.SyncDMSConfigs(dmsPath, compositor, logFunc, ""); err != nil {
|
if err := greeter.SyncDMSConfigs(dmsPath, compositor, logFunc, "", forceAuth); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
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.")
|
||||||
|
if forceAuth {
|
||||||
|
fmt.Println("PAM 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.")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hasDmsShellQml(dir string) bool {
|
||||||
|
info, err := os.Stat(filepath.Join(dir, "shell.qml"))
|
||||||
|
return err == nil && !info.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveDMSLocalCandidate(path string) (string, bool) {
|
||||||
|
if path == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if hasDmsShellQml(path) {
|
||||||
|
abs, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
return path, true
|
||||||
|
}
|
||||||
|
return abs, true
|
||||||
|
}
|
||||||
|
|
||||||
|
quickshellPath := filepath.Join(path, "quickshell")
|
||||||
|
if hasDmsShellQml(quickshellPath) {
|
||||||
|
abs, err := filepath.Abs(quickshellPath)
|
||||||
|
if err != nil {
|
||||||
|
return quickshellPath, true
|
||||||
|
}
|
||||||
|
return abs, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveLocalDMSPath() (string, error) {
|
||||||
|
if override := strings.TrimSpace(os.Getenv("DMS_LOCAL_PATH")); override != "" {
|
||||||
|
if resolved, ok := resolveDMSLocalCandidate(override); ok {
|
||||||
|
return resolved, nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("DMS_LOCAL_PATH is set but does not point to a valid DMS quickshell path: %s", override)
|
||||||
|
}
|
||||||
|
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get current directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := wd
|
||||||
|
for {
|
||||||
|
if resolved, ok := resolveDMSLocalCandidate(dir); ok {
|
||||||
|
return resolved, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parent := filepath.Dir(dir)
|
||||||
|
if parent == dir {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
dir = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err == nil && homeDir != "" {
|
||||||
|
for _, candidate := range []string{
|
||||||
|
filepath.Join(homeDir, "dms"),
|
||||||
|
filepath.Join(homeDir, "DankMaterialShell"),
|
||||||
|
filepath.Join(homeDir, "dankmaterialshell"),
|
||||||
|
filepath.Join(homeDir, "projects", "dms"),
|
||||||
|
filepath.Join(homeDir, "src", "dms"),
|
||||||
|
} {
|
||||||
|
if resolved, ok := resolveDMSLocalCandidate(candidate); ok {
|
||||||
|
return resolved, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if entries, readErr := os.ReadDir(homeDir); readErr == nil {
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := strings.ToLower(entry.Name())
|
||||||
|
if !strings.Contains(name, "dms") && !strings.Contains(name, "dank") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if resolved, ok := resolveDMSLocalCandidate(filepath.Join(homeDir, entry.Name())); ok {
|
||||||
|
return resolved, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("could not locate a local DMS checkout from %s; run from repo root or set DMS_LOCAL_PATH=/absolute/path/to/repo", wd)
|
||||||
|
}
|
||||||
|
|
||||||
func disableDisplayManager(dmName string) (bool, error) {
|
func disableDisplayManager(dmName string) (bool, error) {
|
||||||
state, err := getSystemdServiceState(dmName)
|
state, err := getSystemdServiceState(dmName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -486,22 +716,30 @@ func enableGreeter() error {
|
|||||||
configPath := "/etc/greetd/config.toml"
|
configPath := "/etc/greetd/config.toml"
|
||||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||||
return fmt.Errorf("greetd config not found at %s\nPlease install greetd first", configPath)
|
return fmt.Errorf("greetd config not found at %s\nPlease install greetd first", configPath)
|
||||||
|
} else if err != nil {
|
||||||
|
return fmt.Errorf("failed to access greetd config at %s: %w", configPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := os.ReadFile(configPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read greetd config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
configContent := string(data)
|
|
||||||
if greeter.IsGreeterPackaged() && greeter.HasLegacyLocalGreeterWrapper() {
|
if greeter.IsGreeterPackaged() && greeter.HasLegacyLocalGreeterWrapper() {
|
||||||
return fmt.Errorf("legacy manual wrapper detected at /usr/local/bin/dms-greeter; remove it before using packaged dms-greeter: sudo rm -f /usr/local/bin/dms-greeter")
|
return fmt.Errorf("legacy manual wrapper detected at /usr/local/bin/dms-greeter; remove it before using packaged dms-greeter: sudo rm -f /usr/local/bin/dms-greeter")
|
||||||
}
|
}
|
||||||
|
|
||||||
configAlreadyCorrect := strings.Contains(configContent, "dms-greeter")
|
configAlreadyCorrect := isGreeterEnabled()
|
||||||
|
configuredCompositor := detectConfiguredCompositor()
|
||||||
|
|
||||||
|
logFunc := func(msg string) {
|
||||||
|
fmt.Println(msg)
|
||||||
|
}
|
||||||
|
|
||||||
if configAlreadyCorrect {
|
if configAlreadyCorrect {
|
||||||
fmt.Println("✓ Greeter is already configured with dms-greeter")
|
fmt.Println("✓ Greeter is already configured with dms-greeter")
|
||||||
|
if configuredCompositor != "" {
|
||||||
|
fmt.Printf("✓ Configured compositor: %s\n", configuredCompositor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := greeter.EnsureGreeterCacheDir(logFunc, ""); err != nil {
|
||||||
|
fmt.Printf("⚠ Could not create cache directory: %v\n Run: sudo mkdir -p %s && sudo chown greeter:greeter %s\n", err, greeter.GreeterCacheDir, greeter.GreeterCacheDir)
|
||||||
|
}
|
||||||
|
|
||||||
if err := ensureGraphicalTarget(); err != nil {
|
if err := ensureGraphicalTarget(); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -548,68 +786,22 @@ func enableGreeter() error {
|
|||||||
fmt.Printf("✓ Selected compositor: %s\n", selectedCompositor)
|
fmt.Printf("✓ Selected compositor: %s\n", selectedCompositor)
|
||||||
}
|
}
|
||||||
|
|
||||||
backupPath := configPath + ".backup"
|
greeterPathForConfig := ""
|
||||||
backupCmd := exec.Command("sudo", "cp", configPath, backupPath)
|
if !greeter.IsGreeterPackaged() {
|
||||||
if err := backupCmd.Run(); err != nil {
|
dmsPath, err := greeter.DetectDMSPath()
|
||||||
return fmt.Errorf("failed to backup config: %w", err)
|
if err != nil {
|
||||||
}
|
return fmt.Errorf("failed to detect DMS path for manual greeter configuration: %w", err)
|
||||||
fmt.Printf("✓ Backed up config to %s\n", backupPath)
|
|
||||||
|
|
||||||
lines := strings.Split(configContent, "\n")
|
|
||||||
var newLines []string
|
|
||||||
for _, line := range lines {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") {
|
|
||||||
newLines = append(newLines, line)
|
|
||||||
}
|
}
|
||||||
|
greeterPathForConfig = dmsPath
|
||||||
|
}
|
||||||
|
if err := greeter.ConfigureGreetd(greeterPathForConfig, selectedCompositor, logFunc, ""); err != nil {
|
||||||
|
return fmt.Errorf("failed to configure greetd: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
wrapperCmd, err := findCommandPath("dms-greeter")
|
if err := greeter.EnsureGreeterCacheDir(logFunc, ""); err != nil {
|
||||||
if err != nil {
|
fmt.Printf("⚠ Could not create cache directory: %v\n Run: sudo mkdir -p %s && sudo chown greeter:greeter %s\n", err, greeter.GreeterCacheDir, greeter.GreeterCacheDir)
|
||||||
return fmt.Errorf("dms-greeter not found in PATH. Please ensure it is installed and accessible")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
compositorLower := strings.ToLower(selectedCompositor)
|
|
||||||
commandLine := fmt.Sprintf(`command = "%s --command %s"`, wrapperCmd, compositorLower)
|
|
||||||
|
|
||||||
var finalLines []string
|
|
||||||
inDefaultSession := false
|
|
||||||
commandAdded := false
|
|
||||||
|
|
||||||
for _, line := range newLines {
|
|
||||||
finalLines = append(finalLines, line)
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
|
|
||||||
if trimmed == "[default_session]" {
|
|
||||||
inDefaultSession = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if inDefaultSession && !commandAdded {
|
|
||||||
if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") {
|
|
||||||
finalLines = append(finalLines, commandLine)
|
|
||||||
commandAdded = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !commandAdded {
|
|
||||||
finalLines = append(finalLines, commandLine)
|
|
||||||
}
|
|
||||||
|
|
||||||
newConfig := strings.Join(finalLines, "\n")
|
|
||||||
|
|
||||||
tmpFile := "/tmp/greetd-config.toml"
|
|
||||||
if err := os.WriteFile(tmpFile, []byte(newConfig), 0o644); err != nil {
|
|
||||||
return fmt.Errorf("failed to write temp config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
moveCmd := exec.Command("sudo", "mv", tmpFile, configPath)
|
|
||||||
if err := moveCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to update config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("✓ Updated greetd configuration to use %s\n", selectedCompositor)
|
|
||||||
|
|
||||||
if err := ensureGraphicalTarget(); err != nil {
|
if err := ensureGraphicalTarget(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -631,38 +823,164 @@ func enableGreeter() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isGreeterEnabled() bool {
|
func isGreeterEnabled() bool {
|
||||||
data, err := os.ReadFile("/etc/greetd/config.toml")
|
command := readDefaultSessionCommand("/etc/greetd/config.toml")
|
||||||
if err != nil {
|
return command != "" && strings.Contains(command, "dms-greeter")
|
||||||
return false
|
|
||||||
}
|
|
||||||
return strings.Contains(string(data), "dms-greeter")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func detectConfiguredCompositor() string {
|
func detectConfiguredCompositor() string {
|
||||||
data, err := os.ReadFile("/etc/greetd/config.toml")
|
command := strings.ToLower(readDefaultSessionCommand("/etc/greetd/config.toml"))
|
||||||
|
switch {
|
||||||
|
case strings.Contains(command, "--command niri"):
|
||||||
|
return "niri"
|
||||||
|
case strings.Contains(command, "--command hyprland"):
|
||||||
|
return "hyprland"
|
||||||
|
case strings.Contains(command, "--command sway"):
|
||||||
|
return "sway"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripTomlComment(line string) string {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if idx := strings.Index(trimmed, "#"); idx >= 0 {
|
||||||
|
return strings.TrimSpace(trimmed[:idx])
|
||||||
|
}
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTomlSection(line string) (string, bool) {
|
||||||
|
trimmed := stripTomlComment(line)
|
||||||
|
if len(trimmed) < 3 || !strings.HasPrefix(trimmed, "[") || !strings.HasSuffix(trimmed, "]") {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(trimmed[1 : len(trimmed)-1]), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func readDefaultSessionCommand(configPath string) string {
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, line := range strings.Split(string(data), "\n") {
|
inDefaultSession := false
|
||||||
trimmed := strings.TrimSpace(line)
|
for line := range strings.SplitSeq(string(data), "\n") {
|
||||||
if !strings.HasPrefix(trimmed, "command") || !strings.Contains(trimmed, "dms-greeter") {
|
if section, ok := parseTomlSection(line); ok {
|
||||||
|
inDefaultSession = section == "default_session"
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
if !inDefaultSession {
|
||||||
case strings.Contains(trimmed, "--command niri"):
|
continue
|
||||||
return "niri"
|
}
|
||||||
case strings.Contains(trimmed, "--command hyprland"):
|
|
||||||
return "hyprland"
|
trimmed := stripTomlComment(line)
|
||||||
case strings.Contains(trimmed, "--command sway"):
|
if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") {
|
||||||
return "sway"
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(trimmed, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
command := strings.Trim(strings.TrimSpace(parts[1]), `"`)
|
||||||
|
if command != "" {
|
||||||
|
return command
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func extractGreeterCacheDirFromCommand(command string) string {
|
||||||
|
if command == "" {
|
||||||
|
return greeter.GreeterCacheDir
|
||||||
|
}
|
||||||
|
tokens := strings.Fields(command)
|
||||||
|
for i := 0; i < len(tokens); i++ {
|
||||||
|
token := strings.Trim(tokens[i], "\"")
|
||||||
|
if token == "--cache-dir" && i+1 < len(tokens) {
|
||||||
|
return strings.Trim(tokens[i+1], "\"")
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(token, "--cache-dir=") {
|
||||||
|
value := strings.TrimPrefix(token, "--cache-dir=")
|
||||||
|
value = strings.Trim(value, "\"")
|
||||||
|
if value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return greeter.GreeterCacheDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractGreeterWrapperFromCommand(command string) string {
|
||||||
|
if command == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
tokens := strings.Fields(command)
|
||||||
|
if len(tokens) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.Trim(tokens[0], "\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractGreeterPathOverrideFromCommand(command string) string {
|
||||||
|
if command == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
tokens := strings.Fields(command)
|
||||||
|
for i := 0; i < len(tokens); i++ {
|
||||||
|
token := strings.Trim(tokens[i], "\"")
|
||||||
|
if (token == "-p" || token == "--path") && i+1 < len(tokens) {
|
||||||
|
return strings.Trim(tokens[i+1], "\"")
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(token, "--path=") {
|
||||||
|
value := strings.TrimPrefix(token, "--path=")
|
||||||
|
value = strings.Trim(value, "\"")
|
||||||
|
if value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 {
|
||||||
osInfo, err := distros.GetOSInfo()
|
osInfo, err := distros.GetOSInfo()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -741,39 +1059,40 @@ func checkGreeterStatus() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
configPath := "/etc/greetd/config.toml"
|
configPath := "/etc/greetd/config.toml"
|
||||||
|
configuredCommand := ""
|
||||||
|
allGood := true
|
||||||
fmt.Println("Greeter Configuration:")
|
fmt.Println("Greeter Configuration:")
|
||||||
if data, err := os.ReadFile(configPath); err == nil {
|
if _, err := os.ReadFile(configPath); err == nil {
|
||||||
configContent := string(data)
|
configuredCommand = readDefaultSessionCommand(configPath)
|
||||||
if strings.Contains(configContent, "dms-greeter") {
|
if configuredCommand != "" && strings.Contains(configuredCommand, "dms-greeter") {
|
||||||
lines := strings.SplitSeq(configContent, "\n")
|
fmt.Println(" ✓ Greeter is enabled")
|
||||||
for line := range lines {
|
if wrapper := extractGreeterWrapperFromCommand(configuredCommand); wrapper != "" {
|
||||||
trimmed := strings.TrimSpace(line)
|
fmt.Printf(" Wrapper: %s\n", wrapper)
|
||||||
if strings.HasPrefix(trimmed, "command =") || strings.HasPrefix(trimmed, "command=") {
|
}
|
||||||
parts := strings.SplitN(trimmed, "=", 2)
|
if pathOverride := extractGreeterPathOverrideFromCommand(configuredCommand); pathOverride != "" {
|
||||||
if len(parts) == 2 {
|
fmt.Printf(" DMS path override: %s\n", pathOverride)
|
||||||
command := strings.Trim(strings.TrimSpace(parts[1]), `"`)
|
}
|
||||||
fmt.Println(" ✓ Greeter is enabled")
|
|
||||||
|
|
||||||
if strings.Contains(command, "--command niri") {
|
compositor := detectConfiguredCompositor()
|
||||||
fmt.Println(" Compositor: niri")
|
switch compositor {
|
||||||
} else if strings.Contains(command, "--command hyprland") {
|
case "niri":
|
||||||
fmt.Println(" Compositor: Hyprland")
|
fmt.Println(" Compositor: niri")
|
||||||
} else if strings.Contains(command, "--command sway") {
|
case "hyprland":
|
||||||
fmt.Println(" Compositor: sway")
|
fmt.Println(" Compositor: Hyprland")
|
||||||
} else {
|
case "sway":
|
||||||
fmt.Println(" Compositor: unknown")
|
fmt.Println(" Compositor: sway")
|
||||||
}
|
default:
|
||||||
}
|
fmt.Println(" Compositor: unknown")
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Println(" ✗ Greeter is NOT enabled")
|
fmt.Println(" ✗ Greeter is NOT enabled")
|
||||||
fmt.Println(" Run 'dms greeter enable' to enable it")
|
fmt.Println(" Run 'dms greeter enable' to enable it")
|
||||||
|
allGood = false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Println(" ✗ Greeter config not found")
|
fmt.Println(" ✗ Greeter config not found")
|
||||||
fmt.Printf(" %s\n", packageInstallHint())
|
fmt.Printf(" %s\n", packageInstallHint())
|
||||||
|
allGood = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\nGroup Membership:")
|
fmt.Println("\nGroup Membership:")
|
||||||
@@ -792,8 +1111,12 @@ func checkGreeterStatus() error {
|
|||||||
fmt.Println(" Run 'dms greeter sync' to set up group membership and permissions")
|
fmt.Println(" Run 'dms greeter sync' to set up group membership and permissions")
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheDir := "/var/cache/dms-greeter"
|
cacheDir := extractGreeterCacheDirFromCommand(configuredCommand)
|
||||||
fmt.Println("\nGreeter Cache Directory:")
|
fmt.Println("\nGreeter Cache Directory:")
|
||||||
|
fmt.Printf(" Effective cache dir: %s\n", cacheDir)
|
||||||
|
if cacheDir != greeter.GreeterCacheDir {
|
||||||
|
fmt.Printf(" ⚠ Non-default cache dir detected (default: %s)\n", greeter.GreeterCacheDir)
|
||||||
|
}
|
||||||
if stat, err := os.Stat(cacheDir); err == nil && stat.IsDir() {
|
if stat, err := os.Stat(cacheDir); err == nil && stat.IsDir() {
|
||||||
fmt.Printf(" ✓ %s exists\n", cacheDir)
|
fmt.Printf(" ✓ %s exists\n", cacheDir)
|
||||||
} else {
|
} else {
|
||||||
@@ -825,7 +1148,6 @@ func checkGreeterStatus() error {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
allGood := true
|
|
||||||
for _, link := range symlinks {
|
for _, link := range symlinks {
|
||||||
targetInfo, err := os.Lstat(link.target)
|
targetInfo, err := os.Lstat(link.target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -864,11 +1186,80 @@ func checkGreeterStatus() error {
|
|||||||
fmt.Printf(" ✓ %s: synced correctly\n", link.desc)
|
fmt.Printf(" ✓ %s: synced correctly\n", link.desc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nGreeter Wallpaper Override:")
|
||||||
|
overridePath := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
|
||||||
|
if stat, err := os.Stat(overridePath); err == nil && !stat.IsDir() {
|
||||||
|
fmt.Printf(" ✓ Override file present: %s\n", overridePath)
|
||||||
|
} else if os.IsNotExist(err) {
|
||||||
|
fmt.Println(" ℹ Override file not present (desktop/session wallpaper fallback in effect)")
|
||||||
|
} else if err != nil {
|
||||||
|
fmt.Printf(" ✗ Could not inspect override file: %v\n", err)
|
||||||
|
allGood = false
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" ✗ Override path is not a regular file: %s\n", overridePath)
|
||||||
|
allGood = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("\nGreeter PAM Authentication (DMS-managed block):")
|
||||||
|
if greeter.IsNixOS() {
|
||||||
|
fmt.Println(" ℹ NixOS detected: PAM is managed by NixOS modules.")
|
||||||
|
fmt.Println(" Configure fingerprint/U2F via your greetd NixOS module (security.pam.services.greetd).")
|
||||||
|
fmt.Println()
|
||||||
|
if allGood && inGreeterGroup {
|
||||||
|
fmt.Println("✓ All checks passed! Greeter is properly configured.")
|
||||||
|
} else if !allGood {
|
||||||
|
fmt.Println("⚠ Some issues detected. Run 'dms greeter sync' to repair configuration.")
|
||||||
|
} else if !inGreeterGroup {
|
||||||
|
fmt.Printf("⚠ User is not in %s group. Run 'dms greeter sync' after adding group membership.\n", greeterGroup)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
greetdPamPath := "/etc/pam.d/greetd"
|
||||||
|
pamData, err := os.ReadFile(greetdPamPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(" ✗ Failed to read %s: %v\n", greetdPamPath, err)
|
||||||
|
allGood = false
|
||||||
|
} else {
|
||||||
|
managed, managedFprint, managedU2f, legacyManaged := parseManagedGreeterPamAuth(string(pamData))
|
||||||
|
if managed {
|
||||||
|
fmt.Println(" ✓ Managed auth block present")
|
||||||
|
if managedFprint {
|
||||||
|
fmt.Println(" - fingerprint: enabled")
|
||||||
|
} else {
|
||||||
|
fmt.Println(" - fingerprint: disabled")
|
||||||
|
}
|
||||||
|
if managedU2f {
|
||||||
|
fmt.Println(" - security key (U2F): enabled")
|
||||||
|
} else {
|
||||||
|
fmt.Println(" - security key (U2F): disabled")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println(" ℹ No managed auth block present (fingerprint/U2F disabled for greeter)")
|
||||||
|
}
|
||||||
|
if legacyManaged {
|
||||||
|
fmt.Println(" ⚠ Legacy unmanaged DMS PAM lines detected. Run 'dms greeter sync' to normalize.")
|
||||||
|
allGood = false
|
||||||
|
}
|
||||||
|
includedFprintFile := greeter.DetectIncludedPamModule(string(pamData), "pam_fprintd.so")
|
||||||
|
if managedFprint {
|
||||||
|
if 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.")
|
||||||
|
allGood = false
|
||||||
|
}
|
||||||
|
} else if includedFprintFile != "" {
|
||||||
|
fmt.Printf(" ℹ Fingerprint auth is enabled via included %s.\n", includedFprintFile)
|
||||||
|
fmt.Println(" The DMS toggle only controls the managed block; disable fingerprint in authselect/pam-auth-update for password-only greeter login.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
if allGood && inGreeterGroup {
|
if allGood && inGreeterGroup {
|
||||||
fmt.Println("✓ All checks passed! Greeter is properly configured.")
|
fmt.Println("✓ All checks passed! Greeter is properly configured.")
|
||||||
} else if !allGood {
|
} else if !allGood {
|
||||||
fmt.Println("⚠ Some issues detected. Run 'dms greeter sync' to fix symlinks.")
|
fmt.Println("⚠ Some issues detected. Run 'dms greeter sync' to repair configuration.")
|
||||||
|
} else if !inGreeterGroup {
|
||||||
|
fmt.Printf("⚠ User is not in %s group. Run 'dms greeter sync' after adding group membership.\n", greeterGroup)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -16,9 +16,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var setupCmd = &cobra.Command{
|
var setupCmd = &cobra.Command{
|
||||||
Use: "setup",
|
Use: "setup",
|
||||||
Short: "Deploy DMS configurations",
|
Short: "Deploy DMS configurations",
|
||||||
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
||||||
|
PersistentPreRunE: requireMutableSystemCommand,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if err := runSetup(); err != nil {
|
if err := runSetup(); err != nil {
|
||||||
log.Fatalf("Error during setup: %v", err)
|
log.Fatalf("Error during setup: %v", err)
|
||||||
|
|||||||
271
core/cmd/dms/immutable_policy.go
Normal file
271
core/cmd/dms/immutable_policy.go
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
cliPolicyPackagedPath = "/usr/share/dms/cli-policy.json"
|
||||||
|
cliPolicyAdminPath = "/etc/dms/cli-policy.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
immutablePolicyOnce sync.Once
|
||||||
|
immutablePolicy immutableCommandPolicy
|
||||||
|
immutablePolicyErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed assets/cli-policy.default.json
|
||||||
|
var defaultCLIPolicyJSON []byte
|
||||||
|
|
||||||
|
type immutableCommandPolicy struct {
|
||||||
|
ImmutableSystem bool
|
||||||
|
ImmutableReason string
|
||||||
|
BlockedCommands []string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
type cliPolicyFile struct {
|
||||||
|
PolicyVersion int `json:"policy_version"`
|
||||||
|
ImmutableSystem *bool `json:"immutable_system"`
|
||||||
|
BlockedCommands *[]string `json:"blocked_commands"`
|
||||||
|
Message *string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeCommandSpec(raw string) string {
|
||||||
|
normalized := strings.ToLower(strings.TrimSpace(raw))
|
||||||
|
normalized = strings.TrimPrefix(normalized, "dms ")
|
||||||
|
return strings.Join(strings.Fields(normalized), " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeBlockedCommands(raw []string) []string {
|
||||||
|
normalized := make([]string, 0, len(raw))
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, cmd := range raw {
|
||||||
|
spec := normalizeCommandSpec(cmd)
|
||||||
|
if spec == "" || seen[spec] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[spec] = true
|
||||||
|
normalized = append(normalized, spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandBlockedByPolicy(commandPath string, blocked []string) bool {
|
||||||
|
normalizedPath := normalizeCommandSpec(commandPath)
|
||||||
|
if normalizedPath == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range blocked {
|
||||||
|
spec := normalizeCommandSpec(entry)
|
||||||
|
if spec == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if normalizedPath == spec || strings.HasPrefix(normalizedPath, spec+" ") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadPolicyFile(path string) (*cliPolicyFile, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to read %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var policy cliPolicyFile
|
||||||
|
if err := json.Unmarshal(data, &policy); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &policy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergePolicyFile(base *immutableCommandPolicy, path string) error {
|
||||||
|
policyFile, err := loadPolicyFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if policyFile == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if policyFile.ImmutableSystem != nil {
|
||||||
|
base.ImmutableSystem = *policyFile.ImmutableSystem
|
||||||
|
}
|
||||||
|
if policyFile.BlockedCommands != nil {
|
||||||
|
base.BlockedCommands = normalizeBlockedCommands(*policyFile.BlockedCommands)
|
||||||
|
}
|
||||||
|
if policyFile.Message != nil {
|
||||||
|
msg := strings.TrimSpace(*policyFile.Message)
|
||||||
|
if msg != "" {
|
||||||
|
base.Message = msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readOSReleaseMap(path string) map[string]string {
|
||||||
|
values := make(map[string]string)
|
||||||
|
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := strings.ToUpper(strings.TrimSpace(parts[0]))
|
||||||
|
value := strings.Trim(strings.TrimSpace(parts[1]), "\"")
|
||||||
|
values[key] = strings.ToLower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasAnyToken(text string, tokens ...string) bool {
|
||||||
|
if text == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, token := range tokens {
|
||||||
|
if strings.Contains(text, token) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectImmutableSystem() (bool, string) {
|
||||||
|
if _, err := os.Stat("/run/ostree-booted"); err == nil {
|
||||||
|
return true, "/run/ostree-booted is present"
|
||||||
|
}
|
||||||
|
|
||||||
|
osRelease := readOSReleaseMap("/etc/os-release")
|
||||||
|
if len(osRelease) == 0 {
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
id := osRelease["ID"]
|
||||||
|
idLike := osRelease["ID_LIKE"]
|
||||||
|
variantID := osRelease["VARIANT_ID"]
|
||||||
|
name := osRelease["NAME"]
|
||||||
|
prettyName := osRelease["PRETTY_NAME"]
|
||||||
|
|
||||||
|
immutableIDs := map[string]bool{
|
||||||
|
"bluefin": true,
|
||||||
|
"bazzite": true,
|
||||||
|
"silverblue": true,
|
||||||
|
"kinoite": true,
|
||||||
|
"sericea": true,
|
||||||
|
"onyx": true,
|
||||||
|
"aurora": true,
|
||||||
|
"fedora-iot": true,
|
||||||
|
"fedora-coreos": true,
|
||||||
|
}
|
||||||
|
if immutableIDs[id] {
|
||||||
|
return true, "os-release ID=" + id
|
||||||
|
}
|
||||||
|
|
||||||
|
markers := []string{"silverblue", "kinoite", "sericea", "onyx", "bazzite", "bluefin", "aurora", "ostree", "atomic"}
|
||||||
|
if hasAnyToken(variantID, markers...) {
|
||||||
|
return true, "os-release VARIANT_ID=" + variantID
|
||||||
|
}
|
||||||
|
if hasAnyToken(idLike, "ostree", "rpm-ostree") {
|
||||||
|
return true, "os-release ID_LIKE=" + idLike
|
||||||
|
}
|
||||||
|
if hasAnyToken(name, markers...) || hasAnyToken(prettyName, markers...) {
|
||||||
|
return true, "os-release identifies an atomic/ostree variant"
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func getImmutablePolicy() (*immutableCommandPolicy, error) {
|
||||||
|
immutablePolicyOnce.Do(func() {
|
||||||
|
detectedImmutable, reason := detectImmutableSystem()
|
||||||
|
immutablePolicy = immutableCommandPolicy{
|
||||||
|
ImmutableSystem: detectedImmutable,
|
||||||
|
ImmutableReason: reason,
|
||||||
|
BlockedCommands: []string{"greeter install", "greeter enable", "greeter sync", "setup"},
|
||||||
|
Message: "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes.",
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultPolicy cliPolicyFile
|
||||||
|
if err := json.Unmarshal(defaultCLIPolicyJSON, &defaultPolicy); err != nil {
|
||||||
|
immutablePolicyErr = fmt.Errorf("failed to parse embedded default CLI policy: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if defaultPolicy.BlockedCommands != nil {
|
||||||
|
immutablePolicy.BlockedCommands = normalizeBlockedCommands(*defaultPolicy.BlockedCommands)
|
||||||
|
}
|
||||||
|
if defaultPolicy.Message != nil {
|
||||||
|
msg := strings.TrimSpace(*defaultPolicy.Message)
|
||||||
|
if msg != "" {
|
||||||
|
immutablePolicy.Message = msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mergePolicyFile(&immutablePolicy, cliPolicyPackagedPath); err != nil {
|
||||||
|
immutablePolicyErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := mergePolicyFile(&immutablePolicy, cliPolicyAdminPath); err != nil {
|
||||||
|
immutablePolicyErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if immutablePolicyErr != nil {
|
||||||
|
return nil, immutablePolicyErr
|
||||||
|
}
|
||||||
|
return &immutablePolicy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireMutableSystemCommand(cmd *cobra.Command, _ []string) error {
|
||||||
|
policy, err := getImmutablePolicy()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !policy.ImmutableSystem {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
commandPath := normalizeCommandSpec(cmd.CommandPath())
|
||||||
|
if !commandBlockedByPolicy(commandPath, policy.BlockedCommands) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
reason := ""
|
||||||
|
if policy.ImmutableReason != "" {
|
||||||
|
reason = "Detected immutable system: " + policy.ImmutableReason + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath)
|
||||||
|
}
|
||||||
@@ -7,14 +7,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func findCommandPath(cmd string) (string, error) {
|
|
||||||
path, err := exec.LookPath(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("command '%s' not found in PATH", cmd)
|
|
||||||
}
|
|
||||||
return path, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isArchPackageInstalled(packageName string) bool {
|
func isArchPackageInstalled(packageName string) bool {
|
||||||
cmd := exec.Command("pacman", "-Q", packageName)
|
cmd := exec.Command("pacman", "-Q", packageName)
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package greeter
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -17,10 +18,29 @@ import (
|
|||||||
"github.com/sblinch/kdl-go/document"
|
"github.com/sblinch/kdl-go/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
var includedPamAuthFiles = []string{"system-auth", "common-auth", "password-auth"}
|
||||||
|
|
||||||
func DetectDMSPath() (string, error) {
|
func DetectDMSPath() (string, error) {
|
||||||
return config.LocateDMSConfig()
|
return config.LocateDMSConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsNixOS returns true when running on NixOS, which manages PAM configs through
|
||||||
|
// its module system. The DMS PAM managed block must not be written on NixOS.
|
||||||
|
func IsNixOS() bool {
|
||||||
|
_, err := os.Stat("/etc/NIXOS")
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
func DetectGreeterGroup() string {
|
func DetectGreeterGroup() string {
|
||||||
data, err := os.ReadFile("/etc/group")
|
data, err := os.ReadFile("/etc/group")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -36,6 +56,196 @@ func DetectGreeterGroup() string {
|
|||||||
return "greeter"
|
return "greeter"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hasPasswdUser(passwdData, user string) bool {
|
||||||
|
prefix := user + ":"
|
||||||
|
for line := range strings.SplitSeq(passwdData, "\n") {
|
||||||
|
if strings.HasPrefix(line, prefix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func findPasswdUser(passwdData string, candidates ...string) (string, bool) {
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
if hasPasswdUser(passwdData, candidate) {
|
||||||
|
return candidate, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripTomlComment(line string) string {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if idx := strings.Index(trimmed, "#"); idx >= 0 {
|
||||||
|
return strings.TrimSpace(trimmed[:idx])
|
||||||
|
}
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTomlSection(line string) (string, bool) {
|
||||||
|
trimmed := stripTomlComment(line)
|
||||||
|
if len(trimmed) < 3 || !strings.HasPrefix(trimmed, "[") || !strings.HasSuffix(trimmed, "]") {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(trimmed[1 : len(trimmed)-1]), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractDefaultSessionUser(configContent string) string {
|
||||||
|
inDefaultSession := false
|
||||||
|
for line := range strings.SplitSeq(configContent, "\n") {
|
||||||
|
if section, ok := parseTomlSection(line); ok {
|
||||||
|
inDefaultSession = section == "default_session"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !inDefaultSession {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
trimmed := stripTomlComment(line)
|
||||||
|
if !strings.HasPrefix(trimmed, "user =") && !strings.HasPrefix(trimmed, "user=") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(trimmed, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
user := strings.Trim(strings.TrimSpace(parts[1]), `"`)
|
||||||
|
if user != "" {
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func upsertDefaultSession(configContent, greeterUser, command string) string {
|
||||||
|
lines := strings.Split(configContent, "\n")
|
||||||
|
var out []string
|
||||||
|
|
||||||
|
inDefaultSession := false
|
||||||
|
foundDefaultSession := false
|
||||||
|
defaultSessionUserSet := false
|
||||||
|
defaultSessionCommandSet := false
|
||||||
|
|
||||||
|
appendDefaultSessionFields := func() {
|
||||||
|
if !defaultSessionUserSet {
|
||||||
|
out = append(out, fmt.Sprintf(`user = "%s"`, greeterUser))
|
||||||
|
}
|
||||||
|
if !defaultSessionCommandSet {
|
||||||
|
out = append(out, command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
if section, ok := parseTomlSection(line); ok {
|
||||||
|
if inDefaultSession {
|
||||||
|
appendDefaultSessionFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
inDefaultSession = section == "default_session"
|
||||||
|
if inDefaultSession {
|
||||||
|
foundDefaultSession = true
|
||||||
|
defaultSessionUserSet = false
|
||||||
|
defaultSessionCommandSet = false
|
||||||
|
}
|
||||||
|
|
||||||
|
out = append(out, line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if inDefaultSession {
|
||||||
|
trimmed := stripTomlComment(line)
|
||||||
|
if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") {
|
||||||
|
out = append(out, fmt.Sprintf(`user = "%s"`, greeterUser))
|
||||||
|
defaultSessionUserSet = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(trimmed, "command =") || strings.HasPrefix(trimmed, "command=") {
|
||||||
|
if !defaultSessionCommandSet {
|
||||||
|
out = append(out, command)
|
||||||
|
defaultSessionCommandSet = true
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out = append(out, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
if inDefaultSession {
|
||||||
|
appendDefaultSessionFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundDefaultSession {
|
||||||
|
if len(out) > 0 && strings.TrimSpace(out[len(out)-1]) != "" {
|
||||||
|
out = append(out, "")
|
||||||
|
}
|
||||||
|
out = append(out, "[default_session]")
|
||||||
|
out = append(out, fmt.Sprintf(`user = "%s"`, greeterUser))
|
||||||
|
out = append(out, command)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(out, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func DetectGreeterUser() string {
|
||||||
|
passwdData, err := os.ReadFile("/etc/passwd")
|
||||||
|
if err == nil {
|
||||||
|
passwdContent := string(passwdData)
|
||||||
|
|
||||||
|
if configData, cfgErr := os.ReadFile("/etc/greetd/config.toml"); cfgErr == nil {
|
||||||
|
if configured := extractDefaultSessionUser(string(configData)); configured != "" && hasPasswdUser(passwdContent, configured) {
|
||||||
|
return configured
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if user, found := findPasswdUser(passwdContent, "greeter", "_greeter", "greetd"); found {
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Fprintln(os.Stderr, "⚠ Warning: could not read /etc/passwd, defaulting greeter user to 'greeter'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if configData, cfgErr := os.ReadFile("/etc/greetd/config.toml"); cfgErr == nil {
|
||||||
|
if configured := extractDefaultSessionUser(string(configData)); configured != "" {
|
||||||
|
return configured
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(os.Stderr, "⚠ Warning: no greeter user found, defaulting to 'greeter'")
|
||||||
|
return "greeter"
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveGreeterWrapperPath() string {
|
||||||
|
if override := strings.TrimSpace(os.Getenv("DMS_GREETER_WRAPPER_CMD")); override != "" {
|
||||||
|
return override
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, candidate := range []string{"/usr/bin/dms-greeter", "/usr/local/bin/dms-greeter"} {
|
||||||
|
if info, err := os.Stat(candidate); err == nil && !info.IsDir() && (info.Mode()&0o111) != 0 {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if path, err := exec.LookPath("dms-greeter"); err == nil {
|
||||||
|
resolved := path
|
||||||
|
if realPath, realErr := filepath.EvalSymlinks(path); realErr == nil {
|
||||||
|
resolved = realPath
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(resolved, "/home/") || strings.HasPrefix(resolved, "/tmp/") {
|
||||||
|
fmt.Fprintf(os.Stderr, "⚠ Warning: ignoring non-system dms-greeter on PATH: %s\n", path)
|
||||||
|
} else {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "/usr/bin/dms-greeter"
|
||||||
|
}
|
||||||
|
|
||||||
func DetectCompositors() []string {
|
func DetectCompositors() []string {
|
||||||
var compositors []string
|
var compositors []string
|
||||||
|
|
||||||
@@ -289,9 +499,13 @@ func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool {
|
|||||||
|
|
||||||
// CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory
|
// CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory
|
||||||
func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
||||||
if utils.CommandExists("dms-greeter") {
|
if IsGreeterPackaged() {
|
||||||
logFunc("✓ dms-greeter wrapper already installed")
|
logFunc("✓ dms-greeter package already installed")
|
||||||
} else {
|
} else {
|
||||||
|
if dmsPath == "" {
|
||||||
|
return fmt.Errorf("dms path is required for manual dms-greeter wrapper installs")
|
||||||
|
}
|
||||||
|
|
||||||
assetsDir := filepath.Join(dmsPath, "Modules", "Greetd", "assets")
|
assetsDir := filepath.Join(dmsPath, "Modules", "Greetd", "assets")
|
||||||
wrapperSrc := filepath.Join(assetsDir, "dms-greeter")
|
wrapperSrc := filepath.Join(assetsDir, "dms-greeter")
|
||||||
|
|
||||||
@@ -300,10 +514,14 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
|
|||||||
}
|
}
|
||||||
|
|
||||||
wrapperDst := "/usr/local/bin/dms-greeter"
|
wrapperDst := "/usr/local/bin/dms-greeter"
|
||||||
|
action := "Installed"
|
||||||
|
if info, err := os.Stat(wrapperDst); err == nil && !info.IsDir() {
|
||||||
|
action = "Updated"
|
||||||
|
}
|
||||||
if err := runSudoCmd(sudoPassword, "cp", wrapperSrc, wrapperDst); err != nil {
|
if err := runSudoCmd(sudoPassword, "cp", wrapperSrc, wrapperDst); err != nil {
|
||||||
return fmt.Errorf("failed to copy dms-greeter wrapper: %w", err)
|
return fmt.Errorf("failed to copy dms-greeter wrapper: %w", err)
|
||||||
}
|
}
|
||||||
logFunc(fmt.Sprintf("✓ Installed dms-greeter wrapper to %s", wrapperDst))
|
logFunc(fmt.Sprintf("✓ %s dms-greeter wrapper at %s", action, wrapperDst))
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "chmod", "+x", wrapperDst); err != nil {
|
if err := runSudoCmd(sudoPassword, "chmod", "+x", wrapperDst); err != nil {
|
||||||
return fmt.Errorf("failed to make wrapper executable: %w", err)
|
return fmt.Errorf("failed to make wrapper executable: %w", err)
|
||||||
@@ -328,7 +546,21 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheDir := "/var/cache/dms-greeter"
|
if err := EnsureGreeterCacheDir(logFunc, sudoPassword); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureGreeterCacheDir creates /var/cache/dms-greeter with correct ownership if it does not exist.
|
||||||
|
// It is safe to call multiple times (idempotent).
|
||||||
|
func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
|
||||||
|
cacheDir := GreeterCacheDir
|
||||||
|
if _, err := os.Stat(cacheDir); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "mkdir", "-p", cacheDir); err != nil {
|
if err := runSudoCmd(sudoPassword, "mkdir", "-p", cacheDir); err != nil {
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
}
|
}
|
||||||
@@ -340,11 +572,10 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
|
|||||||
return fmt.Errorf("failed to set cache directory owner: %w", err)
|
return fmt.Errorf("failed to set cache directory owner: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "chmod", "755", cacheDir); err != nil {
|
if err := runSudoCmd(sudoPassword, "chmod", "750", cacheDir); err != nil {
|
||||||
return fmt.Errorf("failed to set cache directory permissions: %w", err)
|
return fmt.Errorf("failed to set cache directory permissions: %w", err)
|
||||||
}
|
}
|
||||||
logFunc(fmt.Sprintf("✓ Created cache directory %s (owner: %s, permissions: 755)", cacheDir, owner))
|
logFunc(fmt.Sprintf("✓ Created cache directory %s (owner: %s, mode: 750)", cacheDir, owner))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,13 +775,13 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPassword string, forceAuth bool) 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheDir := "/var/cache/dms-greeter"
|
cacheDir := GreeterCacheDir
|
||||||
|
|
||||||
symlinks := []struct {
|
symlinks := []struct {
|
||||||
source string
|
source string
|
||||||
@@ -578,28 +809,33 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
|
|||||||
sourceDir := filepath.Dir(link.source)
|
sourceDir := filepath.Dir(link.source)
|
||||||
if _, err := os.Stat(sourceDir); os.IsNotExist(err) {
|
if _, err := os.Stat(sourceDir); os.IsNotExist(err) {
|
||||||
if err := os.MkdirAll(sourceDir, 0o755); err != nil {
|
if err := os.MkdirAll(sourceDir, 0o755); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Could not create directory %s: %v", sourceDir, err))
|
return fmt.Errorf("failed to create source directory %s for %s: %w", sourceDir, link.desc, err)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(link.source); os.IsNotExist(err) {
|
if _, err := os.Stat(link.source); os.IsNotExist(err) {
|
||||||
if err := os.WriteFile(link.source, []byte("{}"), 0o644); err != nil {
|
if err := os.WriteFile(link.source, []byte("{}"), 0o644); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Could not create %s: %v", link.source, err))
|
return fmt.Errorf("failed to create source file %s for %s: %w", link.source, link.desc, err)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = runSudoCmd(sudoPassword, "rm", "-f", link.target)
|
_ = runSudoCmd(sudoPassword, "rm", "-f", link.target)
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "ln", "-sf", link.source, link.target); err != nil {
|
if err := runSudoCmd(sudoPassword, "ln", "-sf", link.source, link.target); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to create symlink for %s: %v", link.desc, err))
|
return fmt.Errorf("failed to create symlink for %s (%s -> %s): %w", link.desc, link.target, link.source, err)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logFunc(fmt.Sprintf("✓ Synced %s", link.desc))
|
logFunc(fmt.Sprintf("✓ Synced %s", link.desc))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := syncGreeterWallpaperOverride(homeDir, cacheDir, logFunc, sudoPassword); err != nil {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
@@ -611,6 +847,293 @@ func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPasswo
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func syncGreeterWallpaperOverride(homeDir, cacheDir string, logFunc func(string), sudoPassword string) error {
|
||||||
|
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
|
||||||
|
data, err := os.ReadFile(settingsPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to read settings at %s: %w", settingsPath, err)
|
||||||
|
}
|
||||||
|
var settings struct {
|
||||||
|
GreeterWallpaperPath string `json:"greeterWallpaperPath"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &settings); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse settings at %s: %w", settingsPath, err)
|
||||||
|
}
|
||||||
|
destPath := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg")
|
||||||
|
if settings.GreeterWallpaperPath == "" {
|
||||||
|
if err := runSudoCmd(sudoPassword, "rm", "-f", destPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to clear override file %s: %w", destPath, err)
|
||||||
|
}
|
||||||
|
logFunc("✓ Cleared greeter wallpaper override")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := runSudoCmd(sudoPassword, "rm", "-f", destPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove old override file %s: %w", destPath, err)
|
||||||
|
}
|
||||||
|
src := settings.GreeterWallpaperPath
|
||||||
|
if !filepath.IsAbs(src) {
|
||||||
|
src = filepath.Join(homeDir, src)
|
||||||
|
}
|
||||||
|
st, err := os.Stat(src)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("configured greeter wallpaper not found at %s: %w", src, err)
|
||||||
|
}
|
||||||
|
if st.IsDir() {
|
||||||
|
return fmt.Errorf("configured greeter wallpaper path points to a directory: %s", src)
|
||||||
|
}
|
||||||
|
if err := runSudoCmd(sudoPassword, "cp", src, destPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to copy override wallpaper to %s: %w", destPath, err)
|
||||||
|
}
|
||||||
|
greeterGroup := DetectGreeterGroup()
|
||||||
|
if err := runSudoCmd(sudoPassword, "chown", "greeter:"+greeterGroup, destPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to set override ownership on %s: %w", destPath, err)
|
||||||
|
}
|
||||||
|
if err := runSudoCmd(sudoPassword, "chmod", "644", destPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to set override permissions on %s: %w", destPath, err)
|
||||||
|
}
|
||||||
|
logFunc("✓ Synced greeter wallpaper override")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pamModuleExists(module string) bool {
|
||||||
|
for _, libDir := range []string{
|
||||||
|
"/usr/lib64/security",
|
||||||
|
"/usr/lib/security",
|
||||||
|
"/lib/x86_64-linux-gnu/security",
|
||||||
|
"/usr/lib/x86_64-linux-gnu/security",
|
||||||
|
"/usr/lib/aarch64-linux-gnu/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 ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncGreeterPamConfig(homeDir string, logFunc func(string), sudoPassword string, forceAuth bool) error {
|
||||||
|
var wantFprint, wantU2f bool
|
||||||
|
if forceAuth {
|
||||||
|
wantFprint = pamModuleExists("pam_fprintd.so")
|
||||||
|
wantU2f = pamModuleExists("pam_u2f.so")
|
||||||
|
} else {
|
||||||
|
settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json")
|
||||||
|
data, err := os.ReadFile(settingsPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
data = []byte("{}")
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("failed to read settings at %s: %w", settingsPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var settings struct {
|
||||||
|
GreeterEnableFprint bool `json:"greeterEnableFprint"`
|
||||||
|
GreeterEnableU2f bool `json:"greeterEnableU2f"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &settings); err != nil {
|
||||||
|
return fmt.Errorf("failed to parse settings at %s: %w", settingsPath, err)
|
||||||
|
}
|
||||||
|
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")
|
||||||
|
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 !wantFprint && includedFprintFile != "" {
|
||||||
|
logFunc("ℹ Fingerprint auth is still enabled via included " + includedFprintFile + ".")
|
||||||
|
logFunc(" Disable fingerprint in your system PAM manager (authselect/pam-auth-update) to force password-only greeter login.")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@@ -752,6 +1275,8 @@ func ensureGreetdNiriConfig(logFunc func(string), sudoPassword string, niriConfi
|
|||||||
}
|
}
|
||||||
// Strip existing -C or --config and their arguments
|
// Strip existing -C or --config and their arguments
|
||||||
command = stripConfigFlag(command)
|
command = stripConfigFlag(command)
|
||||||
|
command = stripCacheDirFlag(command)
|
||||||
|
command = strings.TrimSpace(command + " --cache-dir " + GreeterCacheDir)
|
||||||
|
|
||||||
newCommand := fmt.Sprintf("%s -C %s", command, niriConfigPath)
|
newCommand := fmt.Sprintf("%s -C %s", command, niriConfigPath)
|
||||||
idx := strings.Index(line, "command")
|
idx := strings.Index(line, "command")
|
||||||
@@ -768,10 +1293,6 @@ func ensureGreetdNiriConfig(logFunc func(string), sudoPassword string, niriConfi
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := backupFileIfExists(sudoPassword, configPath, ".backup"); err != nil {
|
|
||||||
return fmt.Errorf("failed to backup greetd config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpFile, err := os.CreateTemp("", "greetd-config-*.toml")
|
tmpFile, err := os.CreateTemp("", "greetd-config-*.toml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create temp greetd config: %w", err)
|
return fmt.Errorf("failed to create temp greetd config: %w", err)
|
||||||
@@ -802,7 +1323,10 @@ func backupFileIfExists(sudoPassword string, path string, suffix string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
backupPath := fmt.Sprintf("%s%s-%s", path, suffix, time.Now().Format("20060102-150405"))
|
backupPath := fmt.Sprintf("%s%s-%s", path, suffix, time.Now().Format("20060102-150405"))
|
||||||
return runSudoCmd(sudoPassword, "cp", "-p", path, backupPath)
|
if err := runSudoCmd(sudoPassword, "cp", path, backupPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return runSudoCmd(sudoPassword, "chmod", "644", backupPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *niriGreeterSync) processFile(filePath string) error {
|
func (s *niriGreeterSync) processFile(filePath string) error {
|
||||||
@@ -948,96 +1472,63 @@ func (s *niriGreeterSync) render() string {
|
|||||||
func ConfigureGreetd(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
func ConfigureGreetd(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
||||||
configPath := "/etc/greetd/config.toml"
|
configPath := "/etc/greetd/config.toml"
|
||||||
|
|
||||||
|
backupPath := fmt.Sprintf("%s.backup-%s", configPath, time.Now().Format("20060102-150405"))
|
||||||
|
if err := backupFileIfExists(sudoPassword, configPath, ".backup"); err != nil {
|
||||||
|
return fmt.Errorf("failed to backup greetd config: %w", err)
|
||||||
|
}
|
||||||
if _, err := os.Stat(configPath); err == nil {
|
if _, err := os.Stat(configPath); err == nil {
|
||||||
backupPath := configPath + ".backup"
|
|
||||||
if err := runSudoCmd(sudoPassword, "cp", configPath, backupPath); err != nil {
|
|
||||||
return fmt.Errorf("failed to backup config: %w", err)
|
|
||||||
}
|
|
||||||
logFunc(fmt.Sprintf("✓ Backed up existing config to %s", backupPath))
|
logFunc(fmt.Sprintf("✓ Backed up existing config to %s", backupPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
greeterUser := DetectGreeterGroup()
|
greeterUser := DetectGreeterUser()
|
||||||
|
|
||||||
var configContent string
|
var configContent string
|
||||||
if data, err := os.ReadFile(configPath); err == nil {
|
if data, err := os.ReadFile(configPath); err == nil {
|
||||||
configContent = string(data)
|
configContent = string(data)
|
||||||
} else {
|
} else if os.IsNotExist(err) {
|
||||||
configContent = fmt.Sprintf(`[terminal]
|
configContent = `[terminal]
|
||||||
vt = 1
|
vt = 1
|
||||||
|
|
||||||
[default_session]
|
[default_session]
|
||||||
|
`
|
||||||
user = "%s"
|
} else {
|
||||||
`, greeterUser)
|
return fmt.Errorf("failed to read greetd config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := strings.Split(configContent, "\n")
|
wrapperCmd := resolveGreeterWrapperPath()
|
||||||
var newLines []string
|
|
||||||
for _, line := range lines {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") {
|
|
||||||
if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") {
|
|
||||||
newLines = append(newLines, fmt.Sprintf(`user = "%s"`, greeterUser))
|
|
||||||
} else {
|
|
||||||
newLines = append(newLines, line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If dmsPath is empty (packaged greeter), omit -p; wrapper finds /usr/share/quickshell/dms-greeter
|
|
||||||
wrapperCmd := "dms-greeter"
|
|
||||||
if !utils.CommandExists("dms-greeter") {
|
|
||||||
wrapperCmd = "/usr/local/bin/dms-greeter"
|
|
||||||
}
|
|
||||||
|
|
||||||
compositorLower := strings.ToLower(compositor)
|
compositorLower := strings.ToLower(compositor)
|
||||||
var command string
|
commandValue := fmt.Sprintf("%s --command %s --cache-dir %s", wrapperCmd, compositorLower, GreeterCacheDir)
|
||||||
if dmsPath == "" {
|
|
||||||
command = fmt.Sprintf(`command = "%s --command %s"`, wrapperCmd, compositorLower)
|
|
||||||
} else {
|
|
||||||
command = fmt.Sprintf(`command = "%s --command %s -p %s"`, wrapperCmd, compositorLower, dmsPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
var finalLines []string
|
|
||||||
inDefaultSession := false
|
|
||||||
commandAdded := false
|
|
||||||
|
|
||||||
for _, line := range newLines {
|
|
||||||
finalLines = append(finalLines, line)
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
|
|
||||||
if trimmed == "[default_session]" {
|
|
||||||
inDefaultSession = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if inDefaultSession && !commandAdded && trimmed != "" && !strings.HasPrefix(trimmed, "[") {
|
|
||||||
if !strings.HasPrefix(trimmed, "#") && !strings.HasPrefix(trimmed, "user") {
|
|
||||||
finalLines = append(finalLines, command)
|
|
||||||
commandAdded = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !commandAdded {
|
|
||||||
finalLines = append(finalLines, command)
|
|
||||||
}
|
|
||||||
|
|
||||||
newConfig := strings.Join(finalLines, "\n")
|
|
||||||
|
|
||||||
tmpFile := "/tmp/greetd-config.toml"
|
|
||||||
if err := os.WriteFile(tmpFile, []byte(newConfig), 0o644); err != nil {
|
|
||||||
return fmt.Errorf("failed to write temp config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := runSudoCmd(sudoPassword, "mv", tmpFile, configPath); err != nil {
|
|
||||||
return fmt.Errorf("failed to move config to /etc/greetd: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmdDesc := fmt.Sprintf("%s --command %s", wrapperCmd, compositorLower)
|
|
||||||
if dmsPath != "" {
|
if dmsPath != "" {
|
||||||
cmdDesc = fmt.Sprintf("%s -p %s", cmdDesc, dmsPath)
|
commandValue = fmt.Sprintf("%s -p %s", commandValue, dmsPath)
|
||||||
}
|
}
|
||||||
logFunc(fmt.Sprintf("✓ Updated greetd configuration (user: %s, command: %s)", greeterUser, cmdDesc))
|
|
||||||
|
commandLine := fmt.Sprintf(`command = "%s"`, commandValue)
|
||||||
|
newConfig := upsertDefaultSession(configContent, greeterUser, commandLine)
|
||||||
|
|
||||||
|
tmpFile, err := os.CreateTemp("", "greetd-config-*.toml")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create temp greetd config: %w", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpFile.Name())
|
||||||
|
|
||||||
|
if _, err := tmpFile.WriteString(newConfig); err != nil {
|
||||||
|
_ = tmpFile.Close()
|
||||||
|
return fmt.Errorf("failed to write temp greetd config: %w", err)
|
||||||
|
}
|
||||||
|
if err := tmpFile.Close(); err != nil {
|
||||||
|
return fmt.Errorf("failed to close temp greetd config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := runSudoCmd(sudoPassword, "mkdir", "-p", "/etc/greetd"); err != nil {
|
||||||
|
return fmt.Errorf("failed to create /etc/greetd: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", "root", "-m", "0644", tmpFile.Name(), configPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to install greetd config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logFunc(fmt.Sprintf("✓ Updated greetd configuration (user: %s, command: %s)", greeterUser, commandValue))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1072,6 +1563,30 @@ func stripConfigFlag(command string) string {
|
|||||||
return command
|
return command
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stripCacheDirFlag(command string) string {
|
||||||
|
fields := strings.Fields(command)
|
||||||
|
if len(fields) == 0 {
|
||||||
|
return strings.TrimSpace(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered := make([]string, 0, len(fields))
|
||||||
|
for i := 0; i < len(fields); i++ {
|
||||||
|
token := fields[i]
|
||||||
|
if token == "--cache-dir" {
|
||||||
|
if i+1 < len(fields) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(token, "--cache-dir=") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(filtered, " ")
|
||||||
|
}
|
||||||
|
|
||||||
// getDebianOBSSlug returns the OBS repository slug for the running Debian version.
|
// getDebianOBSSlug returns the OBS repository slug for the running Debian version.
|
||||||
func getDebianOBSSlug(osInfo *distros.OSInfo) string {
|
func getDebianOBSSlug(osInfo *distros.OSInfo) string {
|
||||||
versionID := strings.ToLower(osInfo.VersionID)
|
versionID := strings.ToLower(osInfo.VersionID)
|
||||||
@@ -1248,7 +1763,7 @@ func AutoSetupGreeter(compositor, sudoPassword string, logFunc func(string)) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
logFunc("Synchronizing DMS configurations...")
|
logFunc("Synchronizing DMS configurations...")
|
||||||
if err := SyncDMSConfigs(dmsPath, compositor, logFunc, sudoPassword); err != nil {
|
if err := SyncDMSConfigs(dmsPath, compositor, logFunc, sudoPassword, false); err != nil {
|
||||||
logFunc(fmt.Sprintf("⚠ Warning: config sync error: %v", err))
|
logFunc(fmt.Sprintf("⚠ Warning: config sync error: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package network
|
package network
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -28,7 +29,13 @@ func TestDetectResult_HasNetworkdField(t *testing.T) {
|
|||||||
|
|
||||||
func TestDetectNetworkStack_Integration(t *testing.T) {
|
func TestDetectNetworkStack_Integration(t *testing.T) {
|
||||||
result, err := DetectNetworkStack()
|
result, err := DetectNetworkStack()
|
||||||
|
|
||||||
|
if err != nil && strings.Contains(err.Error(), "connect system bus") {
|
||||||
|
t.Skipf("system D-Bus unavailable: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, result)
|
if assert.NotNil(t, result) {
|
||||||
assert.NotEmpty(t, result.ChosenReason)
|
assert.NotEmpty(t, result.ChosenReason)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
118
flake.nix
118
flake.nix
@@ -72,76 +72,82 @@
|
|||||||
"${cleanVersion}${dateSuffix}${revSuffix}";
|
"${cleanVersion}${dateSuffix}${revSuffix}";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
dms-shell = pkgs.buildGoModule (
|
dms-shell = pkgs.lib.makeOverridable (
|
||||||
let
|
|
||||||
rootSrc = ./.;
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
inherit version;
|
extraQtPackages ? [ ],
|
||||||
pname = "dms-shell";
|
}:
|
||||||
src = ./core;
|
pkgs.buildGoModule (
|
||||||
vendorHash = "sha256-dEk7IOd6aQwaxZruxQclN7TGMyb8EJOl6NBWRsoZ9HQ=";
|
let
|
||||||
|
rootSrc = ./.;
|
||||||
|
qtPackages = (qmlPkgs pkgs) ++ extraQtPackages;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit version;
|
||||||
|
pname = "dms-shell";
|
||||||
|
src = ./core;
|
||||||
|
vendorHash = "sha256-dEk7IOd6aQwaxZruxQclN7TGMyb8EJOl6NBWRsoZ9HQ=";
|
||||||
|
|
||||||
subPackages = [ "cmd/dms" ];
|
subPackages = [ "cmd/dms" ];
|
||||||
|
|
||||||
ldflags = [
|
ldflags = [
|
||||||
"-s"
|
"-s"
|
||||||
"-w"
|
"-w"
|
||||||
"-X 'main.Version=${version}'"
|
"-X 'main.Version=${version}'"
|
||||||
];
|
];
|
||||||
|
|
||||||
nativeBuildInputs = with pkgs; [
|
nativeBuildInputs = with pkgs; [
|
||||||
installShellFiles
|
installShellFiles
|
||||||
makeWrapper
|
makeWrapper
|
||||||
];
|
];
|
||||||
|
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
mkdir -p $out/share/quickshell/dms
|
mkdir -p $out/share/quickshell/dms
|
||||||
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
|
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
|
||||||
|
|
||||||
chmod u+w $out/share/quickshell/dms/VERSION
|
chmod u+w $out/share/quickshell/dms/VERSION
|
||||||
echo "${version}" > $out/share/quickshell/dms/VERSION
|
echo "${version}" > $out/share/quickshell/dms/VERSION
|
||||||
|
|
||||||
# Install desktop file and icon
|
# Install desktop file and icon
|
||||||
install -D ${rootSrc}/assets/dms-open.desktop \
|
install -D ${rootSrc}/assets/dms-open.desktop \
|
||||||
$out/share/applications/dms-open.desktop
|
$out/share/applications/dms-open.desktop
|
||||||
install -D ${rootSrc}/core/assets/danklogo.svg \
|
install -D ${rootSrc}/core/assets/danklogo.svg \
|
||||||
$out/share/hicolor/scalable/apps/danklogo.svg
|
$out/share/hicolor/scalable/apps/danklogo.svg
|
||||||
|
|
||||||
wrapProgram $out/bin/dms \
|
wrapProgram $out/bin/dms \
|
||||||
--add-flags "-c $out/share/quickshell/dms" \
|
--add-flags "-c $out/share/quickshell/dms" \
|
||||||
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs (qmlPkgs pkgs)}" \
|
--prefix "NIXPKGS_QT6_QML_IMPORT_PATH" ":" "${mkQmlImportPath pkgs qtPackages}" \
|
||||||
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs (qmlPkgs pkgs)}"
|
--prefix "QT_PLUGIN_PATH" ":" "${mkQtPluginPath pkgs qtPackages}"
|
||||||
|
|
||||||
install -Dm644 ${rootSrc}/assets/systemd/dms.service \
|
install -Dm644 ${rootSrc}/assets/systemd/dms.service \
|
||||||
$out/lib/systemd/user/dms.service
|
$out/lib/systemd/user/dms.service
|
||||||
|
|
||||||
substituteInPlace $out/lib/systemd/user/dms.service \
|
substituteInPlace $out/lib/systemd/user/dms.service \
|
||||||
--replace-fail /usr/bin/dms $out/bin/dms \
|
--replace-fail /usr/bin/dms $out/bin/dms \
|
||||||
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
|
--replace-fail /usr/bin/pkill ${pkgs.procps}/bin/pkill
|
||||||
|
|
||||||
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
|
substituteInPlace $out/share/quickshell/dms/Modules/Greetd/assets/dms-greeter \
|
||||||
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
|
--replace-fail /bin/bash ${pkgs.bashInteractive}/bin/bash
|
||||||
|
|
||||||
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
|
substituteInPlace $out/share/quickshell/dms/assets/pam/fprint \
|
||||||
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
|
--replace-fail pam_fprintd.so ${pkgs.fprintd}/lib/security/pam_fprintd.so
|
||||||
|
|
||||||
installShellCompletion --cmd dms \
|
installShellCompletion --cmd dms \
|
||||||
--bash <($out/bin/dms completion bash) \
|
--bash <($out/bin/dms completion bash) \
|
||||||
--fish <($out/bin/dms completion fish) \
|
--fish <($out/bin/dms completion fish) \
|
||||||
--zsh <($out/bin/dms completion zsh)
|
--zsh <($out/bin/dms completion zsh)
|
||||||
'';
|
'';
|
||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
description = "Desktop shell for wayland compositors built with Quickshell & GO";
|
description = "Desktop shell for wayland compositors built with Quickshell & GO";
|
||||||
homepage = "https://danklinux.com";
|
homepage = "https://danklinux.com";
|
||||||
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}";
|
changelog = "https://github.com/AvengeMedia/DankMaterialShell/releases/tag/v${version}";
|
||||||
license = pkgs.lib.licenses.mit;
|
license = pkgs.lib.licenses.mit;
|
||||||
mainProgram = "dms";
|
mainProgram = "dms";
|
||||||
platforms = pkgs.lib.platforms.linux;
|
platforms = pkgs.lib.platforms.linux;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
|
) { };
|
||||||
|
|
||||||
quickshell = quickshell.packages.${system}.default;
|
quickshell = quickshell.packages.${system}.default;
|
||||||
|
|
||||||
|
|||||||
@@ -1,174 +0,0 @@
|
|||||||
pragma Singleton
|
|
||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import qs.Common
|
|
||||||
|
|
||||||
// AnimVariants — Central tuning for animation and Motion Effects variants
|
|
||||||
// (Material/Fluent/Dynamic) (Standard/Directional/Depth)
|
|
||||||
|
|
||||||
Singleton {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readonly property list<real> variantEnterCurve: {
|
|
||||||
if (typeof SettingsData === "undefined")
|
|
||||||
return Anims.expressiveDefaultSpatial;
|
|
||||||
switch (SettingsData.animationVariant) {
|
|
||||||
case 1:
|
|
||||||
return Anims.standardDecel;
|
|
||||||
case 2:
|
|
||||||
return Anims.expressiveFastSpatial;
|
|
||||||
default:
|
|
||||||
return Anims.expressiveDefaultSpatial;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property list<real> variantExitCurve: {
|
|
||||||
if (typeof SettingsData === "undefined")
|
|
||||||
return Anims.emphasized;
|
|
||||||
switch (SettingsData.animationVariant) {
|
|
||||||
case 1:
|
|
||||||
return Anims.standard;
|
|
||||||
case 2:
|
|
||||||
return Anims.emphasized;
|
|
||||||
default:
|
|
||||||
return Anims.emphasized;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modal-specific entry curve
|
|
||||||
readonly property list<real> variantModalEnterCurve: {
|
|
||||||
if (typeof SettingsData === "undefined")
|
|
||||||
return Anims.expressiveDefaultSpatial;
|
|
||||||
if (isDirectionalEffect) {
|
|
||||||
if (SettingsData.animationVariant === 1)
|
|
||||||
return Anims.standardDecel;
|
|
||||||
if (SettingsData.animationVariant === 2)
|
|
||||||
return Anims.expressiveFastSpatial;
|
|
||||||
}
|
|
||||||
return variantEnterCurve;
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property list<real> variantModalExitCurve: {
|
|
||||||
if (typeof SettingsData === "undefined")
|
|
||||||
return Anims.emphasized;
|
|
||||||
if (isDirectionalEffect) {
|
|
||||||
if (SettingsData.animationVariant === 1)
|
|
||||||
return Anims.emphasizedAccel;
|
|
||||||
if (SettingsData.animationVariant === 2)
|
|
||||||
return Anims.emphasizedAccel;
|
|
||||||
}
|
|
||||||
return variantExitCurve;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Popout-specific entry curve
|
|
||||||
readonly property list<real> variantPopoutEnterCurve: {
|
|
||||||
if (typeof SettingsData === "undefined")
|
|
||||||
return Anims.expressiveDefaultSpatial;
|
|
||||||
if (isDirectionalEffect) {
|
|
||||||
if (SettingsData.animationVariant === 1)
|
|
||||||
return Anims.standardDecel;
|
|
||||||
if (SettingsData.animationVariant === 2)
|
|
||||||
return Anims.expressiveFastSpatial;
|
|
||||||
return Anims.standardDecel;
|
|
||||||
}
|
|
||||||
return variantEnterCurve;
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property list<real> variantPopoutExitCurve: {
|
|
||||||
if (typeof SettingsData === "undefined")
|
|
||||||
return Anims.emphasized;
|
|
||||||
if (isDirectionalEffect) {
|
|
||||||
if (SettingsData.animationVariant === 1)
|
|
||||||
return Anims.emphasizedAccel;
|
|
||||||
if (SettingsData.animationVariant === 2)
|
|
||||||
return Anims.emphasizedAccel;
|
|
||||||
}
|
|
||||||
return variantExitCurve;
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property real variantEnterDurationFactor: {
|
|
||||||
if (typeof SettingsData === "undefined")
|
|
||||||
return 1.0;
|
|
||||||
switch (SettingsData.animationVariant) {
|
|
||||||
case 1:
|
|
||||||
return 0.9;
|
|
||||||
case 2:
|
|
||||||
return 1.08;
|
|
||||||
default:
|
|
||||||
return 1.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property real variantExitDurationFactor: {
|
|
||||||
if (typeof SettingsData === "undefined")
|
|
||||||
return 1.0;
|
|
||||||
switch (SettingsData.animationVariant) {
|
|
||||||
case 1:
|
|
||||||
return 0.85;
|
|
||||||
case 2:
|
|
||||||
return 0.92;
|
|
||||||
default:
|
|
||||||
return 1.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fluent: opacity at ~55% of duration; Material/Dynamic: 1:1 with position
|
|
||||||
readonly property real variantOpacityDurationScale: {
|
|
||||||
if (typeof SettingsData === "undefined")
|
|
||||||
return 1.0;
|
|
||||||
return SettingsData.animationVariant === 1 ? 0.55 : 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function variantDuration(baseDuration, entering) {
|
|
||||||
const factor = entering ? variantEnterDurationFactor : variantExitDurationFactor;
|
|
||||||
return Math.max(0, Math.round(baseDuration * factor));
|
|
||||||
}
|
|
||||||
|
|
||||||
function variantExitCleanupPadding() {
|
|
||||||
if (typeof SettingsData === "undefined")
|
|
||||||
return 50;
|
|
||||||
switch (SettingsData.motionEffect) {
|
|
||||||
case 1:
|
|
||||||
return 8;
|
|
||||||
case 2:
|
|
||||||
return 24;
|
|
||||||
default:
|
|
||||||
return 50;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function variantCloseInterval(baseDuration) {
|
|
||||||
return variantDuration(baseDuration, false) + variantExitCleanupPadding();
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property bool isDirectionalEffect: typeof SettingsData !== "undefined" && SettingsData.motionEffect === 1
|
|
||||||
readonly property bool isDepthEffect: typeof SettingsData !== "undefined" && SettingsData.motionEffect === 2
|
|
||||||
|
|
||||||
readonly property real effectScaleCollapsed: {
|
|
||||||
if (typeof SettingsData === "undefined")
|
|
||||||
return 0.96;
|
|
||||||
switch (SettingsData.motionEffect) {
|
|
||||||
case 1:
|
|
||||||
return 1.0;
|
|
||||||
case 2:
|
|
||||||
return 0.88;
|
|
||||||
default:
|
|
||||||
return 0.96;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly property real effectAnimOffset: {
|
|
||||||
if (typeof SettingsData === "undefined")
|
|
||||||
return 16;
|
|
||||||
switch (SettingsData.motionEffect) {
|
|
||||||
case 1:
|
|
||||||
return 144;
|
|
||||||
case 2:
|
|
||||||
return 56;
|
|
||||||
default:
|
|
||||||
return 16;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -22,9 +22,4 @@ Singleton {
|
|||||||
readonly property var standard: [0.20, 0.00, 0.00, 1.00, 1.00, 1.00]
|
readonly property var standard: [0.20, 0.00, 0.00, 1.00, 1.00, 1.00]
|
||||||
readonly property var standardDecel: [0.00, 0.00, 0.00, 1.00, 1.00, 1.00]
|
readonly property var standardDecel: [0.00, 0.00, 0.00, 1.00, 1.00, 1.00]
|
||||||
readonly property var standardAccel: [0.30, 0.00, 1.00, 1.00, 1.00, 1.00]
|
readonly property var standardAccel: [0.30, 0.00, 1.00, 1.00, 1.00, 1.00]
|
||||||
|
|
||||||
// Used by AnimVariants for variant/effect logic
|
|
||||||
readonly property var expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
|
|
||||||
readonly property var expressiveFastSpatial: [0.34, 1.5, 0.2, 1.0, 1.0, 1.0]
|
|
||||||
readonly property var expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ Singleton {
|
|||||||
property bool _hasUnsavedChanges: false
|
property bool _hasUnsavedChanges: false
|
||||||
property var _loadedSessionSnapshot: null
|
property var _loadedSessionSnapshot: null
|
||||||
readonly property var _hooks: ({
|
readonly property var _hooks: ({
|
||||||
"updateLocale": updateLocale
|
"updateLocale": updateLocale
|
||||||
})
|
})
|
||||||
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
|
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
|
||||||
readonly property string _stateDir: Paths.strip(_stateUrl)
|
readonly property string _stateDir: Paths.strip(_stateUrl)
|
||||||
|
|
||||||
@@ -1245,7 +1245,7 @@ Singleton {
|
|||||||
id: greeterSessionFile
|
id: greeterSessionFile
|
||||||
|
|
||||||
path: {
|
path: {
|
||||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms";
|
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
|
||||||
return greetCfgDir + "/session.json";
|
return greetCfgDir + "/session.json";
|
||||||
}
|
}
|
||||||
preload: isGreeterMode
|
preload: isGreeterMode
|
||||||
|
|||||||
@@ -37,18 +37,6 @@ Singleton {
|
|||||||
Custom
|
Custom
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AnimationVariant {
|
|
||||||
Material,
|
|
||||||
Fluent,
|
|
||||||
Dynamic
|
|
||||||
}
|
|
||||||
|
|
||||||
enum AnimationEffect {
|
|
||||||
Standard, // 0 — M3: scale-in, rises from below
|
|
||||||
Directional, // 1 — pure large slide, no scale
|
|
||||||
Depth // 2 — medium slide with deep depth scale pop
|
|
||||||
}
|
|
||||||
|
|
||||||
enum SuspendBehavior {
|
enum SuspendBehavior {
|
||||||
Suspend,
|
Suspend,
|
||||||
Hibernate,
|
Hibernate,
|
||||||
@@ -178,12 +166,6 @@ Singleton {
|
|||||||
property int modalCustomAnimationDuration: 150
|
property int modalCustomAnimationDuration: 150
|
||||||
property bool enableRippleEffects: true
|
property bool enableRippleEffects: true
|
||||||
onEnableRippleEffectsChanged: saveSettings()
|
onEnableRippleEffectsChanged: saveSettings()
|
||||||
property int animationVariant: SettingsData.AnimationVariant.Material
|
|
||||||
onAnimationVariantChanged: saveSettings()
|
|
||||||
property int motionEffect: SettingsData.AnimationEffect.Standard
|
|
||||||
onMotionEffectChanged: saveSettings()
|
|
||||||
property int directionalAnimationMode: 0
|
|
||||||
onDirectionalAnimationModeChanged: saveSettings()
|
|
||||||
property bool m3ElevationEnabled: true
|
property bool m3ElevationEnabled: true
|
||||||
onM3ElevationEnabledChanged: saveSettings()
|
onM3ElevationEnabledChanged: saveSettings()
|
||||||
property int m3ElevationIntensity: 12
|
property int m3ElevationIntensity: 12
|
||||||
@@ -331,6 +313,17 @@ Singleton {
|
|||||||
property string centeringMode: "index"
|
property string centeringMode: "index"
|
||||||
property string clockDateFormat: ""
|
property string clockDateFormat: ""
|
||||||
property string lockDateFormat: ""
|
property string lockDateFormat: ""
|
||||||
|
property bool greeterRememberLastSession: true
|
||||||
|
property bool greeterRememberLastUser: true
|
||||||
|
property bool greeterEnableFprint: false
|
||||||
|
property bool greeterEnableU2f: false
|
||||||
|
property string greeterWallpaperPath: ""
|
||||||
|
property bool greeterUse24HourClock: true
|
||||||
|
property bool greeterShowSeconds: false
|
||||||
|
property bool greeterPadHours12Hour: false
|
||||||
|
property string greeterLockDateFormat: ""
|
||||||
|
property string greeterFontFamily: ""
|
||||||
|
property string greeterWallpaperFillMode: ""
|
||||||
property int mediaSize: 1
|
property int mediaSize: 1
|
||||||
|
|
||||||
property string appLauncherViewMode: "list"
|
property string appLauncherViewMode: "list"
|
||||||
@@ -1173,7 +1166,7 @@ Singleton {
|
|||||||
"updateCompositorLayout": updateCompositorLayout,
|
"updateCompositorLayout": updateCompositorLayout,
|
||||||
"applyStoredIconTheme": applyStoredIconTheme,
|
"applyStoredIconTheme": applyStoredIconTheme,
|
||||||
"updateBarConfigs": updateBarConfigs,
|
"updateBarConfigs": updateBarConfigs,
|
||||||
"updateCompositorCursor": updateCompositorCursor,
|
"updateCompositorCursor": updateCompositorCursor
|
||||||
})
|
})
|
||||||
|
|
||||||
function set(key, value) {
|
function set(key, value) {
|
||||||
|
|||||||
@@ -960,24 +960,6 @@ Singleton {
|
|||||||
"expressiveEffects": [0.34, 0.8, 0.34, 1, 1, 1]
|
"expressiveEffects": [0.34, 0.8, 0.34, 1, 1, 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delegates to AnimVariants.qml for curves, timing, scale, and offsets.
|
|
||||||
readonly property list<real> variantEnterCurve: AnimVariants.variantEnterCurve
|
|
||||||
readonly property list<real> variantExitCurve: AnimVariants.variantExitCurve
|
|
||||||
readonly property list<real> variantModalEnterCurve: AnimVariants.variantModalEnterCurve
|
|
||||||
readonly property list<real> variantModalExitCurve: AnimVariants.variantModalExitCurve
|
|
||||||
readonly property list<real> variantPopoutEnterCurve: AnimVariants.variantPopoutEnterCurve
|
|
||||||
readonly property list<real> variantPopoutExitCurve: AnimVariants.variantPopoutExitCurve
|
|
||||||
readonly property real variantEnterDurationFactor: AnimVariants.variantEnterDurationFactor
|
|
||||||
readonly property real variantExitDurationFactor: AnimVariants.variantExitDurationFactor
|
|
||||||
readonly property real variantOpacityDurationScale: AnimVariants.variantOpacityDurationScale
|
|
||||||
readonly property bool isDirectionalEffect: AnimVariants.isDirectionalEffect
|
|
||||||
readonly property bool isDepthEffect: AnimVariants.isDepthEffect
|
|
||||||
readonly property real effectScaleCollapsed: AnimVariants.effectScaleCollapsed
|
|
||||||
readonly property real effectAnimOffset: AnimVariants.effectAnimOffset
|
|
||||||
function variantDuration(baseDuration, entering) { return AnimVariants.variantDuration(baseDuration, entering); }
|
|
||||||
function variantExitCleanupPadding() { return AnimVariants.variantExitCleanupPadding(); }
|
|
||||||
function variantCloseInterval(baseDuration) { return AnimVariants.variantCloseInterval(baseDuration); }
|
|
||||||
|
|
||||||
readonly property var animationPresetDurations: {
|
readonly property var animationPresetDurations: {
|
||||||
"none": 0,
|
"none": 0,
|
||||||
"short": 250,
|
"short": 250,
|
||||||
@@ -1102,7 +1084,7 @@ Singleton {
|
|||||||
|
|
||||||
property string fontFamily: {
|
property string fontFamily: {
|
||||||
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
|
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
|
||||||
return GreetdSettings.fontFamily;
|
return GreetdSettings.getEffectiveFontFamily();
|
||||||
}
|
}
|
||||||
return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable";
|
return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable";
|
||||||
}
|
}
|
||||||
@@ -2005,7 +1987,7 @@ Singleton {
|
|||||||
FileView {
|
FileView {
|
||||||
id: dynamicColorsFileView
|
id: dynamicColorsFileView
|
||||||
path: {
|
path: {
|
||||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms";
|
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
|
||||||
const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json";
|
const colorsPath = SessionData.isGreeterMode ? greetCfgDir + "/colors.json" : stateDir + "/dms-colors.json";
|
||||||
return colorsPath;
|
return colorsPath;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
property var fprintdDetectionProcess: Process {
|
property var fprintdDetectionProcess: Process {
|
||||||
command: ["sh", "-c", "command -v fprintd-list >/dev/null 2>&1"]
|
command: ["sh", "-c", "command -v fprintd-list >/dev/null 2>&1 && fprintd-list \"${USER:-$(id -un)}\" >/dev/null 2>&1"]
|
||||||
running: false
|
running: false
|
||||||
onExited: function (exitCode) {
|
onExited: function (exitCode) {
|
||||||
if (!settingsRoot)
|
if (!settingsRoot)
|
||||||
|
|||||||
@@ -47,9 +47,6 @@ var SPEC = {
|
|||||||
modalAnimationSpeed: { def: 1 },
|
modalAnimationSpeed: { def: 1 },
|
||||||
modalCustomAnimationDuration: { def: 150 },
|
modalCustomAnimationDuration: { def: 150 },
|
||||||
enableRippleEffects: { def: true },
|
enableRippleEffects: { def: true },
|
||||||
animationVariant: { def: 0 },
|
|
||||||
motionEffect: { def: 0 },
|
|
||||||
directionalAnimationMode: { def: 0 },
|
|
||||||
m3ElevationEnabled: { def: true },
|
m3ElevationEnabled: { def: true },
|
||||||
m3ElevationIntensity: { def: 12 },
|
m3ElevationIntensity: { def: 12 },
|
||||||
m3ElevationOpacity: { def: 30 },
|
m3ElevationOpacity: { def: 30 },
|
||||||
@@ -167,6 +164,17 @@ var SPEC = {
|
|||||||
centeringMode: { def: "index" },
|
centeringMode: { def: "index" },
|
||||||
clockDateFormat: { def: "" },
|
clockDateFormat: { def: "" },
|
||||||
lockDateFormat: { def: "" },
|
lockDateFormat: { def: "" },
|
||||||
|
greeterRememberLastSession: { def: true },
|
||||||
|
greeterRememberLastUser: { def: true },
|
||||||
|
greeterEnableFprint: { def: false },
|
||||||
|
greeterEnableU2f: { def: false },
|
||||||
|
greeterWallpaperPath: { def: "" },
|
||||||
|
greeterUse24HourClock: { def: true },
|
||||||
|
greeterShowSeconds: { def: false },
|
||||||
|
greeterPadHours12Hour: { def: false },
|
||||||
|
greeterLockDateFormat: { def: "" },
|
||||||
|
greeterFontFamily: { def: "" },
|
||||||
|
greeterWallpaperFillMode: { def: "" },
|
||||||
mediaSize: { def: 1 },
|
mediaSize: { def: 1 },
|
||||||
|
|
||||||
appLauncherViewMode: { def: "list" },
|
appLauncherViewMode: { def: "list" },
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ Item {
|
|||||||
property bool closeOnBackgroundClick: true
|
property bool closeOnBackgroundClick: true
|
||||||
property string animationType: "scale"
|
property string animationType: "scale"
|
||||||
property int animationDuration: Theme.modalAnimationDuration
|
property int animationDuration: Theme.modalAnimationDuration
|
||||||
property real animationScaleCollapsed: Theme.effectScaleCollapsed
|
property real animationScaleCollapsed: 0.96
|
||||||
property real animationOffset: Theme.effectAnimOffset
|
property real animationOffset: Theme.spacingL
|
||||||
property list<real> animationEnterCurve: Theme.variantModalEnterCurve
|
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||||
property list<real> animationExitCurve: Theme.variantModalExitCurve
|
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
||||||
property color backgroundColor: Theme.surfaceContainer
|
property color backgroundColor: Theme.surfaceContainer
|
||||||
property color borderColor: Theme.outlineMedium
|
property color borderColor: Theme.outlineMedium
|
||||||
property real borderWidth: 0
|
property real borderWidth: 0
|
||||||
@@ -44,13 +44,11 @@ Item {
|
|||||||
property bool keepPopoutsOpen: false
|
property bool keepPopoutsOpen: false
|
||||||
property var customKeyboardFocus: null
|
property var customKeyboardFocus: null
|
||||||
property bool useOverlayLayer: false
|
property bool useOverlayLayer: false
|
||||||
property real frozenMotionOffsetX: 0
|
|
||||||
property real frozenMotionOffsetY: 0
|
|
||||||
readonly property alias contentWindow: contentWindow
|
readonly property alias contentWindow: contentWindow
|
||||||
readonly property alias clickCatcher: clickCatcher
|
readonly property alias clickCatcher: clickCatcher
|
||||||
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
||||||
readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground
|
readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground
|
||||||
readonly property bool useSingleWindow: CompositorService.isHyprland
|
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackground
|
||||||
|
|
||||||
signal opened
|
signal opened
|
||||||
signal dialogClosed
|
signal dialogClosed
|
||||||
@@ -60,34 +58,19 @@ Item {
|
|||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
closeTimer.stop();
|
closeTimer.stop();
|
||||||
animationsEnabled = false;
|
|
||||||
frozenMotionOffsetX = modalContainer ? modalContainer.offsetX : 0;
|
|
||||||
frozenMotionOffsetY = modalContainer ? modalContainer.offsetY : animationOffset;
|
|
||||||
|
|
||||||
const focusedScreen = CompositorService.getFocusedScreen();
|
const focusedScreen = CompositorService.getFocusedScreen();
|
||||||
if (focusedScreen) {
|
if (focusedScreen) {
|
||||||
contentWindow.screen = focusedScreen;
|
contentWindow.screen = focusedScreen;
|
||||||
if (!useSingleWindow)
|
if (!useSingleWindow)
|
||||||
clickCatcher.screen = focusedScreen;
|
clickCatcher.screen = focusedScreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Theme.isDirectionalEffect || root.useBackground) {
|
|
||||||
if (!useSingleWindow)
|
|
||||||
clickCatcher.visible = true;
|
|
||||||
contentWindow.visible = true;
|
|
||||||
}
|
|
||||||
ModalManager.openModal(root);
|
ModalManager.openModal(root);
|
||||||
|
shouldBeVisible = true;
|
||||||
Qt.callLater(() => {
|
if (!useSingleWindow)
|
||||||
animationsEnabled = true;
|
clickCatcher.visible = true;
|
||||||
shouldBeVisible = true;
|
contentWindow.visible = true;
|
||||||
if (!useSingleWindow && !clickCatcher.visible)
|
shouldHaveFocus = false;
|
||||||
clickCatcher.visible = true;
|
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
|
||||||
if (!contentWindow.visible)
|
|
||||||
contentWindow.visible = true;
|
|
||||||
shouldHaveFocus = false;
|
|
||||||
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
@@ -148,7 +131,7 @@ Item {
|
|||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: closeTimer
|
id: closeTimer
|
||||||
interval: Theme.variantCloseInterval(animationDuration)
|
interval: animationDuration + 50
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (shouldBeVisible)
|
if (shouldBeVisible)
|
||||||
return;
|
return;
|
||||||
@@ -162,17 +145,7 @@ Item {
|
|||||||
readonly property var shadowLevel: Theme.elevationLevel3
|
readonly property var shadowLevel: Theme.elevationLevel3
|
||||||
readonly property real shadowFallbackOffset: 6
|
readonly property real shadowFallbackOffset: 6
|
||||||
readonly property real shadowRenderPadding: (root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
|
readonly property real shadowRenderPadding: (root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
|
||||||
readonly property real shadowMotionPadding: {
|
readonly property real shadowMotionPadding: animationType === "slide" ? 30 : Math.max(0, animationOffset)
|
||||||
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0 && Theme.isDirectionalEffect)
|
|
||||||
return 0; // Wayland native overlap mask
|
|
||||||
if (animationType === "slide")
|
|
||||||
return 30;
|
|
||||||
if (Theme.isDirectionalEffect)
|
|
||||||
return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.9);
|
|
||||||
if (Theme.isDepthEffect)
|
|
||||||
return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.35);
|
|
||||||
return Math.max(0, animationOffset);
|
|
||||||
}
|
|
||||||
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
|
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
|
||||||
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
|
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
|
||||||
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
|
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
|
||||||
@@ -232,26 +205,9 @@ Item {
|
|||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
enabled: !root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible
|
enabled: root.closeOnBackgroundClick && root.shouldBeVisible
|
||||||
onClicked: root.backgroundClicked()
|
onClicked: root.backgroundClicked()
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
z: -1
|
|
||||||
color: "black"
|
|
||||||
opacity: (!root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
|
|
||||||
visible: opacity > 0
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
@@ -294,12 +250,9 @@ Item {
|
|||||||
bottom: root.useSingleWindow
|
bottom: root.useSingleWindow
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property real actualMarginLeft: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
|
|
||||||
readonly property real actualMarginTop: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
|
|
||||||
|
|
||||||
WlrLayershell.margins {
|
WlrLayershell.margins {
|
||||||
left: actualMarginLeft
|
left: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
|
||||||
top: actualMarginTop
|
top: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
|
||||||
right: 0
|
right: 0
|
||||||
bottom: 0
|
bottom: 0
|
||||||
}
|
}
|
||||||
@@ -329,14 +282,13 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
z: -1
|
z: -1
|
||||||
color: "black"
|
color: "black"
|
||||||
opacity: (root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
|
opacity: root.useBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
|
||||||
visible: opacity > 0
|
visible: root.useBackground
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
|
enabled: root.animationsEnabled
|
||||||
NumberAnimation {
|
DankAnim {
|
||||||
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
duration: root.animationDuration
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -344,8 +296,8 @@ Item {
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: modalContainer
|
id: modalContainer
|
||||||
x: (root.useSingleWindow ? root.alignedX : (root.alignedX - contentWindow.actualMarginLeft)) + Theme.snap(animX, root.dpr)
|
x: root.useSingleWindow ? root.alignedX : shadowBuffer
|
||||||
y: (root.useSingleWindow ? root.alignedY : (root.alignedY - contentWindow.actualMarginTop)) + Theme.snap(animY, root.dpr)
|
y: root.useSingleWindow ? root.alignedY : shadowBuffer
|
||||||
|
|
||||||
width: root.alignedWidth
|
width: root.alignedWidth
|
||||||
height: root.alignedHeight
|
height: root.alignedHeight
|
||||||
@@ -361,117 +313,45 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readonly property bool slide: root.animationType === "slide"
|
readonly property bool slide: root.animationType === "slide"
|
||||||
readonly property bool directionalEffect: Theme.isDirectionalEffect
|
readonly property real offsetX: slide ? 15 : 0
|
||||||
readonly property bool depthEffect: Theme.isDepthEffect
|
readonly property real offsetY: slide ? -30 : root.animationOffset
|
||||||
readonly property real directionalTravel: Math.max(root.animationOffset, Math.max(root.alignedWidth, root.alignedHeight) * 0.8)
|
|
||||||
readonly property real depthTravel: Math.max(root.animationOffset * 0.8, 36)
|
|
||||||
readonly property real customAnchorX: root.alignedX + root.alignedWidth * 0.5
|
|
||||||
readonly property real customAnchorY: root.alignedY + root.alignedHeight * 0.5
|
|
||||||
readonly property real customDistLeft: customAnchorX
|
|
||||||
readonly property real customDistRight: root.screenWidth - customAnchorX
|
|
||||||
readonly property real customDistTop: customAnchorY
|
|
||||||
readonly property real customDistBottom: root.screenHeight - customAnchorY
|
|
||||||
readonly property real offsetX: {
|
|
||||||
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
|
|
||||||
return 0;
|
|
||||||
if (slide && !directionalEffect && !depthEffect)
|
|
||||||
return 15;
|
|
||||||
if (directionalEffect) {
|
|
||||||
switch (root.positioning) {
|
|
||||||
case "top-right":
|
|
||||||
return 0;
|
|
||||||
case "custom":
|
|
||||||
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
|
|
||||||
return -directionalTravel;
|
|
||||||
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
|
|
||||||
return directionalTravel;
|
|
||||||
return 0;
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (depthEffect) {
|
|
||||||
switch (root.positioning) {
|
|
||||||
case "top-right":
|
|
||||||
return 0;
|
|
||||||
case "custom":
|
|
||||||
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
|
|
||||||
return -depthTravel;
|
|
||||||
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
|
|
||||||
return depthTravel;
|
|
||||||
return 0;
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
readonly property real offsetY: {
|
|
||||||
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
|
|
||||||
return 0;
|
|
||||||
if (slide && !directionalEffect && !depthEffect)
|
|
||||||
return -30;
|
|
||||||
if (directionalEffect) {
|
|
||||||
switch (root.positioning) {
|
|
||||||
case "top-right":
|
|
||||||
return -Math.max(directionalTravel * 0.65, 96);
|
|
||||||
case "custom":
|
|
||||||
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
|
|
||||||
return -directionalTravel;
|
|
||||||
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
|
|
||||||
return directionalTravel;
|
|
||||||
return 0;
|
|
||||||
default:
|
|
||||||
// Default to sliding down from top when centered
|
|
||||||
return -Math.max(directionalTravel, root.screenHeight * 0.24);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (depthEffect) {
|
|
||||||
switch (root.positioning) {
|
|
||||||
case "top-right":
|
|
||||||
return -depthTravel * 0.75;
|
|
||||||
case "custom":
|
|
||||||
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
|
|
||||||
return -depthTravel;
|
|
||||||
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
|
|
||||||
return depthTravel;
|
|
||||||
return depthTravel * 0.45;
|
|
||||||
default:
|
|
||||||
return -depthTravel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return root.animationOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
property real animX: root.shouldBeVisible ? 0 : root.frozenMotionOffsetX
|
property real animX: 0
|
||||||
property real animY: root.shouldBeVisible ? 0 : root.frozenMotionOffsetY
|
property real animY: 0
|
||||||
|
property real scaleValue: root.animationScaleCollapsed
|
||||||
|
|
||||||
readonly property real computedScaleCollapsed: (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) ? 0.0 : root.animationScaleCollapsed
|
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
|
||||||
property real scaleValue: root.shouldBeVisible ? 1.0 : computedScaleCollapsed
|
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root
|
||||||
|
function onShouldBeVisibleChanged() {
|
||||||
|
modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr);
|
||||||
|
modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr);
|
||||||
|
modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Behavior on animX {
|
Behavior on animX {
|
||||||
enabled: root.animationsEnabled
|
enabled: root.animationsEnabled
|
||||||
NumberAnimation {
|
DankAnim {
|
||||||
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
duration: root.animationDuration
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on animY {
|
Behavior on animY {
|
||||||
enabled: root.animationsEnabled
|
enabled: root.animationsEnabled
|
||||||
NumberAnimation {
|
DankAnim {
|
||||||
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
duration: root.animationDuration
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on scaleValue {
|
Behavior on scaleValue {
|
||||||
enabled: root.animationsEnabled
|
enabled: root.animationsEnabled
|
||||||
NumberAnimation {
|
DankAnim {
|
||||||
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
duration: root.animationDuration
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -487,14 +367,15 @@ Item {
|
|||||||
id: animatedContent
|
id: animatedContent
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
clip: false
|
clip: false
|
||||||
opacity: Theme.isDirectionalEffect ? 1 : (root.shouldBeVisible ? 1 : 0)
|
opacity: root.shouldBeVisible ? 1 : 0
|
||||||
scale: modalContainer.scaleValue
|
scale: modalContainer.scaleValue
|
||||||
transformOrigin: Item.Center
|
x: Theme.snap(modalContainer.animX, root.dpr) + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5
|
||||||
|
y: Theme.snap(modalContainer.animY, root.dpr) + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
|
enabled: root.animationsEnabled
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
duration: animationDuration
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ Item {
|
|||||||
property bool spotlightOpen: false
|
property bool spotlightOpen: false
|
||||||
property bool keyboardActive: false
|
property bool keyboardActive: false
|
||||||
property bool contentVisible: false
|
property bool contentVisible: false
|
||||||
readonly property bool launcherMotionVisible: Theme.isDirectionalEffect ? spotlightOpen : _motionActive
|
|
||||||
property var spotlightContent: launcherContentLoader.item
|
property var spotlightContent: launcherContentLoader.item
|
||||||
property bool openedFromOverview: false
|
property bool openedFromOverview: false
|
||||||
property bool isClosing: false
|
property bool isClosing: false
|
||||||
@@ -24,14 +23,8 @@ Item {
|
|||||||
property string _pendingMode: ""
|
property string _pendingMode: ""
|
||||||
readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
|
readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
|
||||||
|
|
||||||
// Animation state — matches DankPopout/DankModal pattern
|
|
||||||
property bool animationsEnabled: true
|
|
||||||
property bool _motionActive: false
|
|
||||||
property real _frozenMotionX: 0
|
|
||||||
property real _frozenMotionY: 0
|
|
||||||
|
|
||||||
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
||||||
readonly property var effectiveScreen: contentWindow.screen
|
readonly property var effectiveScreen: launcherWindow.screen
|
||||||
readonly property real screenWidth: effectiveScreen?.width ?? 1920
|
readonly property real screenWidth: effectiveScreen?.width ?? 1920
|
||||||
readonly property real screenHeight: effectiveScreen?.height ?? 1080
|
readonly property real screenHeight: effectiveScreen?.height ?? 1080
|
||||||
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
|
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
|
||||||
@@ -85,34 +78,6 @@ Item {
|
|||||||
}
|
}
|
||||||
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
|
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
|
||||||
|
|
||||||
// Shadow padding for the content window (render padding only, no motion padding)
|
|
||||||
readonly property var shadowLevel: Theme.elevationLevel3
|
|
||||||
readonly property real shadowFallbackOffset: 6
|
|
||||||
readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
|
|
||||||
readonly property real shadowPad: Theme.snap(shadowRenderPadding, dpr)
|
|
||||||
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
|
|
||||||
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
|
|
||||||
readonly property real alignedX: Theme.snap(modalX, dpr)
|
|
||||||
readonly property real alignedY: Theme.snap(modalY, dpr)
|
|
||||||
|
|
||||||
// For directional/depth: window extends from screen top (content slides within)
|
|
||||||
// For standard: small window tightly around the modal + shadow padding
|
|
||||||
readonly property bool _needsExtendedWindow: Theme.isDirectionalEffect || Theme.isDepthEffect
|
|
||||||
// Content window geometry
|
|
||||||
readonly property real _cwMarginLeft: Theme.snap(alignedX - shadowPad, dpr)
|
|
||||||
readonly property real _cwMarginTop: _needsExtendedWindow ? 0 : Theme.snap(alignedY - shadowPad, dpr)
|
|
||||||
readonly property real _cwWidth: alignedWidth + shadowPad * 2
|
|
||||||
readonly property real _cwHeight: {
|
|
||||||
if (Theme.isDirectionalEffect)
|
|
||||||
return screenHeight + shadowPad;
|
|
||||||
if (Theme.isDepthEffect)
|
|
||||||
return alignedY + alignedHeight + shadowPad;
|
|
||||||
return alignedHeight + shadowPad * 2;
|
|
||||||
}
|
|
||||||
// Where the content container sits inside the content window
|
|
||||||
readonly property real _ccX: shadowPad
|
|
||||||
readonly property real _ccY: _needsExtendedWindow ? alignedY : shadowPad
|
|
||||||
|
|
||||||
signal dialogClosed
|
signal dialogClosed
|
||||||
|
|
||||||
function _ensureContentLoadedAndInitialize(query, mode) {
|
function _ensureContentLoadedAndInitialize(query, mode) {
|
||||||
@@ -132,8 +97,7 @@ Item {
|
|||||||
if (!spotlightContent)
|
if (!spotlightContent)
|
||||||
return;
|
return;
|
||||||
contentVisible = true;
|
contentVisible = true;
|
||||||
// NOTE: forceActiveFocus() is deliberately NOT called here.
|
spotlightContent.searchField.forceActiveFocus();
|
||||||
// It is deferred to after animation starts to avoid compositor IPC stalls.
|
|
||||||
|
|
||||||
if (spotlightContent.searchField) {
|
if (spotlightContent.searchField) {
|
||||||
spotlightContent.searchField.text = query;
|
spotlightContent.searchField.text = query;
|
||||||
@@ -166,59 +130,40 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _openCommon(query, mode) {
|
function show() {
|
||||||
closeCleanupTimer.stop();
|
closeCleanupTimer.stop();
|
||||||
isClosing = false;
|
isClosing = false;
|
||||||
openedFromOverview = false;
|
openedFromOverview = false;
|
||||||
|
|
||||||
// Disable animations so the snap is instant
|
|
||||||
animationsEnabled = false;
|
|
||||||
|
|
||||||
// Freeze the collapsed offsets (they depend on height which could change)
|
|
||||||
_frozenMotionX = contentContainer ? contentContainer.collapsedMotionX : 0;
|
|
||||||
_frozenMotionY = contentContainer ? contentContainer.collapsedMotionY : (Theme.isDirectionalEffect ? Math.max(root.screenHeight - root._ccY + root.shadowPad, Theme.effectAnimOffset * 1.1) : -Theme.effectAnimOffset);
|
|
||||||
|
|
||||||
var focusedScreen = CompositorService.getFocusedScreen();
|
var focusedScreen = CompositorService.getFocusedScreen();
|
||||||
if (focusedScreen) {
|
if (focusedScreen)
|
||||||
backgroundWindow.screen = focusedScreen;
|
launcherWindow.screen = focusedScreen;
|
||||||
contentWindow.screen = focusedScreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
// _motionActive = false ensures motionX/Y snap to frozen collapsed position
|
|
||||||
_motionActive = false;
|
|
||||||
|
|
||||||
// Make windows visible but do NOT request keyboard focus yet
|
|
||||||
ModalManager.openModal(root);
|
|
||||||
spotlightOpen = true;
|
spotlightOpen = true;
|
||||||
backgroundWindow.visible = true;
|
keyboardActive = true;
|
||||||
contentWindow.visible = true;
|
ModalManager.openModal(root);
|
||||||
if (useHyprlandFocusGrab)
|
if (useHyprlandFocusGrab)
|
||||||
focusGrab.active = true;
|
focusGrab.active = true;
|
||||||
|
|
||||||
// Load content and initialize (but no forceActiveFocus — that's deferred)
|
_ensureContentLoadedAndInitialize("", "");
|
||||||
_ensureContentLoadedAndInitialize(query || "", mode || "");
|
|
||||||
|
|
||||||
// Frame 1: enable animations and trigger enter motion
|
|
||||||
Qt.callLater(() => {
|
|
||||||
root.animationsEnabled = true;
|
|
||||||
root._motionActive = true;
|
|
||||||
|
|
||||||
// Frame 2: request keyboard focus + activate search field
|
|
||||||
// Double-deferred to avoid compositor IPC competing with animation frames
|
|
||||||
Qt.callLater(() => {
|
|
||||||
root.keyboardActive = true;
|
|
||||||
if (root.spotlightContent && root.spotlightContent.searchField)
|
|
||||||
root.spotlightContent.searchField.forceActiveFocus();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
_openCommon("", "");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showWithQuery(query) {
|
function showWithQuery(query) {
|
||||||
_openCommon(query, "");
|
closeCleanupTimer.stop();
|
||||||
|
isClosing = false;
|
||||||
|
openedFromOverview = false;
|
||||||
|
|
||||||
|
var focusedScreen = CompositorService.getFocusedScreen();
|
||||||
|
if (focusedScreen)
|
||||||
|
launcherWindow.screen = focusedScreen;
|
||||||
|
|
||||||
|
spotlightOpen = true;
|
||||||
|
keyboardActive = true;
|
||||||
|
ModalManager.openModal(root);
|
||||||
|
if (useHyprlandFocusGrab)
|
||||||
|
focusGrab.active = true;
|
||||||
|
|
||||||
|
_ensureContentLoadedAndInitialize(query, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
@@ -226,17 +171,13 @@ Item {
|
|||||||
return;
|
return;
|
||||||
openedFromOverview = false;
|
openedFromOverview = false;
|
||||||
isClosing = true;
|
isClosing = true;
|
||||||
// For directional effects, defer contentVisible=false so content stays rendered during exit slide
|
contentVisible = false;
|
||||||
if (!Theme.isDirectionalEffect)
|
|
||||||
contentVisible = false;
|
|
||||||
|
|
||||||
// Trigger exit animation — Behaviors will animate motionX/Y to frozen collapsed position
|
|
||||||
_motionActive = false;
|
|
||||||
|
|
||||||
keyboardActive = false;
|
keyboardActive = false;
|
||||||
spotlightOpen = false;
|
spotlightOpen = false;
|
||||||
focusGrab.active = false;
|
focusGrab.active = false;
|
||||||
ModalManager.closeModal(root);
|
ModalManager.closeModal(root);
|
||||||
|
|
||||||
closeCleanupTimer.start();
|
closeCleanupTimer.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +186,21 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showWithMode(mode) {
|
function showWithMode(mode) {
|
||||||
_openCommon("", mode);
|
closeCleanupTimer.stop();
|
||||||
|
isClosing = false;
|
||||||
|
openedFromOverview = false;
|
||||||
|
|
||||||
|
var focusedScreen = CompositorService.getFocusedScreen();
|
||||||
|
if (focusedScreen)
|
||||||
|
launcherWindow.screen = focusedScreen;
|
||||||
|
|
||||||
|
spotlightOpen = true;
|
||||||
|
keyboardActive = true;
|
||||||
|
ModalManager.openModal(root);
|
||||||
|
if (useHyprlandFocusGrab)
|
||||||
|
focusGrab.active = true;
|
||||||
|
|
||||||
|
_ensureContentLoadedAndInitialize("", mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleWithMode(mode) {
|
function toggleWithMode(mode) {
|
||||||
@@ -266,13 +221,10 @@ Item {
|
|||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: closeCleanupTimer
|
id: closeCleanupTimer
|
||||||
interval: Theme.variantCloseInterval(Theme.modalAnimationDuration)
|
interval: Theme.modalAnimationDuration + 50
|
||||||
repeat: false
|
repeat: false
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
isClosing = false;
|
isClosing = false;
|
||||||
contentVisible = false;
|
|
||||||
contentWindow.visible = false;
|
|
||||||
backgroundWindow.visible = false;
|
|
||||||
if (root.unloadContentOnClose)
|
if (root.unloadContentOnClose)
|
||||||
launcherContentLoader.active = false;
|
launcherContentLoader.active = false;
|
||||||
dialogClosed();
|
dialogClosed();
|
||||||
@@ -290,7 +242,7 @@ Item {
|
|||||||
|
|
||||||
HyprlandFocusGrab {
|
HyprlandFocusGrab {
|
||||||
id: focusGrab
|
id: focusGrab
|
||||||
windows: [contentWindow]
|
windows: [launcherWindow]
|
||||||
active: false
|
active: false
|
||||||
|
|
||||||
onCleared: {
|
onCleared: {
|
||||||
@@ -315,7 +267,7 @@ Item {
|
|||||||
if (Quickshell.screens.length === 0)
|
if (Quickshell.screens.length === 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const screen = contentWindow.screen;
|
const screen = launcherWindow.screen;
|
||||||
const screenName = screen?.name;
|
const screenName = screen?.name;
|
||||||
|
|
||||||
let needsReset = !screen || !screenName;
|
let needsReset = !screen || !screenName;
|
||||||
@@ -337,31 +289,35 @@ Item {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
root._windowEnabled = false;
|
root._windowEnabled = false;
|
||||||
backgroundWindow.screen = newScreen;
|
launcherWindow.screen = newScreen;
|
||||||
contentWindow.screen = newScreen;
|
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
root._windowEnabled = true;
|
root._windowEnabled = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Background window: fullscreen, handles darkening + click-to-dismiss ──
|
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: backgroundWindow
|
id: launcherWindow
|
||||||
visible: false
|
visible: root._windowEnabled && (spotlightOpen || isClosing)
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
WlrLayershell.namespace: "dms:spotlight:bg"
|
WlrLayershell.namespace: "dms:spotlight"
|
||||||
WlrLayershell.layer: WlrLayershell.Top
|
WlrLayershell.layer: {
|
||||||
WlrLayershell.exclusiveZone: -1
|
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
case "bottom":
|
||||||
|
console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||||
WlrLayershell.margins {
|
return WlrLayershell.Top;
|
||||||
top: contentContainer.dockTop ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 0 ? Theme.px(42, root.dpr) : 0)
|
case "background":
|
||||||
bottom: contentContainer.dockBottom ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 1 ? Theme.px(42, root.dpr) : 0)
|
console.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||||
left: contentContainer.dockLeft ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 2 ? Theme.px(42, root.dpr) : 0)
|
return WlrLayershell.Top;
|
||||||
right: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0)
|
case "overlay":
|
||||||
|
return WlrLayershell.Overlay;
|
||||||
|
default:
|
||||||
|
return WlrLayershell.Top;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
|
||||||
|
|
||||||
anchors {
|
anchors {
|
||||||
top: true
|
top: true
|
||||||
@@ -371,11 +327,11 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mask: Region {
|
mask: Region {
|
||||||
item: (spotlightOpen || isClosing) ? bgFullScreenMask : null
|
item: spotlightOpen ? fullScreenMask : null
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: bgFullScreenMask
|
id: fullScreenMask
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,14 +339,13 @@ Item {
|
|||||||
id: backgroundDarken
|
id: backgroundDarken
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: "black"
|
color: "black"
|
||||||
opacity: launcherMotionVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
|
opacity: contentVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
|
||||||
visible: launcherMotionVisible || opacity > 0
|
visible: contentVisible || opacity > 0
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
|
|
||||||
DankAnim {
|
DankAnim {
|
||||||
duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
|
duration: Theme.modalAnimationDuration
|
||||||
easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -398,240 +353,88 @@ Item {
|
|||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
enabled: spotlightOpen
|
enabled: spotlightOpen
|
||||||
onClicked: root.hide()
|
onClicked: mouse => {
|
||||||
}
|
var contentX = modalContainer.x;
|
||||||
}
|
var contentY = modalContainer.y;
|
||||||
|
var contentW = modalContainer.width;
|
||||||
|
var contentH = modalContainer.height;
|
||||||
|
|
||||||
// ── Content window: SMALL, positioned with margins — only renders the modal area ──
|
if (mouse.x < contentX || mouse.x > contentX + contentW || mouse.y < contentY || mouse.y > contentY + contentH) {
|
||||||
PanelWindow {
|
root.hide();
|
||||||
id: contentWindow
|
}
|
||||||
visible: false
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
WlrLayershell.namespace: "dms:spotlight"
|
|
||||||
WlrLayershell.layer: {
|
|
||||||
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
|
||||||
case "bottom":
|
|
||||||
console.error("DankLauncherV2Modal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
|
|
||||||
return WlrLayershell.Top;
|
|
||||||
case "background":
|
|
||||||
console.error("DankLauncherV2Modal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
|
|
||||||
return WlrLayershell.Top;
|
|
||||||
case "overlay":
|
|
||||||
return WlrLayershell.Overlay;
|
|
||||||
default:
|
|
||||||
return WlrLayershell.Top;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WlrLayershell.exclusiveZone: -1
|
|
||||||
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
left: true
|
|
||||||
top: true
|
|
||||||
}
|
|
||||||
|
|
||||||
WlrLayershell.margins {
|
|
||||||
left: root._cwMarginLeft
|
|
||||||
top: root._cwMarginTop
|
|
||||||
}
|
|
||||||
|
|
||||||
implicitWidth: root._cwWidth
|
|
||||||
implicitHeight: root._cwHeight
|
|
||||||
|
|
||||||
mask: Region {
|
|
||||||
item: contentInputMask
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: contentInputMask
|
id: modalContainer
|
||||||
visible: false
|
x: root.modalX
|
||||||
x: contentContainer.x + contentWrapper.x
|
y: root.modalY
|
||||||
y: contentContainer.y + contentWrapper.y
|
width: root.modalWidth
|
||||||
width: root.alignedWidth
|
height: root.modalHeight
|
||||||
height: root.alignedHeight
|
visible: contentVisible || opacity > 0
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
opacity: contentVisible ? 1 : 0
|
||||||
id: contentContainer
|
scale: contentVisible ? 1 : 0.96
|
||||||
|
transformOrigin: Item.Center
|
||||||
|
|
||||||
// For directional/depth: contentContainer is at alignedY from window top (window starts at screen top)
|
Behavior on opacity {
|
||||||
// For standard: contentContainer is at shadowPad from window top (window starts near modal)
|
|
||||||
x: root._ccX
|
|
||||||
y: root._ccY
|
|
||||||
width: root.alignedWidth
|
|
||||||
height: root.alignedHeight
|
|
||||||
|
|
||||||
readonly property int dockEdge: typeof SettingsData !== "undefined" ? SettingsData.dockPosition : 1
|
|
||||||
readonly property bool dockTop: dockEdge === 0
|
|
||||||
readonly property bool dockBottom: dockEdge === 1
|
|
||||||
readonly property bool dockLeft: dockEdge === 2
|
|
||||||
readonly property bool dockRight: dockEdge === 3
|
|
||||||
|
|
||||||
readonly property real dockThickness: typeof SettingsData !== "undefined" && SettingsData.showDock ? Theme.px(SettingsData.dockIconSize + (SettingsData.dockMargin * 2) + SettingsData.dockSpacing + 8, root.dpr) : Theme.px(60, root.dpr)
|
|
||||||
|
|
||||||
readonly property bool directionalEffect: Theme.isDirectionalEffect
|
|
||||||
readonly property bool depthEffect: Theme.isDepthEffect
|
|
||||||
readonly property real collapsedMotionX: {
|
|
||||||
if (directionalEffect) {
|
|
||||||
if (dockLeft)
|
|
||||||
return -(root._ccX + root.alignedWidth + Theme.effectAnimOffset);
|
|
||||||
if (dockRight)
|
|
||||||
return root.screenWidth - root._ccX + Theme.effectAnimOffset;
|
|
||||||
}
|
|
||||||
if (depthEffect)
|
|
||||||
return Theme.effectAnimOffset * 0.25;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
readonly property real collapsedMotionY: {
|
|
||||||
if (directionalEffect) {
|
|
||||||
if (dockTop)
|
|
||||||
return -(root._ccY + root.alignedHeight + Theme.effectAnimOffset);
|
|
||||||
if (dockBottom)
|
|
||||||
return root.screenHeight - root._ccY + root.shadowPad + Theme.effectAnimOffset;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (depthEffect)
|
|
||||||
return -Math.max(Theme.effectAnimOffset * 0.85, 34);
|
|
||||||
return -Math.max((root.shadowPad || 0) + Theme.effectAnimOffset, 40);
|
|
||||||
}
|
|
||||||
|
|
||||||
// animX/animY are Behavior-animated — DankPopout pattern
|
|
||||||
property real animX: 0
|
|
||||||
property real animY: 0
|
|
||||||
property real scaleValue: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed)
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
animX = Theme.snap(root._motionActive ? 0 : collapsedMotionX, root.dpr);
|
|
||||||
animY = Theme.snap(root._motionActive ? 0 : collapsedMotionY, root.dpr);
|
|
||||||
scaleValue = root._motionActive ? 1.0 : (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed));
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root
|
|
||||||
function on_MotionActiveChanged() {
|
|
||||||
contentContainer.animX = Theme.snap(root._motionActive ? 0 : root._frozenMotionX, root.dpr);
|
|
||||||
contentContainer.animY = Theme.snap(root._motionActive ? 0 : root._frozenMotionY, root.dpr);
|
|
||||||
contentContainer.scaleValue = root._motionActive ? 1.0 : (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on animX {
|
|
||||||
enabled: root.animationsEnabled
|
|
||||||
DankAnim {
|
DankAnim {
|
||||||
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive)
|
duration: Theme.modalAnimationDuration
|
||||||
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on animY {
|
Behavior on scale {
|
||||||
enabled: root.animationsEnabled
|
|
||||||
DankAnim {
|
DankAnim {
|
||||||
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive)
|
duration: Theme.modalAnimationDuration
|
||||||
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on scaleValue {
|
ElevationShadow {
|
||||||
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2))
|
id: launcherShadowLayer
|
||||||
DankAnim {
|
anchors.fill: parent
|
||||||
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive)
|
level: Theme.elevationLevel3
|
||||||
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
fallbackOffset: 6
|
||||||
}
|
targetColor: root.backgroundColor
|
||||||
|
borderColor: root.borderColor
|
||||||
|
borderWidth: root.borderWidth
|
||||||
|
targetRadius: root.cornerRadius
|
||||||
|
shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
MouseArea {
|
||||||
id: directionalClipMask
|
anchors.fill: parent
|
||||||
readonly property bool shouldClip: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0
|
onPressed: mouse => mouse.accepted = true
|
||||||
readonly property real clipOversize: 2000
|
}
|
||||||
|
|
||||||
clip: shouldClip
|
FocusScope {
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: keyboardActive
|
||||||
|
|
||||||
x: shouldClip ? (contentContainer.dockRight ? -clipOversize : (contentContainer.dockLeft ? contentContainer.dockThickness - root._ccX : -clipOversize)) : 0
|
Loader {
|
||||||
y: shouldClip ? (contentContainer.dockBottom ? -clipOversize : (contentContainer.dockTop ? contentContainer.dockThickness - root._ccY : -clipOversize)) : 0
|
id: launcherContentLoader
|
||||||
|
anchors.fill: parent
|
||||||
width: shouldClip ? parent.width + clipOversize + (contentContainer.dockRight ? (root.screenWidth - contentContainer.dockThickness - root._ccX - parent.width) : (contentContainer.dockLeft ? clipOversize : clipOversize)) : parent.width
|
active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
|
||||||
height: shouldClip ? parent.height + clipOversize + (contentContainer.dockBottom ? (root.screenHeight - contentContainer.dockThickness - root._ccY - parent.height) : (contentContainer.dockTop ? clipOversize : clipOversize)) : parent.height
|
asynchronous: false
|
||||||
|
sourceComponent: LauncherContent {
|
||||||
Item {
|
focus: true
|
||||||
id: aligner
|
parentModal: root
|
||||||
x: directionalClipMask.x !== 0 ? -directionalClipMask.x : 0
|
|
||||||
y: directionalClipMask.y !== 0 ? -directionalClipMask.y : 0
|
|
||||||
width: contentContainer.width
|
|
||||||
height: contentContainer.height
|
|
||||||
|
|
||||||
// Shadow mirrors contentWrapper position/scale/opacity
|
|
||||||
ElevationShadow {
|
|
||||||
id: launcherShadowLayer
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height
|
|
||||||
opacity: contentWrapper.opacity
|
|
||||||
scale: contentWrapper.scale
|
|
||||||
x: contentWrapper.x
|
|
||||||
y: contentWrapper.y
|
|
||||||
level: root.shadowLevel
|
|
||||||
fallbackOffset: root.shadowFallbackOffset
|
|
||||||
targetColor: root.backgroundColor
|
|
||||||
borderColor: root.borderColor
|
|
||||||
borderWidth: root.borderWidth
|
|
||||||
targetRadius: root.cornerRadius
|
|
||||||
shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// contentWrapper moves inside static contentContainer — DankPopout pattern
|
onLoaded: {
|
||||||
Item {
|
if (root._pendingInitialize) {
|
||||||
id: contentWrapper
|
root._initializeAndShow(root._pendingQuery, root._pendingMode);
|
||||||
width: parent.width
|
root._pendingInitialize = false;
|
||||||
height: parent.height
|
|
||||||
opacity: Theme.isDirectionalEffect ? 1 : (launcherMotionVisible ? 1 : 0)
|
|
||||||
visible: opacity > 0
|
|
||||||
scale: contentContainer.scaleValue
|
|
||||||
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
|
||||||
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
|
|
||||||
DankAnim {
|
|
||||||
duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
|
|
||||||
easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MouseArea {
|
Keys.onEscapePressed: event => {
|
||||||
anchors.fill: parent
|
root.hide();
|
||||||
onPressed: mouse => mouse.accepted = true
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
FocusScope {
|
}
|
||||||
anchors.fill: parent
|
}
|
||||||
focus: keyboardActive
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: launcherContentLoader
|
|
||||||
anchors.fill: parent
|
|
||||||
active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
|
|
||||||
asynchronous: false
|
|
||||||
sourceComponent: LauncherContent {
|
|
||||||
focus: true
|
|
||||||
parentModal: root
|
|
||||||
}
|
|
||||||
|
|
||||||
onLoaded: {
|
|
||||||
if (root._pendingInitialize) {
|
|
||||||
root._initializeAndShow(root._pendingQuery, root._pendingMode);
|
|
||||||
root._pendingInitialize = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Keys.onEscapePressed: event => {
|
|
||||||
root.hide();
|
|
||||||
event.accepted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // contentWrapper
|
|
||||||
} // aligner
|
|
||||||
} // directionalClipMask
|
|
||||||
} // contentContainer
|
|
||||||
} // PanelWindow
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ FocusScope {
|
|||||||
|
|
||||||
Controller {
|
Controller {
|
||||||
id: controller
|
id: controller
|
||||||
active: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
|
active: root.parentModal?.spotlightOpen ?? true
|
||||||
viewModeContext: root.viewModeContext
|
viewModeContext: root.viewModeContext
|
||||||
|
|
||||||
onItemExecuted: {
|
onItemExecuted: {
|
||||||
@@ -462,7 +462,7 @@ FocusScope {
|
|||||||
showClearButton: true
|
showClearButton: true
|
||||||
textColor: Theme.surfaceText
|
textColor: Theme.surfaceText
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
enabled: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
|
enabled: root.parentModal ? root.parentModal.spotlightOpen : true
|
||||||
placeholderText: ""
|
placeholderText: ""
|
||||||
ignoreUpDownKeys: true
|
ignoreUpDownKeys: true
|
||||||
ignoreTabKeys: true
|
ignoreTabKeys: true
|
||||||
@@ -548,6 +548,7 @@ FocusScope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
@@ -696,13 +697,7 @@ FocusScope {
|
|||||||
Item {
|
Item {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2)
|
height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2)
|
||||||
opacity: {
|
opacity: root.parentModal?.isClosing ? 0 : 1
|
||||||
if (!root.parentModal)
|
|
||||||
return 1;
|
|
||||||
if (Theme.isDirectionalEffect && root.parentModal.isClosing)
|
|
||||||
return 1;
|
|
||||||
return root.parentModal.isClosing ? 0 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
ResultsList {
|
ResultsList {
|
||||||
id: resultsList
|
id: resultsList
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
@@ -11,8 +12,45 @@ FloatingWindow {
|
|||||||
property string passwordInput: ""
|
property string passwordInput: ""
|
||||||
property var currentFlow: PolkitService.agent?.flow
|
property var currentFlow: PolkitService.agent?.flow
|
||||||
property bool isLoading: false
|
property bool isLoading: false
|
||||||
|
property bool awaitingFprintForPassword: false
|
||||||
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
|
readonly property int inputFieldHeight: Theme.fontSizeMedium + Theme.spacingL * 2
|
||||||
|
|
||||||
|
property string polkitEtcPamText: ""
|
||||||
|
property string polkitLibPamText: ""
|
||||||
|
property string systemAuthPamText: ""
|
||||||
|
property string commonAuthPamText: ""
|
||||||
|
property string passwordAuthPamText: ""
|
||||||
|
readonly property bool polkitPamHasFprint: {
|
||||||
|
const polkitText = polkitEtcPamText !== "" ? polkitEtcPamText : polkitLibPamText;
|
||||||
|
if (!polkitText)
|
||||||
|
return false;
|
||||||
|
return pamModuleEnabled(polkitText, "pam_fprintd") || (polkitText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_fprintd")) || (polkitText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_fprintd")) || (polkitText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_fprintd"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripPamComment(line) {
|
||||||
|
if (!line)
|
||||||
|
return "";
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith("#"))
|
||||||
|
return "";
|
||||||
|
const hashIdx = trimmed.indexOf("#");
|
||||||
|
if (hashIdx >= 0)
|
||||||
|
return trimmed.substring(0, hashIdx).trim();
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pamModuleEnabled(pamText, moduleName) {
|
||||||
|
if (!pamText || !moduleName)
|
||||||
|
return false;
|
||||||
|
const lines = pamText.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = stripPamComment(lines[i]);
|
||||||
|
if (line && line.includes(moduleName))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function focusPasswordField() {
|
function focusPasswordField() {
|
||||||
passwordField.forceActiveFocus();
|
passwordField.forceActiveFocus();
|
||||||
}
|
}
|
||||||
@@ -20,6 +58,7 @@ FloatingWindow {
|
|||||||
function show() {
|
function show() {
|
||||||
passwordInput = "";
|
passwordInput = "";
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
awaitingFprintForPassword = false;
|
||||||
visible = true;
|
visible = true;
|
||||||
Qt.callLater(focusPasswordField);
|
Qt.callLater(focusPasswordField);
|
||||||
}
|
}
|
||||||
@@ -28,17 +67,27 @@ FloatingWindow {
|
|||||||
visible = false;
|
visible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _commitSubmit() {
|
||||||
|
isLoading = true;
|
||||||
|
awaitingFprintForPassword = false;
|
||||||
|
currentFlow.submit(passwordInput);
|
||||||
|
passwordInput = "";
|
||||||
|
}
|
||||||
|
|
||||||
function submitAuth() {
|
function submitAuth() {
|
||||||
if (!currentFlow || isLoading)
|
if (!currentFlow || isLoading)
|
||||||
return;
|
return;
|
||||||
isLoading = true;
|
if (!currentFlow.isResponseRequired) {
|
||||||
currentFlow.submit(passwordInput);
|
awaitingFprintForPassword = true;
|
||||||
passwordInput = "";
|
return;
|
||||||
|
}
|
||||||
|
_commitSubmit();
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelAuth() {
|
function cancelAuth() {
|
||||||
if (isLoading)
|
if (isLoading)
|
||||||
return;
|
return;
|
||||||
|
awaitingFprintForPassword = false;
|
||||||
if (currentFlow) {
|
if (currentFlow) {
|
||||||
currentFlow.cancelAuthenticationRequest();
|
currentFlow.cancelAuthenticationRequest();
|
||||||
return;
|
return;
|
||||||
@@ -60,6 +109,7 @@ FloatingWindow {
|
|||||||
}
|
}
|
||||||
passwordInput = "";
|
passwordInput = "";
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
awaitingFprintForPassword = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
@@ -83,6 +133,11 @@ FloatingWindow {
|
|||||||
function onIsResponseRequiredChanged() {
|
function onIsResponseRequiredChanged() {
|
||||||
if (!currentFlow.isResponseRequired)
|
if (!currentFlow.isResponseRequired)
|
||||||
return;
|
return;
|
||||||
|
if (awaitingFprintForPassword && passwordInput !== "") {
|
||||||
|
_commitSubmit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
awaitingFprintForPassword = false;
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
passwordInput = "";
|
passwordInput = "";
|
||||||
passwordField.forceActiveFocus();
|
passwordField.forceActiveFocus();
|
||||||
@@ -101,6 +156,41 @@ FloatingWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
path: "/etc/pam.d/polkit-1"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: root.polkitEtcPamText = text()
|
||||||
|
onLoadFailed: root.polkitEtcPamText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
path: "/usr/lib/pam.d/polkit-1"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: root.polkitLibPamText = text()
|
||||||
|
onLoadFailed: root.polkitLibPamText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
path: "/etc/pam.d/system-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: root.systemAuthPamText = text()
|
||||||
|
onLoadFailed: root.systemAuthPamText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
path: "/etc/pam.d/common-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: root.commonAuthPamText = text()
|
||||||
|
onLoadFailed: root.commonAuthPamText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
path: "/etc/pam.d/password-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: root.passwordAuthPamText = text()
|
||||||
|
onLoadFailed: root.passwordAuthPamText = ""
|
||||||
|
}
|
||||||
|
|
||||||
FocusScope {
|
FocusScope {
|
||||||
id: contentFocusScope
|
id: contentFocusScope
|
||||||
|
|
||||||
@@ -205,36 +295,30 @@ FloatingWindow {
|
|||||||
visible: text !== ""
|
visible: text !== ""
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
DankTextField {
|
||||||
|
id: passwordField
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: inputFieldHeight
|
height: inputFieldHeight
|
||||||
radius: Theme.cornerRadius
|
backgroundColor: Theme.surfaceHover
|
||||||
color: Theme.surfaceHover
|
normalBorderColor: Theme.outlineStrong
|
||||||
border.color: passwordField.activeFocus ? Theme.primary : Theme.outlineStrong
|
focusedBorderColor: Theme.primary
|
||||||
border.width: passwordField.activeFocus ? 2 : 1
|
borderWidth: 1
|
||||||
|
focusedBorderWidth: 2
|
||||||
|
leftIconName: polkitPamHasFprint ? "fingerprint" : ""
|
||||||
|
leftIconSize: 20
|
||||||
|
leftIconColor: Theme.primary
|
||||||
|
leftIconFocusedColor: Theme.primary
|
||||||
opacity: isLoading ? 0.5 : 1
|
opacity: isLoading ? 0.5 : 1
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
MouseArea {
|
textColor: Theme.surfaceText
|
||||||
anchors.fill: parent
|
text: passwordInput
|
||||||
enabled: !isLoading
|
showPasswordToggle: !(currentFlow?.responseVisible ?? false)
|
||||||
onClicked: passwordField.forceActiveFocus()
|
echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
|
||||||
}
|
placeholderText: ""
|
||||||
|
enabled: !isLoading
|
||||||
DankTextField {
|
onTextEdited: passwordInput = text
|
||||||
id: passwordField
|
onAccepted: submitAuth()
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
textColor: Theme.surfaceText
|
|
||||||
text: passwordInput
|
|
||||||
showPasswordToggle: !(currentFlow?.responseVisible ?? false)
|
|
||||||
echoMode: (currentFlow?.responseVisible ?? false) || passwordVisible ? TextInput.Normal : TextInput.Password
|
|
||||||
placeholderText: ""
|
|
||||||
backgroundColor: "transparent"
|
|
||||||
enabled: !isLoading
|
|
||||||
onTextEdited: passwordInput = text
|
|
||||||
onAccepted: submitAuth()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
|
|||||||
@@ -241,6 +241,21 @@ FocusScope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: greeterLoader
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.currentIndex === 31
|
||||||
|
visible: active
|
||||||
|
focus: active
|
||||||
|
|
||||||
|
sourceComponent: GreeterTab {}
|
||||||
|
|
||||||
|
onActiveChanged: {
|
||||||
|
if (active && item)
|
||||||
|
Qt.callLater(() => item.forceActiveFocus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: pluginsLoader
|
id: pluginsLoader
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -470,7 +485,7 @@ FocusScope {
|
|||||||
|
|
||||||
onActiveChanged: {
|
onActiveChanged: {
|
||||||
if (active && item)
|
if (active && item)
|
||||||
Qt.callLater(() => item.forceActiveFocus());
|
Qt.callLater(() => item.forceActiveFocus());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,7 +500,7 @@ FocusScope {
|
|||||||
|
|
||||||
onActiveChanged: {
|
onActiveChanged: {
|
||||||
if (active && item)
|
if (active && item)
|
||||||
Qt.callLater(() => item.forceActiveFocus());
|
Qt.callLater(() => item.forceActiveFocus());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -287,6 +287,12 @@ Rectangle {
|
|||||||
"icon": "lock",
|
"icon": "lock",
|
||||||
"tabIndex": 11
|
"tabIndex": 11
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "greeter",
|
||||||
|
"text": I18n.tr("Greeter"),
|
||||||
|
"icon": "login",
|
||||||
|
"tabIndex": 31
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "power_sleep",
|
"id": "power_sleep",
|
||||||
"text": I18n.tr("Power & Sleep"),
|
"text": I18n.tr("Power & Sleep"),
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ DankPopout {
|
|||||||
QtObject {
|
QtObject {
|
||||||
id: modalAdapter
|
id: modalAdapter
|
||||||
property bool spotlightOpen: appDrawerPopout.shouldBeVisible
|
property bool spotlightOpen: appDrawerPopout.shouldBeVisible
|
||||||
property bool isClosing: appDrawerPopout.isClosing
|
property bool isClosing: false
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
appDrawerPopout.close();
|
appDrawerPopout.close();
|
||||||
|
|||||||
@@ -126,11 +126,9 @@ DankPopout {
|
|||||||
z: 5000
|
z: 5000
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
enabled: !Theme.isDirectionalEffect
|
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.shortDuration
|
duration: 200
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.OutCubic
|
||||||
easing.bezierCurve: root.shouldBeVisible ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1117,7 +1117,6 @@ Item {
|
|||||||
if (!notificationCenterLoader.item) {
|
if (!notificationCenterLoader.item) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
notificationCenterLoader.item.triggerScreen = barWindow.screen;
|
|
||||||
const effectiveBarConfig = topBarContent.barConfig;
|
const effectiveBarConfig = topBarContent.barConfig;
|
||||||
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
|
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
|
||||||
if (notificationCenterLoader.item.setBarContext) {
|
if (notificationCenterLoader.item.setBarContext) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ DankPopout {
|
|||||||
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
|
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
|
||||||
triggerWidth: 80
|
triggerWidth: 80
|
||||||
screen: triggerScreen
|
screen: triggerScreen
|
||||||
|
shouldBeVisible: dashVisible
|
||||||
|
|
||||||
property bool __focusArmed: false
|
property bool __focusArmed: false
|
||||||
property bool __contentReady: false
|
property bool __contentReady: false
|
||||||
|
|||||||
@@ -44,43 +44,6 @@ Item {
|
|||||||
|
|
||||||
property int __volumeHoverCount: 0
|
property int __volumeHoverCount: 0
|
||||||
|
|
||||||
readonly property bool directionalEffect: Theme.isDirectionalEffect
|
|
||||||
readonly property bool depthEffect: Theme.isDepthEffect
|
|
||||||
|
|
||||||
function panelMotionX(panelWidth, active) {
|
|
||||||
if (active)
|
|
||||||
return 0;
|
|
||||||
if (directionalEffect) {
|
|
||||||
const travel = Math.max(Theme.effectAnimOffset, panelWidth * 0.85);
|
|
||||||
return isRightEdge ? -travel : travel;
|
|
||||||
}
|
|
||||||
if (depthEffect) {
|
|
||||||
const travel = Math.max(Theme.effectAnimOffset * 0.7, panelWidth * 0.32);
|
|
||||||
return isRightEdge ? -travel : travel;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function panelMotionY(panelType, panelHeight, active) {
|
|
||||||
if (active)
|
|
||||||
return 0;
|
|
||||||
if (directionalEffect) {
|
|
||||||
if (panelType === 2)
|
|
||||||
return panelHeight * 0.08;
|
|
||||||
if (panelType === 3)
|
|
||||||
return -panelHeight * 0.08;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (depthEffect) {
|
|
||||||
if (panelType === 2)
|
|
||||||
return panelHeight * 0.04;
|
|
||||||
if (panelType === 3)
|
|
||||||
return -panelHeight * 0.04;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function volumeAreaEntered() {
|
function volumeAreaEntered() {
|
||||||
__volumeHoverCount++;
|
__volumeHoverCount++;
|
||||||
panelEntered();
|
panelEntered();
|
||||||
@@ -99,47 +62,30 @@ Item {
|
|||||||
visible: dropdownType === 1 && volumeAvailable
|
visible: dropdownType === 1 && volumeAvailable
|
||||||
width: 60
|
width: 60
|
||||||
height: 180
|
height: 180
|
||||||
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 1)
|
x: isRightEdge ? anchorPos.x : anchorPos.x - width
|
||||||
y: anchorPos.y - height / 2 + panelMotionY(1, height, dropdownType === 1)
|
y: anchorPos.y - height / 2
|
||||||
radius: Theme.cornerRadius * 2
|
radius: Theme.cornerRadius * 2
|
||||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
|
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||||
border.width: 1
|
border.width: 1
|
||||||
|
|
||||||
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : 0)
|
opacity: dropdownType === 1 ? 1 : 0
|
||||||
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : Theme.effectScaleCollapsed)
|
scale: dropdownType === 1 ? 1 : 0.96
|
||||||
transformOrigin: isRightEdge ? Item.Left : Item.Right
|
transformOrigin: isRightEdge ? Item.Left : Item.Right
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
enabled: !Theme.isDirectionalEffect
|
NumberAnimation {
|
||||||
DankAnim {
|
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||||
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1) * Theme.variantOpacityDurationScale)
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on scale {
|
Behavior on scale {
|
||||||
enabled: !Theme.isDirectionalEffect
|
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
|
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on x {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on y {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,50 +197,33 @@ Item {
|
|||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: audioDevicesPanel
|
id: audioDevicesPanel
|
||||||
visible: dropdownType === 2 && activePlayer !== null
|
visible: dropdownType === 2
|
||||||
width: 280
|
width: 280
|
||||||
height: Math.max(200, Math.min(280, availableDevices.length * 50 + 100))
|
height: Math.max(200, Math.min(280, availableDevices.length * 50 + 100))
|
||||||
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 2)
|
x: isRightEdge ? anchorPos.x : anchorPos.x - width
|
||||||
y: anchorPos.y - height / 2 + panelMotionY(2, height, dropdownType === 2)
|
y: anchorPos.y - height / 2
|
||||||
radius: Theme.cornerRadius * 2
|
radius: Theme.cornerRadius * 2
|
||||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
|
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
|
||||||
border.width: 2
|
border.width: 2
|
||||||
|
|
||||||
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : 0)
|
opacity: dropdownType === 2 ? 1 : 0
|
||||||
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : Theme.effectScaleCollapsed)
|
scale: dropdownType === 2 ? 1 : 0.96
|
||||||
transformOrigin: isRightEdge ? Item.Left : Item.Right
|
transformOrigin: isRightEdge ? Item.Left : Item.Right
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
enabled: !Theme.isDirectionalEffect
|
NumberAnimation {
|
||||||
DankAnim {
|
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||||
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2) * Theme.variantOpacityDurationScale)
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on scale {
|
Behavior on scale {
|
||||||
enabled: !Theme.isDirectionalEffect
|
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
|
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on x {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on y {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,47 +354,30 @@ Item {
|
|||||||
visible: dropdownType === 3
|
visible: dropdownType === 3
|
||||||
width: 240
|
width: 240
|
||||||
height: Math.max(180, Math.min(240, (allPlayers?.length || 0) * 50 + 80))
|
height: Math.max(180, Math.min(240, (allPlayers?.length || 0) * 50 + 80))
|
||||||
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 3)
|
x: isRightEdge ? anchorPos.x : anchorPos.x - width
|
||||||
y: anchorPos.y - height / 2 + panelMotionY(3, height, dropdownType === 3)
|
y: anchorPos.y - height / 2
|
||||||
radius: Theme.cornerRadius * 2
|
radius: Theme.cornerRadius * 2
|
||||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
|
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
|
||||||
border.width: 2
|
border.width: 2
|
||||||
|
|
||||||
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : 0)
|
opacity: dropdownType === 3 ? 1 : 0
|
||||||
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : Theme.effectScaleCollapsed)
|
scale: dropdownType === 3 ? 1 : 0.96
|
||||||
transformOrigin: isRightEdge ? Item.Left : Item.Right
|
transformOrigin: isRightEdge ? Item.Left : Item.Right
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
enabled: !Theme.isDirectionalEffect
|
NumberAnimation {
|
||||||
DankAnim {
|
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||||
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3) * Theme.variantOpacityDurationScale)
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on scale {
|
Behavior on scale {
|
||||||
enabled: !Theme.isDirectionalEffect
|
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
|
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on x {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on y {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
20
quickshell/Modules/Greetd/GreetdEnv.js
Normal file
20
quickshell/Modules/Greetd/GreetdEnv.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
.pragma library
|
||||||
|
|
||||||
|
function readBoolOverride(envReader, names, fallbackValue) {
|
||||||
|
for (let i = 0; i < names.length; i++) {
|
||||||
|
const name = names[i];
|
||||||
|
const raw = envReader(name);
|
||||||
|
if (raw === undefined || raw === null || raw === "")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const normalized = String(raw).trim().toLowerCase();
|
||||||
|
if (normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on")
|
||||||
|
return true;
|
||||||
|
if (normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
console.warn("Invalid boolean override for", name + ":", raw, "- trying next override/fallback");
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallbackValue;
|
||||||
|
}
|
||||||
@@ -4,13 +4,16 @@ pragma ComponentBehavior: Bound
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
|
import "GreetdEnv.js" as GreetdEnv
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms"
|
readonly property string greetCfgDir: Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter"
|
||||||
readonly property string sessionConfigPath: greetCfgDir + "/session.json"
|
readonly property string sessionConfigPath: greetCfgDir + "/session.json"
|
||||||
readonly property string memoryFile: greetCfgDir + "/memory.json"
|
readonly property string memoryFile: greetCfgDir + "/memory.json"
|
||||||
|
readonly property bool rememberLastSession: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], true)
|
||||||
|
readonly property bool rememberLastUser: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], true)
|
||||||
|
|
||||||
property string lastSessionId: ""
|
property string lastSessionId: ""
|
||||||
property string lastSuccessfulUser: ""
|
property string lastSuccessfulUser: ""
|
||||||
@@ -49,26 +52,44 @@ Singleton {
|
|||||||
if (!content || !content.trim())
|
if (!content || !content.trim())
|
||||||
return;
|
return;
|
||||||
const memory = JSON.parse(content);
|
const memory = JSON.parse(content);
|
||||||
lastSessionId = memory.lastSessionId || "";
|
lastSessionId = rememberLastSession ? (memory.lastSessionId || "") : "";
|
||||||
lastSuccessfulUser = memory.lastSuccessfulUser || "";
|
lastSuccessfulUser = rememberLastUser ? (memory.lastSuccessfulUser || "") : "";
|
||||||
|
if (!rememberLastSession || !rememberLastUser)
|
||||||
|
saveMemory();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to parse greetd memory:", e);
|
console.warn("Failed to parse greetd memory:", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveMemory() {
|
function saveMemory() {
|
||||||
memoryFileView.setText(JSON.stringify({
|
let memory = {};
|
||||||
"lastSessionId": lastSessionId,
|
if (rememberLastSession && lastSessionId)
|
||||||
"lastSuccessfulUser": lastSuccessfulUser
|
memory.lastSessionId = lastSessionId;
|
||||||
}, null, 2));
|
if (rememberLastUser && lastSuccessfulUser)
|
||||||
|
memory.lastSuccessfulUser = lastSuccessfulUser;
|
||||||
|
memoryFileView.setText(JSON.stringify(memory, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLastSessionId(id) {
|
function setLastSessionId(id) {
|
||||||
|
if (!rememberLastSession) {
|
||||||
|
if (lastSessionId !== "") {
|
||||||
|
lastSessionId = "";
|
||||||
|
saveMemory();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
lastSessionId = id || "";
|
lastSessionId = id || "";
|
||||||
saveMemory();
|
saveMemory();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLastSuccessfulUser(username) {
|
function setLastSuccessfulUser(username) {
|
||||||
|
if (!rememberLastUser) {
|
||||||
|
if (lastSuccessfulUser !== "") {
|
||||||
|
lastSuccessfulUser = "";
|
||||||
|
saveMemory();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
lastSuccessfulUser = username || "";
|
lastSuccessfulUser = username || "";
|
||||||
saveMemory();
|
saveMemory();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,22 @@ import QtQuick
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
import "GreetdEnv.js" as GreetdEnv
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property string configPath: {
|
readonly property string configPath: {
|
||||||
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/etc/greetd/.dms";
|
const greetCfgDir = Quickshell.env("DMS_GREET_CFG_DIR") || "/var/cache/dms-greeter";
|
||||||
return greetCfgDir + "/settings.json";
|
return greetCfgDir + "/settings.json";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly property string _greeterCacheDir: {
|
||||||
|
const i = root.configPath.lastIndexOf("/");
|
||||||
|
return i >= 0 ? root.configPath.substring(0, i) : "";
|
||||||
|
}
|
||||||
|
readonly property string greeterWallpaperOverridePath: root._greeterCacheDir ? (root._greeterCacheDir + "/greeter_wallpaper_override.jpg") : ""
|
||||||
|
|
||||||
property string currentThemeName: "purple"
|
property string currentThemeName: "purple"
|
||||||
property bool settingsLoaded: false
|
property bool settingsLoaded: false
|
||||||
property string customThemeFile: ""
|
property string customThemeFile: ""
|
||||||
@@ -21,6 +28,12 @@ Singleton {
|
|||||||
property bool use24HourClock: true
|
property bool use24HourClock: true
|
||||||
property bool showSeconds: false
|
property bool showSeconds: false
|
||||||
property bool padHours12Hour: false
|
property bool padHours12Hour: false
|
||||||
|
property bool greeterUse24HourClock: true
|
||||||
|
property bool greeterShowSeconds: false
|
||||||
|
property bool greeterPadHours12Hour: false
|
||||||
|
property string greeterLockDateFormat: ""
|
||||||
|
property string greeterFontFamily: ""
|
||||||
|
property string greeterWallpaperFillMode: ""
|
||||||
property bool useFahrenheit: false
|
property bool useFahrenheit: false
|
||||||
property bool nightModeEnabled: false
|
property bool nightModeEnabled: false
|
||||||
property string weatherLocation: "New York, NY"
|
property string weatherLocation: "New York, NY"
|
||||||
@@ -41,6 +54,11 @@ Singleton {
|
|||||||
property string lockDateFormat: ""
|
property string lockDateFormat: ""
|
||||||
property bool lockScreenShowPowerActions: true
|
property bool lockScreenShowPowerActions: true
|
||||||
property bool lockScreenShowProfileImage: true
|
property bool lockScreenShowProfileImage: true
|
||||||
|
property bool rememberLastSession: true
|
||||||
|
property bool rememberLastUser: true
|
||||||
|
property bool greeterEnableFprint: false
|
||||||
|
property bool greeterEnableU2f: false
|
||||||
|
property string greeterWallpaperPath: ""
|
||||||
property bool powerActionConfirm: true
|
property bool powerActionConfirm: true
|
||||||
property real powerActionHoldDuration: 0.5
|
property real powerActionHoldDuration: 0.5
|
||||||
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
|
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
|
||||||
@@ -52,66 +70,103 @@ Singleton {
|
|||||||
|
|
||||||
function parseSettings(content) {
|
function parseSettings(content) {
|
||||||
try {
|
try {
|
||||||
|
let settings = {};
|
||||||
if (content && content.trim()) {
|
if (content && content.trim()) {
|
||||||
const settings = JSON.parse(content);
|
settings = JSON.parse(content);
|
||||||
currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "purple";
|
}
|
||||||
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : "";
|
|
||||||
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot";
|
|
||||||
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
|
|
||||||
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false;
|
|
||||||
padHours12Hour = settings.padHours12Hour !== undefined ? settings.padHours12Hour : false;
|
|
||||||
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false;
|
|
||||||
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
|
|
||||||
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY";
|
|
||||||
weatherCoordinates = settings.weatherCoordinates !== undefined ? settings.weatherCoordinates : "40.7128,-74.0060";
|
|
||||||
useAutoLocation = settings.useAutoLocation !== undefined ? settings.useAutoLocation : false;
|
|
||||||
weatherEnabled = settings.weatherEnabled !== undefined ? settings.weatherEnabled : true;
|
|
||||||
iconTheme = settings.iconTheme !== undefined ? settings.iconTheme : "System Default";
|
|
||||||
useOSLogo = settings.useOSLogo !== undefined ? settings.useOSLogo : false;
|
|
||||||
osLogoColorOverride = settings.osLogoColorOverride !== undefined ? settings.osLogoColorOverride : "";
|
|
||||||
osLogoBrightness = settings.osLogoBrightness !== undefined ? settings.osLogoBrightness : 0.5;
|
|
||||||
osLogoContrast = settings.osLogoContrast !== undefined ? settings.osLogoContrast : 1;
|
|
||||||
fontFamily = settings.fontFamily !== undefined ? settings.fontFamily : Theme.defaultFontFamily;
|
|
||||||
monoFontFamily = settings.monoFontFamily !== undefined ? settings.monoFontFamily : Theme.defaultMonoFontFamily;
|
|
||||||
fontWeight = settings.fontWeight !== undefined ? settings.fontWeight : Font.Normal;
|
|
||||||
fontScale = settings.fontScale !== undefined ? settings.fontScale : 1.0;
|
|
||||||
cornerRadius = settings.cornerRadius !== undefined ? settings.cornerRadius : 12;
|
|
||||||
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch";
|
|
||||||
lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : "";
|
|
||||||
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true;
|
|
||||||
lockScreenShowProfileImage = settings.lockScreenShowProfileImage !== undefined ? settings.lockScreenShowProfileImage : true;
|
|
||||||
powerActionConfirm = settings.powerActionConfirm !== undefined ? settings.powerActionConfirm : true;
|
|
||||||
powerActionHoldDuration = settings.powerActionHoldDuration !== undefined ? settings.powerActionHoldDuration : 0.5;
|
|
||||||
powerMenuActions = settings.powerMenuActions !== undefined ? settings.powerMenuActions : ["reboot", "logout", "poweroff", "lock", "suspend", "restart"];
|
|
||||||
powerMenuDefaultAction = settings.powerMenuDefaultAction !== undefined ? settings.powerMenuDefaultAction : "logout";
|
|
||||||
powerMenuGridLayout = settings.powerMenuGridLayout !== undefined ? settings.powerMenuGridLayout : false;
|
|
||||||
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({});
|
|
||||||
animationSpeed = settings.animationSpeed !== undefined ? settings.animationSpeed : 2;
|
|
||||||
wallpaperFillMode = settings.wallpaperFillMode !== undefined ? settings.wallpaperFillMode : "Fill";
|
|
||||||
settingsLoaded = true;
|
|
||||||
|
|
||||||
if (typeof Theme !== "undefined") {
|
const envRememberLastSession = GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], undefined);
|
||||||
if (currentThemeName === "custom" && customThemeFile) {
|
const envRememberLastUser = GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_USER", "DMS_SAVE_USERNAME"], undefined);
|
||||||
Theme.loadCustomThemeFromFile(customThemeFile);
|
|
||||||
}
|
currentThemeName = settings.currentThemeName !== undefined ? settings.currentThemeName : "purple";
|
||||||
Theme.applyGreeterTheme(currentThemeName);
|
customThemeFile = settings.customThemeFile !== undefined ? settings.customThemeFile : "";
|
||||||
|
matugenScheme = settings.matugenScheme !== undefined ? settings.matugenScheme : "scheme-tonal-spot";
|
||||||
|
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
|
||||||
|
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false;
|
||||||
|
padHours12Hour = settings.padHours12Hour !== undefined ? settings.padHours12Hour : false;
|
||||||
|
greeterUse24HourClock = settings.greeterUse24HourClock !== undefined ? settings.greeterUse24HourClock : use24HourClock;
|
||||||
|
greeterShowSeconds = settings.greeterShowSeconds !== undefined ? settings.greeterShowSeconds : showSeconds;
|
||||||
|
greeterPadHours12Hour = settings.greeterPadHours12Hour !== undefined ? settings.greeterPadHours12Hour : padHours12Hour;
|
||||||
|
greeterLockDateFormat = settings.greeterLockDateFormat !== undefined ? settings.greeterLockDateFormat : "";
|
||||||
|
greeterFontFamily = settings.greeterFontFamily !== undefined ? settings.greeterFontFamily : "";
|
||||||
|
greeterWallpaperFillMode = settings.greeterWallpaperFillMode !== undefined ? settings.greeterWallpaperFillMode : "";
|
||||||
|
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false;
|
||||||
|
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
|
||||||
|
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY";
|
||||||
|
weatherCoordinates = settings.weatherCoordinates !== undefined ? settings.weatherCoordinates : "40.7128,-74.0060";
|
||||||
|
useAutoLocation = settings.useAutoLocation !== undefined ? settings.useAutoLocation : false;
|
||||||
|
weatherEnabled = settings.weatherEnabled !== undefined ? settings.weatherEnabled : true;
|
||||||
|
iconTheme = settings.iconTheme !== undefined ? settings.iconTheme : "System Default";
|
||||||
|
useOSLogo = settings.useOSLogo !== undefined ? settings.useOSLogo : false;
|
||||||
|
osLogoColorOverride = settings.osLogoColorOverride !== undefined ? settings.osLogoColorOverride : "";
|
||||||
|
osLogoBrightness = settings.osLogoBrightness !== undefined ? settings.osLogoBrightness : 0.5;
|
||||||
|
osLogoContrast = settings.osLogoContrast !== undefined ? settings.osLogoContrast : 1;
|
||||||
|
fontFamily = settings.fontFamily !== undefined ? settings.fontFamily : Theme.defaultFontFamily;
|
||||||
|
monoFontFamily = settings.monoFontFamily !== undefined ? settings.monoFontFamily : Theme.defaultMonoFontFamily;
|
||||||
|
fontWeight = settings.fontWeight !== undefined ? settings.fontWeight : Font.Normal;
|
||||||
|
fontScale = settings.fontScale !== undefined ? settings.fontScale : 1.0;
|
||||||
|
cornerRadius = settings.cornerRadius !== undefined ? settings.cornerRadius : 12;
|
||||||
|
widgetBackgroundColor = settings.widgetBackgroundColor !== undefined ? settings.widgetBackgroundColor : "sch";
|
||||||
|
lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : "";
|
||||||
|
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true;
|
||||||
|
lockScreenShowProfileImage = settings.lockScreenShowProfileImage !== undefined ? settings.lockScreenShowProfileImage : true;
|
||||||
|
if (envRememberLastSession !== undefined) {
|
||||||
|
rememberLastSession = envRememberLastSession;
|
||||||
|
} else {
|
||||||
|
rememberLastSession = settings.greeterRememberLastSession !== undefined ? settings.greeterRememberLastSession : settings.rememberLastSession !== undefined ? settings.rememberLastSession : true;
|
||||||
|
}
|
||||||
|
if (envRememberLastUser !== undefined) {
|
||||||
|
rememberLastUser = envRememberLastUser;
|
||||||
|
} else {
|
||||||
|
rememberLastUser = settings.greeterRememberLastUser !== undefined ? settings.greeterRememberLastUser : settings.rememberLastUser !== undefined ? settings.rememberLastUser : true;
|
||||||
|
}
|
||||||
|
greeterEnableFprint = settings.greeterEnableFprint !== undefined ? settings.greeterEnableFprint : false;
|
||||||
|
greeterEnableU2f = settings.greeterEnableU2f !== undefined ? settings.greeterEnableU2f : false;
|
||||||
|
greeterWallpaperPath = settings.greeterWallpaperPath !== undefined ? settings.greeterWallpaperPath : "";
|
||||||
|
powerActionConfirm = settings.powerActionConfirm !== undefined ? settings.powerActionConfirm : true;
|
||||||
|
powerActionHoldDuration = settings.powerActionHoldDuration !== undefined ? settings.powerActionHoldDuration : 0.5;
|
||||||
|
powerMenuActions = settings.powerMenuActions !== undefined ? settings.powerMenuActions : ["reboot", "logout", "poweroff", "lock", "suspend", "restart"];
|
||||||
|
powerMenuDefaultAction = settings.powerMenuDefaultAction !== undefined ? settings.powerMenuDefaultAction : "logout";
|
||||||
|
powerMenuGridLayout = settings.powerMenuGridLayout !== undefined ? settings.powerMenuGridLayout : false;
|
||||||
|
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({});
|
||||||
|
animationSpeed = settings.animationSpeed !== undefined ? settings.animationSpeed : 2;
|
||||||
|
wallpaperFillMode = settings.wallpaperFillMode !== undefined ? settings.wallpaperFillMode : "Fill";
|
||||||
|
|
||||||
|
if (typeof Theme !== "undefined") {
|
||||||
|
if (currentThemeName === "custom" && customThemeFile) {
|
||||||
|
Theme.loadCustomThemeFromFile(customThemeFile);
|
||||||
}
|
}
|
||||||
|
Theme.applyGreeterTheme(currentThemeName);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to parse greetd settings:", e);
|
console.warn("Failed to parse greetd settings:", e);
|
||||||
|
} finally {
|
||||||
|
settingsLoaded = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEffectiveTimeFormat() {
|
function getEffectiveTimeFormat() {
|
||||||
if (use24HourClock)
|
const use24 = greeterUse24HourClock;
|
||||||
return showSeconds ? "hh:mm:ss" : "hh:mm";
|
const secs = greeterShowSeconds;
|
||||||
if (padHours12Hour)
|
const pad = greeterPadHours12Hour;
|
||||||
return showSeconds ? "hh:mm:ss AP" : "hh:mm AP";
|
if (use24)
|
||||||
return showSeconds ? "h:mm:ss AP" : "h:mm AP";
|
return secs ? "hh:mm:ss" : "hh:mm";
|
||||||
|
if (pad)
|
||||||
|
return secs ? "hh:mm:ss AP" : "hh:mm AP";
|
||||||
|
return secs ? "h:mm:ss AP" : "h:mm AP";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEffectiveLockDateFormat() {
|
function getEffectiveLockDateFormat() {
|
||||||
return lockDateFormat && lockDateFormat.length > 0 ? lockDateFormat : Locale.LongFormat;
|
const fmt = (greeterLockDateFormat !== undefined && greeterLockDateFormat !== "") ? greeterLockDateFormat : lockDateFormat;
|
||||||
|
return fmt && fmt.length > 0 ? fmt : Locale.LongFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEffectiveWallpaperFillMode() {
|
||||||
|
return (greeterWallpaperFillMode && greeterWallpaperFillMode !== "") ? greeterWallpaperFillMode : wallpaperFillMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEffectiveFontFamily() {
|
||||||
|
return (greeterFontFamily && greeterFontFamily !== "") ? greeterFontFamily : fontFamily;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFilteredScreens(componentId) {
|
function getFilteredScreens(componentId) {
|
||||||
@@ -133,5 +188,9 @@ Singleton {
|
|||||||
onLoaded: {
|
onLoaded: {
|
||||||
parseSettings(settingsFile.text());
|
parseSettings(settingsFile.text());
|
||||||
}
|
}
|
||||||
|
onLoadFailed: error => {
|
||||||
|
console.warn("Failed to load greetd settings:", error);
|
||||||
|
root.parseSettings("");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,25 @@ Item {
|
|||||||
signal launchRequested
|
signal launchRequested
|
||||||
|
|
||||||
property bool weatherInitialized: false
|
property bool weatherInitialized: false
|
||||||
|
property bool awaitingExternalAuth: false
|
||||||
|
property bool pendingPasswordResponse: false
|
||||||
|
property bool passwordSubmitRequested: false
|
||||||
|
property bool cancelingExternalAuthForPassword: false
|
||||||
|
property int defaultAuthTimeoutMs: 12000
|
||||||
|
property int externalAuthTimeoutMs: 45000
|
||||||
|
property int passwordFailureCount: 0
|
||||||
|
property int passwordAttemptLimitHint: 0
|
||||||
|
property string authFeedbackMessage: ""
|
||||||
|
property string greetdPamText: ""
|
||||||
|
property string systemAuthPamText: ""
|
||||||
|
property string commonAuthPamText: ""
|
||||||
|
property string passwordAuthPamText: ""
|
||||||
|
property string faillockConfigText: ""
|
||||||
|
property bool greeterWallpaperOverrideExists: false
|
||||||
|
property string externalAuthAutoStartedForUser: ""
|
||||||
|
readonly property bool greeterPamHasFprint: pamModuleEnabled(greetdPamText, "pam_fprintd") || (greetdPamText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_fprintd")) || (greetdPamText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_fprintd")) || (greetdPamText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_fprintd"))
|
||||||
|
readonly property bool greeterPamHasU2f: pamModuleEnabled(greetdPamText, "pam_u2f") || (greetdPamText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_u2f")) || (greetdPamText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_u2f")) || (greetdPamText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_u2f"))
|
||||||
|
readonly property bool greeterExternalAuthAvailable: greeterPamHasFprint || greeterPamHasU2f
|
||||||
|
|
||||||
function initWeatherService() {
|
function initWeatherService() {
|
||||||
if (weatherInitialized)
|
if (weatherInitialized)
|
||||||
@@ -44,16 +63,253 @@ Item {
|
|||||||
WeatherService.forceRefresh();
|
WeatherService.forceRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stripPamComment(line) {
|
||||||
|
if (!line)
|
||||||
|
return "";
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed || trimmed.startsWith("#"))
|
||||||
|
return "";
|
||||||
|
const hashIdx = trimmed.indexOf("#");
|
||||||
|
if (hashIdx >= 0)
|
||||||
|
return trimmed.substring(0, hashIdx).trim();
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pamModuleEnabled(pamText, moduleName) {
|
||||||
|
if (!pamText || !moduleName)
|
||||||
|
return false;
|
||||||
|
const lines = pamText.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = stripPamComment(lines[i]);
|
||||||
|
if (!line)
|
||||||
|
continue;
|
||||||
|
if (line.includes(moduleName))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function usesPamLockoutPolicy(pamText) {
|
||||||
|
if (!pamText)
|
||||||
|
return false;
|
||||||
|
const lines = pamText.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = stripPamComment(lines[i]);
|
||||||
|
if (!line)
|
||||||
|
continue;
|
||||||
|
if (line.includes("pam_faillock.so") || line.includes("pam_tally2.so") || line.includes("pam_tally.so"))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePamLineDenyValue(pamText) {
|
||||||
|
if (!pamText)
|
||||||
|
return -1;
|
||||||
|
const lines = pamText.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = stripPamComment(lines[i]);
|
||||||
|
if (!line)
|
||||||
|
continue;
|
||||||
|
if (!line.includes("pam_faillock.so") && !line.includes("pam_tally2.so") && !line.includes("pam_tally.so"))
|
||||||
|
continue;
|
||||||
|
const denyMatch = line.match(/\bdeny\s*=\s*(\d+)\b/i);
|
||||||
|
if (!denyMatch)
|
||||||
|
continue;
|
||||||
|
const parsed = parseInt(denyMatch[1], 10);
|
||||||
|
if (!isNaN(parsed))
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFaillockDenyValue(configText) {
|
||||||
|
if (!configText)
|
||||||
|
return -1;
|
||||||
|
const lines = configText.split(/\r?\n/);
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = stripPamComment(lines[i]);
|
||||||
|
if (!line)
|
||||||
|
continue;
|
||||||
|
const denyMatch = line.match(/^deny\s*=\s*(\d+)\s*$/i);
|
||||||
|
if (!denyMatch)
|
||||||
|
continue;
|
||||||
|
const parsed = parseInt(denyMatch[1], 10);
|
||||||
|
if (!isNaN(parsed))
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshPasswordAttemptPolicyHint() {
|
||||||
|
const pamSources = [greetdPamText, systemAuthPamText, commonAuthPamText, passwordAuthPamText];
|
||||||
|
let lockoutConfigured = false;
|
||||||
|
let denyFromPam = -1;
|
||||||
|
for (let i = 0; i < pamSources.length; i++) {
|
||||||
|
const source = pamSources[i];
|
||||||
|
if (!source)
|
||||||
|
continue;
|
||||||
|
if (usesPamLockoutPolicy(source))
|
||||||
|
lockoutConfigured = true;
|
||||||
|
const denyValue = parsePamLineDenyValue(source);
|
||||||
|
if (denyValue >= 0 && (denyFromPam < 0 || denyValue < denyFromPam))
|
||||||
|
denyFromPam = denyValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lockoutConfigured) {
|
||||||
|
passwordAttemptLimitHint = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const denyFromConfig = parseFaillockDenyValue(faillockConfigText);
|
||||||
|
if (denyFromConfig >= 0) {
|
||||||
|
passwordAttemptLimitHint = denyFromConfig;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (denyFromPam >= 0) {
|
||||||
|
passwordAttemptLimitHint = denyFromPam;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pam_faillock default deny value when no explicit config is set.
|
||||||
|
passwordAttemptLimitHint = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLikelyLockoutMessage(message) {
|
||||||
|
const lower = (message || "").toLowerCase();
|
||||||
|
return lower.includes("account is locked") || lower.includes("too many") || lower.includes("maximum number of") || lower.includes("auth_err");
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentAuthMessage() {
|
||||||
|
if (GreeterState.pamState === "error")
|
||||||
|
return "Authentication error - try again";
|
||||||
|
if (GreeterState.pamState === "max")
|
||||||
|
return "Too many failed attempts - account may be locked";
|
||||||
|
if (GreeterState.pamState === "fail") {
|
||||||
|
if (passwordAttemptLimitHint > 0) {
|
||||||
|
const attempt = Math.max(1, Math.min(passwordFailureCount, passwordAttemptLimitHint));
|
||||||
|
const remaining = Math.max(passwordAttemptLimitHint - attempt, 0);
|
||||||
|
if (remaining > 0) {
|
||||||
|
return "Incorrect password - attempt " + attempt + " of " + passwordAttemptLimitHint + " (lockout may follow)";
|
||||||
|
}
|
||||||
|
return "Incorrect password - next failures may trigger account lockout";
|
||||||
|
}
|
||||||
|
return "Incorrect password";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAuthFeedback() {
|
||||||
|
GreeterState.pamState = "";
|
||||||
|
authFeedbackMessage = "";
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: GreetdSettings
|
target: GreetdSettings
|
||||||
function onSettingsLoadedChanged() {
|
function onSettingsLoadedChanged() {
|
||||||
if (GreetdSettings.settingsLoaded)
|
if (GreetdSettings.settingsLoaded) {
|
||||||
initWeatherService();
|
initWeatherService();
|
||||||
|
if (isPrimaryScreen) {
|
||||||
|
applyLastSuccessfulUser();
|
||||||
|
finalizeSessionSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRememberLastUserChanged() {
|
||||||
|
if (!isPrimaryScreen)
|
||||||
|
return;
|
||||||
|
if (!GreetdSettings.rememberLastUser && GreetdMemory.lastSuccessfulUser) {
|
||||||
|
GreetdMemory.setLastSuccessfulUser("");
|
||||||
|
}
|
||||||
|
applyLastSuccessfulUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRememberLastSessionChanged() {
|
||||||
|
if (!isPrimaryScreen)
|
||||||
|
return;
|
||||||
|
if (!GreetdSettings.rememberLastSession && GreetdMemory.lastSessionId) {
|
||||||
|
GreetdMemory.setLastSessionId("");
|
||||||
|
}
|
||||||
|
finalizeSessionSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: greetdPamWatcher
|
||||||
|
path: "/etc/pam.d/greetd"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.greetdPamText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
root.maybeAutoStartExternalAuth();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.greetdPamText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: systemAuthPamWatcher
|
||||||
|
path: "/etc/pam.d/system-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.systemAuthPamText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.systemAuthPamText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: commonAuthPamWatcher
|
||||||
|
path: "/etc/pam.d/common-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.commonAuthPamText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.commonAuthPamText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: passwordAuthPamWatcher
|
||||||
|
path: "/etc/pam.d/password-auth"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.passwordAuthPamText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.passwordAuthPamText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: faillockConfigWatcher
|
||||||
|
path: "/etc/security/faillock.conf"
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
root.faillockConfigText = text();
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
}
|
||||||
|
onLoadFailed: {
|
||||||
|
root.faillockConfigText = "";
|
||||||
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
initWeatherService();
|
initWeatherService();
|
||||||
|
refreshPasswordAttemptPolicyHint();
|
||||||
|
|
||||||
if (isPrimaryScreen)
|
if (isPrimaryScreen)
|
||||||
applyLastSuccessfulUser();
|
applyLastSuccessfulUser();
|
||||||
@@ -63,15 +319,116 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyLastSuccessfulUser() {
|
function applyLastSuccessfulUser() {
|
||||||
|
if (!GreetdSettings.settingsLoaded || !GreetdSettings.rememberLastUser)
|
||||||
|
return;
|
||||||
const lastUser = GreetdMemory.lastSuccessfulUser;
|
const lastUser = GreetdMemory.lastSuccessfulUser;
|
||||||
if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) {
|
if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) {
|
||||||
GreeterState.username = lastUser;
|
GreeterState.username = lastUser;
|
||||||
GreeterState.usernameInput = lastUser;
|
GreeterState.usernameInput = lastUser;
|
||||||
GreeterState.showPasswordInput = true;
|
GreeterState.showPasswordInput = true;
|
||||||
PortalService.getGreeterUserProfileImage(lastUser);
|
PortalService.getGreeterUserProfileImage(lastUser);
|
||||||
|
maybeAutoStartExternalAuth();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function submitUsername(rawValue) {
|
||||||
|
const user = (rawValue || "").trim();
|
||||||
|
if (!user)
|
||||||
|
return;
|
||||||
|
if (GreeterState.username !== user) {
|
||||||
|
passwordFailureCount = 0;
|
||||||
|
clearAuthFeedback();
|
||||||
|
externalAuthAutoStartedForUser = "";
|
||||||
|
}
|
||||||
|
GreeterState.username = user;
|
||||||
|
GreeterState.showPasswordInput = true;
|
||||||
|
PortalService.getGreeterUserProfileImage(user);
|
||||||
|
GreeterState.passwordBuffer = "";
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
passwordSubmitRequested = false;
|
||||||
|
cancelingExternalAuthForPassword = false;
|
||||||
|
maybeAutoStartExternalAuth();
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitBufferedPassword() {
|
||||||
|
if (!GreeterState.passwordBuffer || GreeterState.passwordBuffer.length === 0)
|
||||||
|
return false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
passwordSubmitRequested = false;
|
||||||
|
cancelingExternalAuthForPassword = false;
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.restart();
|
||||||
|
Greetd.respond(GreeterState.passwordBuffer);
|
||||||
|
GreeterState.passwordBuffer = "";
|
||||||
|
inputField.text = "";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestPasswordSessionTransition() {
|
||||||
|
if (cancelingExternalAuthForPassword)
|
||||||
|
return;
|
||||||
|
cancelingExternalAuthForPassword = true;
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.stop();
|
||||||
|
Greetd.cancelSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAuthSession() {
|
||||||
|
if (!GreeterState.showPasswordInput || !GreeterState.username)
|
||||||
|
return;
|
||||||
|
if (GreeterState.unlocking)
|
||||||
|
return;
|
||||||
|
const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
|
||||||
|
if (Greetd.state !== GreetdState.Inactive) {
|
||||||
|
if (pendingPasswordResponse && hasPasswordBuffer)
|
||||||
|
submitBufferedPassword();
|
||||||
|
else if (awaitingExternalAuth && hasPasswordBuffer) {
|
||||||
|
passwordSubmitRequested = true;
|
||||||
|
} else if (hasPasswordBuffer)
|
||||||
|
passwordSubmitRequested = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cancelingExternalAuthForPassword) {
|
||||||
|
if (hasPasswordBuffer)
|
||||||
|
passwordSubmitRequested = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!hasPasswordBuffer && !root.greeterExternalAuthAvailable)
|
||||||
|
return;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
passwordSubmitRequested = hasPasswordBuffer;
|
||||||
|
awaitingExternalAuth = !hasPasswordBuffer && root.greeterExternalAuthAvailable;
|
||||||
|
authTimeout.interval = awaitingExternalAuth ? externalAuthTimeoutMs : defaultAuthTimeoutMs;
|
||||||
|
authTimeout.restart();
|
||||||
|
Greetd.createSession(GreeterState.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeAutoStartExternalAuth() {
|
||||||
|
if (!GreeterState.showPasswordInput || !GreeterState.username)
|
||||||
|
return;
|
||||||
|
if (!root.greeterExternalAuthAvailable)
|
||||||
|
return;
|
||||||
|
if (GreeterState.unlocking || Greetd.state !== GreetdState.Inactive)
|
||||||
|
return;
|
||||||
|
if (passwordSubmitRequested || cancelingExternalAuthForPassword)
|
||||||
|
return;
|
||||||
|
if (GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0)
|
||||||
|
return;
|
||||||
|
if (externalAuthAutoStartedForUser === GreeterState.username)
|
||||||
|
return;
|
||||||
|
|
||||||
|
externalAuthAutoStartedForUser = GreeterState.username;
|
||||||
|
startAuthSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExternalAuthPrompt(message, responseRequired) {
|
||||||
|
// Non-response PAM messages commonly represent waiting states (fprint/U2F/token touch).
|
||||||
|
return !responseRequired;
|
||||||
|
}
|
||||||
|
|
||||||
Component.onDestruction: {
|
Component.onDestruction: {
|
||||||
if (weatherInitialized)
|
if (weatherInitialized)
|
||||||
WeatherService.removeRef();
|
WeatherService.removeRef();
|
||||||
@@ -143,10 +500,39 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: greeterWallpaperOverrideFile
|
||||||
|
path: GreetdSettings.greeterWallpaperOverridePath
|
||||||
|
printErrors: false
|
||||||
|
watchChanges: true
|
||||||
|
onLoaded: root.greeterWallpaperOverrideExists = true
|
||||||
|
onLoadFailed: root.greeterWallpaperOverrideExists = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: GreetdSettings
|
||||||
|
function onGreeterWallpaperOverridePathChanged() {
|
||||||
|
if (!GreetdSettings.greeterWallpaperOverridePath) {
|
||||||
|
root.greeterWallpaperOverrideExists = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
greeterWallpaperOverrideFile.reload();
|
||||||
|
}
|
||||||
|
function onGreeterWallpaperPathChanged() {
|
||||||
|
if (!GreetdSettings.greeterWallpaperPath) {
|
||||||
|
root.greeterWallpaperOverrideExists = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
greeterWallpaperOverrideFile.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DankBackdrop {
|
DankBackdrop {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
screenName: root.screenName
|
screenName: root.screenName
|
||||||
visible: {
|
visible: {
|
||||||
|
if (GreetdSettings.greeterWallpaperPath !== "" && root.greeterWallpaperOverrideExists)
|
||||||
|
return false;
|
||||||
var _ = SessionData.perMonitorWallpaper;
|
var _ = SessionData.perMonitorWallpaper;
|
||||||
var __ = SessionData.monitorWallpapers;
|
var __ = SessionData.monitorWallpapers;
|
||||||
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
|
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
|
||||||
@@ -159,12 +545,14 @@ Item {
|
|||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: {
|
source: {
|
||||||
|
if (GreetdSettings.greeterWallpaperPath !== "" && root.greeterWallpaperOverrideExists)
|
||||||
|
return encodeFileUrl(GreetdSettings.greeterWallpaperOverridePath);
|
||||||
var _ = SessionData.perMonitorWallpaper;
|
var _ = SessionData.perMonitorWallpaper;
|
||||||
var __ = SessionData.monitorWallpapers;
|
var __ = SessionData.monitorWallpapers;
|
||||||
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
|
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
|
||||||
return (currentWallpaper && !currentWallpaper.startsWith("#")) ? encodeFileUrl(currentWallpaper) : "";
|
return (currentWallpaper && !currentWallpaper.startsWith("#")) ? encodeFileUrl(currentWallpaper) : "";
|
||||||
}
|
}
|
||||||
fillMode: Theme.getFillMode(GreetdSettings.wallpaperFillMode)
|
fillMode: Theme.getFillMode(GreetdSettings.getEffectiveWallpaperFillMode())
|
||||||
smooth: true
|
smooth: true
|
||||||
asynchronous: false
|
asynchronous: false
|
||||||
cache: true
|
cache: true
|
||||||
@@ -327,10 +715,7 @@ Item {
|
|||||||
anchors.top: clockContainer.bottom
|
anchors.top: clockContainer.bottom
|
||||||
anchors.topMargin: 4
|
anchors.topMargin: 4
|
||||||
text: {
|
text: {
|
||||||
if (GreetdSettings.lockDateFormat && GreetdSettings.lockDateFormat.length > 0) {
|
return systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.getEffectiveLockDateFormat());
|
||||||
return systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.lockDateFormat);
|
|
||||||
}
|
|
||||||
return systemClock.date.toLocaleDateString(I18n.locale(), Locale.LongFormat);
|
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeXLarge
|
font.pixelSize: Theme.fontSizeXLarge
|
||||||
color: "white"
|
color: "white"
|
||||||
@@ -399,6 +784,9 @@ Item {
|
|||||||
if (GreeterState.showPasswordInput && revealButton.visible) {
|
if (GreeterState.showPasswordInput && revealButton.visible) {
|
||||||
margin += revealButton.width;
|
margin += revealButton.width;
|
||||||
}
|
}
|
||||||
|
if (externalAuthButton.visible) {
|
||||||
|
margin += externalAuthButton.width;
|
||||||
|
}
|
||||||
if (virtualKeyboardButton.visible) {
|
if (virtualKeyboardButton.visible) {
|
||||||
margin += virtualKeyboardButton.width;
|
margin += virtualKeyboardButton.width;
|
||||||
}
|
}
|
||||||
@@ -415,21 +803,18 @@ Item {
|
|||||||
return;
|
return;
|
||||||
if (GreeterState.showPasswordInput) {
|
if (GreeterState.showPasswordInput) {
|
||||||
GreeterState.passwordBuffer = text;
|
GreeterState.passwordBuffer = text;
|
||||||
|
if (!text || text.length === 0)
|
||||||
|
root.passwordSubmitRequested = false;
|
||||||
} else {
|
} else {
|
||||||
GreeterState.usernameInput = text;
|
GreeterState.usernameInput = text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
if (GreeterState.showPasswordInput) {
|
if (GreeterState.showPasswordInput) {
|
||||||
if (Greetd.state === GreetdState.Inactive && GreeterState.username) {
|
root.startAuthSession();
|
||||||
Greetd.createSession(GreeterState.username);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (text.trim()) {
|
if (text.trim()) {
|
||||||
GreeterState.username = text.trim();
|
root.submitUsername(text);
|
||||||
GreeterState.showPasswordInput = true;
|
|
||||||
PortalService.getGreeterUserProfileImage(GreeterState.username);
|
|
||||||
GreeterState.passwordBuffer = "";
|
|
||||||
syncingFromState = true;
|
syncingFromState = true;
|
||||||
text = "";
|
text = "";
|
||||||
syncingFromState = false;
|
syncingFromState = false;
|
||||||
@@ -461,14 +846,14 @@ Item {
|
|||||||
|
|
||||||
anchors.left: lockIcon.right
|
anchors.left: lockIcon.right
|
||||||
anchors.leftMargin: Theme.spacingM
|
anchors.leftMargin: Theme.spacingM
|
||||||
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)))
|
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right))))
|
||||||
anchors.rightMargin: 2
|
anchors.rightMargin: 2
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: {
|
text: {
|
||||||
if (GreeterState.unlocking) {
|
if (GreeterState.unlocking) {
|
||||||
return "Logging in...";
|
return "Logging in...";
|
||||||
}
|
}
|
||||||
if (Greetd.state !== GreetdState.Inactive) {
|
if (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse) {
|
||||||
return "Authenticating...";
|
return "Authenticating...";
|
||||||
}
|
}
|
||||||
if (GreeterState.showPasswordInput) {
|
if (GreeterState.showPasswordInput) {
|
||||||
@@ -476,7 +861,7 @@ Item {
|
|||||||
}
|
}
|
||||||
return "Username...";
|
return "Username...";
|
||||||
}
|
}
|
||||||
color: GreeterState.unlocking ? Theme.primary : (Greetd.state !== GreetdState.Inactive ? Theme.primary : Theme.outline)
|
color: (GreeterState.unlocking || (Greetd.state !== GreetdState.Inactive && !awaitingExternalAuth && !pendingPasswordResponse)) ? Theme.primary : Theme.outline
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : GreeterState.usernameInput.length === 0) ? 1 : 0
|
opacity: (GreeterState.showPasswordInput ? GreeterState.passwordBuffer.length === 0 : GreeterState.usernameInput.length === 0) ? 1 : 0
|
||||||
|
|
||||||
@@ -498,7 +883,7 @@ Item {
|
|||||||
StyledText {
|
StyledText {
|
||||||
anchors.left: lockIcon.right
|
anchors.left: lockIcon.right
|
||||||
anchors.leftMargin: Theme.spacingM
|
anchors.leftMargin: Theme.spacingM
|
||||||
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)))
|
anchors.right: (GreeterState.showPasswordInput && revealButton.visible ? revealButton.left : (externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right))))
|
||||||
anchors.rightMargin: 2
|
anchors.rightMargin: 2
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: {
|
text: {
|
||||||
@@ -528,15 +913,27 @@ Item {
|
|||||||
DankActionButton {
|
DankActionButton {
|
||||||
id: revealButton
|
id: revealButton
|
||||||
|
|
||||||
anchors.right: virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)
|
anchors.right: externalAuthButton.visible ? externalAuthButton.left : (virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right))
|
||||||
anchors.rightMargin: 0
|
anchors.rightMargin: 0
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
iconName: parent.showPassword ? "visibility_off" : "visibility"
|
iconName: parent.showPassword ? "visibility_off" : "visibility"
|
||||||
buttonSize: 32
|
buttonSize: 32
|
||||||
visible: GreeterState.showPasswordInput && GreeterState.passwordBuffer.length > 0 && Greetd.state === GreetdState.Inactive && !GreeterState.unlocking
|
visible: GreeterState.showPasswordInput && GreeterState.passwordBuffer.length > 0 && (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
|
||||||
enabled: visible
|
enabled: visible
|
||||||
onClicked: parent.showPassword = !parent.showPassword
|
onClicked: parent.showPassword = !parent.showPassword
|
||||||
}
|
}
|
||||||
|
DankActionButton {
|
||||||
|
id: externalAuthButton
|
||||||
|
|
||||||
|
anchors.right: virtualKeyboardButton.visible ? virtualKeyboardButton.left : (enterButton.visible ? enterButton.left : parent.right)
|
||||||
|
anchors.rightMargin: 0
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
iconName: root.greeterPamHasFprint ? "fingerprint" : "key"
|
||||||
|
buttonSize: 32
|
||||||
|
visible: GreeterState.showPasswordInput && root.greeterExternalAuthAvailable && GreeterState.passwordBuffer.length === 0 && (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
|
||||||
|
enabled: visible
|
||||||
|
onClicked: root.startAuthSession()
|
||||||
|
}
|
||||||
DankActionButton {
|
DankActionButton {
|
||||||
id: virtualKeyboardButton
|
id: virtualKeyboardButton
|
||||||
|
|
||||||
@@ -545,7 +942,7 @@ Item {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
iconName: "keyboard"
|
iconName: "keyboard"
|
||||||
buttonSize: 32
|
buttonSize: 32
|
||||||
visible: Greetd.state === GreetdState.Inactive && !GreeterState.unlocking
|
visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
|
||||||
enabled: visible
|
enabled: visible
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (keyboard_controller.isKeyboardActive) {
|
if (keyboard_controller.isKeyboardActive) {
|
||||||
@@ -564,19 +961,14 @@ Item {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
iconName: "keyboard_return"
|
iconName: "keyboard_return"
|
||||||
buttonSize: 36
|
buttonSize: 36
|
||||||
visible: Greetd.state === GreetdState.Inactive && !GreeterState.unlocking
|
visible: (Greetd.state === GreetdState.Inactive || awaitingExternalAuth || pendingPasswordResponse) && !GreeterState.unlocking
|
||||||
enabled: true
|
enabled: true
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (GreeterState.showPasswordInput) {
|
if (GreeterState.showPasswordInput) {
|
||||||
if (GreeterState.username) {
|
root.startAuthSession();
|
||||||
Greetd.createSession(GreeterState.username);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (inputField.text.trim()) {
|
if (inputField.text.trim()) {
|
||||||
GreeterState.username = inputField.text.trim();
|
root.submitUsername(inputField.text);
|
||||||
GreeterState.showPasswordInput = true;
|
|
||||||
PortalService.getGreeterUserProfileImage(GreeterState.username);
|
|
||||||
GreeterState.passwordBuffer = "";
|
|
||||||
inputField.text = "";
|
inputField.text = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -601,20 +993,16 @@ Item {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: 20
|
Layout.preferredHeight: 38
|
||||||
Layout.topMargin: -Theme.spacingS
|
Layout.topMargin: -Theme.spacingS
|
||||||
Layout.bottomMargin: -Theme.spacingS
|
Layout.bottomMargin: -Theme.spacingS
|
||||||
text: {
|
text: root.authFeedbackMessage
|
||||||
if (GreeterState.pamState === "error")
|
|
||||||
return "Authentication error - try again";
|
|
||||||
if (GreeterState.pamState === "fail")
|
|
||||||
return "Incorrect password";
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
color: Theme.error
|
color: Theme.error
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
opacity: GreeterState.pamState !== "" ? 1 : 0
|
wrapMode: Text.WordWrap
|
||||||
|
maximumLineCount: 2
|
||||||
|
opacity: root.authFeedbackMessage !== "" ? 1 : 0
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
@@ -667,6 +1055,7 @@ Item {
|
|||||||
enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput
|
enabled: !GreeterState.unlocking && Greetd.state === GreetdState.Inactive && GreeterState.showPasswordInput
|
||||||
onClicked: {
|
onClicked: {
|
||||||
GreeterState.reset();
|
GreeterState.reset();
|
||||||
|
root.externalAuthAutoStartedForUser = "";
|
||||||
inputField.text = "";
|
inputField.text = "";
|
||||||
PortalService.profileImage = "";
|
PortalService.profileImage = "";
|
||||||
}
|
}
|
||||||
@@ -1029,9 +1418,11 @@ Item {
|
|||||||
return;
|
return;
|
||||||
if (!GreetdMemory.memoryReady)
|
if (!GreetdMemory.memoryReady)
|
||||||
return;
|
return;
|
||||||
|
if (!GreetdSettings.settingsLoaded)
|
||||||
|
return;
|
||||||
|
|
||||||
const savedSession = GreetdMemory.lastSessionId;
|
const savedSession = GreetdSettings.rememberLastSession ? GreetdMemory.lastSessionId : "";
|
||||||
if (savedSession) {
|
if (savedSession && GreetdSettings.rememberLastSession) {
|
||||||
for (var i = 0; i < GreeterState.sessionPaths.length; i++) {
|
for (var i = 0; i < GreeterState.sessionPaths.length; i++) {
|
||||||
if (GreeterState.sessionPaths[i] === savedSession) {
|
if (GreeterState.sessionPaths[i] === savedSession) {
|
||||||
GreeterState.currentSessionIndex = i;
|
GreeterState.currentSessionIndex = i;
|
||||||
@@ -1164,44 +1555,125 @@ Item {
|
|||||||
|
|
||||||
function onAuthMessage(message, error, responseRequired, echoResponse) {
|
function onAuthMessage(message, error, responseRequired, echoResponse) {
|
||||||
if (responseRequired) {
|
if (responseRequired) {
|
||||||
Greetd.respond(GreeterState.passwordBuffer);
|
cancelingExternalAuthForPassword = false;
|
||||||
GreeterState.passwordBuffer = "";
|
awaitingExternalAuth = false;
|
||||||
inputField.text = "";
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.restart();
|
||||||
|
pendingPasswordResponse = true;
|
||||||
|
if (passwordSubmitRequested && !root.submitBufferedPassword())
|
||||||
|
passwordSubmitRequested = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!error)
|
pendingPasswordResponse = false;
|
||||||
Greetd.respond("");
|
if (!passwordSubmitRequested)
|
||||||
|
awaitingExternalAuth = root.isExternalAuthPrompt(message, responseRequired);
|
||||||
|
authTimeout.interval = awaitingExternalAuth ? externalAuthTimeoutMs : defaultAuthTimeoutMs;
|
||||||
|
authTimeout.restart();
|
||||||
|
Greetd.respond("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function onStateChanged() {
|
||||||
|
if (Greetd.state === GreetdState.Inactive) {
|
||||||
|
const resumePasswordSubmit = cancelingExternalAuthForPassword && passwordSubmitRequested && GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
cancelingExternalAuthForPassword = false;
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.stop();
|
||||||
|
if (resumePasswordSubmit) {
|
||||||
|
Qt.callLater(root.startAuthSession);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
passwordSubmitRequested = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onReadyToLaunch() {
|
function onReadyToLaunch() {
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
passwordSubmitRequested = false;
|
||||||
|
cancelingExternalAuthForPassword = false;
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.stop();
|
||||||
|
passwordFailureCount = 0;
|
||||||
|
clearAuthFeedback();
|
||||||
const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex];
|
const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex];
|
||||||
const sessionPath = GreeterState.selectedSessionPath || GreeterState.sessionPaths[GreeterState.currentSessionIndex];
|
const sessionPath = GreeterState.selectedSessionPath || GreeterState.sessionPaths[GreeterState.currentSessionIndex];
|
||||||
if (!sessionCmd) {
|
if (!sessionCmd) {
|
||||||
GreeterState.pamState = "error";
|
GreeterState.pamState = "error";
|
||||||
|
authFeedbackMessage = currentAuthMessage();
|
||||||
placeholderDelay.restart();
|
placeholderDelay.restart();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
GreeterState.unlocking = true;
|
GreeterState.unlocking = true;
|
||||||
launchTimeout.restart();
|
launchTimeout.restart();
|
||||||
GreetdMemory.setLastSessionId(sessionPath);
|
if (GreetdSettings.rememberLastSession) {
|
||||||
GreetdMemory.setLastSuccessfulUser(GreeterState.username);
|
GreetdMemory.setLastSessionId(sessionPath);
|
||||||
|
} else if (GreetdMemory.lastSessionId) {
|
||||||
|
GreetdMemory.setLastSessionId("");
|
||||||
|
}
|
||||||
|
if (GreetdSettings.rememberLastUser) {
|
||||||
|
GreetdMemory.setLastSuccessfulUser(GreeterState.username);
|
||||||
|
} else if (GreetdMemory.lastSuccessfulUser) {
|
||||||
|
GreetdMemory.setLastSuccessfulUser("");
|
||||||
|
}
|
||||||
Greetd.launch(sessionCmd.split(" "), ["XDG_SESSION_TYPE=wayland"]);
|
Greetd.launch(sessionCmd.split(" "), ["XDG_SESSION_TYPE=wayland"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAuthFailure(message) {
|
function onAuthFailure(message) {
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
passwordSubmitRequested = false;
|
||||||
|
cancelingExternalAuthForPassword = false;
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.stop();
|
||||||
launchTimeout.stop();
|
launchTimeout.stop();
|
||||||
GreeterState.unlocking = false;
|
GreeterState.unlocking = false;
|
||||||
GreeterState.pamState = "fail";
|
if (isLikelyLockoutMessage(message)) {
|
||||||
|
GreeterState.pamState = "max";
|
||||||
|
} else {
|
||||||
|
GreeterState.pamState = "fail";
|
||||||
|
passwordFailureCount = passwordFailureCount + 1;
|
||||||
|
}
|
||||||
|
authFeedbackMessage = currentAuthMessage();
|
||||||
GreeterState.passwordBuffer = "";
|
GreeterState.passwordBuffer = "";
|
||||||
inputField.text = "";
|
inputField.text = "";
|
||||||
placeholderDelay.restart();
|
placeholderDelay.restart();
|
||||||
|
Greetd.cancelSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onError(error) {
|
function onError(error) {
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
passwordSubmitRequested = false;
|
||||||
|
cancelingExternalAuthForPassword = false;
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
authTimeout.stop();
|
||||||
launchTimeout.stop();
|
launchTimeout.stop();
|
||||||
GreeterState.unlocking = false;
|
GreeterState.unlocking = false;
|
||||||
GreeterState.pamState = "error";
|
GreeterState.pamState = "error";
|
||||||
|
authFeedbackMessage = currentAuthMessage();
|
||||||
|
GreeterState.passwordBuffer = "";
|
||||||
|
inputField.text = "";
|
||||||
|
placeholderDelay.restart();
|
||||||
|
Greetd.cancelSession();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: authTimeout
|
||||||
|
interval: defaultAuthTimeoutMs
|
||||||
|
onTriggered: {
|
||||||
|
if (GreeterState.unlocking || Greetd.state === GreetdState.Inactive)
|
||||||
|
return;
|
||||||
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
passwordSubmitRequested = false;
|
||||||
|
cancelingExternalAuthForPassword = false;
|
||||||
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
|
GreeterState.pamState = "error";
|
||||||
|
authFeedbackMessage = currentAuthMessage();
|
||||||
GreeterState.passwordBuffer = "";
|
GreeterState.passwordBuffer = "";
|
||||||
inputField.text = "";
|
inputField.text = "";
|
||||||
placeholderDelay.restart();
|
placeholderDelay.restart();
|
||||||
@@ -1215,8 +1687,12 @@ Item {
|
|||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (!GreeterState.unlocking)
|
if (!GreeterState.unlocking)
|
||||||
return;
|
return;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
passwordSubmitRequested = false;
|
||||||
|
cancelingExternalAuthForPassword = false;
|
||||||
GreeterState.unlocking = false;
|
GreeterState.unlocking = false;
|
||||||
GreeterState.pamState = "error";
|
GreeterState.pamState = "error";
|
||||||
|
authFeedbackMessage = currentAuthMessage();
|
||||||
placeholderDelay.restart();
|
placeholderDelay.restart();
|
||||||
Greetd.cancelSession();
|
Greetd.cancelSession();
|
||||||
}
|
}
|
||||||
@@ -1225,7 +1701,7 @@ Item {
|
|||||||
Timer {
|
Timer {
|
||||||
id: placeholderDelay
|
id: placeholderDelay
|
||||||
interval: 4000
|
interval: 4000
|
||||||
onTriggered: GreeterState.pamState = ""
|
onTriggered: clearAuthFeedback()
|
||||||
}
|
}
|
||||||
|
|
||||||
LockPowerMenu {
|
LockPowerMenu {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ A greeter for [greetd](https://github.com/kennylevinsen/greetd) that follows the
|
|||||||
- **Multiple compositors**: Supports niri, Hyprland, Sway, or mangowc.
|
- **Multiple compositors**: Supports niri, Hyprland, Sway, or mangowc.
|
||||||
- **Custom PAM**: Supports custom PAM configuration in `/etc/pam.d/greetd`
|
- **Custom PAM**: Supports custom PAM configuration in `/etc/pam.d/greetd`
|
||||||
- **Session Memory**: Remembers last selected session and user
|
- **Session Memory**: Remembers last selected session and user
|
||||||
|
- Can be disabled via `settings.json` keys: `greeterRememberLastSession` and `greeterRememberLastUser`
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -212,6 +213,7 @@ dms-greeter --command hyprland
|
|||||||
dms-greeter --command sway
|
dms-greeter --command sway
|
||||||
dms-greeter --command mangowc
|
dms-greeter --command mangowc
|
||||||
dms-greeter --command niri -C /path/to/custom-niri.kdl
|
dms-greeter --command niri -C /path/to/custom-niri.kdl
|
||||||
|
dms-greeter --command niri --remember-last-user false --remember-last-session false
|
||||||
```
|
```
|
||||||
|
|
||||||
Configure greetd to use it in `/etc/greetd/config.toml`:
|
Configure greetd to use it in `/etc/greetd/config.toml`:
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ COMPOSITOR=""
|
|||||||
COMPOSITOR_CONFIG=""
|
COMPOSITOR_CONFIG=""
|
||||||
DMS_PATH="dms-greeter"
|
DMS_PATH="dms-greeter"
|
||||||
CACHE_DIR="/var/cache/dms-greeter"
|
CACHE_DIR="/var/cache/dms-greeter"
|
||||||
|
REMEMBER_LAST_SESSION=""
|
||||||
|
REMEMBER_LAST_USER=""
|
||||||
|
DEBUG_MODE=0
|
||||||
|
|
||||||
show_help() {
|
show_help() {
|
||||||
cat << EOF
|
cat << EOF
|
||||||
@@ -22,6 +25,15 @@ Options:
|
|||||||
(default: dms-greeter)
|
(default: dms-greeter)
|
||||||
--cache-dir PATH Cache directory for greeter data
|
--cache-dir PATH Cache directory for greeter data
|
||||||
(default: /var/cache/dms-greeter)
|
(default: /var/cache/dms-greeter)
|
||||||
|
--remember-last-session BOOL
|
||||||
|
Persist selected session to greeter memory
|
||||||
|
(BOOL: true/false, default: from settings.json)
|
||||||
|
--remember-last-user BOOL
|
||||||
|
Persist last successful username to greeter memory
|
||||||
|
(BOOL: true/false, default: from settings.json)
|
||||||
|
--no-save-session Alias for --remember-last-session false
|
||||||
|
--no-save-username Alias for --remember-last-user false
|
||||||
|
--debug Enable verbose startup logging to stderr
|
||||||
-h, --help Show this help message
|
-h, --help Show this help message
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
@@ -30,6 +42,7 @@ Examples:
|
|||||||
dms-greeter --command sway -p /home/user/.config/quickshell/custom-dms
|
dms-greeter --command sway -p /home/user/.config/quickshell/custom-dms
|
||||||
dms-greeter --command scroll -p /home/user/.config/quickshell/custom-dms
|
dms-greeter --command scroll -p /home/user/.config/quickshell/custom-dms
|
||||||
dms-greeter --command niri --cache-dir /tmp/dmsgreeter
|
dms-greeter --command niri --cache-dir /tmp/dmsgreeter
|
||||||
|
dms-greeter --command niri --no-save-session --no-save-username
|
||||||
dms-greeter --command mango
|
dms-greeter --command mango
|
||||||
dms-greeter --command labwc
|
dms-greeter --command labwc
|
||||||
EOF
|
EOF
|
||||||
@@ -43,6 +56,41 @@ require_command() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
normalize_bool_flag() {
|
||||||
|
local flag_name="$1"
|
||||||
|
local value="$2"
|
||||||
|
local normalized="${value,,}"
|
||||||
|
|
||||||
|
case "$normalized" in
|
||||||
|
1|true|yes|on)
|
||||||
|
echo "1"
|
||||||
|
;;
|
||||||
|
0|false|no|off)
|
||||||
|
echo "0"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Error: $flag_name must be true/false (or 1/0, yes/no, on/off)" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
exec_compositor() {
|
||||||
|
local log_tag="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
if [[ "$DEBUG_MODE" == "1" ]]; then
|
||||||
|
exec "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v systemd-cat >/dev/null 2>&1; then
|
||||||
|
exec "$@" > >(systemd-cat -t "dms-greeter/$log_tag" -p info) 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local log_file="$CACHE_DIR/$log_tag.log"
|
||||||
|
exec "$@" >> "$log_file" 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
--command)
|
--command)
|
||||||
@@ -61,6 +109,26 @@ while [[ $# -gt 0 ]]; do
|
|||||||
CACHE_DIR="$2"
|
CACHE_DIR="$2"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
--remember-last-session)
|
||||||
|
REMEMBER_LAST_SESSION="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--remember-last-user)
|
||||||
|
REMEMBER_LAST_USER="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--no-save-session)
|
||||||
|
REMEMBER_LAST_SESSION="0"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--no-save-username)
|
||||||
|
REMEMBER_LAST_USER="0"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--debug)
|
||||||
|
DEBUG_MODE=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
show_help
|
show_help
|
||||||
exit 0
|
exit 0
|
||||||
@@ -113,7 +181,45 @@ export EGL_PLATFORM=gbm
|
|||||||
export DMS_RUN_GREETER=1
|
export DMS_RUN_GREETER=1
|
||||||
export DMS_GREET_CFG_DIR="$CACHE_DIR"
|
export DMS_GREET_CFG_DIR="$CACHE_DIR"
|
||||||
|
|
||||||
|
if [[ -n "$REMEMBER_LAST_SESSION" ]]; then
|
||||||
|
DMS_GREET_REMEMBER_LAST_SESSION=$(normalize_bool_flag "--remember-last-session" "$REMEMBER_LAST_SESSION")
|
||||||
|
export DMS_GREET_REMEMBER_LAST_SESSION
|
||||||
|
if [[ "$DMS_GREET_REMEMBER_LAST_SESSION" == "1" ]]; then
|
||||||
|
DMS_SAVE_SESSION=true
|
||||||
|
else
|
||||||
|
DMS_SAVE_SESSION=false
|
||||||
|
fi
|
||||||
|
export DMS_SAVE_SESSION
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$REMEMBER_LAST_USER" ]]; then
|
||||||
|
DMS_GREET_REMEMBER_LAST_USER=$(normalize_bool_flag "--remember-last-user" "$REMEMBER_LAST_USER")
|
||||||
|
export DMS_GREET_REMEMBER_LAST_USER
|
||||||
|
if [[ "$DMS_GREET_REMEMBER_LAST_USER" == "1" ]]; then
|
||||||
|
DMS_SAVE_USERNAME=true
|
||||||
|
else
|
||||||
|
DMS_SAVE_USERNAME=false
|
||||||
|
fi
|
||||||
|
export DMS_SAVE_USERNAME
|
||||||
|
fi
|
||||||
|
|
||||||
mkdir -p "$CACHE_DIR"
|
mkdir -p "$CACHE_DIR"
|
||||||
|
mkdir -p "$CACHE_DIR/.local/state"
|
||||||
|
mkdir -p "$CACHE_DIR/.local/share"
|
||||||
|
mkdir -p "$CACHE_DIR/.cache"
|
||||||
|
|
||||||
|
export HOME="$CACHE_DIR"
|
||||||
|
export XDG_STATE_HOME="$CACHE_DIR/.local/state"
|
||||||
|
export XDG_DATA_HOME="$CACHE_DIR/.local/share"
|
||||||
|
export XDG_CACHE_HOME="$CACHE_DIR/.cache"
|
||||||
|
|
||||||
|
# Keep greeter VT clean by default; callers can override via env or --debug.
|
||||||
|
if [[ -z "${RUST_LOG:-}" ]]; then
|
||||||
|
export RUST_LOG=warn
|
||||||
|
fi
|
||||||
|
if [[ -z "${NIRI_LOG:-}" ]]; then
|
||||||
|
export NIRI_LOG=warn
|
||||||
|
fi
|
||||||
|
|
||||||
if command -v qs >/dev/null 2>&1; then
|
if command -v qs >/dev/null 2>&1; then
|
||||||
QS_BIN="qs"
|
QS_BIN="qs"
|
||||||
@@ -130,7 +236,9 @@ if [[ "$DMS_PATH" == /* ]]; then
|
|||||||
else
|
else
|
||||||
RESOLVED_PATH=$(locate_dms_config "$DMS_PATH")
|
RESOLVED_PATH=$(locate_dms_config "$DMS_PATH")
|
||||||
if [[ $? -eq 0 && -n "$RESOLVED_PATH" ]]; then
|
if [[ $? -eq 0 && -n "$RESOLVED_PATH" ]]; then
|
||||||
echo "Located DMS config at: $RESOLVED_PATH" >&2
|
if [[ "$DEBUG_MODE" == "1" ]]; then
|
||||||
|
echo "Located DMS config at: $RESOLVED_PATH" >&2
|
||||||
|
fi
|
||||||
QS_CMD="$QS_BIN -p $RESOLVED_PATH"
|
QS_CMD="$QS_BIN -p $RESOLVED_PATH"
|
||||||
else
|
else
|
||||||
echo "Error: Could not find DMS config '$DMS_PATH' (shell.qml) in any valid config path" >&2
|
echo "Error: Could not find DMS config '$DMS_PATH' (shell.qml) in any valid config path" >&2
|
||||||
@@ -192,7 +300,7 @@ NIRI_EOF
|
|||||||
spawn-at-startup "sh" "-c" "$QS_CMD; niri msg action quit --skip-confirmation"
|
spawn-at-startup "sh" "-c" "$QS_CMD; niri msg action quit --skip-confirmation"
|
||||||
NIRI_EOF
|
NIRI_EOF
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
exec niri -c "$COMPOSITOR_CONFIG"
|
exec_compositor "niri" niri -c "$COMPOSITOR_CONFIG"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
hyprland)
|
hyprland)
|
||||||
@@ -222,9 +330,9 @@ HYPRLAND_EOF
|
|||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
fi
|
fi
|
||||||
if command -v start-hyprland >/dev/null 2>&1; then
|
if command -v start-hyprland >/dev/null 2>&1; then
|
||||||
exec start-hyprland -- --config "$COMPOSITOR_CONFIG"
|
exec_compositor "hyprland" start-hyprland -- --config "$COMPOSITOR_CONFIG"
|
||||||
else
|
else
|
||||||
exec Hyprland -c "$COMPOSITOR_CONFIG"
|
exec_compositor "hyprland" Hyprland -c "$COMPOSITOR_CONFIG"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
@@ -245,7 +353,7 @@ exec "$QS_CMD; swaymsg exit"
|
|||||||
SWAY_EOF
|
SWAY_EOF
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
fi
|
fi
|
||||||
exec sway --unsupported-gpu -c "$COMPOSITOR_CONFIG"
|
exec_compositor "sway" sway --unsupported-gpu -c "$COMPOSITOR_CONFIG"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
scroll)
|
scroll)
|
||||||
@@ -265,7 +373,7 @@ exec "$QS_CMD; scrollmsg exit"
|
|||||||
SCROLL_EOF
|
SCROLL_EOF
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
fi
|
fi
|
||||||
exec scroll -c "$COMPOSITOR_CONFIG"
|
exec_compositor "scroll" scroll -c "$COMPOSITOR_CONFIG"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
miracle|miracle-wm)
|
miracle|miracle-wm)
|
||||||
@@ -285,24 +393,24 @@ exec "$QS_CMD; miraclemsg exit"
|
|||||||
MIRACLE_EOF
|
MIRACLE_EOF
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
fi
|
fi
|
||||||
exec miracle-wm -c "$COMPOSITOR_CONFIG"
|
exec_compositor "miracle" miracle-wm -c "$COMPOSITOR_CONFIG"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
labwc)
|
labwc)
|
||||||
require_command "labwc"
|
require_command "labwc"
|
||||||
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
|
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
|
||||||
exec labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD"
|
exec_compositor "labwc" labwc --config "$COMPOSITOR_CONFIG" --session "$QS_CMD"
|
||||||
else
|
else
|
||||||
exec labwc --session "$QS_CMD"
|
exec_compositor "labwc" labwc --session "$QS_CMD"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
mango|mangowc)
|
mango|mangowc)
|
||||||
require_command "mango"
|
require_command "mango"
|
||||||
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
|
if [[ -n "$COMPOSITOR_CONFIG" ]]; then
|
||||||
exec mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg -d quit"
|
exec_compositor "mango" mango -c "$COMPOSITOR_CONFIG" -s "$QS_CMD && mmsg -d quit"
|
||||||
else
|
else
|
||||||
exec mango -s "$QS_CMD && mmsg -d quit"
|
exec_compositor "mango" mango -s "$QS_CMD && mmsg -d quit"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
|||||||
@@ -755,7 +755,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
if (!demoMode && !pam.passwd.active && !pam.u2fPending) {
|
if (!demoMode && !root.unlocking && !pam.passwd.active && !pam.u2fPending) {
|
||||||
pam.passwd.start();
|
pam.passwd.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -764,6 +764,11 @@ Item {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (root.unlocking) {
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
if (event.key === Qt.Key_Escape) {
|
||||||
if (pam.u2fPending) {
|
if (pam.u2fPending) {
|
||||||
pam.cancelU2fPending();
|
pam.cancelU2fPending();
|
||||||
@@ -1017,7 +1022,7 @@ Item {
|
|||||||
visible: (demoMode || (!pam.passwd.active && !root.unlocking && !pam.u2fPending))
|
visible: (demoMode || (!pam.passwd.active && !root.unlocking && !pam.u2fPending))
|
||||||
enabled: !demoMode
|
enabled: !demoMode
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!demoMode && !pam.u2fPending) {
|
if (!demoMode && !root.unlocking && !pam.u2fPending) {
|
||||||
pam.passwd.start();
|
pam.passwd.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1626,6 +1631,7 @@ Item {
|
|||||||
onStateChanged: {
|
onStateChanged: {
|
||||||
root.pamState = state;
|
root.pamState = state;
|
||||||
if (state !== "") {
|
if (state !== "") {
|
||||||
|
root.unlocking = false;
|
||||||
placeholderDelay.restart();
|
placeholderDelay.restart();
|
||||||
passwordField.text = "";
|
passwordField.text = "";
|
||||||
root.passwordBuffer = "";
|
root.passwordBuffer = "";
|
||||||
@@ -1641,6 +1647,15 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: pam
|
||||||
|
|
||||||
|
function onUnlockInProgressChanged() {
|
||||||
|
if (!pam.unlockInProgress && root.unlocking)
|
||||||
|
root.unlocking = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Binding {
|
Binding {
|
||||||
target: pam
|
target: pam
|
||||||
property: "buffer"
|
property: "buffer"
|
||||||
|
|||||||
@@ -25,6 +25,29 @@ Scope {
|
|||||||
signal flashMsg
|
signal flashMsg
|
||||||
signal unlockRequested
|
signal unlockRequested
|
||||||
|
|
||||||
|
function resetAuthFlows(): void {
|
||||||
|
passwd.abort();
|
||||||
|
fprint.abort();
|
||||||
|
u2f.abort();
|
||||||
|
errorRetry.running = false;
|
||||||
|
u2fErrorRetry.running = false;
|
||||||
|
u2fPendingTimeout.running = false;
|
||||||
|
passwdActiveTimeout.running = false;
|
||||||
|
unlockRequestTimeout.running = false;
|
||||||
|
u2fPending = false;
|
||||||
|
u2fState = "";
|
||||||
|
unlockInProgress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function recoverFromAuthStall(newState: string): void {
|
||||||
|
resetAuthFlows();
|
||||||
|
state = newState;
|
||||||
|
flashMsg();
|
||||||
|
stateReset.restart();
|
||||||
|
fprint.checkAvail();
|
||||||
|
u2f.checkAvail();
|
||||||
|
}
|
||||||
|
|
||||||
function completeUnlock(): void {
|
function completeUnlock(): void {
|
||||||
if (!unlockInProgress) {
|
if (!unlockInProgress) {
|
||||||
unlockInProgress = true;
|
unlockInProgress = true;
|
||||||
@@ -36,6 +59,7 @@ Scope {
|
|||||||
u2fPendingTimeout.running = false;
|
u2fPendingTimeout.running = false;
|
||||||
u2fPending = false;
|
u2fPending = false;
|
||||||
u2fState = "";
|
u2fState = "";
|
||||||
|
unlockRequestTimeout.restart();
|
||||||
unlockRequested();
|
unlockRequested();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,6 +126,13 @@ Scope {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unlockRequestTimeout.running = false;
|
||||||
|
root.unlockInProgress = false;
|
||||||
|
root.u2fPending = false;
|
||||||
|
root.u2fState = "";
|
||||||
|
u2fPendingTimeout.running = false;
|
||||||
|
u2f.abort();
|
||||||
|
|
||||||
if (res === PamResult.Error)
|
if (res === PamResult.Error)
|
||||||
root.state = "error";
|
root.state = "error";
|
||||||
else if (res === PamResult.MaxTries)
|
else if (res === PamResult.MaxTries)
|
||||||
@@ -114,6 +145,18 @@ Scope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: passwd
|
||||||
|
|
||||||
|
function onActiveChanged() {
|
||||||
|
if (passwd.active) {
|
||||||
|
passwdActiveTimeout.restart();
|
||||||
|
} else {
|
||||||
|
passwdActiveTimeout.running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PamContext {
|
PamContext {
|
||||||
id: fprint
|
id: fprint
|
||||||
|
|
||||||
@@ -241,7 +284,7 @@ Scope {
|
|||||||
Process {
|
Process {
|
||||||
id: availProc
|
id: availProc
|
||||||
|
|
||||||
command: ["sh", "-c", "fprintd-list $USER"]
|
command: ["sh", "-c", "fprintd-list \"${USER:-$(id -un)}\""]
|
||||||
onExited: code => {
|
onExited: code => {
|
||||||
fprint.available = code === 0;
|
fprint.available = code === 0;
|
||||||
fprint.checkAvail();
|
fprint.checkAvail();
|
||||||
@@ -279,6 +322,26 @@ Scope {
|
|||||||
onTriggered: root.cancelU2fPending()
|
onTriggered: root.cancelU2fPending()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: passwdActiveTimeout
|
||||||
|
|
||||||
|
interval: 15000
|
||||||
|
onTriggered: {
|
||||||
|
if (passwd.active)
|
||||||
|
root.recoverFromAuthStall("error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: unlockRequestTimeout
|
||||||
|
|
||||||
|
interval: 8000
|
||||||
|
onTriggered: {
|
||||||
|
if (root.unlockInProgress)
|
||||||
|
root.recoverFromAuthStall("error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: stateReset
|
id: stateReset
|
||||||
|
|
||||||
@@ -308,17 +371,9 @@ Scope {
|
|||||||
root.u2fState = "";
|
root.u2fState = "";
|
||||||
root.u2fPending = false;
|
root.u2fPending = false;
|
||||||
root.lockMessage = "";
|
root.lockMessage = "";
|
||||||
root.unlockInProgress = false;
|
root.resetAuthFlows();
|
||||||
} else {
|
} else {
|
||||||
fprint.abort();
|
root.resetAuthFlows();
|
||||||
passwd.abort();
|
|
||||||
u2f.abort();
|
|
||||||
errorRetry.running = false;
|
|
||||||
u2fErrorRetry.running = false;
|
|
||||||
u2fPendingTimeout.running = false;
|
|
||||||
root.u2fPending = false;
|
|
||||||
root.u2fState = "";
|
|
||||||
root.unlockInProgress = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,6 +393,7 @@ Scope {
|
|||||||
u2f.abort();
|
u2f.abort();
|
||||||
u2fErrorRetry.running = false;
|
u2fErrorRetry.running = false;
|
||||||
u2fPendingTimeout.running = false;
|
u2fPendingTimeout.running = false;
|
||||||
|
unlockRequestTimeout.running = false;
|
||||||
root.u2fPending = false;
|
root.u2fPending = false;
|
||||||
root.u2fState = "";
|
root.u2fState = "";
|
||||||
u2f.checkAvail();
|
u2f.checkAvail();
|
||||||
|
|||||||
@@ -39,9 +39,11 @@ DankPopout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
popupWidth: 400
|
popupWidth: triggerScreen ? Math.min(500, Math.max(380, triggerScreen.width - 48)) : 400
|
||||||
popupHeight: stablePopupHeight
|
popupHeight: stablePopupHeight
|
||||||
positioning: ""
|
positioning: ""
|
||||||
|
animationScaleCollapsed: 0.94
|
||||||
|
animationOffset: 0
|
||||||
suspendShadowWhileResizing: false
|
suspendShadowWhileResizing: false
|
||||||
|
|
||||||
screen: triggerScreen
|
screen: triggerScreen
|
||||||
|
|||||||
@@ -24,29 +24,6 @@ PanelWindow {
|
|||||||
property real _lastReportedAlignedHeight: -1
|
property real _lastReportedAlignedHeight: -1
|
||||||
property real _storedTopMargin: 0
|
property real _storedTopMargin: 0
|
||||||
property real _storedBottomMargin: 0
|
property real _storedBottomMargin: 0
|
||||||
readonly property bool directionalEffect: Theme.isDirectionalEffect
|
|
||||||
readonly property bool depthEffect: Theme.isDepthEffect
|
|
||||||
readonly property real entryTravel: {
|
|
||||||
const base = Math.abs(Theme.effectAnimOffset);
|
|
||||||
if (directionalEffect) {
|
|
||||||
if (isCenterPosition)
|
|
||||||
return Math.max(base, Math.round(content.height * 1.1));
|
|
||||||
return Math.max(base, Math.round(content.width * 0.95));
|
|
||||||
}
|
|
||||||
if (depthEffect)
|
|
||||||
return Math.max(base, 44);
|
|
||||||
return base;
|
|
||||||
}
|
|
||||||
readonly property real exitTravel: {
|
|
||||||
if (directionalEffect) {
|
|
||||||
if (isCenterPosition)
|
|
||||||
return content.height + entryTravel;
|
|
||||||
return content.width + entryTravel;
|
|
||||||
}
|
|
||||||
if (depthEffect)
|
|
||||||
return Math.round(entryTravel * 1.35);
|
|
||||||
return Anims.slidePx;
|
|
||||||
}
|
|
||||||
readonly property string clearText: I18n.tr("Dismiss")
|
readonly property string clearText: I18n.tr("Dismiss")
|
||||||
property bool descriptionExpanded: false
|
property bool descriptionExpanded: false
|
||||||
readonly property bool hasExpandableBody: (notificationData?.htmlBody || "").replace(/<[^>]*>/g, "").trim().length > 0
|
readonly property bool hasExpandableBody: (notificationData?.htmlBody || "").replace(/<[^>]*>/g, "").trim().length > 0
|
||||||
@@ -160,9 +137,9 @@ PanelWindow {
|
|||||||
enabled: !exiting && !_isDestroying
|
enabled: !exiting && !_isDestroying
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
id: implicitHeightAnim
|
id: implicitHeightAnim
|
||||||
duration: Theme.variantDuration(descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration, descriptionExpanded)
|
duration: descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: descriptionExpanded ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
easing.bezierCurve: Theme.expressiveCurves.emphasized
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -934,9 +911,9 @@ PanelWindow {
|
|||||||
if (isCenterPosition)
|
if (isCenterPosition)
|
||||||
return 0;
|
return 0;
|
||||||
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
|
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
|
||||||
return isLeft ? -entryTravel : entryTravel;
|
return isLeft ? -Anims.slidePx : Anims.slidePx;
|
||||||
}
|
}
|
||||||
y: isTopCenter ? -entryTravel : isBottomCenter ? entryTravel : 0
|
y: isTopCenter ? -Anims.slidePx : isBottomCenter ? Anims.slidePx : 0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -948,16 +925,16 @@ PanelWindow {
|
|||||||
property: isCenterPosition ? "y" : "x"
|
property: isCenterPosition ? "y" : "x"
|
||||||
from: {
|
from: {
|
||||||
if (isTopCenter)
|
if (isTopCenter)
|
||||||
return -entryTravel;
|
return -Anims.slidePx;
|
||||||
if (isBottomCenter)
|
if (isBottomCenter)
|
||||||
return entryTravel;
|
return Anims.slidePx;
|
||||||
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
|
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
|
||||||
return isLeft ? -entryTravel : entryTravel;
|
return isLeft ? -Anims.slidePx : Anims.slidePx;
|
||||||
}
|
}
|
||||||
to: 0
|
to: 0
|
||||||
duration: Theme.variantDuration(Theme.notificationEnterDuration, true)
|
duration: Theme.notificationEnterDuration
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: Theme.variantPopoutEnterCurve
|
easing.bezierCurve: isCenterPosition ? Theme.expressiveCurves.standardDecel : Theme.expressiveCurves.emphasizedDecel
|
||||||
onStopped: {
|
onStopped: {
|
||||||
if (!win.exiting && !win._isDestroying) {
|
if (!win.exiting && !win._isDestroying) {
|
||||||
if (isCenterPosition) {
|
if (isCenterPosition) {
|
||||||
@@ -982,35 +959,35 @@ PanelWindow {
|
|||||||
from: 0
|
from: 0
|
||||||
to: {
|
to: {
|
||||||
if (isTopCenter)
|
if (isTopCenter)
|
||||||
return -exitTravel;
|
return -Anims.slidePx;
|
||||||
if (isBottomCenter)
|
if (isBottomCenter)
|
||||||
return exitTravel;
|
return Anims.slidePx;
|
||||||
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
|
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
|
||||||
return isLeft ? -exitTravel : exitTravel;
|
return isLeft ? -Anims.slidePx : Anims.slidePx;
|
||||||
}
|
}
|
||||||
duration: Theme.variantDuration(Theme.notificationExitDuration, false)
|
duration: Theme.notificationExitDuration
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: Theme.variantPopoutExitCurve
|
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
|
||||||
}
|
}
|
||||||
|
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
target: content
|
target: content
|
||||||
property: "opacity"
|
property: "opacity"
|
||||||
from: 1
|
from: 1
|
||||||
to: Theme.isDirectionalEffect ? 1 : 0
|
to: 0
|
||||||
duration: Theme.variantDuration(Theme.notificationExitDuration, false)
|
duration: Theme.notificationExitDuration
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: Theme.variantPopoutExitCurve
|
easing.bezierCurve: Theme.expressiveCurves.standardAccel
|
||||||
}
|
}
|
||||||
|
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
target: content
|
target: content
|
||||||
property: "scale"
|
property: "scale"
|
||||||
from: 1
|
from: 1
|
||||||
to: Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed
|
to: 0.98
|
||||||
duration: Theme.variantDuration(Theme.notificationExitDuration, false)
|
duration: Theme.notificationExitDuration
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: Theme.variantPopoutExitCurve
|
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
587
quickshell/Modules/Settings/GreeterTab.qml
Normal file
587
quickshell/Modules/Settings/GreeterTab.qml
Normal file
@@ -0,0 +1,587 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
import qs.Modals.FileBrowser
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.Settings.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
FileBrowserModal {
|
||||||
|
id: greeterWallpaperBrowserModal
|
||||||
|
browserTitle: I18n.tr("Select greeter background image")
|
||||||
|
browserIcon: "wallpaper"
|
||||||
|
browserType: "wallpaper"
|
||||||
|
showHiddenFiles: true
|
||||||
|
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp", "*.jxl", "*.avif", "*.heif"]
|
||||||
|
onFileSelected: path => {
|
||||||
|
SettingsData.set("greeterWallpaperPath", path);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property string greeterStatusText: ""
|
||||||
|
property bool greeterStatusRunning: false
|
||||||
|
property bool greeterSyncRunning: false
|
||||||
|
property string greeterStatusStdout: ""
|
||||||
|
property string greeterStatusStderr: ""
|
||||||
|
property string greeterSyncStdout: ""
|
||||||
|
property string greeterSyncStderr: ""
|
||||||
|
property string greeterSudoProbeStderr: ""
|
||||||
|
property string greeterTerminalFallbackStderr: ""
|
||||||
|
property bool greeterTerminalFallbackFromPrecheck: false
|
||||||
|
property var cachedFontFamilies: []
|
||||||
|
property bool fontsEnumerated: false
|
||||||
|
|
||||||
|
function runGreeterStatus() {
|
||||||
|
greeterStatusText = "";
|
||||||
|
greeterStatusStdout = "";
|
||||||
|
greeterStatusStderr = "";
|
||||||
|
greeterStatusRunning = true;
|
||||||
|
greeterStatusProcess.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runGreeterSync() {
|
||||||
|
greeterSyncStdout = "";
|
||||||
|
greeterSyncStderr = "";
|
||||||
|
greeterSudoProbeStderr = "";
|
||||||
|
greeterTerminalFallbackStderr = "";
|
||||||
|
greeterTerminalFallbackFromPrecheck = false;
|
||||||
|
greeterStatusText = I18n.tr("Checking whether sudo authentication is needed…");
|
||||||
|
greeterSyncRunning = true;
|
||||||
|
greeterSudoProbeProcess.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function launchGreeterSyncTerminalFallback(fromPrecheck, statusText) {
|
||||||
|
greeterTerminalFallbackFromPrecheck = fromPrecheck;
|
||||||
|
if (statusText && statusText !== "")
|
||||||
|
greeterStatusText = statusText;
|
||||||
|
greeterTerminalFallbackStderr = "";
|
||||||
|
greeterTerminalFallbackProcess.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function enumerateFonts() {
|
||||||
|
if (fontsEnumerated)
|
||||||
|
return;
|
||||||
|
var fonts = [];
|
||||||
|
var availableFonts = Qt.fontFamilies();
|
||||||
|
for (var i = 0; i < availableFonts.length; i++) {
|
||||||
|
var fontName = availableFonts[i];
|
||||||
|
if (fontName.startsWith("."))
|
||||||
|
continue;
|
||||||
|
fonts.push(fontName);
|
||||||
|
}
|
||||||
|
fonts.sort();
|
||||||
|
fonts.unshift("Default");
|
||||||
|
cachedFontFamilies = fonts;
|
||||||
|
fontsEnumerated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: Qt.callLater(enumerateFonts)
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: greeterStatusProcess
|
||||||
|
command: ["dms", "greeter", "status"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
root.greeterStatusStdout = text || "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: root.greeterStatusStderr = text || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
root.greeterStatusRunning = false;
|
||||||
|
const out = (root.greeterStatusStdout || "").trim();
|
||||||
|
const err = (root.greeterStatusStderr || "").trim();
|
||||||
|
if (exitCode === 0) {
|
||||||
|
root.greeterStatusText = out !== "" ? out : I18n.tr("No status output.");
|
||||||
|
if (err !== "")
|
||||||
|
root.greeterStatusText = root.greeterStatusText + "\n\nstderr:\n" + err;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var failure = I18n.tr("Failed to run 'dms greeter status'. Ensure DMS is installed and dms is in PATH.", "greeter status error") + " (exit " + exitCode + ")";
|
||||||
|
if (out !== "")
|
||||||
|
failure = failure + "\n\n" + out;
|
||||||
|
if (err !== "")
|
||||||
|
failure = failure + "\n\nstderr:\n" + err;
|
||||||
|
root.greeterStatusText = failure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: greeterSyncProcess
|
||||||
|
command: ["dms", "greeter", "sync", "--yes"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: root.greeterSyncStdout = text || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: root.greeterSyncStderr = text || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
root.greeterSyncRunning = false;
|
||||||
|
const out = (root.greeterSyncStdout || "").trim();
|
||||||
|
const err = (root.greeterSyncStderr || "").trim();
|
||||||
|
if (exitCode === 0) {
|
||||||
|
var success = I18n.tr("Sync completed successfully.");
|
||||||
|
if (out !== "")
|
||||||
|
success = success + "\n\n" + out;
|
||||||
|
if (err !== "")
|
||||||
|
success = success + "\n\nstderr:\n" + err;
|
||||||
|
root.greeterStatusText = success;
|
||||||
|
} else {
|
||||||
|
var failure = I18n.tr("Sync failed in background mode. Trying terminal mode so you can authenticate interactively.") + " (exit " + exitCode + ")";
|
||||||
|
if (out !== "")
|
||||||
|
failure = failure + "\n\n" + out;
|
||||||
|
if (err !== "")
|
||||||
|
failure = failure + "\n\nstderr:\n" + err;
|
||||||
|
root.greeterStatusText = failure;
|
||||||
|
root.launchGreeterSyncTerminalFallback(false, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: greeterSudoProbeProcess
|
||||||
|
command: ["sudo", "-n", "true"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: root.greeterSudoProbeStderr = text || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
const err = (root.greeterSudoProbeStderr || "").trim();
|
||||||
|
if (exitCode === 0) {
|
||||||
|
root.greeterStatusText = I18n.tr("Running greeter sync…");
|
||||||
|
greeterSyncProcess.running = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var authNeeded = I18n.tr("Sync needs sudo authentication. Opening terminal so you can use password or fingerprint.");
|
||||||
|
if (err !== "")
|
||||||
|
authNeeded = authNeeded + "\n\n" + err;
|
||||||
|
root.launchGreeterSyncTerminalFallback(true, authNeeded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: greeterTerminalFallbackProcess
|
||||||
|
command: ["dms", "greeter", "sync", "--terminal", "--yes"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stderr: StdioCollector {
|
||||||
|
onStreamFinished: root.greeterTerminalFallbackStderr = text || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
root.greeterSyncRunning = false;
|
||||||
|
if (exitCode === 0) {
|
||||||
|
var launched = root.greeterTerminalFallbackFromPrecheck ? I18n.tr("Terminal opened. Complete sync authentication there; it will close automatically when done.") : I18n.tr("Terminal fallback opened. Complete sync there; it will close automatically when done.");
|
||||||
|
root.greeterStatusText = root.greeterStatusText ? root.greeterStatusText + "\n\n" + launched : launched;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var fallback = I18n.tr("Terminal fallback failed. Install one of the supported terminal emulators or run 'dms greeter sync' manually.") + " (exit " + exitCode + ")";
|
||||||
|
const err = (root.greeterTerminalFallbackStderr || "").trim();
|
||||||
|
if (err !== "")
|
||||||
|
fallback = fallback + "\n\nstderr:\n" + err;
|
||||||
|
root.greeterStatusText = root.greeterStatusText ? root.greeterStatusText + "\n\n" + fallback : fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var _lockDateFormatPresets: [
|
||||||
|
{
|
||||||
|
format: "",
|
||||||
|
label: I18n.tr("System Default", "date format option")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "ddd d",
|
||||||
|
label: I18n.tr("Day Date", "date format option")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "ddd MMM d",
|
||||||
|
label: I18n.tr("Day Month Date", "date format option")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "MMM d",
|
||||||
|
label: I18n.tr("Month Date", "date format option")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "M/d",
|
||||||
|
label: I18n.tr("Numeric (M/D)", "date format option")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "d/M",
|
||||||
|
label: I18n.tr("Numeric (D/M)", "date format option")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "ddd d MMM yyyy",
|
||||||
|
label: I18n.tr("Full with Year", "date format option")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "yyyy-MM-dd",
|
||||||
|
label: I18n.tr("ISO Date", "date format option")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: "dddd, MMMM d",
|
||||||
|
label: I18n.tr("Full Day & Month", "date format option")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
readonly property var _wallpaperFillModes: ["Stretch", "Fit", "Fill", "Tile", "TileVertically", "TileHorizontally", "Pad"]
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "info"
|
||||||
|
title: I18n.tr("Greeter Status")
|
||||||
|
settingKey: "greeterStatus"
|
||||||
|
|
||||||
|
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.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 1
|
||||||
|
height: Theme.spacingS
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: Math.min(180, statusTextArea.implicitHeight + Theme.spacingM * 2)
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.surfaceContainerHighest
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: statusTextArea
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
text: root.greeterStatusRunning ? I18n.tr("Checking…", "greeter status loading") : (root.greeterStatusText || I18n.tr("Click Refresh to check status.", "greeter status placeholder"))
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.family: "monospace"
|
||||||
|
color: root.greeterStatusRunning ? Theme.surfaceVariantText : Theme.surfaceText
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
verticalAlignment: Text.AlignTop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
topPadding: Theme.spacingM
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
text: I18n.tr("Refresh")
|
||||||
|
iconName: "refresh"
|
||||||
|
onClicked: root.runGreeterStatus()
|
||||||
|
enabled: !root.greeterStatusRunning
|
||||||
|
}
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
text: I18n.tr("Sync")
|
||||||
|
iconName: "sync"
|
||||||
|
onClicked: root.runGreeterSync()
|
||||||
|
enabled: !root.greeterSyncRunning
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "fingerprint"
|
||||||
|
title: I18n.tr("Login Authentication")
|
||||||
|
settingKey: "greeterAuth"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Enable fingerprint or security key for DMS Greeter. Run Sync to apply and configure PAM.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "greeterEnableFprint"
|
||||||
|
tags: ["greeter", "fingerprint", "fprintd", "login", "auth"]
|
||||||
|
text: I18n.tr("Enable fingerprint at login")
|
||||||
|
description: {
|
||||||
|
if (!SettingsData.fprintdAvailable)
|
||||||
|
return I18n.tr("Not available — install fprintd and enroll fingerprints.");
|
||||||
|
return SettingsData.greeterEnableFprint ? I18n.tr("Run Sync to apply. Fingerprint-only login may not unlock GNOME Keyring.") : I18n.tr("Only off for DMS-managed PAM lines. If greetd includes system-auth/common-auth/password-auth with pam_fprintd, fingerprint still stays enabled.");
|
||||||
|
}
|
||||||
|
descriptionColor: SettingsData.fprintdAvailable ? Theme.surfaceVariantText : Theme.warning
|
||||||
|
checked: SettingsData.greeterEnableFprint
|
||||||
|
enabled: SettingsData.fprintdAvailable
|
||||||
|
onToggled: checked => SettingsData.set("greeterEnableFprint", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "greeterEnableU2f"
|
||||||
|
tags: ["greeter", "u2f", "security", "key", "login", "auth"]
|
||||||
|
text: I18n.tr("Enable security key at login")
|
||||||
|
description: {
|
||||||
|
if (!SettingsData.u2fAvailable)
|
||||||
|
return I18n.tr("Not available — install pam_u2f and enroll keys.");
|
||||||
|
return SettingsData.greeterEnableU2f ? I18n.tr("Run Sync to apply.") : I18n.tr("Disabled.");
|
||||||
|
}
|
||||||
|
descriptionColor: SettingsData.u2fAvailable ? Theme.surfaceVariantText : Theme.warning
|
||||||
|
checked: SettingsData.greeterEnableU2f
|
||||||
|
enabled: SettingsData.u2fAvailable
|
||||||
|
onToggled: checked => SettingsData.set("greeterEnableU2f", checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "palette"
|
||||||
|
title: I18n.tr("Greeter Appearance")
|
||||||
|
settingKey: "greeterAppearance"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Font")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
topPadding: Theme.spacingM
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsDropdownRow {
|
||||||
|
settingKey: "greeterFontFamily"
|
||||||
|
tags: ["greeter", "font", "typography"]
|
||||||
|
text: I18n.tr("Greeter font")
|
||||||
|
description: I18n.tr("Font used on the login screen")
|
||||||
|
options: root.fontsEnumerated ? root.cachedFontFamilies : ["Default"]
|
||||||
|
currentValue: (!SettingsData.greeterFontFamily || SettingsData.greeterFontFamily === "" || SettingsData.greeterFontFamily === Theme.defaultFontFamily) ? "Default" : (SettingsData.greeterFontFamily || "Default")
|
||||||
|
enableFuzzySearch: true
|
||||||
|
popupWidthOffset: 100
|
||||||
|
maxPopupHeight: 400
|
||||||
|
onValueChanged: value => {
|
||||||
|
if (value === "Default")
|
||||||
|
SettingsData.set("greeterFontFamily", "");
|
||||||
|
else
|
||||||
|
SettingsData.set("greeterFontFamily", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Time format")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
topPadding: Theme.spacingM
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "greeterUse24Hour"
|
||||||
|
tags: ["greeter", "time", "24hour"]
|
||||||
|
text: I18n.tr("24-hour clock")
|
||||||
|
description: I18n.tr("Greeter only — does not affect main clock")
|
||||||
|
checked: SettingsData.greeterUse24HourClock
|
||||||
|
onToggled: checked => SettingsData.set("greeterUse24HourClock", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "greeterShowSeconds"
|
||||||
|
tags: ["greeter", "time", "seconds"]
|
||||||
|
text: I18n.tr("Show seconds")
|
||||||
|
checked: SettingsData.greeterShowSeconds
|
||||||
|
onToggled: checked => SettingsData.set("greeterShowSeconds", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "greeterPadHours"
|
||||||
|
tags: ["greeter", "time", "12hour"]
|
||||||
|
text: I18n.tr("Pad hours (02:00 vs 2:00)")
|
||||||
|
visible: !SettingsData.greeterUse24HourClock
|
||||||
|
checked: SettingsData.greeterPadHours12Hour
|
||||||
|
onToggled: checked => SettingsData.set("greeterPadHours12Hour", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Date format on greeter")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
topPadding: Theme.spacingM
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsDropdownRow {
|
||||||
|
settingKey: "greeterLockDateFormat"
|
||||||
|
tags: ["greeter", "date", "format"]
|
||||||
|
text: I18n.tr("Date format")
|
||||||
|
description: I18n.tr("Greeter only — format for the date on the login screen")
|
||||||
|
options: root._lockDateFormatPresets.map(p => p.label)
|
||||||
|
currentValue: {
|
||||||
|
var current = (SettingsData.greeterLockDateFormat !== undefined && SettingsData.greeterLockDateFormat !== "") ? SettingsData.greeterLockDateFormat : SettingsData.lockDateFormat || "";
|
||||||
|
var match = root._lockDateFormatPresets.find(p => p.format === current);
|
||||||
|
return match ? match.label : (current ? I18n.tr("Custom: ") + current : root._lockDateFormatPresets[0].label);
|
||||||
|
}
|
||||||
|
onValueChanged: value => {
|
||||||
|
var preset = root._lockDateFormatPresets.find(p => p.label === value);
|
||||||
|
SettingsData.set("greeterLockDateFormat", preset ? preset.format : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Background")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
topPadding: Theme.spacingM
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Use a custom image for the login screen, or leave empty to use your desktop wallpaper.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: greeterWallpaperPathField
|
||||||
|
width: parent.width - browseGreeterWallpaperButton.width - Theme.spacingS
|
||||||
|
placeholderText: I18n.tr("Use desktop wallpaper")
|
||||||
|
text: SettingsData.greeterWallpaperPath
|
||||||
|
backgroundColor: Theme.surfaceContainerHighest
|
||||||
|
onTextChanged: {
|
||||||
|
if (text !== SettingsData.greeterWallpaperPath)
|
||||||
|
SettingsData.set("greeterWallpaperPath", text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
id: browseGreeterWallpaperButton
|
||||||
|
text: I18n.tr("Browse")
|
||||||
|
onClicked: greeterWallpaperBrowserModal.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsDropdownRow {
|
||||||
|
settingKey: "greeterWallpaperFillMode"
|
||||||
|
tags: ["greeter", "wallpaper", "background", "fill"]
|
||||||
|
text: I18n.tr("Wallpaper fill mode")
|
||||||
|
description: I18n.tr("How the background image is scaled")
|
||||||
|
options: root._wallpaperFillModes.map(m => I18n.tr(m, "wallpaper fill mode"))
|
||||||
|
currentValue: {
|
||||||
|
var mode = (SettingsData.greeterWallpaperFillMode && SettingsData.greeterWallpaperFillMode !== "") ? SettingsData.greeterWallpaperFillMode : (SettingsData.wallpaperFillMode || "Fill");
|
||||||
|
var idx = root._wallpaperFillModes.indexOf(mode);
|
||||||
|
return idx >= 0 ? I18n.tr(root._wallpaperFillModes[idx], "wallpaper fill mode") : I18n.tr("Fill", "wallpaper fill mode");
|
||||||
|
}
|
||||||
|
onValueChanged: value => {
|
||||||
|
var idx = root._wallpaperFillModes.map(m => I18n.tr(m, "wallpaper fill mode")).indexOf(value);
|
||||||
|
if (idx >= 0)
|
||||||
|
SettingsData.set("greeterWallpaperFillMode", root._wallpaperFillModes[idx]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Layout and module positions on the greeter are synced from your shell (e.g. bar config). Run Sync to apply.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
topPadding: Theme.spacingS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "history"
|
||||||
|
title: I18n.tr("Greeter Behavior")
|
||||||
|
settingKey: "greeterBehavior"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Convenience options for the login screen. Sync to apply.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "greeterRememberLastSession"
|
||||||
|
tags: ["greeter", "session", "remember", "login"]
|
||||||
|
text: I18n.tr("Remember last session")
|
||||||
|
description: I18n.tr("Pre-select the last used session on the greeter")
|
||||||
|
checked: SettingsData.greeterRememberLastSession
|
||||||
|
onToggled: checked => SettingsData.set("greeterRememberLastSession", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "greeterRememberLastUser"
|
||||||
|
tags: ["greeter", "user", "remember", "login", "username"]
|
||||||
|
text: I18n.tr("Remember last user")
|
||||||
|
description: I18n.tr("Pre-fill the last successful username on the greeter")
|
||||||
|
checked: SettingsData.greeterRememberLastUser
|
||||||
|
onToggled: checked => SettingsData.set("greeterRememberLastUser", checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "extension"
|
||||||
|
title: I18n.tr("Dependencies & documentation")
|
||||||
|
settingKey: "greeterDeps"
|
||||||
|
|
||||||
|
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.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Installation and PAM setup: see the ") + "<a href=\"https://danklinux.com/docs/dankgreeter/installation\" style=\"text-decoration:none; color:" + Theme.primary + ";\">DankGreeter docs</a> " + I18n.tr("or run ") + "'dms greeter install'."
|
||||||
|
textFormat: Text.RichText
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
linkColor: Theme.primary
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
onLinkActivated: url => Qt.openUrlExternally(url)
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
acceptedButtons: Qt.NoButton
|
||||||
|
propagateComposedEvents: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -245,7 +245,7 @@ Item {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("Path to a video file or folder containing videos")
|
text: I18n.tr("Path to a video file or folder containing videos")
|
||||||
font.pixelSize: Theme.fontSizeXSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.outlineVariant
|
color: Theme.outlineVariant
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|||||||
@@ -1935,11 +1935,6 @@ Item {
|
|||||||
label: I18n.tr("Auth Type"),
|
label: I18n.tr("Auth Type"),
|
||||||
value: data["connection-type"]
|
value: data["connection-type"]
|
||||||
});
|
});
|
||||||
fields.push({
|
|
||||||
label: I18n.tr("Autoconnect"),
|
|
||||||
value: configData.autoconnect ? I18n.tr("Yes") : I18n.tr("No")
|
|
||||||
});
|
|
||||||
|
|
||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1978,6 +1973,16 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DankToggle {
|
||||||
|
width: parent.width
|
||||||
|
text: I18n.tr("Autoconnect")
|
||||||
|
checked: configData ? (configData.autoconnect || false) : false
|
||||||
|
visible: !VPNService.configLoading && configData !== null
|
||||||
|
onToggled: checked => {
|
||||||
|
VPNService.updateConfig(modelData.uuid, {autoconnect: checked});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
width: 1
|
width: 1
|
||||||
height: Theme.spacingXS
|
height: Theme.spacingXS
|
||||||
|
|||||||
@@ -55,180 +55,6 @@ Item {
|
|||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
spacing: Theme.spacingXL
|
spacing: Theme.spacingXL
|
||||||
|
|
||||||
SettingsCard {
|
|
||||||
tab: "typography"
|
|
||||||
tags: ["animation", "variant", "style", "slide", "fluent", "dynamic", "motion"]
|
|
||||||
title: I18n.tr("Animation Style")
|
|
||||||
settingKey: "animationVariant"
|
|
||||||
iconName: "auto_awesome_motion"
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: animVariantGroup.implicitHeight
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
DankButtonGroup {
|
|
||||||
id: animVariantGroup
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
buttonPadding: parent.width < 480 ? Theme.spacingS : Theme.spacingL
|
|
||||||
minButtonWidth: parent.width < 480 ? 64 : 96
|
|
||||||
textSize: parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium
|
|
||||||
model: [I18n.tr("Material"), I18n.tr("Fluent"), I18n.tr("Dynamic")]
|
|
||||||
selectionMode: "single"
|
|
||||||
currentIndex: SettingsData.animationVariant
|
|
||||||
onSelectionChanged: (index, selected) => {
|
|
||||||
if (!selected)
|
|
||||||
return;
|
|
||||||
SettingsData.set("animationVariant", index);
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: SettingsData
|
|
||||||
function onAnimationVariantChanged() {
|
|
||||||
animVariantGroup.currentIndex = SettingsData.animationVariant;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Theme.outline
|
|
||||||
opacity: 0.15
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: variantDescription.implicitHeight + Theme.spacingS * 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: variantDescription
|
|
||||||
x: Theme.spacingM
|
|
||||||
y: Theme.spacingS
|
|
||||||
width: parent.width - Theme.spacingM * 2
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
text: {
|
|
||||||
switch (SettingsData.animationVariant) {
|
|
||||||
case 1:
|
|
||||||
return I18n.tr("Fluent: Smooth cubic deceleration in, quick snap out — clean, elegant curves.");
|
|
||||||
case 2:
|
|
||||||
return I18n.tr("Dynamic: Spring bezier with overshoot — entry briefly exceeds its target then settles. Expressive and alive.");
|
|
||||||
default:
|
|
||||||
return I18n.tr("Material: Material Design 3 Expressive bezier curves. The DMS default feel.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsCard {
|
|
||||||
tab: "typography"
|
|
||||||
tags: ["animation", "motion", "effect", "slide", "directional", "depth", "spring", "physics"]
|
|
||||||
title: I18n.tr("Motion Effects")
|
|
||||||
settingKey: "motionEffect"
|
|
||||||
iconName: "motion_photos_on"
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: motionEffectGroup.implicitHeight
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
DankButtonGroup {
|
|
||||||
id: motionEffectGroup
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
buttonPadding: parent.width < 480 ? Theme.spacingS : Theme.spacingL
|
|
||||||
minButtonWidth: parent.width < 480 ? 64 : 96
|
|
||||||
textSize: parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium
|
|
||||||
model: [I18n.tr("Standard"), I18n.tr("Directional"), I18n.tr("Depth")]
|
|
||||||
selectionMode: "single"
|
|
||||||
currentIndex: SettingsData.motionEffect
|
|
||||||
onSelectionChanged: (index, selected) => {
|
|
||||||
if (!selected)
|
|
||||||
return;
|
|
||||||
SettingsData.set("motionEffect", index);
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: SettingsData
|
|
||||||
function onMotionEffectChanged() {
|
|
||||||
motionEffectGroup.currentIndex = SettingsData.motionEffect;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Theme.outline
|
|
||||||
opacity: 0.15
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
|
||||||
height: motionEffectDescription.implicitHeight + Theme.spacingS * 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: motionEffectDescription
|
|
||||||
x: Theme.spacingM
|
|
||||||
y: Theme.spacingS
|
|
||||||
width: parent.width - Theme.spacingM * 2
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceVariantText
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
text: {
|
|
||||||
switch (SettingsData.motionEffect) {
|
|
||||||
case 1:
|
|
||||||
return I18n.tr("Directional: Panels glide in from a larger distance at full size — no scale change, pure clean motion.");
|
|
||||||
case 2:
|
|
||||||
return I18n.tr("Depth: Panels scale up from small as they slide in — a dramatic pop-forward depth effect.");
|
|
||||||
default:
|
|
||||||
return I18n.tr("Standard: Classic Material Design 3 — panels rise from below with a subtle scale. The DMS default.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Theme.outline
|
|
||||||
opacity: 0.15
|
|
||||||
visible: SettingsData.motionEffect === 1
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsDropdownRow {
|
|
||||||
visible: SettingsData.motionEffect === 1
|
|
||||||
tab: "typography"
|
|
||||||
tags: ["animation", "directional", "behavior", "overlap", "sticky", "roll"]
|
|
||||||
settingKey: "directionalAnimationMode"
|
|
||||||
text: I18n.tr("Directional Behavior")
|
|
||||||
description: I18n.tr("How the popout emerges from the DankBar")
|
|
||||||
options: [I18n.tr("Overlap"), I18n.tr("Slide"), I18n.tr("Roll")]
|
|
||||||
currentValue: {
|
|
||||||
switch (SettingsData.directionalAnimationMode) {
|
|
||||||
case 1:
|
|
||||||
return I18n.tr("Slide");
|
|
||||||
case 2:
|
|
||||||
return I18n.tr("Roll");
|
|
||||||
default:
|
|
||||||
return I18n.tr("Overlap");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onValueChanged: value => {
|
|
||||||
if (value === I18n.tr("Slide"))
|
|
||||||
SettingsData.set("directionalAnimationMode", 1);
|
|
||||||
else if (value === I18n.tr("Roll"))
|
|
||||||
SettingsData.set("directionalAnimationMode", 2);
|
|
||||||
else
|
|
||||||
SettingsData.set("directionalAnimationMode", 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
tab: "typography"
|
tab: "typography"
|
||||||
tags: ["font", "family", "text", "typography"]
|
tags: ["font", "family", "text", "typography"]
|
||||||
|
|||||||
@@ -121,9 +121,9 @@ Scope {
|
|||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
|
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,69 +154,45 @@ Scope {
|
|||||||
id: scaleTransform
|
id: scaleTransform
|
||||||
origin.x: contentContainer.width / 2
|
origin.x: contentContainer.width / 2
|
||||||
origin.y: contentContainer.height / 2
|
origin.y: contentContainer.height / 2
|
||||||
xScale: overviewScope.overviewOpen ? 1 : Theme.effectScaleCollapsed
|
xScale: overviewScope.overviewOpen ? 1 : 0.96
|
||||||
yScale: overviewScope.overviewOpen ? 1 : Theme.effectScaleCollapsed
|
yScale: overviewScope.overviewOpen ? 1 : 0.96
|
||||||
|
|
||||||
Behavior on xScale {
|
Behavior on xScale {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
|
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on yScale {
|
Behavior on yScale {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
|
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Translate {
|
Translate {
|
||||||
id: motionTransform
|
id: motionTransform
|
||||||
x: {
|
x: 0
|
||||||
if (overviewScope.overviewOpen)
|
y: overviewScope.overviewOpen ? 0 : Theme.spacingL
|
||||||
return 0;
|
|
||||||
if (Theme.isDirectionalEffect)
|
|
||||||
return 0;
|
|
||||||
if (Theme.isDepthEffect)
|
|
||||||
return Theme.effectAnimOffset * 0.25;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
y: {
|
|
||||||
if (overviewScope.overviewOpen)
|
|
||||||
return 0;
|
|
||||||
if (Theme.isDirectionalEffect)
|
|
||||||
return -Math.max(contentContainer.height * 0.8, Theme.effectAnimOffset * 1.1);
|
|
||||||
if (Theme.isDepthEffect)
|
|
||||||
return Math.max(Theme.effectAnimOffset * 0.85, 28);
|
|
||||||
return Theme.effectAnimOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on x {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on y {
|
Behavior on y {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
|
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
|
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -202,18 +202,8 @@ Scope {
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: spotlightContainer
|
id: spotlightContainer
|
||||||
readonly property bool directionalEffect: Theme.isDirectionalEffect
|
x: Theme.snap((parent.width - width) / 2, overlayWindow.dpr)
|
||||||
readonly property bool depthEffect: Theme.isDepthEffect
|
y: Theme.snap((parent.height - height) / 2, overlayWindow.dpr)
|
||||||
readonly property real collapsedMotionX: depthEffect ? Theme.effectAnimOffset * 0.25 : 0
|
|
||||||
readonly property real collapsedMotionY: {
|
|
||||||
if (directionalEffect)
|
|
||||||
return Math.max(height * 0.85, Theme.effectAnimOffset * 1.1);
|
|
||||||
if (depthEffect)
|
|
||||||
return Math.max(Theme.effectAnimOffset * 0.8, 30);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
x: Theme.snap((parent.width - width) / 2 + (overlayWindow.shouldShowSpotlight ? 0 : collapsedMotionX), overlayWindow.dpr)
|
|
||||||
y: Theme.snap((parent.height - height) / 2 + (overlayWindow.shouldShowSpotlight ? 0 : collapsedMotionY), overlayWindow.dpr)
|
|
||||||
|
|
||||||
readonly property int baseWidth: {
|
readonly property int baseWidth: {
|
||||||
switch (SettingsData.dankLauncherV2Size) {
|
switch (SettingsData.dankLauncherV2Size) {
|
||||||
@@ -244,8 +234,8 @@ Scope {
|
|||||||
|
|
||||||
readonly property bool animatingOut: niriOverviewScope.isClosing && overlayWindow.isSpotlightScreen
|
readonly property bool animatingOut: niriOverviewScope.isClosing && overlayWindow.isSpotlightScreen
|
||||||
|
|
||||||
scale: Theme.isDirectionalEffect ? 1 : (overlayWindow.shouldShowSpotlight ? 1.0 : Theme.effectScaleCollapsed)
|
scale: overlayWindow.shouldShowSpotlight ? 1.0 : 0.96
|
||||||
opacity: Theme.isDirectionalEffect ? 1 : (overlayWindow.shouldShowSpotlight ? 1 : 0)
|
opacity: overlayWindow.shouldShowSpotlight ? 1 : 0
|
||||||
visible: overlayWindow.shouldShowSpotlight || animatingOut
|
visible: overlayWindow.shouldShowSpotlight || animatingOut
|
||||||
enabled: overlayWindow.shouldShowSpotlight
|
enabled: overlayWindow.shouldShowSpotlight
|
||||||
|
|
||||||
@@ -255,11 +245,10 @@ Scope {
|
|||||||
|
|
||||||
Behavior on scale {
|
Behavior on scale {
|
||||||
id: scaleAnimation
|
id: scaleAnimation
|
||||||
enabled: !Theme.isDirectionalEffect
|
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight)
|
duration: Theme.expressiveDurations.fast
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
|
||||||
onRunningChanged: {
|
onRunningChanged: {
|
||||||
if (running || !spotlightContainer.animatingOut)
|
if (running || !spotlightContainer.animatingOut)
|
||||||
return;
|
return;
|
||||||
@@ -269,27 +258,10 @@ Scope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
enabled: !Theme.isDirectionalEffect
|
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight)
|
duration: Theme.expressiveDurations.fast
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on x {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight)
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on y {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight)
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,30 +62,30 @@ Item {
|
|||||||
|
|
||||||
Behavior on x {
|
Behavior on x {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: Theme.variantModalEnterCurve
|
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on y {
|
Behavior on y {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: Theme.variantModalEnterCurve
|
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on width {
|
Behavior on width {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: Theme.variantModalEnterCurve
|
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on height {
|
Behavior on height {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: Theme.variantModalEnterCurve
|
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,16 +124,16 @@ Item {
|
|||||||
|
|
||||||
Behavior on width {
|
Behavior on width {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: Theme.variantModalEnterCurve
|
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on height {
|
Behavior on height {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: Theme.variantModalEnterCurve
|
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ pragma ComponentBehavior: Bound
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
ToastService.showInfo(I18n.tr("VPN configuration updated"));
|
ToastService.showInfo(I18n.tr("VPN configuration updated"));
|
||||||
DMSNetworkService.refreshVpnProfiles();
|
DMSNetworkService.refreshVpnProfiles();
|
||||||
|
getConfig(uuid);
|
||||||
configUpdated();
|
configUpdated();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ Item {
|
|||||||
property string triggerSection: ""
|
property string triggerSection: ""
|
||||||
property string positioning: "center"
|
property string positioning: "center"
|
||||||
property int animationDuration: Theme.popoutAnimationDuration
|
property int animationDuration: Theme.popoutAnimationDuration
|
||||||
property real animationScaleCollapsed: Theme.effectScaleCollapsed
|
property real animationScaleCollapsed: 0.96
|
||||||
property real animationOffset: Theme.effectAnimOffset
|
property real animationOffset: Theme.spacingL
|
||||||
property list<real> animationEnterCurve: Theme.variantPopoutEnterCurve
|
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||||
property list<real> animationExitCurve: Theme.variantPopoutExitCurve
|
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
||||||
property bool suspendShadowWhileResizing: false
|
property bool suspendShadowWhileResizing: false
|
||||||
property bool shouldBeVisible: false
|
property bool shouldBeVisible: false
|
||||||
property var customKeyboardFocus: null
|
property var customKeyboardFocus: null
|
||||||
@@ -74,7 +74,6 @@ Item {
|
|||||||
signal backgroundClicked
|
signal backgroundClicked
|
||||||
|
|
||||||
property var _lastOpenedScreen: null
|
property var _lastOpenedScreen: null
|
||||||
property bool isClosing: false
|
|
||||||
|
|
||||||
property int effectiveBarPosition: 0
|
property int effectiveBarPosition: 0
|
||||||
property real effectiveBarBottomGap: 0
|
property real effectiveBarBottomGap: 0
|
||||||
@@ -157,14 +156,10 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
property bool animationsEnabled: true
|
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
if (!screen)
|
if (!screen)
|
||||||
return;
|
return;
|
||||||
closeTimer.stop();
|
closeTimer.stop();
|
||||||
isClosing = false;
|
|
||||||
animationsEnabled = false;
|
|
||||||
|
|
||||||
// Snapshot mask geometry
|
// Snapshot mask geometry
|
||||||
_frozenMaskX = maskX;
|
_frozenMaskX = maskX;
|
||||||
@@ -179,22 +174,12 @@ Item {
|
|||||||
}
|
}
|
||||||
_lastOpenedScreen = screen;
|
_lastOpenedScreen = screen;
|
||||||
|
|
||||||
if (contentContainer) {
|
shouldBeVisible = true;
|
||||||
contentContainer.animX = Theme.snap(contentContainer.offsetX, root.dpr);
|
|
||||||
contentContainer.animY = Theme.snap(contentContainer.offsetY, root.dpr);
|
|
||||||
contentContainer.scaleValue = root.animationScaleCollapsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (useBackgroundWindow) {
|
if (useBackgroundWindow) {
|
||||||
_surfaceMarginLeft = alignedX - shadowBuffer;
|
_surfaceMarginLeft = alignedX - shadowBuffer;
|
||||||
_surfaceW = alignedWidth + shadowBuffer * 2;
|
_surfaceW = alignedWidth + shadowBuffer * 2;
|
||||||
backgroundWindow.visible = true;
|
|
||||||
}
|
}
|
||||||
contentWindow.visible = true;
|
|
||||||
|
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
animationsEnabled = true;
|
|
||||||
shouldBeVisible = true;
|
|
||||||
if (shouldBeVisible && screen) {
|
if (shouldBeVisible && screen) {
|
||||||
if (useBackgroundWindow)
|
if (useBackgroundWindow)
|
||||||
backgroundWindow.visible = true;
|
backgroundWindow.visible = true;
|
||||||
@@ -206,7 +191,6 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
isClosing = true;
|
|
||||||
shouldBeVisible = false;
|
shouldBeVisible = false;
|
||||||
_primeContent = false;
|
_primeContent = false;
|
||||||
PopoutManager.popoutChanged();
|
PopoutManager.popoutChanged();
|
||||||
@@ -238,10 +222,9 @@ Item {
|
|||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: closeTimer
|
id: closeTimer
|
||||||
interval: Theme.variantCloseInterval(animationDuration)
|
interval: animationDuration
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (!shouldBeVisible) {
|
if (!shouldBeVisible) {
|
||||||
isClosing = false;
|
|
||||||
contentWindow.visible = false;
|
contentWindow.visible = false;
|
||||||
if (useBackgroundWindow)
|
if (useBackgroundWindow)
|
||||||
backgroundWindow.visible = false;
|
backgroundWindow.visible = false;
|
||||||
@@ -258,16 +241,7 @@ Item {
|
|||||||
readonly property var shadowLevel: Theme.elevationLevel3
|
readonly property var shadowLevel: Theme.elevationLevel3
|
||||||
readonly property real shadowFallbackOffset: 6
|
readonly property real shadowFallbackOffset: 6
|
||||||
readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.popoutElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, effectiveShadowDirection, shadowFallbackOffset, 8, 16) : 0
|
readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.popoutElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, effectiveShadowDirection, shadowFallbackOffset, 8, 16) : 0
|
||||||
readonly property real shadowMotionPadding: {
|
readonly property real shadowMotionPadding: Math.max(0, animationOffset)
|
||||||
if (Theme.isDirectionalEffect) {
|
|
||||||
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode !== 0)
|
|
||||||
return 16; // Slide Behind and Roll Out do not add animationOffset, enabling strict Wayland clipping.
|
|
||||||
return Math.max(0, animationOffset) + 16;
|
|
||||||
}
|
|
||||||
if (Theme.isDepthEffect)
|
|
||||||
return Math.max(0, animationOffset) + 8;
|
|
||||||
return Math.max(0, animationOffset);
|
|
||||||
}
|
|
||||||
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
|
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
|
||||||
readonly property real alignedWidth: Theme.px(popupWidth, dpr)
|
readonly property real alignedWidth: Theme.px(popupWidth, dpr)
|
||||||
readonly property real alignedHeight: Theme.px(popupHeight, dpr)
|
readonly property real alignedHeight: Theme.px(popupHeight, dpr)
|
||||||
@@ -379,10 +353,6 @@ Item {
|
|||||||
|
|
||||||
mask: Region {
|
mask: Region {
|
||||||
item: maskRect
|
item: maskRect
|
||||||
Region {
|
|
||||||
item: contentExclusionRect
|
|
||||||
intersection: Intersection.Subtract
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -391,70 +361,26 @@ Item {
|
|||||||
color: "transparent"
|
color: "transparent"
|
||||||
x: root._frozenMaskX
|
x: root._frozenMaskX
|
||||||
y: root._frozenMaskY
|
y: root._frozenMaskY
|
||||||
width: (backgroundWindow.visible && backgroundInteractive) ? root._frozenMaskWidth : 0
|
width: (shouldBeVisible && backgroundInteractive) ? root._frozenMaskWidth : 0
|
||||||
height: (backgroundWindow.visible && backgroundInteractive) ? root._frozenMaskHeight : 0
|
height: (shouldBeVisible && backgroundInteractive) ? root._frozenMaskHeight : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
MouseArea {
|
||||||
id: contentExclusionRect
|
|
||||||
visible: false
|
|
||||||
x: root.alignedX
|
|
||||||
y: root.alignedY
|
|
||||||
width: root.alignedWidth
|
|
||||||
height: root.alignedHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: outsideClickCatcher
|
|
||||||
x: root._frozenMaskX
|
x: root._frozenMaskX
|
||||||
y: root._frozenMaskY
|
y: root._frozenMaskY
|
||||||
width: root._frozenMaskWidth
|
width: root._frozenMaskWidth
|
||||||
height: root._frozenMaskHeight
|
height: root._frozenMaskHeight
|
||||||
enabled: root.shouldBeVisible && root.backgroundInteractive
|
hoverEnabled: false
|
||||||
|
enabled: shouldBeVisible && backgroundInteractive
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||||
|
onClicked: mouse => {
|
||||||
|
const clickX = mouse.x + root._frozenMaskX;
|
||||||
|
const clickY = mouse.y + root._frozenMaskY;
|
||||||
|
const outsideContent = clickX < root.alignedX || clickX > root.alignedX + root.alignedWidth || clickY < root.alignedY || clickY > root.alignedY + root.alignedHeight;
|
||||||
|
|
||||||
readonly property real contentLeft: Math.max(0, root.alignedX - x)
|
if (!outsideContent)
|
||||||
readonly property real contentTop: Math.max(0, root.alignedY - y)
|
return;
|
||||||
readonly property real contentRight: Math.min(width, contentLeft + root.alignedWidth)
|
backgroundClicked();
|
||||||
readonly property real contentBottom: Math.min(height, contentTop + root.alignedHeight)
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
x: 0
|
|
||||||
y: 0
|
|
||||||
width: outsideClickCatcher.width
|
|
||||||
height: Math.max(0, outsideClickCatcher.contentTop)
|
|
||||||
enabled: parent.enabled
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
|
||||||
onClicked: root.backgroundClicked()
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
x: 0
|
|
||||||
y: outsideClickCatcher.contentBottom
|
|
||||||
width: outsideClickCatcher.width
|
|
||||||
height: Math.max(0, outsideClickCatcher.height - outsideClickCatcher.contentBottom)
|
|
||||||
enabled: parent.enabled
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
|
||||||
onClicked: root.backgroundClicked()
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
x: 0
|
|
||||||
y: outsideClickCatcher.contentTop
|
|
||||||
width: Math.max(0, outsideClickCatcher.contentLeft)
|
|
||||||
height: Math.max(0, outsideClickCatcher.contentBottom - outsideClickCatcher.contentTop)
|
|
||||||
enabled: parent.enabled
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
|
||||||
onClicked: root.backgroundClicked()
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
x: outsideClickCatcher.contentRight
|
|
||||||
y: outsideClickCatcher.contentTop
|
|
||||||
width: Math.max(0, outsideClickCatcher.width - outsideClickCatcher.contentRight)
|
|
||||||
height: Math.max(0, outsideClickCatcher.contentBottom - outsideClickCatcher.contentTop)
|
|
||||||
enabled: parent.enabled
|
|
||||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
|
||||||
onClicked: root.backgroundClicked()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,6 +425,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readonly property bool _fullHeight: useBackgroundWindow && root.fullHeightSurface
|
readonly property bool _fullHeight: useBackgroundWindow && root.fullHeightSurface
|
||||||
|
|
||||||
anchors {
|
anchors {
|
||||||
left: true
|
left: true
|
||||||
top: true
|
top: true
|
||||||
@@ -556,70 +483,12 @@ Item {
|
|||||||
readonly property bool barBottom: effectiveBarPosition === SettingsData.Position.Bottom
|
readonly property bool barBottom: effectiveBarPosition === SettingsData.Position.Bottom
|
||||||
readonly property bool barLeft: effectiveBarPosition === SettingsData.Position.Left
|
readonly property bool barLeft: effectiveBarPosition === SettingsData.Position.Left
|
||||||
readonly property bool barRight: effectiveBarPosition === SettingsData.Position.Right
|
readonly property bool barRight: effectiveBarPosition === SettingsData.Position.Right
|
||||||
readonly property bool directionalEffect: Theme.isDirectionalEffect
|
readonly property real offsetX: barLeft ? root.animationOffset : (barRight ? -root.animationOffset : 0)
|
||||||
readonly property bool depthEffect: Theme.isDepthEffect
|
readonly property real offsetY: barBottom ? -root.animationOffset : (barTop ? root.animationOffset : 0)
|
||||||
readonly property real directionalTravelX: Math.max(root.animationOffset, root.alignedWidth + Theme.spacingL)
|
|
||||||
readonly property real directionalTravelY: Math.max(root.animationOffset, root.alignedHeight + Theme.spacingL)
|
|
||||||
readonly property real depthTravel: Math.max(root.animationOffset * 0.7, 28)
|
|
||||||
readonly property real sectionTilt: (triggerSection === "left" ? -1 : (triggerSection === "right" ? 1 : 0))
|
|
||||||
readonly property real offsetX: {
|
|
||||||
if (directionalEffect) {
|
|
||||||
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2)
|
|
||||||
return 0;
|
|
||||||
if (barLeft)
|
|
||||||
return -directionalTravelX;
|
|
||||||
if (barRight)
|
|
||||||
return directionalTravelX;
|
|
||||||
if (barTop || barBottom)
|
|
||||||
return 0;
|
|
||||||
return sectionTilt * directionalTravelX * 0.2;
|
|
||||||
}
|
|
||||||
if (depthEffect) {
|
|
||||||
if (barLeft)
|
|
||||||
return -depthTravel;
|
|
||||||
if (barRight)
|
|
||||||
return depthTravel;
|
|
||||||
if (barTop || barBottom)
|
|
||||||
return 0;
|
|
||||||
return sectionTilt * depthTravel * 0.2;
|
|
||||||
}
|
|
||||||
return barLeft ? root.animationOffset : (barRight ? -root.animationOffset : 0);
|
|
||||||
}
|
|
||||||
readonly property real offsetY: {
|
|
||||||
if (directionalEffect) {
|
|
||||||
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2)
|
|
||||||
return 0;
|
|
||||||
if (barBottom)
|
|
||||||
return directionalTravelY;
|
|
||||||
if (barTop)
|
|
||||||
return -directionalTravelY;
|
|
||||||
if (barLeft || barRight)
|
|
||||||
return 0;
|
|
||||||
return directionalTravelY;
|
|
||||||
}
|
|
||||||
if (depthEffect) {
|
|
||||||
if (barBottom)
|
|
||||||
return depthTravel;
|
|
||||||
if (barTop)
|
|
||||||
return -depthTravel;
|
|
||||||
if (barLeft || barRight)
|
|
||||||
return 0;
|
|
||||||
return depthTravel;
|
|
||||||
}
|
|
||||||
return barBottom ? -root.animationOffset : (barTop ? root.animationOffset : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
property real animX: 0
|
property real animX: 0
|
||||||
property real animY: 0
|
property real animY: 0
|
||||||
|
property real scaleValue: root.animationScaleCollapsed
|
||||||
readonly property real computedScaleCollapsed: (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) ? 0.0 : root.animationScaleCollapsed
|
|
||||||
property real scaleValue: computedScaleCollapsed
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr);
|
|
||||||
animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr);
|
|
||||||
scaleValue = root.shouldBeVisible ? 1.0 : computedScaleCollapsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
|
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
|
||||||
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
|
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
|
||||||
@@ -629,131 +498,89 @@ Item {
|
|||||||
function onShouldBeVisibleChanged() {
|
function onShouldBeVisibleChanged() {
|
||||||
contentContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetX, root.dpr);
|
contentContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetX, root.dpr);
|
||||||
contentContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetY, root.dpr);
|
contentContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetY, root.dpr);
|
||||||
contentContainer.scaleValue = root.shouldBeVisible ? 1.0 : contentContainer.computedScaleCollapsed;
|
contentContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on animX {
|
Behavior on animX {
|
||||||
enabled: root.animationsEnabled
|
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
duration: root.animationDuration
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on animY {
|
Behavior on animY {
|
||||||
enabled: root.animationsEnabled
|
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
duration: root.animationDuration
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on scaleValue {
|
Behavior on scaleValue {
|
||||||
enabled: root.animationsEnabled
|
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
duration: root.animationDuration
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ElevationShadow {
|
||||||
|
id: shadowSource
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height
|
||||||
|
opacity: contentWrapper.opacity
|
||||||
|
scale: contentWrapper.scale
|
||||||
|
x: contentWrapper.x
|
||||||
|
y: contentWrapper.y
|
||||||
|
level: root.shadowLevel
|
||||||
|
direction: root.effectiveShadowDirection
|
||||||
|
fallbackOffset: root.shadowFallbackOffset
|
||||||
|
targetRadius: Theme.cornerRadius
|
||||||
|
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
|
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !(root.suspendShadowWhileResizing && root._resizeActive)
|
||||||
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: directionalClipMask
|
id: contentWrapper
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height
|
||||||
|
opacity: shouldBeVisible ? 1 : 0
|
||||||
|
visible: opacity > 0
|
||||||
|
scale: contentContainer.scaleValue
|
||||||
|
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
||||||
|
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
||||||
|
|
||||||
readonly property bool shouldClip: typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0 && Theme.isDirectionalEffect
|
layer.enabled: contentWrapper.opacity < 1
|
||||||
readonly property real clipOversize: 1000
|
layer.smooth: false
|
||||||
|
layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0)
|
||||||
|
|
||||||
clip: shouldClip
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: animationDuration
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Bound the clipping strictly to the bar side, allowing massive overflow on the other 3 sides for shadows
|
Rectangle {
|
||||||
x: shouldClip ? (contentContainer.barRight ? -clipOversize : (contentContainer.barLeft ? 0 : -clipOversize)) : 0
|
anchors.fill: parent
|
||||||
y: shouldClip ? (contentContainer.barBottom ? -clipOversize : (contentContainer.barTop ? 0 : -clipOversize)) : 0
|
radius: Theme.cornerRadius
|
||||||
|
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
|
border.color: Theme.outlineMedium
|
||||||
|
border.width: 0
|
||||||
|
}
|
||||||
|
|
||||||
width: shouldClip ? parent.width + clipOversize + (contentContainer.barLeft || contentContainer.barRight ? 0 : clipOversize) : parent.width
|
Loader {
|
||||||
height: shouldClip ? parent.height + clipOversize + (contentContainer.barTop || contentContainer.barBottom ? 0 : clipOversize) : parent.height
|
id: contentLoader
|
||||||
|
anchors.fill: parent
|
||||||
Item {
|
active: root._primeContent || shouldBeVisible || contentWindow.visible
|
||||||
id: aligner
|
asynchronous: false
|
||||||
readonly property real baseWidth: contentContainer.width
|
}
|
||||||
readonly property real baseHeight: contentContainer.height
|
}
|
||||||
readonly property bool isRollOut: typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect
|
}
|
||||||
|
|
||||||
x: (directionalClipMask.x !== 0 ? -directionalClipMask.x : 0) + (isRollOut && contentContainer.barRight ? baseWidth * (1 - contentContainer.scaleValue) : 0)
|
|
||||||
y: (directionalClipMask.y !== 0 ? -directionalClipMask.y : 0) + (isRollOut && contentContainer.barBottom ? baseHeight * (1 - contentContainer.scaleValue) : 0)
|
|
||||||
width: isRollOut && (contentContainer.barLeft || contentContainer.barRight) ? Math.max(0, baseWidth * contentContainer.scaleValue) : baseWidth
|
|
||||||
height: isRollOut && (contentContainer.barTop || contentContainer.barBottom) ? Math.max(0, baseHeight * contentContainer.scaleValue) : baseHeight
|
|
||||||
|
|
||||||
clip: isRollOut
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: unrollCounteract
|
|
||||||
x: aligner.isRollOut && contentContainer.barRight ? -(aligner.baseWidth * (1 - contentContainer.scaleValue)) : 0
|
|
||||||
y: aligner.isRollOut && contentContainer.barBottom ? -(aligner.baseHeight * (1 - contentContainer.scaleValue)) : 0
|
|
||||||
width: aligner.baseWidth
|
|
||||||
height: aligner.baseHeight
|
|
||||||
|
|
||||||
ElevationShadow {
|
|
||||||
id: shadowSource
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height
|
|
||||||
opacity: contentWrapper.opacity
|
|
||||||
scale: contentWrapper.scale
|
|
||||||
x: contentWrapper.x
|
|
||||||
y: contentWrapper.y
|
|
||||||
level: root.shadowLevel
|
|
||||||
direction: root.effectiveShadowDirection
|
|
||||||
fallbackOffset: root.shadowFallbackOffset
|
|
||||||
targetRadius: Theme.cornerRadius
|
|
||||||
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
|
||||||
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !(root.suspendShadowWhileResizing && root._resizeActive)
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: contentWrapper
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height
|
|
||||||
opacity: Theme.isDirectionalEffect ? 1 : (shouldBeVisible ? 1 : 0)
|
|
||||||
visible: opacity > 0
|
|
||||||
|
|
||||||
scale: aligner.isRollOut ? 1.0 : contentContainer.scaleValue
|
|
||||||
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - scale) * 0.5, root.dpr)
|
|
||||||
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - scale) * 0.5, root.dpr)
|
|
||||||
|
|
||||||
layer.enabled: contentWrapper.opacity < 1
|
|
||||||
layer.smooth: false
|
|
||||||
layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0)
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
enabled: !Theme.isDirectionalEffect
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Math.round(Theme.variantDuration(animationDuration, shouldBeVisible) * Theme.variantOpacityDurationScale)
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
border.width: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: contentLoader
|
|
||||||
anchors.fill: parent
|
|
||||||
active: root._primeContent || shouldBeVisible || contentWindow.visible
|
|
||||||
asynchronous: false
|
|
||||||
}
|
|
||||||
} // closes contentWrapper
|
|
||||||
} // closes unrollCounteract
|
|
||||||
} // closes aligner
|
|
||||||
} // closes directionalClipMask
|
|
||||||
} // closes contentContainer
|
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: focusHelper
|
id: focusHelper
|
||||||
|
|||||||
@@ -75,11 +75,6 @@ Rectangle {
|
|||||||
"label": I18n.tr("Auth Type"),
|
"label": I18n.tr("Auth Type"),
|
||||||
"value": data["connection-type"]
|
"value": data["connection-type"]
|
||||||
});
|
});
|
||||||
fields.push({
|
|
||||||
"key": "auto",
|
|
||||||
"label": I18n.tr("Autoconnect"),
|
|
||||||
"value": configData.autoconnect ? I18n.tr("Yes") : I18n.tr("No")
|
|
||||||
});
|
|
||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,6 +266,16 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DankToggle {
|
||||||
|
width: parent.width
|
||||||
|
text: I18n.tr("Autoconnect")
|
||||||
|
checked: configData ? (configData.autoconnect || false) : false
|
||||||
|
visible: !VPNService.configLoading && configData !== null
|
||||||
|
onToggled: checked => {
|
||||||
|
VPNService.updateConfig(profile.uuid, {autoconnect: checked});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
width: 1
|
width: 1
|
||||||
height: Theme.spacingXS
|
height: Theme.spacingXS
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#%PAM-1.0
|
#%PAM-1.0
|
||||||
|
|
||||||
auth required pam_fprintd.so max-tries=1
|
auth required pam_fprintd.so max-tries=1 timeout=5
|
||||||
|
|||||||
Reference in New Issue
Block a user