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
parent c984b0b9ae
commit bb2081a936
7 changed files with 865 additions and 52 deletions

View File

@@ -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."

View File

@@ -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 ""
}

View File

@@ -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)

View File

@@ -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.")
}

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 (
"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)

View File

@@ -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()
}
}