1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-03 20:32:07 -04:00

feat(Greeter): Add install/uninstall/activate cli commands & new UI opts

- AppArmor profile management
- Introduced `dms greeter uninstall` command to remove DMS greeter configuration and restore previous display manager.
- Implemented AppArmor profile installation and uninstallation for enhanced security.
This commit is contained in:
purian23
2026-03-07 20:44:19 -05:00
committed by bbedward
parent 7156e1e299
commit 31b415b086
7 changed files with 865 additions and 52 deletions

View File

@@ -4,6 +4,7 @@
"greeter install", "greeter install",
"greeter enable", "greeter enable",
"greeter sync", "greeter sync",
"greeter uninstall",
"setup" "setup"
], ],
"message": "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes." "message": "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes."

View File

@@ -1,11 +1,13 @@
package main package main
import ( import (
"bufio"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"os/user" "os/user"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros" "github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
@@ -29,7 +31,20 @@ var greeterInstallCmd = &cobra.Command{
Long: "Install greetd and configure it to use DMS as the greeter interface", Long: "Install greetd and configure it to use DMS as the greeter interface",
PreRunE: requireMutableSystemCommand, PreRunE: requireMutableSystemCommand,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if err := installGreeter(); err != nil { yes, _ := cmd.Flags().GetBool("yes")
term, _ := cmd.Flags().GetBool("terminal")
if term {
installCmd := "dms greeter install"
if yes {
installCmd += " --yes"
}
installCmd += "; echo; echo \"Install finished. Closing in 3 seconds...\"; sleep 3"
if err := runCommandInTerminal(installCmd); err != nil {
log.Fatalf("Error launching install in terminal: %v", err)
}
return
}
if err := installGreeter(yes); err != nil {
log.Fatalf("Error installing greeter: %v", err) log.Fatalf("Error installing greeter: %v", err)
} }
}, },
@@ -70,7 +85,20 @@ var greeterEnableCmd = &cobra.Command{
Long: "Configure greetd to use DMS as the greeter", Long: "Configure greetd to use DMS as the greeter",
PreRunE: requireMutableSystemCommand, PreRunE: requireMutableSystemCommand,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
if err := enableGreeter(); err != nil { yes, _ := cmd.Flags().GetBool("yes")
term, _ := cmd.Flags().GetBool("terminal")
if term {
enableCmd := "dms greeter enable"
if yes {
enableCmd += " --yes"
}
enableCmd += "; echo; echo \"Enable finished. Closing in 3 seconds...\"; sleep 3"
if err := runCommandInTerminal(enableCmd); err != nil {
log.Fatalf("Error launching enable in terminal: %v", err)
}
return
}
if err := enableGreeter(yes); err != nil {
log.Fatalf("Error enabling greeter: %v", err) log.Fatalf("Error enabling greeter: %v", err)
} }
}, },
@@ -87,18 +115,62 @@ var greeterStatusCmd = &cobra.Command{
}, },
} }
func installGreeter() error { var greeterUninstallCmd = &cobra.Command{
Use: "uninstall",
Short: "Remove DMS greeter configuration and restore previous display manager",
Long: "Disable greetd, remove DMS managed configs, and restore the system to its pre-DMS-greeter state",
PreRunE: requireMutableSystemCommand,
Run: func(cmd *cobra.Command, args []string) {
yes, _ := cmd.Flags().GetBool("yes")
term, _ := cmd.Flags().GetBool("terminal")
if term {
uninstallCmd := "dms greeter uninstall"
if yes {
uninstallCmd += " --yes"
}
uninstallCmd += "; echo; echo \"Uninstall finished. Closing in 3 seconds...\"; sleep 3"
if err := runCommandInTerminal(uninstallCmd); err != nil {
log.Fatalf("Error launching uninstall in terminal: %v", err)
}
return
}
if err := uninstallGreeter(yes); err != nil {
log.Fatalf("Error uninstalling greeter: %v", err)
}
},
}
func init() {
greeterInstallCmd.Flags().BoolP("yes", "y", false, "Non-interactive: skip confirmation prompt")
greeterInstallCmd.Flags().BoolP("terminal", "t", false, "Run in a new terminal (for entering sudo password)")
greeterEnableCmd.Flags().BoolP("yes", "y", false, "Non-interactive: skip confirmation prompt")
greeterEnableCmd.Flags().BoolP("terminal", "t", false, "Run in a new terminal (for entering sudo password)")
greeterUninstallCmd.Flags().BoolP("yes", "y", false, "Non-interactive: skip confirmation prompt")
greeterUninstallCmd.Flags().BoolP("terminal", "t", false, "Run in a new terminal (for entering sudo password)")
}
func installGreeter(nonInteractive bool) error {
fmt.Println("=== DMS Greeter Installation ===") fmt.Println("=== DMS Greeter Installation ===")
logFunc := func(msg string) { logFunc := func(msg string) {
fmt.Println(msg) fmt.Println(msg)
} }
if !nonInteractive {
fmt.Print("\nThis will install greetd (if needed), configure the DMS greeter, and enable it. Continue? [Y/n]: ")
var response string
fmt.Scanln(&response)
if strings.ToLower(strings.TrimSpace(response)) == "n" || strings.ToLower(strings.TrimSpace(response)) == "no" {
fmt.Println("Aborted.")
return nil
}
fmt.Println()
}
if err := greeter.EnsureGreetdInstalled(logFunc, ""); err != nil { if err := greeter.EnsureGreetdInstalled(logFunc, ""); err != nil {
return err return err
} }
// Debian/openSUSE
greeter.TryInstallGreeterPackage(logFunc, "") greeter.TryInstallGreeterPackage(logFunc, "")
if isPackageOnlyGreeterDistro() && !greeter.IsGreeterPackaged() { if isPackageOnlyGreeterDistro() && !greeter.IsGreeterPackaged() {
return fmt.Errorf("dms-greeter must be installed from distro packages on this distribution. %s", packageInstallHint()) return fmt.Errorf("dms-greeter must be installed from distro packages on this distribution. %s", packageInstallHint())
@@ -107,7 +179,6 @@ func installGreeter() error {
return fmt.Errorf("legacy manual wrapper detected at /usr/local/bin/dms-greeter; remove it before using packaged dms-greeter: sudo rm -f /usr/local/bin/dms-greeter") return fmt.Errorf("legacy manual wrapper detected at /usr/local/bin/dms-greeter; remove it before using packaged dms-greeter: sudo rm -f /usr/local/bin/dms-greeter")
} }
// If already fully configured, prompt the user
if isGreeterEnabled() { if isGreeterEnabled() {
fmt.Print("\nGreeter is already installed and configured. Re-run to re-sync settings and permissions? [Y/n]: ") fmt.Print("\nGreeter is already installed and configured. Re-run to re-sync settings and permissions? [Y/n]: ")
var response string var response string
@@ -156,8 +227,12 @@ func installGreeter() error {
return err return err
} }
fmt.Println("\nConfiguring AppArmor profile...")
if err := greeter.InstallAppArmorProfile(logFunc, ""); err != nil {
logFunc(fmt.Sprintf("⚠ AppArmor profile setup failed: %v", err))
}
fmt.Println("\nConfiguring greetd...") fmt.Println("\nConfiguring greetd...")
// Use empty path when packaged (greeter finds /usr/share/quickshell/dms-greeter); else use user's DMS path
greeterPathForConfig := "" greeterPathForConfig := ""
if !greeter.IsGreeterPackaged() { if !greeter.IsGreeterPackaged() {
greeterPathForConfig = dmsPath greeterPathForConfig = dmsPath
@@ -191,22 +266,225 @@ func installGreeter() error {
return nil return nil
} }
func syncInTerminal(nonInteractive bool, forceAuth bool, local bool) error { func uninstallGreeter(nonInteractive bool) error {
syncFlags := make([]string, 0, 3) fmt.Println("=== DMS Greeter Uninstall ===")
if nonInteractive {
syncFlags = append(syncFlags, "--yes") logFunc := func(msg string) { fmt.Println(msg) }
if !isGreeterEnabled() {
fmt.Println(" DMS greeter is not currently configured in /etc/greetd/config.toml.")
fmt.Println(" Nothing to undo for greetd configuration.")
} }
if forceAuth {
syncFlags = append(syncFlags, "--auth") if !nonInteractive {
fmt.Print("\nThis will:\n • Stop and disable greetd\n • Remove the DMS PAM managed block\n • Remove the DMS AppArmor profile\n • Restore the most recent pre-DMS greetd config (if available)\n\nContinue? [y/N]: ")
var response string
fmt.Scanln(&response)
if strings.ToLower(strings.TrimSpace(response)) != "y" {
fmt.Println("Aborted.")
return nil
}
} }
if local {
syncFlags = append(syncFlags, "--local") fmt.Println("\nStopping and disabling greetd...")
stopCmd := exec.Command("sudo", "systemctl", "stop", "greetd")
stopCmd.Stdout = os.Stdout
stopCmd.Stderr = os.Stderr
_ = stopCmd.Run() // not fatal — service may already be stopped
disableCmd := exec.Command("sudo", "systemctl", "disable", "greetd")
disableCmd.Stdout = os.Stdout
disableCmd.Stderr = os.Stderr
if err := disableCmd.Run(); err != nil {
fmt.Printf(" ⚠ Could not disable greetd: %v\n", err)
} else {
fmt.Println(" ✓ greetd stopped and disabled")
} }
shellSyncCmd := "dms greeter sync"
if len(syncFlags) > 0 { fmt.Println("\nRemoving DMS PAM configuration...")
shellSyncCmd += " " + strings.Join(syncFlags, " ") if err := greeter.RemoveGreeterPamManagedBlock(logFunc, ""); err != nil {
fmt.Printf(" ⚠ PAM cleanup failed: %v\n", err)
} }
shellCmd := shellSyncCmd + `; echo; echo "Sync finished. Closing in 3 seconds..."; sleep 3`
fmt.Println("\nRemoving DMS AppArmor profile...")
if err := greeter.UninstallAppArmorProfile(logFunc, ""); err != nil {
fmt.Printf(" ⚠ AppArmor cleanup failed: %v\n", err)
}
fmt.Println("\nRestoring greetd configuration...")
if err := restorePreDMSGreetdConfig(""); err != nil {
fmt.Printf(" ⚠ Could not restore previous greetd config: %v\n", err)
fmt.Println(" You may need to manually edit /etc/greetd/config.toml.")
}
fmt.Println("\nChecking for other display managers to re-enable...")
suggestDisplayManagerRestore(nonInteractive)
fmt.Println("\n=== Uninstall Complete ===")
fmt.Println("\nTo start a display manager, run e.g.:")
fmt.Println(" sudo systemctl enable --now gdm (or lightdm, sddm, etc.)")
fmt.Println("\nTo re-enable DMS greeter at any time, run: dms greeter install")
return nil
}
// restorePreDMSGreetdConfig finds the most recent timestamped backup of
// /etc/greetd/config.toml that does not reference dms-greeter and restores it.
// If no such backup exists, a minimal passthrough config is written so greetd
// can at least be started without error (users must configure it themselves).
func restorePreDMSGreetdConfig(sudoPassword string) error {
const configPath = "/etc/greetd/config.toml"
const backupGlob = "/etc/greetd/config.toml.backup-*"
matches, _ := filepath.Glob(backupGlob)
for i := 0; i < len(matches)-1; i++ {
for j := i + 1; j < len(matches); j++ {
if matches[j] > matches[i] {
matches[i], matches[j] = matches[j], matches[i]
}
}
}
for _, candidate := range matches {
data, err := os.ReadFile(candidate)
if err != nil {
continue
}
if strings.Contains(string(data), "dms-greeter") {
continue
}
tmp, err := os.CreateTemp("", "greetd-restore-*")
if err != nil {
return fmt.Errorf("could not create temp file: %w", err)
}
tmpPath := tmp.Name()
defer os.Remove(tmpPath)
if _, err := tmp.Write(data); err != nil {
tmp.Close()
return err
}
tmp.Close()
if err := runSudoCommand(sudoPassword, "cp", tmpPath, configPath); err != nil {
return fmt.Errorf("failed to restore %s: %w", candidate, err)
}
if err := runSudoCommand(sudoPassword, "chmod", "644", configPath); err != nil {
return err
}
fmt.Printf(" ✓ Restored greetd config from %s\n", candidate)
return nil
}
minimal := `[terminal]
vt = 1
# DMS greeter has been uninstalled.
# Configure a greeter command here or re-enable a display manager.
[default_session]
user = "greeter"
command = "agreety --cmd /bin/bash"
`
tmp, err := os.CreateTemp("", "greetd-minimal-*")
if err != nil {
return fmt.Errorf("could not create temp file: %w", err)
}
tmpPath := tmp.Name()
defer os.Remove(tmpPath)
if _, err := tmp.WriteString(minimal); err != nil {
tmp.Close()
return err
}
tmp.Close()
if err := runSudoCommand(sudoPassword, "cp", tmpPath, configPath); err != nil {
return fmt.Errorf("failed to write fallback greetd config: %w", err)
}
_ = runSudoCommand(sudoPassword, "chmod", "644", configPath)
fmt.Println(" ✓ Wrote minimal fallback greetd config (configure a greeter command manually if needed)")
return nil
}
func runSudoCommand(_ string, command string, args ...string) error {
cmd := exec.Command("sudo", append([]string{command}, args...)...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
// suggestDisplayManagerRestore scans for installed DMs and re-enables one
func suggestDisplayManagerRestore(nonInteractive bool) {
knownDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"}
var found []string
for _, dm := range knownDMs {
if utils.CommandExists(dm) || isSystemdUnitInstalled(dm) {
found = append(found, dm)
}
}
if len(found) == 0 {
fmt.Println(" No other display managers detected.")
fmt.Println(" You can install one (e.g. gdm, lightdm, sddm) and then run:")
fmt.Println(" sudo systemctl enable --now <dm-name>")
return
}
enableDM := func(dm string) {
fmt.Printf(" Enabling %s...\n", dm)
cmd := exec.Command("sudo", "systemctl", "enable", "--force", dm)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Printf(" ⚠ Failed to enable %s: %v\n", dm, err)
} else {
fmt.Printf(" ✓ %s enabled (will take effect on next boot).\n", dm)
}
}
if len(found) == 1 || nonInteractive {
chosen := found[0]
if len(found) > 1 {
fmt.Printf(" Multiple display managers found (%s); enabling %s automatically.\n",
strings.Join(found, ", "), chosen)
} else {
fmt.Printf(" Found display manager: %s\n", chosen)
}
enableDM(chosen)
return
}
fmt.Println(" Found the following display managers:")
for i, dm := range found {
fmt.Printf(" %d) %s\n", i+1, dm)
}
fmt.Print(" Choose a number to re-enable (or press Enter to skip): ")
scanner := bufio.NewScanner(os.Stdin)
if !scanner.Scan() {
return
}
input := strings.TrimSpace(scanner.Text())
if input == "" {
fmt.Println(" Skipped. You can re-enable a display manager later with:")
fmt.Println(" sudo systemctl enable --now <dm-name>")
return
}
n, err := strconv.Atoi(input)
if err != nil || n < 1 || n > len(found) {
fmt.Printf(" Invalid selection %q — skipping.\n", input)
return
}
enableDM(found[n-1])
}
func isSystemdUnitInstalled(unit string) bool {
cmd := exec.Command("systemctl", "list-unit-files", unit+".service", "--no-legend", "--no-pager")
out, err := cmd.Output()
return err == nil && strings.Contains(string(out), unit)
}
func runCommandInTerminal(shellCmd string) error {
terminals := []struct { terminals := []struct {
name string name string
args []string args []string
@@ -227,15 +505,33 @@ func syncInTerminal(nonInteractive bool, forceAuth bool, local bool) error {
cmd := exec.Command(t.name, t.args...) cmd := exec.Command(t.name, t.args...)
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil { if err := cmd.Run(); err != nil {
continue return err
} }
_ = cmd.Process.Release()
return nil return nil
} }
return fmt.Errorf("no terminal emulator found (tried: gnome-terminal, konsole, xfce4-terminal, ghostty, wezterm, alacritty, kitty, xterm)") return fmt.Errorf("no terminal emulator found (tried: gnome-terminal, konsole, xfce4-terminal, ghostty, wezterm, alacritty, kitty, xterm)")
} }
func syncInTerminal(nonInteractive bool, forceAuth bool, local bool) error {
syncFlags := make([]string, 0, 3)
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`
return runCommandInTerminal(shellCmd)
}
func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error { func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
if !nonInteractive { if !nonInteractive {
fmt.Println("=== DMS Greeter Theme Sync ===") fmt.Println("=== DMS Greeter Theme Sync ===")
@@ -281,7 +577,7 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
response = strings.ToLower(strings.TrimSpace(response)) response = strings.ToLower(strings.TrimSpace(response))
if response != "n" && response != "no" { if response != "n" && response != "no" {
if err := enableGreeter(); err != nil { if err := enableGreeter(false); err != nil {
return err return err
} }
} else { } else {
@@ -417,6 +713,11 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
return err return err
} }
fmt.Println("\nConfiguring AppArmor profile...")
if err := greeter.InstallAppArmorProfile(logFunc, ""); err != nil {
logFunc(fmt.Sprintf("⚠ AppArmor profile setup failed: %v", 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 { if forceAuth {
@@ -709,7 +1010,7 @@ func handleConflictingDisplayManagers() error {
return nil return nil
} }
func enableGreeter() error { func enableGreeter(nonInteractive bool) error {
fmt.Println("=== DMS Greeter Enable ===") fmt.Println("=== DMS Greeter Enable ===")
fmt.Println() fmt.Println()
@@ -762,6 +1063,17 @@ func enableGreeter() error {
return nil return nil
} }
if !nonInteractive {
fmt.Print("\nThis will configure greetd to use the DMS greeter and may disable other display managers. Continue? [Y/n]: ")
var response string
fmt.Scanln(&response)
if strings.ToLower(strings.TrimSpace(response)) == "n" || strings.ToLower(strings.TrimSpace(response)) == "no" {
fmt.Println("Aborted.")
return nil
}
fmt.Println()
}
fmt.Println("Detecting installed compositors...") fmt.Println("Detecting installed compositors...")
compositors := greeter.DetectCompositors() compositors := greeter.DetectCompositors()
@@ -802,6 +1114,10 @@ func enableGreeter() error {
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) 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 := greeter.InstallAppArmorProfile(logFunc, ""); err != nil {
logFunc(fmt.Sprintf("⚠ AppArmor profile setup failed: %v", err))
}
if err := ensureGraphicalTarget(); err != nil { if err := ensureGraphicalTarget(); err != nil {
return err return err
} }
@@ -1086,7 +1402,7 @@ 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, or use the Activate button in Settings → Greeter, then Sync.")
allGood = false allGood = false
} }
} else { } else {
@@ -1253,6 +1569,47 @@ func checkGreeterStatus() error {
} }
} }
fmt.Println("\nSecurity (AppArmor):")
appArmorEnabled, appArmorErr := isAppArmorEnabled()
if appArmorErr != nil {
fmt.Printf(" Could not determine AppArmor status: %v\n", appArmorErr)
} else if !appArmorEnabled {
fmt.Println(" AppArmor not enabled")
} else {
fmt.Println(" AppArmor is enabled")
const appArmorProfilePath = "/etc/apparmor.d/usr.bin.dms-greeter"
if _, err := os.Stat(appArmorProfilePath); os.IsNotExist(err) {
fmt.Println(" ⚠ DMS AppArmor profile not installed")
fmt.Println(" Run 'dms greeter sync' to install it and prevent potential TTY fallback")
allGood = false
} else {
mode := appArmorProfileMode("dms-greeter")
if mode != "" {
fmt.Printf(" ✓ DMS AppArmor profile installed (%s mode)\n", mode)
} else {
fmt.Println(" ✓ DMS AppArmor profile installed")
}
}
denialCount, denialSamples, denialErr := recentAppArmorGreeterDenials(3)
if denialErr != nil {
fmt.Printf(" Could not inspect AppArmor denials automatically: %v\n", denialErr)
fmt.Println(" If greetd falls back to TTY, run: sudo journalctl -b -k | grep 'apparmor.*DENIED'")
} else if denialCount > 0 {
fmt.Printf(" ⚠ Found %d recent AppArmor denial(s) related to greeter runtime.\n", denialCount)
fmt.Println(" This can cause greetd fallback to TTY (for example: 'Failed to create stream fd: Permission denied').")
fmt.Println(" Review denials with: sudo journalctl -b -k | grep 'apparmor.*DENIED'")
fmt.Println(" Then refine the profile with: sudo aa-logprof")
for i, sample := range denialSamples {
fmt.Printf(" %d) %s\n", i+1, sample)
}
allGood = false
} else {
fmt.Println(" ✓ No recent AppArmor denials detected for common greeter components")
}
}
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.")
@@ -1264,3 +1621,129 @@ func checkGreeterStatus() error {
return nil return nil
} }
func isAppArmorEnabled() (bool, error) {
data, err := os.ReadFile("/sys/module/apparmor/parameters/enabled")
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
value := strings.TrimSpace(strings.ToLower(string(data)))
return strings.HasPrefix(value, "y"), nil
}
func recentAppArmorGreeterDenials(sampleLimit int) (int, []string, error) {
if sampleLimit <= 0 {
sampleLimit = 3
}
if !utils.CommandExists("journalctl") {
return 0, nil, fmt.Errorf("journalctl not found")
}
queries := [][]string{
{"-b", "-k", "--no-pager", "-n", "2000", "-o", "cat"},
{"-b", "--no-pager", "-n", "2000", "-o", "cat"},
}
seen := make(map[string]bool)
samples := make([]string, 0, sampleLimit)
total := 0
var lastErr error
successfulQuery := false
for _, query := range queries {
cmd := exec.Command("journalctl", query...)
output, err := cmd.CombinedOutput()
if err != nil {
lastErr = err
continue
}
successfulQuery = true
total += collectGreeterAppArmorDenials(string(output), seen, &samples, sampleLimit)
}
if !successfulQuery && lastErr != nil {
return 0, nil, lastErr
}
return total, samples, nil
}
func collectGreeterAppArmorDenials(text string, seen map[string]bool, samples *[]string, sampleLimit int) int {
count := 0
for _, rawLine := range strings.Split(text, "\n") {
line := strings.TrimSpace(rawLine)
if line == "" || !isGreeterRelatedAppArmorDenial(line) {
continue
}
if seen[line] {
continue
}
seen[line] = true
count++
if len(*samples) < sampleLimit {
*samples = append(*samples, line)
}
}
return count
}
func isGreeterRelatedAppArmorDenial(line string) bool {
lower := strings.ToLower(line)
if !strings.Contains(lower, "apparmor") || !strings.Contains(lower, "denied") {
return false
}
greeterTokens := []string{
"dms-greeter",
"/usr/bin/dms-greeter",
"greetd",
"quickshell",
"/usr/bin/qs",
"/usr/bin/quickshell",
"niri",
"hyprland",
"sway",
"mango",
"miracle",
"labwc",
"pipewire",
"wireplumber",
"stream fd",
}
for _, token := range greeterTokens {
if strings.Contains(lower, token) {
return true
}
}
return false
}
// appArmorProfileMode returns "complain", "enforce", or "" (unknown) for a named AppArmor
// profile by reading /sys/kernel/security/apparmor/profiles.
func appArmorProfileMode(profileName string) string {
data, err := os.ReadFile("/sys/kernel/security/apparmor/profiles")
if err != nil {
return ""
}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if !strings.Contains(line, profileName) {
continue
}
lower := strings.ToLower(line)
if strings.Contains(lower, "(complain)") {
return "complain"
}
if strings.Contains(lower, "(enforce)") {
return "enforce"
}
if strings.Contains(lower, "(kill)") {
return "kill"
}
}
return ""
}

View File

@@ -16,19 +16,10 @@ func init() {
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)") runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().MarkHidden("daemon-child") runCmd.Flags().MarkHidden("daemon-child")
// Add subcommands to greeter greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
// Add subcommands to setup
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
// Add subcommands to update
updateCmd.AddCommand(updateCheckCmd) updateCmd.AddCommand(updateCheckCmd)
// Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
// Add common commands to root
rootCmd.AddCommand(getCommonCommands()...) rootCmd.AddCommand(getCommonCommands()...)
rootCmd.AddCommand(updateCmd) rootCmd.AddCommand(updateCmd)

View File

@@ -11,29 +11,20 @@ import (
var Version = "dev" var Version = "dev"
func init() { func init() {
// Add flags
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode") runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process") runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)") runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
runCmd.Flags().MarkHidden("daemon-child") runCmd.Flags().MarkHidden("daemon-child")
// Add subcommands to greeter greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
// Add subcommands to setup
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
// Add subcommands to plugins
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
// Add common commands to root
rootCmd.AddCommand(getCommonCommands()...) rootCmd.AddCommand(getCommonCommands()...)
rootCmd.SetHelpTemplate(getHelpTemplate()) rootCmd.SetHelpTemplate(getHelpTemplate())
} }
func main() { func main() {
// Block root
if os.Geteuid() == 0 { if os.Geteuid() == 0 {
log.Fatal("This program should not be run as root. Exiting.") log.Fatal("This program should not be run as root. Exiting.")
} }

View File

@@ -0,0 +1,91 @@
# AppArmor profile for dms-greeter
#
# Managed by DMS — regenerated on every `dms greeter install` / `dms greeter sync`.
# Manual edits will be overwritten on next sync.
#
# Mode: complain (denials are logged, nothing is blocked)
# To switch to enforce after validating with `aa-logprof`:
# sudo aa-enforce /etc/apparmor.d/usr.bin.dms-greeter
#
#include <tunables/global>
profile dms-greeter /usr/bin/dms-greeter flags=(complain) {
#include <abstractions/base>
#include <abstractions/bash>
# The launcher script itself
/usr/bin/dms-greeter r,
# Cache directory — created by dms greeter sync/enable with greeter:greeter ownership
/var/cache/dms-greeter/ rw,
/var/cache/dms-greeter/** rwlk,
# DMS config — packaged path
/usr/share/quickshell/dms-greeter/ r,
/usr/share/quickshell/dms-greeter/** r,
/usr/share/quickshell/ r,
/usr/share/quickshell/** r,
# DMS config — system and user overrides
/etc/dms/ r,
/etc/dms/** r,
/usr/share/dms/ r,
/usr/share/dms/** r,
/home/*/.config/quickshell/ r,
/home/*/.config/quickshell/** r,
/root/.config/quickshell/ r,
/root/.config/quickshell/** r,
# greetd / PAM — read-only for session setup
/etc/greetd/ r,
/etc/greetd/** r,
/etc/pam.d/ r,
/etc/pam.d/** r,
/usr/lib/pam.d/ r,
/usr/lib/pam.d/** r,
# Compositor binaries — run unconfined so each compositor uses its own profile
/usr/bin/niri Ux,
/usr/bin/hyprland Ux,
/usr/bin/Hyprland Ux,
/usr/bin/sway Ux,
/usr/bin/labwc Ux,
/usr/bin/scroll Ux,
/usr/bin/miracle-wm Ux,
/usr/bin/mango Ux,
# Quickshell — run unconfined (has its own compositor profile on some distros)
/usr/bin/qs Ux,
/usr/bin/quickshell Ux,
# Wayland / XDG runtime (pipewire, wireplumber, wayland socket)
/run/user/[0-9]*/ rw,
/run/user/[0-9]*/** rw,
# DRM / GPU devices (required for Wayland compositor startup)
/dev/dri/ r,
/dev/dri/* rw,
/dev/udmabuf rw,
# Input devices
/dev/input/ r,
/dev/input/* r,
# Systemd journal / logging
/run/systemd/journal/socket rw,
/dev/log rw,
# Shell helper binaries invoked by the launcher script
/usr/bin/env ix,
/usr/bin/mkdir ix,
/usr/bin/cat ix,
/usr/bin/grep ix,
/usr/bin/dirname ix,
/usr/bin/basename ix,
/usr/bin/command ix,
/bin/env ix,
/bin/mkdir ix,
# Signal management (compositor lifecycle)
signal (send, receive) set=("term", "int", "hup", "kill"),
}

View File

@@ -3,6 +3,7 @@ package greeter
import ( import (
"bufio" "bufio"
"context" "context"
_ "embed"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
@@ -18,6 +19,10 @@ import (
"github.com/sblinch/kdl-go/document" "github.com/sblinch/kdl-go/document"
) )
var appArmorProfileData []byte
const appArmorProfileDest = "/etc/apparmor.d/usr.bin.dms-greeter"
const ( const (
GreeterCacheDir = "/var/cache/dms-greeter" GreeterCacheDir = "/var/cache/dms-greeter"
@@ -527,7 +532,6 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
return fmt.Errorf("failed to make wrapper executable: %w", err) return fmt.Errorf("failed to make wrapper executable: %w", err)
} }
// Set SELinux context on Fedora and openSUSE
osInfo, err := distros.GetOSInfo() osInfo, err := distros.GetOSInfo()
if err == nil { if err == nil {
if config, exists := distros.Registry[osInfo.Distribution.ID]; exists && (config.Family == distros.FamilyFedora || config.Family == distros.FamilySUSE) { if config, exists := distros.Registry[osInfo.Distribution.ID]; exists && (config.Family == distros.FamilyFedora || config.Family == distros.FamilySUSE) {
@@ -579,6 +583,134 @@ func EnsureGreeterCacheDir(logFunc func(string), sudoPassword string) error {
return nil return nil
} }
// InstallAppArmorProfile writes the bundled AppArmor profile for dms-greeter and reloads
// it with apparmor_parser. It is safe to call multiple times (idempotent reload).
//
// Skipped silently when:
// - AppArmor kernel module is absent (/sys/module/apparmor does not exist)
// - Running on NixOS (profiles are managed via security.apparmor.policies)
// - SELinux is active (/sys/fs/selinux/enforce exists and equals "1") — Fedora/RHEL
func InstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
if IsNixOS() {
logFunc(" Skipping AppArmor profile on NixOS (manage via security.apparmor.policies)")
return nil
}
if _, err := os.Stat("/sys/module/apparmor"); os.IsNotExist(err) {
return nil
}
if data, err := os.ReadFile("/sys/fs/selinux/enforce"); err == nil {
if strings.TrimSpace(string(data)) == "1" {
return nil
}
}
if err := runSudoCmd(sudoPassword, "mkdir", "-p", "/etc/apparmor.d"); err != nil {
return fmt.Errorf("failed to create /etc/apparmor.d: %w", err)
}
tmp, err := os.CreateTemp("", "dms-apparmor-*")
if err != nil {
return fmt.Errorf("failed to create temp file for AppArmor profile: %w", err)
}
tmpPath := tmp.Name()
defer os.Remove(tmpPath)
if _, err := tmp.Write(appArmorProfileData); err != nil {
tmp.Close()
return fmt.Errorf("failed to write AppArmor profile: %w", err)
}
tmp.Close()
if err := runSudoCmd(sudoPassword, "cp", tmpPath, appArmorProfileDest); err != nil {
return fmt.Errorf("failed to install AppArmor profile to %s: %w", appArmorProfileDest, err)
}
if err := runSudoCmd(sudoPassword, "chmod", "644", appArmorProfileDest); err != nil {
return fmt.Errorf("failed to set AppArmor profile permissions: %w", err)
}
if utils.CommandExists("apparmor_parser") {
if err := runSudoCmd(sudoPassword, "apparmor_parser", "-r", appArmorProfileDest); err != nil {
logFunc(fmt.Sprintf(" ⚠ AppArmor profile installed but reload failed: %v", err))
logFunc(" Run: sudo apparmor_parser -r " + appArmorProfileDest)
} else {
logFunc(" ✓ AppArmor profile installed and loaded (complain mode)")
}
} else {
logFunc(" ✓ AppArmor profile installed at " + appArmorProfileDest)
logFunc(" apparmor_parser not found — profile will be loaded on next boot")
}
return nil
}
// RemoveGreeterPamManagedBlock strips the DMS managed auth block from /etc/pam.d/greetd
func RemoveGreeterPamManagedBlock(logFunc func(string), sudoPassword string) error {
if IsNixOS() {
return nil
}
const greetdPamPath = "/etc/pam.d/greetd"
data, err := os.ReadFile(greetdPamPath)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("failed to read %s: %w", greetdPamPath, err)
}
stripped, removed := stripManagedGreeterPamBlock(string(data))
strippedAgain, removedLegacy := stripLegacyGreeterPamLines(stripped)
if !removed && !removedLegacy {
return nil
}
tmp, err := os.CreateTemp("", "dms-pam-greetd-*")
if err != nil {
return fmt.Errorf("failed to create temp PAM file: %w", err)
}
tmpPath := tmp.Name()
defer os.Remove(tmpPath)
if _, err := tmp.WriteString(strippedAgain); err != nil {
tmp.Close()
return fmt.Errorf("failed to write temp PAM file: %w", err)
}
tmp.Close()
if err := runSudoCmd(sudoPassword, "cp", tmpPath, greetdPamPath); err != nil {
return fmt.Errorf("failed to write PAM config: %w", err)
}
if err := runSudoCmd(sudoPassword, "chmod", "644", greetdPamPath); err != nil {
return fmt.Errorf("failed to set PAM config permissions: %w", err)
}
logFunc(" ✓ Removed DMS managed PAM block from " + greetdPamPath)
return nil
}
// UninstallAppArmorProfile removes the DMS AppArmor profile and reloads AppArmor.
// It is a no-op when AppArmor is not active or the profile does not exist.
func UninstallAppArmorProfile(logFunc func(string), sudoPassword string) error {
if IsNixOS() {
return nil
}
if _, err := os.Stat("/sys/module/apparmor"); os.IsNotExist(err) {
return nil
}
if _, err := os.Stat(appArmorProfileDest); os.IsNotExist(err) {
return nil
}
if utils.CommandExists("apparmor_parser") {
_ = runSudoCmd(sudoPassword, "apparmor_parser", "--remove", appArmorProfileDest)
}
if err := runSudoCmd(sudoPassword, "rm", "-f", appArmorProfileDest); err != nil {
return fmt.Errorf("failed to remove AppArmor profile: %w", err)
}
logFunc(" ✓ Removed DMS AppArmor profile")
return nil
}
// EnsureACLInstalled installs the acl package (setfacl/getfacl) if not already present // EnsureACLInstalled installs the acl package (setfacl/getfacl) if not already present
func EnsureACLInstalled(logFunc func(string), sudoPassword string) error { func EnsureACLInstalled(logFunc func(string), sudoPassword string) error {
if utils.CommandExists("setfacl") { if utils.CommandExists("setfacl") {
@@ -661,7 +793,6 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
return nil return nil
} }
if !utils.CommandExists("setfacl") { if !utils.CommandExists("setfacl") {
// setfacl still not found after install attempt (e.g. unsupported filesystem)
logFunc("⚠ Warning: setfacl still not available after install attempt; skipping ACL setup.") logFunc("⚠ Warning: setfacl still not available after install attempt; skipping ACL setup.")
return nil return nil
} }
@@ -723,7 +854,6 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
group := DetectGreeterGroup() group := DetectGreeterGroup()
// Check if user is already in greeter group
groupsCmd := exec.Command("groups", currentUser) groupsCmd := exec.Command("groups", currentUser)
groupsOutput, err := groupsCmd.Output() groupsOutput, err := groupsCmd.Output()
if err == nil && strings.Contains(string(groupsOutput), group) { if err == nil && strings.Contains(string(groupsOutput), group) {
@@ -1273,7 +1403,6 @@ func ensureGreetdNiriConfig(logFunc func(string), sudoPassword string, niriConfi
if !strings.Contains(command, "--command niri") { if !strings.Contains(command, "--command niri") {
continue continue
} }
// Strip existing -C or --config and their arguments
command = stripConfigFlag(command) command = stripConfigFlag(command)
command = stripCacheDirFlag(command) command = stripCacheDirFlag(command)
command = strings.TrimSpace(command + " --cache-dir " + GreeterCacheDir) command = strings.TrimSpace(command + " --cache-dir " + GreeterCacheDir)

View File

@@ -1,9 +1,11 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import qs.Modals.Common
import qs.Modals.FileBrowser import qs.Modals.FileBrowser
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -12,6 +14,10 @@ import qs.Modules.Settings.Widgets
Item { Item {
id: root id: root
ConfirmModal {
id: greeterActionConfirm
}
FileBrowserModal { FileBrowserModal {
id: greeterWallpaperBrowserModal id: greeterWallpaperBrowserModal
browserTitle: I18n.tr("Select greeter background image") browserTitle: I18n.tr("Select greeter background image")
@@ -28,6 +34,7 @@ Item {
property string greeterStatusText: "" property string greeterStatusText: ""
property bool greeterStatusRunning: false property bool greeterStatusRunning: false
property bool greeterSyncRunning: false property bool greeterSyncRunning: false
property bool greeterInstallActionRunning: false
property string greeterStatusStdout: "" property string greeterStatusStdout: ""
property string greeterStatusStderr: "" property string greeterStatusStderr: ""
property string greeterSyncStdout: "" property string greeterSyncStdout: ""
@@ -37,6 +44,31 @@ Item {
property bool greeterTerminalFallbackFromPrecheck: false property bool greeterTerminalFallbackFromPrecheck: false
property var cachedFontFamilies: [] property var cachedFontFamilies: []
property bool fontsEnumerated: false property bool fontsEnumerated: false
property bool greeterBinaryExists: false
property bool greeterEnabled: false
readonly property bool greeterInstalled: greeterBinaryExists || greeterEnabled
readonly property string greeterActionLabel: {
if (!root.greeterInstalled) return I18n.tr("Install");
if (!root.greeterEnabled) return I18n.tr("Activate");
return I18n.tr("Uninstall");
}
readonly property string greeterActionIcon: {
if (!root.greeterInstalled) return "download";
if (!root.greeterEnabled) return "login";
return "delete";
}
readonly property var greeterActionCommand: {
if (!root.greeterInstalled) return ["dms", "greeter", "install", "--terminal"];
if (!root.greeterEnabled) return ["dms", "greeter", "enable", "--terminal"];
return ["dms", "greeter", "uninstall", "--terminal", "--yes"];
}
property string greeterPendingAction: ""
function checkGreeterInstallState() {
greetdEnabledCheckProcess.running = true;
greeterBinaryCheckProcess.running = true;
}
function runGreeterStatus() { function runGreeterStatus() {
greeterStatusText = ""; greeterStatusText = "";
@@ -46,6 +78,41 @@ Item {
greeterStatusProcess.running = true; greeterStatusProcess.running = true;
} }
function runGreeterInstallAction() {
root.greeterPendingAction = !root.greeterInstalled ? "install"
: !root.greeterEnabled ? "activate"
: "uninstall";
greeterStatusText = I18n.tr("Opening terminal: ") + root.greeterActionLabel + "…";
greeterInstallActionRunning = true;
greeterInstallActionProcess.running = true;
}
function promptGreeterActionConfirm() {
var title, message, confirmText;
if (!root.greeterInstalled) {
title = I18n.tr("Install Greeter", "greeter action confirmation");
message = I18n.tr("Install the DMS greeter? A terminal will open for sudo authentication.");
confirmText = I18n.tr("Install");
} else if (!root.greeterEnabled) {
title = I18n.tr("Activate Greeter", "greeter action confirmation");
message = I18n.tr("Activate the DMS greeter? A terminal will open for sudo authentication. Run Sync after activation to apply your settings.");
confirmText = I18n.tr("Activate");
} else {
title = I18n.tr("Uninstall Greeter", "greeter action confirmation");
message = I18n.tr("Uninstall the DMS greeter? This will remove configuration and restore your previous display manager. A terminal will open for sudo authentication.");
confirmText = I18n.tr("Uninstall");
}
greeterActionConfirm.showWithOptions({
"title": title,
"message": message,
"confirmText": confirmText,
"cancelText": I18n.tr("Cancel"),
"confirmColor": Theme.primary,
"onConfirm": () => root.runGreeterInstallAction(),
"onCancel": () => {}
});
}
function runGreeterSync() { function runGreeterSync() {
greeterSyncStdout = ""; greeterSyncStdout = "";
greeterSyncStderr = ""; greeterSyncStderr = "";
@@ -82,7 +149,30 @@ Item {
fontsEnumerated = true; fontsEnumerated = true;
} }
Component.onCompleted: Qt.callLater(enumerateFonts) Component.onCompleted: {
Qt.callLater(enumerateFonts);
Qt.callLater(checkGreeterInstallState);
}
Process {
id: greetdEnabledCheckProcess
command: ["systemctl", "is-enabled", "greetd"]
running: false
stdout: StdioCollector {
onStreamFinished: root.greeterEnabled = text.trim() === "enabled"
}
}
Process {
id: greeterBinaryCheckProcess
command: ["sh", "-c", "test -f /usr/bin/dms-greeter || test -f /usr/local/bin/dms-greeter"]
running: false
onExited: exitCode => {
root.greeterBinaryExists = (exitCode === 0);
}
}
Process { Process {
id: greeterStatusProcess id: greeterStatusProcess
@@ -202,6 +292,29 @@ Item {
} }
} }
Process {
id: greeterInstallActionProcess
command: root.greeterActionCommand
running: false
onExited: exitCode => {
root.greeterInstallActionRunning = false;
const pending = root.greeterPendingAction;
root.greeterPendingAction = "";
if (exitCode === 0) {
if (pending === "install")
root.greeterStatusText = I18n.tr("Install complete. Greeter has been installed.");
else if (pending === "activate")
root.greeterStatusText = I18n.tr("Greeter activated. greetd is now enabled.");
else
root.greeterStatusText = I18n.tr("Uninstall complete. Greeter has been removed.");
} else {
root.greeterStatusText = I18n.tr("Action failed or terminal was closed.") + " (exit " + exitCode + ")";
}
root.checkGreeterInstallState();
}
}
readonly property var _lockDateFormatPresets: [ readonly property var _lockDateFormatPresets: [
{ {
format: "", format: "",
@@ -293,14 +406,26 @@ Item {
} }
} }
Row { Item { width: 1; height: Theme.spacingM }
RowLayout {
width: parent.width width: parent.width
spacing: Theme.spacingS spacing: Theme.spacingS
topPadding: Theme.spacingM
DankButton {
text: root.greeterActionLabel
iconName: root.greeterActionIcon
horizontalPadding: Theme.spacingL
onClicked: root.promptGreeterActionConfirm()
enabled: !root.greeterInstallActionRunning && !root.greeterSyncRunning
}
Item { Layout.fillWidth: true }
DankButton { DankButton {
text: I18n.tr("Refresh") text: I18n.tr("Refresh")
iconName: "refresh" iconName: "refresh"
horizontalPadding: Theme.spacingL
onClicked: root.runGreeterStatus() onClicked: root.runGreeterStatus()
enabled: !root.greeterStatusRunning enabled: !root.greeterStatusRunning
} }
@@ -308,8 +433,9 @@ Item {
DankButton { DankButton {
text: I18n.tr("Sync") text: I18n.tr("Sync")
iconName: "sync" iconName: "sync"
horizontalPadding: Theme.spacingL
onClicked: root.runGreeterSync() onClicked: root.runGreeterSync()
enabled: !root.greeterSyncRunning enabled: root.greeterInstalled && !root.greeterSyncRunning && !root.greeterInstallActionRunning
} }
} }
} }
@@ -485,6 +611,7 @@ Item {
DankButton { DankButton {
id: browseGreeterWallpaperButton id: browseGreeterWallpaperButton
text: I18n.tr("Browse") text: I18n.tr("Browse")
horizontalPadding: Theme.spacingL
onClicked: greeterWallpaperBrowserModal.open() onClicked: greeterWallpaperBrowserModal.open()
} }
} }