mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-04 12:52:06 -04:00
436 lines
11 KiB
Go
436 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var setupCmd = &cobra.Command{
|
|
Use: "setup",
|
|
Short: "Deploy DMS configurations",
|
|
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
if err := runSetup(); err != nil {
|
|
log.Fatalf("Error during setup: %v", err)
|
|
}
|
|
},
|
|
}
|
|
|
|
var setupBindsCmd = &cobra.Command{
|
|
Use: "binds",
|
|
Short: "Deploy default keybinds config",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
if err := runSetupDmsConfig("binds"); err != nil {
|
|
log.Fatalf("Error: %v", err)
|
|
}
|
|
},
|
|
}
|
|
|
|
var setupLayoutCmd = &cobra.Command{
|
|
Use: "layout",
|
|
Short: "Deploy default layout config",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
if err := runSetupDmsConfig("layout"); err != nil {
|
|
log.Fatalf("Error: %v", err)
|
|
}
|
|
},
|
|
}
|
|
|
|
var setupColorsCmd = &cobra.Command{
|
|
Use: "colors",
|
|
Short: "Deploy default colors config",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
if err := runSetupDmsConfig("colors"); err != nil {
|
|
log.Fatalf("Error: %v", err)
|
|
}
|
|
},
|
|
}
|
|
|
|
var setupAlttabCmd = &cobra.Command{
|
|
Use: "alttab",
|
|
Short: "Deploy default alt-tab config (niri only)",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
if err := runSetupDmsConfig("alttab"); err != nil {
|
|
log.Fatalf("Error: %v", err)
|
|
}
|
|
},
|
|
}
|
|
|
|
var setupOutputsCmd = &cobra.Command{
|
|
Use: "outputs",
|
|
Short: "Deploy default outputs config",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
if err := runSetupDmsConfig("outputs"); err != nil {
|
|
log.Fatalf("Error: %v", err)
|
|
}
|
|
},
|
|
}
|
|
|
|
var setupCursorCmd = &cobra.Command{
|
|
Use: "cursor",
|
|
Short: "Deploy default cursor config",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
if err := runSetupDmsConfig("cursor"); err != nil {
|
|
log.Fatalf("Error: %v", err)
|
|
}
|
|
},
|
|
}
|
|
|
|
var setupWindowrulesCmd = &cobra.Command{
|
|
Use: "windowrules",
|
|
Short: "Deploy default window rules config",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
if err := runSetupDmsConfig("windowrules"); err != nil {
|
|
log.Fatalf("Error: %v", err)
|
|
}
|
|
},
|
|
}
|
|
|
|
type dmsConfigSpec struct {
|
|
niriFile string
|
|
hyprFile string
|
|
niriContent func(terminal string) string
|
|
hyprContent func(terminal string) string
|
|
}
|
|
|
|
var dmsConfigSpecs = map[string]dmsConfigSpec{
|
|
"binds": {
|
|
niriFile: "binds.kdl",
|
|
hyprFile: "binds.conf",
|
|
niriContent: func(t string) string {
|
|
return strings.ReplaceAll(config.NiriBindsConfig, "{{TERMINAL_COMMAND}}", t)
|
|
},
|
|
hyprContent: func(t string) string {
|
|
return strings.ReplaceAll(config.HyprBindsConfig, "{{TERMINAL_COMMAND}}", t)
|
|
},
|
|
},
|
|
"layout": {
|
|
niriFile: "layout.kdl",
|
|
hyprFile: "layout.conf",
|
|
niriContent: func(_ string) string { return config.NiriLayoutConfig },
|
|
hyprContent: func(_ string) string { return config.HyprLayoutConfig },
|
|
},
|
|
"colors": {
|
|
niriFile: "colors.kdl",
|
|
hyprFile: "colors.conf",
|
|
niriContent: func(_ string) string { return config.NiriColorsConfig },
|
|
hyprContent: func(_ string) string { return config.HyprColorsConfig },
|
|
},
|
|
"alttab": {
|
|
niriFile: "alttab.kdl",
|
|
niriContent: func(_ string) string { return config.NiriAlttabConfig },
|
|
},
|
|
"outputs": {
|
|
niriFile: "outputs.kdl",
|
|
hyprFile: "outputs.conf",
|
|
niriContent: func(_ string) string { return "" },
|
|
hyprContent: func(_ string) string { return "" },
|
|
},
|
|
"cursor": {
|
|
niriFile: "cursor.kdl",
|
|
hyprFile: "cursor.conf",
|
|
niriContent: func(_ string) string { return "" },
|
|
hyprContent: func(_ string) string { return "" },
|
|
},
|
|
"windowrules": {
|
|
niriFile: "windowrules.kdl",
|
|
hyprFile: "windowrules.conf",
|
|
niriContent: func(_ string) string { return "" },
|
|
hyprContent: func(_ string) string { return "" },
|
|
},
|
|
}
|
|
|
|
func detectTerminal() (string, error) {
|
|
terminals := []string{"ghostty", "foot", "kitty", "alacritty"}
|
|
var found []string
|
|
for _, t := range terminals {
|
|
if utils.CommandExists(t) {
|
|
found = append(found, t)
|
|
}
|
|
}
|
|
|
|
switch len(found) {
|
|
case 0:
|
|
return "ghostty", nil
|
|
case 1:
|
|
return found[0], nil
|
|
}
|
|
|
|
fmt.Println("Multiple terminals detected:")
|
|
for i, t := range found {
|
|
fmt.Printf("%d) %s\n", i+1, t)
|
|
}
|
|
fmt.Printf("\nChoice (1-%d): ", len(found))
|
|
|
|
var response string
|
|
fmt.Scanln(&response)
|
|
response = strings.TrimSpace(response)
|
|
|
|
choice := 0
|
|
fmt.Sscanf(response, "%d", &choice)
|
|
if choice < 1 || choice > len(found) {
|
|
return "", fmt.Errorf("invalid choice")
|
|
}
|
|
return found[choice-1], nil
|
|
}
|
|
|
|
func detectCompositorForSetup() (string, error) {
|
|
compositors := greeter.DetectCompositors()
|
|
|
|
switch len(compositors) {
|
|
case 0:
|
|
return "", fmt.Errorf("no supported compositors found (niri or Hyprland required)")
|
|
case 1:
|
|
return strings.ToLower(compositors[0]), nil
|
|
}
|
|
|
|
selected, err := greeter.PromptCompositorChoice(compositors)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return strings.ToLower(selected), nil
|
|
}
|
|
|
|
func runSetupDmsConfig(name string) error {
|
|
spec, ok := dmsConfigSpecs[name]
|
|
if !ok {
|
|
return fmt.Errorf("unknown config: %s", name)
|
|
}
|
|
|
|
compositor, err := detectCompositorForSetup()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var filename string
|
|
var contentFn func(string) string
|
|
switch compositor {
|
|
case "niri":
|
|
filename = spec.niriFile
|
|
contentFn = spec.niriContent
|
|
case "hyprland":
|
|
filename = spec.hyprFile
|
|
contentFn = spec.hyprContent
|
|
default:
|
|
return fmt.Errorf("unsupported compositor: %s", compositor)
|
|
}
|
|
|
|
if filename == "" {
|
|
return fmt.Errorf("%s is not supported for %s", name, compositor)
|
|
}
|
|
|
|
var dmsDir string
|
|
switch compositor {
|
|
case "niri":
|
|
dmsDir = filepath.Join(os.Getenv("HOME"), ".config", "niri", "dms")
|
|
case "hyprland":
|
|
dmsDir = filepath.Join(os.Getenv("HOME"), ".config", "hypr", "dms")
|
|
}
|
|
|
|
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
|
return fmt.Errorf("failed to create dms directory: %w", err)
|
|
}
|
|
|
|
path := filepath.Join(dmsDir, filename)
|
|
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
|
|
return fmt.Errorf("%s already exists and is not empty: %s", name, path)
|
|
}
|
|
|
|
terminal := "ghostty"
|
|
if contentFn != nil && name == "binds" {
|
|
terminal, err = detectTerminal()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
content := contentFn(terminal)
|
|
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
|
return fmt.Errorf("failed to write %s: %w", filename, err)
|
|
}
|
|
|
|
fmt.Printf("Deployed %s to %s\n", name, path)
|
|
return nil
|
|
}
|
|
|
|
func runSetup() error {
|
|
fmt.Println("=== DMS Configuration Setup ===")
|
|
|
|
wm, wmSelected := promptCompositor()
|
|
terminal, terminalSelected := promptTerminal()
|
|
useSystemd := promptSystemd()
|
|
|
|
if !wmSelected && !terminalSelected {
|
|
fmt.Println("No configurations selected. Exiting.")
|
|
return nil
|
|
}
|
|
|
|
if wmSelected || terminalSelected {
|
|
willBackup := checkExistingConfigs(wm, wmSelected, terminal, terminalSelected)
|
|
if willBackup {
|
|
fmt.Println("\n⚠ Existing configurations will be backed up with timestamps.")
|
|
}
|
|
|
|
fmt.Print("\nProceed with deployment? (y/N): ")
|
|
var response string
|
|
fmt.Scanln(&response)
|
|
response = strings.ToLower(strings.TrimSpace(response))
|
|
|
|
if response != "y" && response != "yes" {
|
|
fmt.Println("Setup cancelled.")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
fmt.Println("\nDeploying configurations...")
|
|
logChan := make(chan string, 100)
|
|
deployer := config.NewConfigDeployer(logChan)
|
|
|
|
go func() {
|
|
for msg := range logChan {
|
|
fmt.Println(" " + msg)
|
|
}
|
|
}()
|
|
|
|
ctx := context.Background()
|
|
var results []config.DeploymentResult
|
|
var err error
|
|
|
|
if wmSelected && terminalSelected {
|
|
results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, terminal, useSystemd)
|
|
} else if wmSelected {
|
|
results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, deps.TerminalGhostty, useSystemd)
|
|
if len(results) > 1 {
|
|
results = results[:1]
|
|
}
|
|
} else if terminalSelected {
|
|
results, err = deployer.DeployConfigurationsWithSystemd(ctx, deps.WindowManagerNiri, terminal, useSystemd)
|
|
if len(results) > 0 && results[0].ConfigType == "Niri" {
|
|
results = results[1:]
|
|
}
|
|
}
|
|
|
|
close(logChan)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("deployment failed: %w", err)
|
|
}
|
|
|
|
fmt.Println("\n=== Deployment Complete ===")
|
|
for _, result := range results {
|
|
if result.Deployed {
|
|
fmt.Printf("✓ %s: %s\n", result.ConfigType, result.Path)
|
|
if result.BackupPath != "" {
|
|
fmt.Printf(" Backup: %s\n", result.BackupPath)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func promptCompositor() (deps.WindowManager, bool) {
|
|
fmt.Println("Select compositor:")
|
|
fmt.Println("1) Niri")
|
|
fmt.Println("2) Hyprland")
|
|
fmt.Println("3) None")
|
|
|
|
var response string
|
|
fmt.Print("\nChoice (1-3): ")
|
|
fmt.Scanln(&response)
|
|
response = strings.TrimSpace(response)
|
|
|
|
switch response {
|
|
case "1":
|
|
return deps.WindowManagerNiri, true
|
|
case "2":
|
|
return deps.WindowManagerHyprland, true
|
|
default:
|
|
return deps.WindowManagerNiri, false
|
|
}
|
|
}
|
|
|
|
func promptTerminal() (deps.Terminal, bool) {
|
|
fmt.Println("\nSelect terminal:")
|
|
fmt.Println("1) Ghostty")
|
|
fmt.Println("2) Kitty")
|
|
fmt.Println("3) Alacritty")
|
|
fmt.Println("4) None")
|
|
|
|
var response string
|
|
fmt.Print("\nChoice (1-4): ")
|
|
fmt.Scanln(&response)
|
|
response = strings.TrimSpace(response)
|
|
|
|
switch response {
|
|
case "1":
|
|
return deps.TerminalGhostty, true
|
|
case "2":
|
|
return deps.TerminalKitty, true
|
|
case "3":
|
|
return deps.TerminalAlacritty, true
|
|
default:
|
|
return deps.TerminalGhostty, false
|
|
}
|
|
}
|
|
|
|
func promptSystemd() bool {
|
|
fmt.Println("\nUse systemd for session management?")
|
|
fmt.Println("1) Yes (recommended for most distros)")
|
|
fmt.Println("2) No (standalone, no systemd integration)")
|
|
|
|
var response string
|
|
fmt.Print("\nChoice (1-2): ")
|
|
fmt.Scanln(&response)
|
|
response = strings.TrimSpace(response)
|
|
|
|
return response != "2"
|
|
}
|
|
|
|
func checkExistingConfigs(wm deps.WindowManager, wmSelected bool, terminal deps.Terminal, terminalSelected bool) bool {
|
|
homeDir := os.Getenv("HOME")
|
|
willBackup := false
|
|
|
|
if wmSelected {
|
|
var configPath string
|
|
switch wm {
|
|
case deps.WindowManagerNiri:
|
|
configPath = filepath.Join(homeDir, ".config", "niri", "config.kdl")
|
|
case deps.WindowManagerHyprland:
|
|
configPath = filepath.Join(homeDir, ".config", "hypr", "hyprland.conf")
|
|
}
|
|
|
|
if _, err := os.Stat(configPath); err == nil {
|
|
willBackup = true
|
|
}
|
|
}
|
|
|
|
if terminalSelected {
|
|
var configPath string
|
|
switch terminal {
|
|
case deps.TerminalGhostty:
|
|
configPath = filepath.Join(homeDir, ".config", "ghostty", "config")
|
|
case deps.TerminalKitty:
|
|
configPath = filepath.Join(homeDir, ".config", "kitty", "kitty.conf")
|
|
case deps.TerminalAlacritty:
|
|
configPath = filepath.Join(homeDir, ".config", "alacritty", "alacritty.toml")
|
|
}
|
|
|
|
if _, err := os.Stat(configPath); err == nil {
|
|
willBackup = true
|
|
}
|
|
}
|
|
|
|
return willBackup
|
|
}
|