mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-06 05:25:41 -05:00
494 lines
15 KiB
Go
494 lines
15 KiB
Go
package greeter
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
|
)
|
|
|
|
// DetectDMSPath checks for DMS installation following XDG Base Directory specification
|
|
func DetectDMSPath() (string, error) {
|
|
return config.LocateDMSConfig()
|
|
}
|
|
|
|
// DetectCompositors checks which compositors are installed
|
|
func DetectCompositors() []string {
|
|
var compositors []string
|
|
|
|
if commandExists("niri") {
|
|
compositors = append(compositors, "niri")
|
|
}
|
|
if commandExists("Hyprland") {
|
|
compositors = append(compositors, "Hyprland")
|
|
}
|
|
|
|
return compositors
|
|
}
|
|
|
|
// PromptCompositorChoice asks user to choose between compositors
|
|
func PromptCompositorChoice(compositors []string) (string, error) {
|
|
fmt.Println("\nMultiple compositors detected:")
|
|
for i, comp := range compositors {
|
|
fmt.Printf("%d) %s\n", i+1, comp)
|
|
}
|
|
|
|
reader := bufio.NewReader(os.Stdin)
|
|
fmt.Print("Choose compositor for greeter (1-2): ")
|
|
response, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
return "", fmt.Errorf("error reading input: %w", err)
|
|
}
|
|
|
|
response = strings.TrimSpace(response)
|
|
switch response {
|
|
case "1":
|
|
return compositors[0], nil
|
|
case "2":
|
|
if len(compositors) > 1 {
|
|
return compositors[1], nil
|
|
}
|
|
return "", fmt.Errorf("invalid choice")
|
|
default:
|
|
return "", fmt.Errorf("invalid choice")
|
|
}
|
|
}
|
|
|
|
// EnsureGreetdInstalled checks if greetd is installed and installs it if not
|
|
func EnsureGreetdInstalled(logFunc func(string), sudoPassword string) error {
|
|
if commandExists("greetd") {
|
|
logFunc("✓ greetd is already installed")
|
|
return nil
|
|
}
|
|
|
|
logFunc("greetd is not installed. Installing...")
|
|
|
|
osInfo, err := distros.GetOSInfo()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to detect OS: %w", err)
|
|
}
|
|
|
|
config, exists := distros.Registry[osInfo.Distribution.ID]
|
|
if !exists {
|
|
return fmt.Errorf("unsupported distribution for automatic greetd installation: %s", osInfo.Distribution.ID)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
var installCmd *exec.Cmd
|
|
|
|
switch config.Family {
|
|
case distros.FamilyArch:
|
|
if sudoPassword != "" {
|
|
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
|
"pacman -S --needed --noconfirm greetd")
|
|
} else {
|
|
installCmd = exec.CommandContext(ctx, "sudo", "pacman", "-S", "--needed", "--noconfirm", "greetd")
|
|
}
|
|
|
|
case distros.FamilyFedora:
|
|
if sudoPassword != "" {
|
|
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
|
"dnf install -y greetd")
|
|
} else {
|
|
installCmd = exec.CommandContext(ctx, "sudo", "dnf", "install", "-y", "greetd")
|
|
}
|
|
|
|
case distros.FamilySUSE:
|
|
if sudoPassword != "" {
|
|
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
|
"zypper install -y greetd")
|
|
} else {
|
|
installCmd = exec.CommandContext(ctx, "sudo", "zypper", "install", "-y", "greetd")
|
|
}
|
|
|
|
case distros.FamilyUbuntu:
|
|
if sudoPassword != "" {
|
|
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
|
"apt-get install -y greetd")
|
|
} else {
|
|
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "greetd")
|
|
}
|
|
|
|
case distros.FamilyDebian:
|
|
if sudoPassword != "" {
|
|
installCmd = distros.ExecSudoCommand(ctx, sudoPassword,
|
|
"apt-get install -y greetd")
|
|
} else {
|
|
installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "greetd")
|
|
}
|
|
|
|
case distros.FamilyNix:
|
|
return fmt.Errorf("on NixOS, please add greetd to your configuration.nix")
|
|
|
|
default:
|
|
return fmt.Errorf("unsupported distribution family for automatic greetd installation: %s", config.Family)
|
|
}
|
|
|
|
installCmd.Stdout = os.Stdout
|
|
installCmd.Stderr = os.Stderr
|
|
|
|
if err := installCmd.Run(); err != nil {
|
|
return fmt.Errorf("failed to install greetd: %w", err)
|
|
}
|
|
|
|
logFunc("✓ greetd installed successfully")
|
|
return nil
|
|
}
|
|
|
|
// CopyGreeterFiles installs the dms-greeter wrapper and sets up cache directory
|
|
func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
|
// Check if dms-greeter is already in PATH
|
|
if commandExists("dms-greeter") {
|
|
logFunc("✓ dms-greeter wrapper already installed")
|
|
} else {
|
|
// Install the wrapper script
|
|
assetsDir := filepath.Join(dmsPath, "Modules", "Greetd", "assets")
|
|
wrapperSrc := filepath.Join(assetsDir, "dms-greeter")
|
|
|
|
if _, err := os.Stat(wrapperSrc); os.IsNotExist(err) {
|
|
return fmt.Errorf("dms-greeter wrapper not found at %s", wrapperSrc)
|
|
}
|
|
|
|
wrapperDst := "/usr/local/bin/dms-greeter"
|
|
if err := runSudoCmd(sudoPassword, "cp", wrapperSrc, wrapperDst); err != nil {
|
|
return fmt.Errorf("failed to copy dms-greeter wrapper: %w", err)
|
|
}
|
|
logFunc(fmt.Sprintf("✓ Installed dms-greeter wrapper to %s", wrapperDst))
|
|
|
|
if err := runSudoCmd(sudoPassword, "chmod", "+x", wrapperDst); err != nil {
|
|
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) {
|
|
if err := runSudoCmd(sudoPassword, "semanage", "fcontext", "-a", "-t", "bin_t", wrapperDst); err != nil {
|
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to set SELinux fcontext: %v", err))
|
|
} else {
|
|
logFunc("✓ Set SELinux fcontext for dms-greeter")
|
|
}
|
|
|
|
if err := runSudoCmd(sudoPassword, "restorecon", "-v", wrapperDst); err != nil {
|
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to restore SELinux context: %v", err))
|
|
} else {
|
|
logFunc("✓ Restored SELinux context for dms-greeter")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create cache directory with proper permissions
|
|
cacheDir := "/var/cache/dms-greeter"
|
|
if err := runSudoCmd(sudoPassword, "mkdir", "-p", cacheDir); err != nil {
|
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
|
}
|
|
|
|
if err := runSudoCmd(sudoPassword, "chown", "greeter:greeter", cacheDir); err != nil {
|
|
return fmt.Errorf("failed to set cache directory owner: %w", err)
|
|
}
|
|
|
|
if err := runSudoCmd(sudoPassword, "chmod", "750", cacheDir); err != nil {
|
|
return fmt.Errorf("failed to set cache directory permissions: %w", err)
|
|
}
|
|
logFunc(fmt.Sprintf("✓ Created cache directory %s (owner: greeter:greeter, permissions: 750)", cacheDir))
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetupParentDirectoryACLs sets ACLs on parent directories to allow traversal
|
|
func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
|
|
if !commandExists("setfacl") {
|
|
logFunc("⚠ Warning: setfacl command not found. ACL support may not be available on this filesystem.")
|
|
logFunc(" If theme sync doesn't work, you may need to install acl package:")
|
|
logFunc(" - Fedora/RHEL: sudo dnf install acl")
|
|
logFunc(" - Debian/Ubuntu: sudo apt-get install acl")
|
|
logFunc(" - Arch: sudo pacman -S acl")
|
|
return nil
|
|
}
|
|
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get user home directory: %w", err)
|
|
}
|
|
|
|
parentDirs := []struct {
|
|
path string
|
|
desc string
|
|
}{
|
|
{homeDir, "home directory"},
|
|
{filepath.Join(homeDir, ".config"), ".config directory"},
|
|
{filepath.Join(homeDir, ".local"), ".local directory"},
|
|
{filepath.Join(homeDir, ".cache"), ".cache directory"},
|
|
{filepath.Join(homeDir, ".local", "state"), ".local/state directory"},
|
|
{filepath.Join(homeDir, ".local", "share"), ".local/share directory"},
|
|
}
|
|
|
|
logFunc("\nSetting up parent directory ACLs for greeter user access...")
|
|
|
|
for _, dir := range parentDirs {
|
|
if _, err := os.Stat(dir.path); os.IsNotExist(err) {
|
|
if err := os.MkdirAll(dir.path, 0755); err != nil {
|
|
logFunc(fmt.Sprintf("⚠ Warning: Could not create %s: %v", dir.desc, err))
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Set ACL to allow greeter user read+execute permission (for session discovery)
|
|
if err := runSudoCmd(sudoPassword, "setfacl", "-m", "u:greeter:rx", dir.path); err != nil {
|
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err))
|
|
logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m u:greeter:x %s", dir.path))
|
|
continue
|
|
}
|
|
|
|
logFunc(fmt.Sprintf("✓ Set ACL on %s", dir.desc))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get user home directory: %w", err)
|
|
}
|
|
|
|
currentUser := os.Getenv("USER")
|
|
if currentUser == "" {
|
|
currentUser = os.Getenv("LOGNAME")
|
|
}
|
|
if currentUser == "" {
|
|
return fmt.Errorf("failed to determine current user")
|
|
}
|
|
|
|
// Check if user is already in greeter group
|
|
groupsCmd := exec.Command("groups", currentUser)
|
|
groupsOutput, err := groupsCmd.Output()
|
|
if err == nil && strings.Contains(string(groupsOutput), "greeter") {
|
|
logFunc(fmt.Sprintf("✓ %s is already in greeter group", currentUser))
|
|
} else {
|
|
// Add current user to greeter group for file access permissions
|
|
if err := runSudoCmd(sudoPassword, "usermod", "-aG", "greeter", currentUser); err != nil {
|
|
return fmt.Errorf("failed to add %s to greeter group: %w", currentUser, err)
|
|
}
|
|
logFunc(fmt.Sprintf("✓ Added %s to greeter group (logout/login required for changes to take effect)", currentUser))
|
|
}
|
|
|
|
configDirs := []struct {
|
|
path string
|
|
desc string
|
|
}{
|
|
{filepath.Join(homeDir, ".config", "DankMaterialShell"), "DankMaterialShell config"},
|
|
{filepath.Join(homeDir, ".local", "state", "DankMaterialShell"), "DankMaterialShell state"},
|
|
{filepath.Join(homeDir, ".cache", "quickshell"), "quickshell cache"},
|
|
{filepath.Join(homeDir, ".config", "quickshell"), "quickshell config"},
|
|
{filepath.Join(homeDir, ".local", "share", "wayland-sessions"), "wayland sessions"},
|
|
{filepath.Join(homeDir, ".local", "share", "xsessions"), "xsessions"},
|
|
}
|
|
|
|
for _, dir := range configDirs {
|
|
if _, err := os.Stat(dir.path); os.IsNotExist(err) {
|
|
if err := os.MkdirAll(dir.path, 0755); err != nil {
|
|
logFunc(fmt.Sprintf("⚠ Warning: Could not create %s: %v", dir.path, err))
|
|
continue
|
|
}
|
|
}
|
|
|
|
if err := runSudoCmd(sudoPassword, "chgrp", "-R", "greeter", dir.path); err != nil {
|
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to set group for %s: %v", dir.desc, err))
|
|
continue
|
|
}
|
|
|
|
if err := runSudoCmd(sudoPassword, "chmod", "-R", "g+rX", dir.path); err != nil {
|
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to set permissions for %s: %v", dir.desc, err))
|
|
continue
|
|
}
|
|
|
|
logFunc(fmt.Sprintf("✓ Set group permissions for %s", dir.desc))
|
|
}
|
|
|
|
// Set up ACLs on parent directories to allow greeter user traversal
|
|
if err := SetupParentDirectoryACLs(logFunc, sudoPassword); err != nil {
|
|
return fmt.Errorf("failed to setup parent directory ACLs: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func SyncDMSConfigs(dmsPath string, logFunc func(string), sudoPassword string) error {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get user home directory: %w", err)
|
|
}
|
|
|
|
cacheDir := "/var/cache/dms-greeter"
|
|
|
|
symlinks := []struct {
|
|
source string
|
|
target string
|
|
desc string
|
|
}{
|
|
{
|
|
source: filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"),
|
|
target: filepath.Join(cacheDir, "settings.json"),
|
|
desc: "core settings (theme, clock formats, etc)",
|
|
},
|
|
{
|
|
source: filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"),
|
|
target: filepath.Join(cacheDir, "session.json"),
|
|
desc: "state (wallpaper configuration)",
|
|
},
|
|
{
|
|
source: filepath.Join(homeDir, ".cache", "DankMaterialShell", "dms-colors.json"),
|
|
target: filepath.Join(cacheDir, "colors.json"),
|
|
desc: "wallpaper based theming",
|
|
},
|
|
}
|
|
|
|
for _, link := range symlinks {
|
|
sourceDir := filepath.Dir(link.source)
|
|
if _, err := os.Stat(sourceDir); os.IsNotExist(err) {
|
|
if err := os.MkdirAll(sourceDir, 0755); err != nil {
|
|
logFunc(fmt.Sprintf("⚠ Warning: Could not create directory %s: %v", sourceDir, err))
|
|
continue
|
|
}
|
|
}
|
|
|
|
if _, err := os.Stat(link.source); os.IsNotExist(err) {
|
|
if err := os.WriteFile(link.source, []byte("{}"), 0644); err != nil {
|
|
logFunc(fmt.Sprintf("⚠ Warning: Could not create %s: %v", link.source, err))
|
|
continue
|
|
}
|
|
}
|
|
|
|
runSudoCmd(sudoPassword, "rm", "-f", link.target)
|
|
|
|
if err := runSudoCmd(sudoPassword, "ln", "-sf", link.source, link.target); err != nil {
|
|
logFunc(fmt.Sprintf("⚠ Warning: Failed to create symlink for %s: %v", link.desc, err))
|
|
continue
|
|
}
|
|
|
|
logFunc(fmt.Sprintf("✓ Synced %s", link.desc))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ConfigureGreetd(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
|
configPath := "/etc/greetd/config.toml"
|
|
|
|
if _, err := os.Stat(configPath); err == nil {
|
|
backupPath := configPath + ".backup"
|
|
if err := runSudoCmd(sudoPassword, "cp", configPath, backupPath); err != nil {
|
|
return fmt.Errorf("failed to backup config: %w", err)
|
|
}
|
|
logFunc(fmt.Sprintf("✓ Backed up existing config to %s", backupPath))
|
|
}
|
|
|
|
var configContent string
|
|
if data, err := os.ReadFile(configPath); err == nil {
|
|
configContent = string(data)
|
|
} else {
|
|
configContent = `[terminal]
|
|
vt = 1
|
|
|
|
[default_session]
|
|
|
|
user = "greeter"
|
|
`
|
|
}
|
|
|
|
lines := strings.Split(configContent, "\n")
|
|
var newLines []string
|
|
for _, line := range lines {
|
|
trimmed := strings.TrimSpace(line)
|
|
if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") {
|
|
if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") {
|
|
newLines = append(newLines, `user = "greeter"`)
|
|
} else {
|
|
newLines = append(newLines, line)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Determine wrapper command path
|
|
wrapperCmd := "dms-greeter"
|
|
if !commandExists("dms-greeter") {
|
|
wrapperCmd = "/usr/local/bin/dms-greeter"
|
|
}
|
|
|
|
// Build command based on compositor and dms path
|
|
compositorLower := strings.ToLower(compositor)
|
|
command := fmt.Sprintf(`command = "%s --command %s -p %s"`, wrapperCmd, compositorLower, dmsPath)
|
|
|
|
var finalLines []string
|
|
inDefaultSession := false
|
|
commandAdded := false
|
|
|
|
for _, line := range newLines {
|
|
finalLines = append(finalLines, line)
|
|
trimmed := strings.TrimSpace(line)
|
|
|
|
if trimmed == "[default_session]" {
|
|
inDefaultSession = true
|
|
}
|
|
|
|
if inDefaultSession && !commandAdded && trimmed != "" && !strings.HasPrefix(trimmed, "[") {
|
|
if !strings.HasPrefix(trimmed, "#") && !strings.HasPrefix(trimmed, "user") {
|
|
finalLines = append(finalLines, command)
|
|
commandAdded = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if !commandAdded {
|
|
finalLines = append(finalLines, command)
|
|
}
|
|
|
|
newConfig := strings.Join(finalLines, "\n")
|
|
|
|
tmpFile := "/tmp/greetd-config.toml"
|
|
if err := os.WriteFile(tmpFile, []byte(newConfig), 0644); err != nil {
|
|
return fmt.Errorf("failed to write temp config: %w", err)
|
|
}
|
|
|
|
if err := runSudoCmd(sudoPassword, "mv", tmpFile, configPath); err != nil {
|
|
return fmt.Errorf("failed to move config to /etc/greetd: %w", err)
|
|
}
|
|
|
|
logFunc(fmt.Sprintf("✓ Updated greetd configuration (user: greeter, command: %s --command %s -p %s)", wrapperCmd, compositorLower, dmsPath))
|
|
return nil
|
|
}
|
|
|
|
func runSudoCmd(sudoPassword string, command string, args ...string) error {
|
|
var cmd *exec.Cmd
|
|
|
|
if sudoPassword != "" {
|
|
fullArgs := append([]string{command}, args...)
|
|
quotedArgs := make([]string, len(fullArgs))
|
|
for i, arg := range fullArgs {
|
|
quotedArgs[i] = "'" + strings.ReplaceAll(arg, "'", "'\\''") + "'"
|
|
}
|
|
cmdStr := strings.Join(quotedArgs, " ")
|
|
|
|
cmd = distros.ExecSudoCommand(context.Background(), sudoPassword, cmdStr)
|
|
} else {
|
|
cmd = exec.Command("sudo", append([]string{command}, args...)...)
|
|
}
|
|
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
return cmd.Run()
|
|
}
|
|
|
|
func commandExists(cmd string) bool {
|
|
_, err := exec.LookPath(cmd)
|
|
return err == nil
|
|
}
|