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:
@@ -4,6 +4,7 @@
|
||||
"greeter install",
|
||||
"greeter enable",
|
||||
"greeter sync",
|
||||
"greeter uninstall",
|
||||
"setup"
|
||||
],
|
||||
"message": "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes."
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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",
|
||||
PreRunE: requireMutableSystemCommand,
|
||||
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)
|
||||
}
|
||||
},
|
||||
@@ -70,7 +85,20 @@ var greeterEnableCmd = &cobra.Command{
|
||||
Long: "Configure greetd to use DMS as the greeter",
|
||||
PreRunE: requireMutableSystemCommand,
|
||||
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)
|
||||
}
|
||||
},
|
||||
@@ -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 ===")
|
||||
|
||||
logFunc := func(msg string) {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
// Debian/openSUSE
|
||||
greeter.TryInstallGreeterPackage(logFunc, "")
|
||||
if isPackageOnlyGreeterDistro() && !greeter.IsGreeterPackaged() {
|
||||
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")
|
||||
}
|
||||
|
||||
// If already fully configured, prompt the user
|
||||
if isGreeterEnabled() {
|
||||
fmt.Print("\nGreeter is already installed and configured. Re-run to re-sync settings and permissions? [Y/n]: ")
|
||||
var response string
|
||||
@@ -156,8 +227,12 @@ func installGreeter() error {
|
||||
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...")
|
||||
// Use empty path when packaged (greeter finds /usr/share/quickshell/dms-greeter); else use user's DMS path
|
||||
greeterPathForConfig := ""
|
||||
if !greeter.IsGreeterPackaged() {
|
||||
greeterPathForConfig = dmsPath
|
||||
@@ -191,22 +266,225 @@ func installGreeter() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func syncInTerminal(nonInteractive bool, forceAuth bool, local bool) error {
|
||||
syncFlags := make([]string, 0, 3)
|
||||
if nonInteractive {
|
||||
syncFlags = append(syncFlags, "--yes")
|
||||
func uninstallGreeter(nonInteractive bool) error {
|
||||
fmt.Println("=== DMS Greeter Uninstall ===")
|
||||
|
||||
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 {
|
||||
shellSyncCmd += " " + strings.Join(syncFlags, " ")
|
||||
|
||||
fmt.Println("\nRemoving DMS PAM configuration...")
|
||||
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 {
|
||||
name string
|
||||
args []string
|
||||
@@ -227,15 +505,33 @@ func syncInTerminal(nonInteractive bool, forceAuth bool, local bool) error {
|
||||
cmd := exec.Command(t.name, t.args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Start(); err != nil {
|
||||
continue
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = cmd.Process.Release()
|
||||
return nil
|
||||
}
|
||||
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 {
|
||||
if !nonInteractive {
|
||||
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))
|
||||
|
||||
if response != "n" && response != "no" {
|
||||
if err := enableGreeter(); err != nil {
|
||||
if err := enableGreeter(false); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
@@ -417,6 +713,11 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error {
|
||||
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("\nYour theme, settings, and wallpaper configuration have been synced with the greeter.")
|
||||
if forceAuth {
|
||||
@@ -709,7 +1010,7 @@ func handleConflictingDisplayManagers() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func enableGreeter() error {
|
||||
func enableGreeter(nonInteractive bool) error {
|
||||
fmt.Println("=== DMS Greeter Enable ===")
|
||||
fmt.Println()
|
||||
|
||||
@@ -762,6 +1063,17 @@ func enableGreeter() error {
|
||||
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...")
|
||||
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)
|
||||
}
|
||||
|
||||
if err := greeter.InstallAppArmorProfile(logFunc, ""); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ AppArmor profile setup failed: %v", err))
|
||||
}
|
||||
|
||||
if err := ensureGraphicalTarget(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1086,7 +1402,7 @@ func checkGreeterStatus() error {
|
||||
}
|
||||
} else {
|
||||
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
|
||||
}
|
||||
} 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()
|
||||
if allGood && inGreeterGroup {
|
||||
fmt.Println("✓ All checks passed! Greeter is properly configured.")
|
||||
@@ -1264,3 +1621,129 @@ func checkGreeterStatus() error {
|
||||
|
||||
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 ""
|
||||
}
|
||||
|
||||
@@ -16,19 +16,10 @@ func init() {
|
||||
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
||||
runCmd.Flags().MarkHidden("daemon-child")
|
||||
|
||||
// Add subcommands to greeter
|
||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
||||
|
||||
// Add subcommands to setup
|
||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||
|
||||
// Add subcommands to update
|
||||
updateCmd.AddCommand(updateCheckCmd)
|
||||
|
||||
// Add subcommands to plugins
|
||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||
|
||||
// Add common commands to root
|
||||
rootCmd.AddCommand(getCommonCommands()...)
|
||||
|
||||
rootCmd.AddCommand(updateCmd)
|
||||
|
||||
@@ -11,29 +11,20 @@ import (
|
||||
var Version = "dev"
|
||||
|
||||
func init() {
|
||||
// Add flags
|
||||
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("session", false, "Session managed (like as a systemd unit)")
|
||||
runCmd.Flags().MarkHidden("daemon-child")
|
||||
|
||||
// Add subcommands to greeter
|
||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
||||
|
||||
// Add subcommands to setup
|
||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||
|
||||
// Add subcommands to plugins
|
||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||
|
||||
// Add common commands to root
|
||||
rootCmd.AddCommand(getCommonCommands()...)
|
||||
|
||||
rootCmd.SetHelpTemplate(getHelpTemplate())
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Block root
|
||||
if os.Geteuid() == 0 {
|
||||
log.Fatal("This program should not be run as root. Exiting.")
|
||||
}
|
||||
|
||||
91
core/internal/greeter/assets/apparmor/usr.bin.dms-greeter
Normal file
91
core/internal/greeter/assets/apparmor/usr.bin.dms-greeter
Normal 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"),
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package greeter
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -18,6 +19,10 @@ import (
|
||||
"github.com/sblinch/kdl-go/document"
|
||||
)
|
||||
|
||||
var appArmorProfileData []byte
|
||||
|
||||
const appArmorProfileDest = "/etc/apparmor.d/usr.bin.dms-greeter"
|
||||
|
||||
const (
|
||||
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)
|
||||
}
|
||||
|
||||
// Set SELinux context on Fedora and openSUSE
|
||||
osInfo, err := distros.GetOSInfo()
|
||||
if err == nil {
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
func EnsureACLInstalled(logFunc func(string), sudoPassword string) error {
|
||||
if utils.CommandExists("setfacl") {
|
||||
@@ -661,7 +793,6 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
|
||||
return nil
|
||||
}
|
||||
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.")
|
||||
return nil
|
||||
}
|
||||
@@ -723,7 +854,6 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
||||
|
||||
group := DetectGreeterGroup()
|
||||
|
||||
// Check if user is already in greeter group
|
||||
groupsCmd := exec.Command("groups", currentUser)
|
||||
groupsOutput, err := groupsCmd.Output()
|
||||
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") {
|
||||
continue
|
||||
}
|
||||
// Strip existing -C or --config and their arguments
|
||||
command = stripConfigFlag(command)
|
||||
command = stripCacheDirFlag(command)
|
||||
command = strings.TrimSpace(command + " --cache-dir " + GreeterCacheDir)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Modals.FileBrowser
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -12,6 +14,10 @@ import qs.Modules.Settings.Widgets
|
||||
Item {
|
||||
id: root
|
||||
|
||||
ConfirmModal {
|
||||
id: greeterActionConfirm
|
||||
}
|
||||
|
||||
FileBrowserModal {
|
||||
id: greeterWallpaperBrowserModal
|
||||
browserTitle: I18n.tr("Select greeter background image")
|
||||
@@ -28,6 +34,7 @@ Item {
|
||||
property string greeterStatusText: ""
|
||||
property bool greeterStatusRunning: false
|
||||
property bool greeterSyncRunning: false
|
||||
property bool greeterInstallActionRunning: false
|
||||
property string greeterStatusStdout: ""
|
||||
property string greeterStatusStderr: ""
|
||||
property string greeterSyncStdout: ""
|
||||
@@ -37,6 +44,31 @@ Item {
|
||||
property bool greeterTerminalFallbackFromPrecheck: false
|
||||
property var cachedFontFamilies: []
|
||||
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() {
|
||||
greeterStatusText = "";
|
||||
@@ -46,6 +78,41 @@ Item {
|
||||
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() {
|
||||
greeterSyncStdout = "";
|
||||
greeterSyncStderr = "";
|
||||
@@ -82,7 +149,30 @@ Item {
|
||||
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 {
|
||||
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: [
|
||||
{
|
||||
format: "",
|
||||
@@ -293,14 +406,26 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
Item { width: 1; height: Theme.spacingM }
|
||||
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
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 {
|
||||
text: I18n.tr("Refresh")
|
||||
iconName: "refresh"
|
||||
horizontalPadding: Theme.spacingL
|
||||
onClicked: root.runGreeterStatus()
|
||||
enabled: !root.greeterStatusRunning
|
||||
}
|
||||
@@ -308,8 +433,9 @@ Item {
|
||||
DankButton {
|
||||
text: I18n.tr("Sync")
|
||||
iconName: "sync"
|
||||
horizontalPadding: Theme.spacingL
|
||||
onClicked: root.runGreeterSync()
|
||||
enabled: !root.greeterSyncRunning
|
||||
enabled: root.greeterInstalled && !root.greeterSyncRunning && !root.greeterInstallActionRunning
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -485,6 +611,7 @@ Item {
|
||||
DankButton {
|
||||
id: browseGreeterWallpaperButton
|
||||
text: I18n.tr("Browse")
|
||||
horizontalPadding: Theme.spacingL
|
||||
onClicked: greeterWallpaperBrowserModal.open()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user