mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-03 20:32:07 -04:00
greeter: New Greeter Settings UI & Sync fixes
- Add PAM Auth via GUI - Added new sync flags - Refactored cache directory management & many others - Fix for wireplumber permissions - Fix for polkit auth w/icon - Add pam_fprintd timeout=5 to prevent 30s auth blocks when using password
This commit is contained in:
@@ -39,12 +39,29 @@ var greeterSyncCmd = &cobra.Command{
|
|||||||
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",
|
||||||
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",
|
||||||
@@ -147,7 +164,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 +188,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 +286,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 +310,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 +344,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 +359,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 {
|
||||||
@@ -497,12 +724,20 @@ func enableGreeter() error {
|
|||||||
configAlreadyCorrect := isGreeterEnabled()
|
configAlreadyCorrect := isGreeterEnabled()
|
||||||
configuredCompositor := detectConfiguredCompositor()
|
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 != "" {
|
if configuredCompositor != "" {
|
||||||
fmt.Printf("✓ Configured compositor: %s\n", 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
|
||||||
}
|
}
|
||||||
@@ -556,13 +791,14 @@ func enableGreeter() error {
|
|||||||
}
|
}
|
||||||
greeterPathForConfig = dmsPath
|
greeterPathForConfig = dmsPath
|
||||||
}
|
}
|
||||||
logFunc := func(msg string) {
|
|
||||||
fmt.Println(msg)
|
|
||||||
}
|
|
||||||
if err := greeter.ConfigureGreetd(greeterPathForConfig, selectedCompositor, logFunc, ""); err != nil {
|
if err := greeter.ConfigureGreetd(greeterPathForConfig, selectedCompositor, logFunc, ""); err != nil {
|
||||||
return fmt.Errorf("failed to configure greetd: %w", err)
|
return fmt.Errorf("failed to configure greetd: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
@@ -653,6 +889,95 @@ func readDefaultSessionCommand(configPath string) string {
|
|||||||
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 {
|
||||||
@@ -731,11 +1056,19 @@ 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 _, err := os.ReadFile(configPath); err == nil {
|
if _, err := os.ReadFile(configPath); err == nil {
|
||||||
command := readDefaultSessionCommand(configPath)
|
configuredCommand = readDefaultSessionCommand(configPath)
|
||||||
if command != "" && strings.Contains(command, "dms-greeter") {
|
if configuredCommand != "" && strings.Contains(configuredCommand, "dms-greeter") {
|
||||||
fmt.Println(" ✓ Greeter is enabled")
|
fmt.Println(" ✓ Greeter is enabled")
|
||||||
|
if wrapper := extractGreeterWrapperFromCommand(configuredCommand); wrapper != "" {
|
||||||
|
fmt.Printf(" Wrapper: %s\n", wrapper)
|
||||||
|
}
|
||||||
|
if pathOverride := extractGreeterPathOverrideFromCommand(configuredCommand); pathOverride != "" {
|
||||||
|
fmt.Printf(" DMS path override: %s\n", pathOverride)
|
||||||
|
}
|
||||||
|
|
||||||
compositor := detectConfiguredCompositor()
|
compositor := detectConfiguredCompositor()
|
||||||
switch compositor {
|
switch compositor {
|
||||||
@@ -751,10 +1084,12 @@ func checkGreeterStatus() error {
|
|||||||
} 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:")
|
||||||
@@ -773,8 +1108,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 {
|
||||||
@@ -806,7 +1145,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 {
|
||||||
@@ -845,11 +1183,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
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -201,17 +221,29 @@ func DetectGreeterUser() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func resolveGreeterWrapperPath() string {
|
func resolveGreeterWrapperPath() string {
|
||||||
if path, err := exec.LookPath("dms-greeter"); err == nil {
|
if override := strings.TrimSpace(os.Getenv("DMS_GREETER_WRAPPER_CMD")); override != "" {
|
||||||
return path
|
return override
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, candidate := range []string{"/usr/local/bin/dms-greeter", "/usr/bin/dms-greeter"} {
|
for _, candidate := range []string{"/usr/bin/dms-greeter", "/usr/local/bin/dms-greeter"} {
|
||||||
if _, err := os.Stat(candidate); err == nil {
|
if info, err := os.Stat(candidate); err == nil && !info.IsDir() && (info.Mode()&0o111) != 0 {
|
||||||
return candidate
|
return candidate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "dms-greeter"
|
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 {
|
||||||
@@ -514,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)
|
||||||
}
|
}
|
||||||
@@ -526,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -730,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
|
||||||
@@ -764,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
|
||||||
}
|
}
|
||||||
@@ -797,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
|
||||||
@@ -938,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")
|
||||||
@@ -954,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)
|
||||||
@@ -988,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 {
|
||||||
@@ -1134,14 +1472,12 @@ 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))
|
||||||
} else if err != nil && !os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("failed to access %s: %w", configPath, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
greeterUser := DetectGreeterUser()
|
greeterUser := DetectGreeterUser()
|
||||||
@@ -1164,7 +1500,7 @@ vt = 1
|
|||||||
// Build command based on compositor and dms path
|
// Build command based on compositor and dms path
|
||||||
// When dmsPath is empty (packaged greeter), omit -p; wrapper finds /usr/share/quickshell/dms-greeter
|
// When dmsPath is empty (packaged greeter), omit -p; wrapper finds /usr/share/quickshell/dms-greeter
|
||||||
compositorLower := strings.ToLower(compositor)
|
compositorLower := strings.ToLower(compositor)
|
||||||
commandValue := fmt.Sprintf("%s --command %s", wrapperCmd, compositorLower)
|
commandValue := fmt.Sprintf("%s --command %s --cache-dir %s", wrapperCmd, compositorLower, GreeterCacheDir)
|
||||||
if dmsPath != "" {
|
if dmsPath != "" {
|
||||||
commandValue = fmt.Sprintf("%s -p %s", commandValue, dmsPath)
|
commandValue = fmt.Sprintf("%s -p %s", commandValue, dmsPath)
|
||||||
}
|
}
|
||||||
@@ -1229,6 +1565,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)
|
||||||
@@ -1405,7 +1765,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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1204,7 +1204,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
|
||||||
|
|||||||
@@ -296,6 +296,15 @@ Singleton {
|
|||||||
property string lockDateFormat: ""
|
property string lockDateFormat: ""
|
||||||
property bool greeterRememberLastSession: true
|
property bool greeterRememberLastSession: true
|
||||||
property bool greeterRememberLastUser: 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"
|
||||||
|
|||||||
@@ -858,7 +858,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";
|
||||||
}
|
}
|
||||||
@@ -1763,7 +1763,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,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)
|
||||||
|
|||||||
@@ -156,6 +156,15 @@ var SPEC = {
|
|||||||
lockDateFormat: { def: "" },
|
lockDateFormat: { def: "" },
|
||||||
greeterRememberLastSession: { def: true },
|
greeterRememberLastSession: { def: true },
|
||||||
greeterRememberLastUser: { 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" },
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -281,6 +281,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"),
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ 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 rememberLastSession: GreetdEnv.readBoolOverride(Quickshell.env, ["DMS_GREET_REMEMBER_LAST_SESSION", "DMS_SAVE_SESSION"], true)
|
||||||
|
|||||||
@@ -11,10 +11,16 @@ 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: ""
|
||||||
@@ -22,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"
|
||||||
@@ -44,6 +56,9 @@ Singleton {
|
|||||||
property bool lockScreenShowProfileImage: true
|
property bool lockScreenShowProfileImage: true
|
||||||
property bool rememberLastSession: true
|
property bool rememberLastSession: true
|
||||||
property bool rememberLastUser: 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"]
|
||||||
@@ -69,6 +84,12 @@ Singleton {
|
|||||||
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
|
use24HourClock = settings.use24HourClock !== undefined ? settings.use24HourClock : true;
|
||||||
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false;
|
showSeconds = settings.showSeconds !== undefined ? settings.showSeconds : false;
|
||||||
padHours12Hour = settings.padHours12Hour !== undefined ? settings.padHours12Hour : 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;
|
useFahrenheit = settings.useFahrenheit !== undefined ? settings.useFahrenheit : false;
|
||||||
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
|
nightModeEnabled = settings.nightModeEnabled !== undefined ? settings.nightModeEnabled : false;
|
||||||
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY";
|
weatherLocation = settings.weatherLocation !== undefined ? settings.weatherLocation : "New York, NY";
|
||||||
@@ -92,17 +113,16 @@ Singleton {
|
|||||||
if (envRememberLastSession !== undefined) {
|
if (envRememberLastSession !== undefined) {
|
||||||
rememberLastSession = envRememberLastSession;
|
rememberLastSession = envRememberLastSession;
|
||||||
} else {
|
} else {
|
||||||
rememberLastSession = settings.greeterRememberLastSession !== undefined
|
rememberLastSession = settings.greeterRememberLastSession !== undefined ? settings.greeterRememberLastSession : settings.rememberLastSession !== undefined ? settings.rememberLastSession : true;
|
||||||
? settings.greeterRememberLastSession
|
|
||||||
: settings.rememberLastSession !== undefined ? settings.rememberLastSession : true;
|
|
||||||
}
|
}
|
||||||
if (envRememberLastUser !== undefined) {
|
if (envRememberLastUser !== undefined) {
|
||||||
rememberLastUser = envRememberLastUser;
|
rememberLastUser = envRememberLastUser;
|
||||||
} else {
|
} else {
|
||||||
rememberLastUser = settings.greeterRememberLastUser !== undefined
|
rememberLastUser = settings.greeterRememberLastUser !== undefined ? settings.greeterRememberLastUser : settings.rememberLastUser !== undefined ? settings.rememberLastUser : true;
|
||||||
? 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;
|
powerActionConfirm = settings.powerActionConfirm !== undefined ? settings.powerActionConfirm : true;
|
||||||
powerActionHoldDuration = settings.powerActionHoldDuration !== undefined ? settings.powerActionHoldDuration : 0.5;
|
powerActionHoldDuration = settings.powerActionHoldDuration !== undefined ? settings.powerActionHoldDuration : 0.5;
|
||||||
powerMenuActions = settings.powerMenuActions !== undefined ? settings.powerMenuActions : ["reboot", "logout", "poweroff", "lock", "suspend", "restart"];
|
powerMenuActions = settings.powerMenuActions !== undefined ? settings.powerMenuActions : ["reboot", "logout", "poweroff", "lock", "suspend", "restart"];
|
||||||
@@ -126,15 +146,27 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ Item {
|
|||||||
|
|
||||||
property bool weatherInitialized: false
|
property bool weatherInitialized: false
|
||||||
property bool awaitingExternalAuth: false
|
property bool awaitingExternalAuth: false
|
||||||
|
property bool pendingPasswordResponse: false
|
||||||
|
property bool passwordSubmitRequested: false
|
||||||
|
property bool cancelingExternalAuthForPassword: false
|
||||||
property int defaultAuthTimeoutMs: 12000
|
property int defaultAuthTimeoutMs: 12000
|
||||||
property int externalAuthTimeoutMs: 45000
|
property int externalAuthTimeoutMs: 45000
|
||||||
property int passwordFailureCount: 0
|
property int passwordFailureCount: 0
|
||||||
@@ -42,6 +45,11 @@ Item {
|
|||||||
property string commonAuthPamText: ""
|
property string commonAuthPamText: ""
|
||||||
property string passwordAuthPamText: ""
|
property string passwordAuthPamText: ""
|
||||||
property string faillockConfigText: ""
|
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)
|
||||||
@@ -67,6 +75,20 @@ Item {
|
|||||||
return trimmed;
|
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) {
|
function usesPamLockoutPolicy(pamText) {
|
||||||
if (!pamText)
|
if (!pamText)
|
||||||
return false;
|
return false;
|
||||||
@@ -221,6 +243,7 @@ Item {
|
|||||||
onLoaded: {
|
onLoaded: {
|
||||||
root.greetdPamText = text();
|
root.greetdPamText = text();
|
||||||
root.refreshPasswordAttemptPolicyHint();
|
root.refreshPasswordAttemptPolicyHint();
|
||||||
|
root.maybeAutoStartExternalAuth();
|
||||||
}
|
}
|
||||||
onLoadFailed: {
|
onLoadFailed: {
|
||||||
root.greetdPamText = "";
|
root.greetdPamText = "";
|
||||||
@@ -304,6 +327,7 @@ Item {
|
|||||||
GreeterState.usernameInput = lastUser;
|
GreeterState.usernameInput = lastUser;
|
||||||
GreeterState.showPasswordInput = true;
|
GreeterState.showPasswordInput = true;
|
||||||
PortalService.getGreeterUserProfileImage(lastUser);
|
PortalService.getGreeterUserProfileImage(lastUser);
|
||||||
|
maybeAutoStartExternalAuth();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,26 +338,92 @@ Item {
|
|||||||
if (GreeterState.username !== user) {
|
if (GreeterState.username !== user) {
|
||||||
passwordFailureCount = 0;
|
passwordFailureCount = 0;
|
||||||
clearAuthFeedback();
|
clearAuthFeedback();
|
||||||
|
externalAuthAutoStartedForUser = "";
|
||||||
}
|
}
|
||||||
GreeterState.username = user;
|
GreeterState.username = user;
|
||||||
GreeterState.showPasswordInput = true;
|
GreeterState.showPasswordInput = true;
|
||||||
PortalService.getGreeterUserProfileImage(user);
|
PortalService.getGreeterUserProfileImage(user);
|
||||||
GreeterState.passwordBuffer = "";
|
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() {
|
function startAuthSession() {
|
||||||
if (!GreeterState.showPasswordInput || !GreeterState.username)
|
if (!GreeterState.showPasswordInput || !GreeterState.username)
|
||||||
return;
|
return;
|
||||||
if (GreeterState.unlocking || Greetd.state !== GreetdState.Inactive)
|
if (GreeterState.unlocking)
|
||||||
return;
|
return;
|
||||||
if (!GreeterState.passwordBuffer || GreeterState.passwordBuffer.length === 0)
|
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;
|
return;
|
||||||
awaitingExternalAuth = false;
|
}
|
||||||
authTimeout.interval = defaultAuthTimeoutMs;
|
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();
|
authTimeout.restart();
|
||||||
Greetd.createSession(GreeterState.username);
|
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) {
|
function isExternalAuthPrompt(message, responseRequired) {
|
||||||
// Non-response PAM messages commonly represent waiting states (fprint/U2F/token touch).
|
// Non-response PAM messages commonly represent waiting states (fprint/U2F/token touch).
|
||||||
return !responseRequired;
|
return !responseRequired;
|
||||||
@@ -410,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);
|
||||||
@@ -426,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
|
||||||
@@ -594,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(Qt.locale(), GreetdSettings.getEffectiveLockDateFormat());
|
||||||
return systemClock.date.toLocaleDateString(Qt.locale(), GreetdSettings.lockDateFormat);
|
|
||||||
}
|
|
||||||
return systemClock.date.toLocaleDateString(Qt.locale(), Locale.LongFormat);
|
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeXLarge
|
font.pixelSize: Theme.fontSizeXLarge
|
||||||
color: "white"
|
color: "white"
|
||||||
@@ -666,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;
|
||||||
}
|
}
|
||||||
@@ -682,6 +803,8 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -723,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) {
|
||||||
@@ -738,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
|
||||||
|
|
||||||
@@ -760,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: {
|
||||||
@@ -790,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
|
||||||
|
|
||||||
@@ -807,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) {
|
||||||
@@ -826,7 +961,7 @@ 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) {
|
||||||
@@ -920,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 = "";
|
||||||
}
|
}
|
||||||
@@ -1418,28 +1554,45 @@ Item {
|
|||||||
enabled: isPrimaryScreen
|
enabled: isPrimaryScreen
|
||||||
|
|
||||||
function onAuthMessage(message, error, responseRequired, echoResponse) {
|
function onAuthMessage(message, error, responseRequired, echoResponse) {
|
||||||
awaitingExternalAuth = root.isExternalAuthPrompt(message, responseRequired);
|
|
||||||
authTimeout.interval = awaitingExternalAuth ? externalAuthTimeoutMs : defaultAuthTimeoutMs;
|
|
||||||
authTimeout.restart();
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
if (!passwordSubmitRequested)
|
||||||
|
awaitingExternalAuth = root.isExternalAuthPrompt(message, responseRequired);
|
||||||
|
authTimeout.interval = awaitingExternalAuth ? externalAuthTimeoutMs : defaultAuthTimeoutMs;
|
||||||
|
authTimeout.restart();
|
||||||
Greetd.respond("");
|
Greetd.respond("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function onStateChanged() {
|
function onStateChanged() {
|
||||||
if (Greetd.state === GreetdState.Inactive) {
|
if (Greetd.state === GreetdState.Inactive) {
|
||||||
|
const resumePasswordSubmit = cancelingExternalAuthForPassword && passwordSubmitRequested && GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0;
|
||||||
awaitingExternalAuth = false;
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
cancelingExternalAuthForPassword = false;
|
||||||
authTimeout.interval = defaultAuthTimeoutMs;
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
authTimeout.stop();
|
authTimeout.stop();
|
||||||
|
if (resumePasswordSubmit) {
|
||||||
|
Qt.callLater(root.startAuthSession);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
passwordSubmitRequested = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onReadyToLaunch() {
|
function onReadyToLaunch() {
|
||||||
awaitingExternalAuth = false;
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
passwordSubmitRequested = false;
|
||||||
|
cancelingExternalAuthForPassword = false;
|
||||||
authTimeout.interval = defaultAuthTimeoutMs;
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
authTimeout.stop();
|
authTimeout.stop();
|
||||||
passwordFailureCount = 0;
|
passwordFailureCount = 0;
|
||||||
@@ -1470,6 +1623,9 @@ Item {
|
|||||||
|
|
||||||
function onAuthFailure(message) {
|
function onAuthFailure(message) {
|
||||||
awaitingExternalAuth = false;
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
passwordSubmitRequested = false;
|
||||||
|
cancelingExternalAuthForPassword = false;
|
||||||
authTimeout.interval = defaultAuthTimeoutMs;
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
authTimeout.stop();
|
authTimeout.stop();
|
||||||
launchTimeout.stop();
|
launchTimeout.stop();
|
||||||
@@ -1489,6 +1645,9 @@ Item {
|
|||||||
|
|
||||||
function onError(error) {
|
function onError(error) {
|
||||||
awaitingExternalAuth = false;
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
passwordSubmitRequested = false;
|
||||||
|
cancelingExternalAuthForPassword = false;
|
||||||
authTimeout.interval = defaultAuthTimeoutMs;
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
authTimeout.stop();
|
authTimeout.stop();
|
||||||
launchTimeout.stop();
|
launchTimeout.stop();
|
||||||
@@ -1509,6 +1668,9 @@ Item {
|
|||||||
if (GreeterState.unlocking || Greetd.state === GreetdState.Inactive)
|
if (GreeterState.unlocking || Greetd.state === GreetdState.Inactive)
|
||||||
return;
|
return;
|
||||||
awaitingExternalAuth = false;
|
awaitingExternalAuth = false;
|
||||||
|
pendingPasswordResponse = false;
|
||||||
|
passwordSubmitRequested = false;
|
||||||
|
cancelingExternalAuthForPassword = false;
|
||||||
authTimeout.interval = defaultAuthTimeoutMs;
|
authTimeout.interval = defaultAuthTimeoutMs;
|
||||||
GreeterState.pamState = "error";
|
GreeterState.pamState = "error";
|
||||||
authFeedbackMessage = currentAuthMessage();
|
authFeedbackMessage = currentAuthMessage();
|
||||||
@@ -1525,6 +1687,9 @@ 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();
|
authFeedbackMessage = currentAuthMessage();
|
||||||
|
|||||||
@@ -204,6 +204,14 @@ if [[ -n "$REMEMBER_LAST_USER" ]]; then
|
|||||||
fi
|
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.
|
# Keep greeter VT clean by default; callers can override via env or --debug.
|
||||||
if [[ -z "${RUST_LOG:-}" ]]; then
|
if [[ -z "${RUST_LOG:-}" ]]; then
|
||||||
|
|||||||
@@ -215,7 +215,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();
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -166,6 +166,114 @@ Item {
|
|||||||
visible: SettingsData.fprintdAvailable
|
visible: SettingsData.fprintdAvailable
|
||||||
onToggled: checked => SettingsData.set("enableFprint", checked)
|
onToggled: checked => SettingsData.set("enableFprint", checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "enableU2f"
|
||||||
|
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "fido", "authentication", "hardware"]
|
||||||
|
text: I18n.tr("Enable security key authentication", "Enable FIDO2/U2F hardware security key for lock screen")
|
||||||
|
description: SettingsData.u2fAvailable ? I18n.tr("Use a FIDO2/U2F security key (e.g. YubiKey) for lock screen authentication (requires enrolled keys)", "lock screen U2F security key setting") : I18n.tr("Not enrolled", "security key not detected status")
|
||||||
|
descriptionColor: SettingsData.u2fAvailable ? Theme.surfaceVariantText : Theme.warning
|
||||||
|
checked: SettingsData.enableU2f
|
||||||
|
enabled: SettingsData.u2fAvailable
|
||||||
|
onToggled: checked => SettingsData.set("enableU2f", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsDropdownRow {
|
||||||
|
settingKey: "u2fMode"
|
||||||
|
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "mode", "factor", "second"]
|
||||||
|
text: I18n.tr("Security key mode", "lock screen U2F security key mode setting")
|
||||||
|
description: I18n.tr("'Alternative' lets the key unlock on its own. 'Second factor' requires password or fingerprint first, then the key.", "lock screen U2F security key mode setting")
|
||||||
|
visible: SettingsData.u2fAvailable && SettingsData.enableU2f
|
||||||
|
options: [I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method"), I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint")]
|
||||||
|
currentValue: SettingsData.u2fMode === "and" ? I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint") : I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method")
|
||||||
|
onValueChanged: value => {
|
||||||
|
if (value === I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint"))
|
||||||
|
SettingsData.set("u2fMode", "and");
|
||||||
|
else
|
||||||
|
SettingsData.set("u2fMode", "or");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "movie"
|
||||||
|
title: I18n.tr("Video Screensaver")
|
||||||
|
settingKey: "videoScreensaver"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
visible: !MultimediaService.available
|
||||||
|
text: I18n.tr("QtMultimedia is not available - video screensaver requires qt multimedia services")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.warning
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "lockScreenVideoEnabled"
|
||||||
|
tags: ["lock", "screen", "video", "screensaver", "animation", "movie"]
|
||||||
|
text: I18n.tr("Enable Video Screensaver")
|
||||||
|
description: I18n.tr("Play a video when the screen locks.")
|
||||||
|
enabled: MultimediaService.available
|
||||||
|
checked: SettingsData.lockScreenVideoEnabled
|
||||||
|
onToggled: checked => SettingsData.set("lockScreenVideoEnabled", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
visible: SettingsData.lockScreenVideoEnabled && MultimediaService.available
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Video Path")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Path to a video file or folder containing videos")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.outlineVariant
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: videoPathField
|
||||||
|
width: parent.width - browseVideoButton.width - Theme.spacingS
|
||||||
|
placeholderText: I18n.tr("/path/to/videos")
|
||||||
|
text: SettingsData.lockScreenVideoPath
|
||||||
|
backgroundColor: Theme.surfaceContainerHighest
|
||||||
|
onTextChanged: {
|
||||||
|
if (text !== SettingsData.lockScreenVideoPath) {
|
||||||
|
SettingsData.set("lockScreenVideoPath", text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
id: browseVideoButton
|
||||||
|
text: I18n.tr("Browse")
|
||||||
|
onClicked: videoBrowserModal.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "lockScreenVideoCycling"
|
||||||
|
tags: ["lock", "screen", "video", "screensaver", "cycling", "random", "shuffle"]
|
||||||
|
text: I18n.tr("Automatic Cycling")
|
||||||
|
description: I18n.tr("Pick a different random video each time from the same folder")
|
||||||
|
visible: SettingsData.lockScreenVideoEnabled && MultimediaService.available
|
||||||
|
enabled: MultimediaService.available
|
||||||
|
checked: SettingsData.lockScreenVideoCycling
|
||||||
|
onToggled: checked => SettingsData.set("lockScreenVideoCycling", checked)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
|
|||||||
@@ -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