diff options
| author | AlexanderCurl <alexc@alexc.hu> | 2026-04-18 17:46:06 +0100 |
|---|---|---|
| committer | AlexanderCurl <alexc@alexc.hu> | 2026-04-18 17:46:06 +0100 |
| commit | 70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 (patch) | |
| tree | ab1123d4067c1b086dd6faa7ee4ea643236b565a /raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms | |
| parent | 5d94c0a7d44a2255b81815a52a7056a94a39842d (diff) | |
| download | RaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.tar.gz RaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.zip | |
Replaced file structures for themes in the correct format
Diffstat (limited to 'raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms')
34 files changed, 0 insertions, 10521 deletions
diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/assets/cli-policy.default.json b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/assets/cli-policy.default.json deleted file mode 100644 index de9b8ad..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/assets/cli-policy.default.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "policy_version": 1, - "blocked_commands": [ - "greeter install", - "greeter enable", - "greeter uninstall", - "setup" - ], - "message": "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes." -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_auth.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_auth.go deleted file mode 100644 index 727700e..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_auth.go +++ /dev/null @@ -1,76 +0,0 @@ -package main - -import ( - "fmt" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam" - "github.com/spf13/cobra" -) - -var authCmd = &cobra.Command{ - Use: "auth", - Short: "Manage DMS authentication sync", - Long: "Manage shared PAM/authentication setup for DMS greeter and lock screen", -} - -var authSyncCmd = &cobra.Command{ - Use: "sync", - Short: "Sync DMS authentication configuration", - Long: "Apply shared PAM/authentication changes for the lock screen and greeter based on current DMS settings", - Run: func(cmd *cobra.Command, args []string) { - yes, _ := cmd.Flags().GetBool("yes") - term, _ := cmd.Flags().GetBool("terminal") - if term { - if err := syncAuthInTerminal(yes); err != nil { - log.Fatalf("Error launching auth sync in terminal: %v", err) - } - return - } - if err := syncAuth(yes); err != nil { - log.Fatalf("Error syncing authentication: %v", err) - } - }, -} - -func init() { - authSyncCmd.Flags().BoolP("yes", "y", false, "Non-interactive mode: skip prompts") - authSyncCmd.Flags().BoolP("terminal", "t", false, "Run auth sync in a new terminal (for entering sudo password)") -} - -func syncAuth(nonInteractive bool) error { - if !nonInteractive { - fmt.Println("=== DMS Authentication Sync ===") - fmt.Println() - } - - logFunc := func(msg string) { - fmt.Println(msg) - } - - if err := sharedpam.SyncAuthConfig(logFunc, "", sharedpam.SyncAuthOptions{}); err != nil { - return err - } - - if !nonInteractive { - fmt.Println("\n=== Authentication Sync Complete ===") - fmt.Println("\nAuthentication changes have been applied.") - } - - return nil -} - -func syncAuthInTerminal(nonInteractive bool) error { - syncFlags := make([]string, 0, 1) - if nonInteractive { - syncFlags = append(syncFlags, "--yes") - } - - shellSyncCmd := "dms auth sync" - if len(syncFlags) > 0 { - shellSyncCmd += " " + strings.Join(syncFlags, " ") - } - shellCmd := shellSyncCmd + `; echo; echo "Authentication sync finished. Closing in 3 seconds..."; sleep 3` - return runCommandInTerminal(shellCmd) -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_blur.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_blur.go deleted file mode 100644 index d01eb93..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_blur.go +++ /dev/null @@ -1,40 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/blur" - "github.com/spf13/cobra" -) - -var blurCmd = &cobra.Command{ - Use: "blur", - Short: "Background blur utilities", -} - -var blurCheckCmd = &cobra.Command{ - Use: "check", - Short: "Check if the compositor supports background blur (ext-background-effect-v1)", - Args: cobra.NoArgs, - Run: runBlurCheck, -} - -func init() { - blurCmd.AddCommand(blurCheckCmd) -} - -func runBlurCheck(cmd *cobra.Command, args []string) { - supported, err := blur.ProbeSupport() - if err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) - } - - switch supported { - case true: - fmt.Println("supported") - default: - fmt.Println("unsupported") - } -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_brightness.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_brightness.go deleted file mode 100644 index 0f73493..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_brightness.go +++ /dev/null @@ -1,311 +0,0 @@ -package main - -import ( - "fmt" - "strings" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness" - "github.com/spf13/cobra" -) - -var brightnessCmd = &cobra.Command{ - Use: "brightness", - Short: "Control device brightness", - Long: "Control brightness for backlight and LED devices (use --ddc to include DDC/I2C monitors)", -} - -var brightnessListCmd = &cobra.Command{ - Use: "list", - Short: "List all brightness devices", - Long: "List all available brightness devices with their current values", - Run: runBrightnessList, -} - -var brightnessSetCmd = &cobra.Command{ - Use: "set <device_id> <percent>", - Short: "Set brightness for a device", - Long: "Set brightness percentage (0-100) for a specific device", - Args: cobra.ExactArgs(2), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) != 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - includeDDC, _ := cmd.Flags().GetBool("ddc") - return getBrightnessDevices(includeDDC), cobra.ShellCompDirectiveNoFileComp - }, - Run: runBrightnessSet, -} - -var brightnessGetCmd = &cobra.Command{ - Use: "get <device_id>", - Short: "Get brightness for a device", - Long: "Get current brightness percentage for a specific device", - Args: cobra.ExactArgs(1), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) != 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - includeDDC, _ := cmd.Flags().GetBool("ddc") - return getBrightnessDevices(includeDDC), cobra.ShellCompDirectiveNoFileComp - }, - Run: runBrightnessGet, -} - -func init() { - brightnessListCmd.Flags().Bool("ddc", false, "Include DDC/I2C monitors (slower)") - brightnessSetCmd.Flags().Bool("ddc", false, "Include DDC/I2C monitors (slower)") - brightnessSetCmd.Flags().Bool("exponential", false, "Use exponential brightness scaling") - brightnessSetCmd.Flags().Float64("exponent", 1.2, "Exponent for exponential scaling (default 1.2)") - brightnessGetCmd.Flags().Bool("ddc", false, "Include DDC/I2C monitors (slower)") - - brightnessCmd.SetHelpTemplate(`{{.Long}} - -Usage: - {{.UseLine}}{{if .HasAvailableSubCommands}} - -Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} - {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} - -Flags: -{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} - -Global Flags: -{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} - -Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} - {{rpad .Name .NamePadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} - -Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} -`) - - brightnessListCmd.SetHelpTemplate(`{{.Long}} - -Usage: - {{.UseLine}}{{if .HasAvailableLocalFlags}} - -Flags: -{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} - -Global Flags: -{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}} -`) - - brightnessSetCmd.SetHelpTemplate(`{{.Long}} - -Usage: - {{.UseLine}}{{if .HasAvailableLocalFlags}} - -Flags: -{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} - -Global Flags: -{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}} -`) - - brightnessGetCmd.SetHelpTemplate(`{{.Long}} - -Usage: - {{.UseLine}}{{if .HasAvailableLocalFlags}} - -Flags: -{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} - -Global Flags: -{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}} -`) - - brightnessCmd.AddCommand(brightnessListCmd, brightnessSetCmd, brightnessGetCmd) -} - -func getAllBrightnessDevices(includeDDC bool) []brightness.Device { - allDevices := []brightness.Device{} - - sysfs, err := brightness.NewSysfsBackend() - if err != nil { - log.Debugf("Failed to initialize sysfs backend: %v", err) - } else { - devices, err := sysfs.GetDevices() - if err != nil { - log.Debugf("Failed to get sysfs devices: %v", err) - } else { - allDevices = append(allDevices, devices...) - } - } - - if includeDDC { - ddc, err := brightness.NewDDCBackend() - if err != nil { - fmt.Printf("Warning: Failed to initialize DDC backend: %v\n", err) - } else { - time.Sleep(100 * time.Millisecond) - devices, err := ddc.GetDevices() - if err != nil { - fmt.Printf("Warning: Failed to get DDC devices: %v\n", err) - } else { - allDevices = append(allDevices, devices...) - } - ddc.Close() - } - } - - return allDevices -} - -func runBrightnessList(cmd *cobra.Command, args []string) { - includeDDC, _ := cmd.Flags().GetBool("ddc") - allDevices := getAllBrightnessDevices(includeDDC) - - if len(allDevices) == 0 { - fmt.Println("No brightness devices found") - return - } - - maxIDLen := len("Device") - maxNameLen := len("Name") - for _, dev := range allDevices { - if len(dev.ID) > maxIDLen { - maxIDLen = len(dev.ID) - } - if len(dev.Name) > maxNameLen { - maxNameLen = len(dev.Name) - } - } - - idPad := maxIDLen + 2 - namePad := maxNameLen + 2 - - fmt.Printf("%-*s %-12s %-*s %s\n", idPad, "Device", "Class", namePad, "Name", "Brightness") - - sepLen := idPad + 2 + 12 + 2 + namePad + 2 + 15 - for range sepLen { - fmt.Print("─") - } - fmt.Println() - - for _, device := range allDevices { - fmt.Printf("%-*s %-12s %-*s %3d%%\n", - idPad, - device.ID, - device.Class, - namePad, - device.Name, - device.CurrentPercent, - ) - } -} - -func runBrightnessSet(cmd *cobra.Command, args []string) { - deviceID := args[0] - var percent int - if _, err := fmt.Sscanf(args[1], "%d", &percent); err != nil { - log.Fatalf("Invalid percent value: %s", args[1]) - } - - if percent < 0 || percent > 100 { - log.Fatalf("Percent must be between 0 and 100") - } - - includeDDC, _ := cmd.Flags().GetBool("ddc") - exponential, _ := cmd.Flags().GetBool("exponential") - exponent, _ := cmd.Flags().GetFloat64("exponent") - - parts := strings.SplitN(deviceID, ":", 2) - if len(parts) == 2 && (parts[0] == "backlight" || parts[0] == "leds") { - if ok := tryLogindBrightness(parts[0], parts[1], deviceID, percent, exponential, exponent); ok { - return - } - } - - sysfs, err := brightness.NewSysfsBackend() - if err == nil { - if err := sysfs.SetBrightnessWithExponent(deviceID, percent, exponential, exponent); err == nil { - fmt.Printf("Set %s to %d%%\n", deviceID, percent) - return - } - log.Debugf("sysfs.SetBrightness failed: %v", err) - } else { - log.Debugf("NewSysfsBackend failed: %v", err) - } - - // Try DDC if requested - if includeDDC { - ddc, err := brightness.NewDDCBackend() - if err == nil { - defer ddc.Close() - time.Sleep(100 * time.Millisecond) - if err := ddc.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, nil); err == nil { - ddc.WaitPending() - fmt.Printf("Set %s to %d%%\n", deviceID, percent) - return - } - log.Debugf("ddc.SetBrightness failed: %v", err) - } else { - log.Debugf("NewDDCBackend failed: %v", err) - } - } - - log.Fatalf("Failed to set brightness for device: %s", deviceID) -} - -func tryLogindBrightness(subsystem, name, deviceID string, percent int, exponential bool, exponent float64) bool { - sysfs, err := brightness.NewSysfsBackend() - if err != nil { - log.Debugf("NewSysfsBackend failed: %v", err) - return false - } - - logind, err := brightness.NewLogindBackend() - if err != nil { - log.Debugf("NewLogindBackend failed: %v", err) - return false - } - defer logind.Close() - - dev, err := sysfs.GetDevice(deviceID) - if err != nil { - log.Debugf("sysfs.GetDeviceByID failed: %v", err) - return false - } - - value := sysfs.PercentToValueWithExponent(percent, dev, exponential, exponent) - if err := logind.SetBrightness(subsystem, name, uint32(value)); err != nil { - log.Debugf("logind.SetBrightness failed: %v", err) - return false - } - - log.Debugf("set %s to %d%% (%d) via logind", deviceID, percent, value) - fmt.Printf("Set %s to %d%%\n", deviceID, percent) - return true -} - -func getBrightnessDevices(includeDDC bool) []string { - allDevices := getAllBrightnessDevices(includeDDC) - - var deviceIDs []string - for _, device := range allDevices { - deviceIDs = append(deviceIDs, device.ID) - } - return deviceIDs -} - -func runBrightnessGet(cmd *cobra.Command, args []string) { - deviceID := args[0] - includeDDC, _ := cmd.Flags().GetBool("ddc") - allDevices := getAllBrightnessDevices(includeDDC) - - for _, device := range allDevices { - if device.ID == deviceID { - fmt.Printf("%s: %d%% (%d/%d)\n", - device.ID, - device.CurrentPercent, - device.Current, - device.Max, - ) - return - } - } - - log.Fatalf("Device not found: %s", deviceID) -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_chroma.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_chroma.go deleted file mode 100644 index 4945f33..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_chroma.go +++ /dev/null @@ -1,300 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "io" - "os" - "strings" - "sync" - - "github.com/alecthomas/chroma/v2" - "github.com/alecthomas/chroma/v2/formatters/html" - "github.com/alecthomas/chroma/v2/lexers" - "github.com/alecthomas/chroma/v2/styles" - "github.com/spf13/cobra" - "github.com/yuin/goldmark" - highlighting "github.com/yuin/goldmark-highlighting/v2" - "github.com/yuin/goldmark/extension" - "github.com/yuin/goldmark/parser" - ghtml "github.com/yuin/goldmark/renderer/html" -) - -var ( - chromaLanguage string - chromaStyle string - chromaInline bool - chromaMarkdown bool - chromaLineNumbers bool - - // Caching layer for performance - lexerCache = make(map[string]chroma.Lexer) - styleCache = make(map[string]*chroma.Style) - formatterCache = make(map[string]*html.Formatter) - cacheMutex sync.RWMutex - maxFileSize = int64(5 * 1024 * 1024) // 5MB default -) - -var chromaCmd = &cobra.Command{ - Use: "chroma [file]", - Short: "Syntax highlight source code", - Long: `Generate syntax-highlighted HTML from source code. - -Reads from file or stdin, outputs HTML with syntax highlighting. -Language is auto-detected from filename or can be specified with --language. - -Examples: - dms chroma main.go - dms chroma --language python script.py - echo "def foo(): pass" | dms chroma -l python - cat code.rs | dms chroma -l rust --style dracula - dms chroma --markdown README.md - dms chroma --markdown --style github-dark notes.md - dms chroma list-languages - dms chroma list-styles`, - Args: cobra.MaximumNArgs(1), - Run: runChroma, -} - -var chromaListLanguagesCmd = &cobra.Command{ - Use: "list-languages", - Short: "List all supported languages", - Run: func(cmd *cobra.Command, args []string) { - for _, name := range lexers.Names(true) { - fmt.Println(name) - } - }, -} - -var chromaListStylesCmd = &cobra.Command{ - Use: "list-styles", - Short: "List all available color styles", - Run: func(cmd *cobra.Command, args []string) { - for _, name := range styles.Names() { - fmt.Println(name) - } - }, -} - -func init() { - chromaCmd.Flags().StringVarP(&chromaLanguage, "language", "l", "", "Language for highlighting (auto-detect if not specified)") - chromaCmd.Flags().StringVarP(&chromaStyle, "style", "s", "monokai", "Color style (monokai, dracula, github, etc.)") - chromaCmd.Flags().BoolVar(&chromaInline, "inline", false, "Output inline styles instead of CSS classes") - chromaCmd.Flags().BoolVar(&chromaLineNumbers, "line-numbers", false, "Show line numbers in output") - chromaCmd.Flags().BoolVarP(&chromaMarkdown, "markdown", "m", false, "Render markdown with syntax-highlighted code blocks") - chromaCmd.Flags().Int64Var(&maxFileSize, "max-size", 5*1024*1024, "Maximum file size to process without warning (bytes)") - - chromaCmd.AddCommand(chromaListLanguagesCmd) - chromaCmd.AddCommand(chromaListStylesCmd) -} - -func getCachedLexer(key string, fallbackFunc func() chroma.Lexer) chroma.Lexer { - cacheMutex.RLock() - if lexer, ok := lexerCache[key]; ok { - cacheMutex.RUnlock() - return lexer - } - cacheMutex.RUnlock() - - lexer := fallbackFunc() - if lexer != nil { - cacheMutex.Lock() - lexerCache[key] = lexer - cacheMutex.Unlock() - } - return lexer -} - -func getCachedStyle(name string) *chroma.Style { - cacheMutex.RLock() - if style, ok := styleCache[name]; ok { - cacheMutex.RUnlock() - return style - } - cacheMutex.RUnlock() - - style := styles.Get(name) - if style == nil { - fmt.Fprintf(os.Stderr, "Warning: Style '%s' not found, using fallback\n", name) - style = styles.Fallback - } - - cacheMutex.Lock() - styleCache[name] = style - cacheMutex.Unlock() - return style -} - -func getCachedFormatter(inline bool, lineNumbers bool) *html.Formatter { - key := fmt.Sprintf("inline=%t,lineNumbers=%t", inline, lineNumbers) - - cacheMutex.RLock() - if formatter, ok := formatterCache[key]; ok { - cacheMutex.RUnlock() - return formatter - } - cacheMutex.RUnlock() - - var opts []html.Option - if inline { - opts = append(opts, html.WithClasses(false)) - } else { - opts = append(opts, html.WithClasses(true)) - } - opts = append(opts, html.TabWidth(4)) - - if lineNumbers { - opts = append(opts, html.WithLineNumbers(true)) - opts = append(opts, html.LineNumbersInTable(false)) - opts = append(opts, html.WithLinkableLineNumbers(false, "")) - } - - formatter := html.New(opts...) - - cacheMutex.Lock() - formatterCache[key] = formatter - cacheMutex.Unlock() - return formatter -} - -func runChroma(cmd *cobra.Command, args []string) { - var source string - var filename string - - // Read from file or stdin - if len(args) > 0 { - filename = args[0] - - // Check file size before reading - fileInfo, err := os.Stat(filename) - if err != nil { - fmt.Fprintf(os.Stderr, "Error reading file info: %v\n", err) - os.Exit(1) - } - - if fileInfo.Size() > maxFileSize { - fmt.Fprintf(os.Stderr, "Warning: File size (%d bytes) exceeds recommended limit (%d bytes)\n", - fileInfo.Size(), maxFileSize) - fmt.Fprintf(os.Stderr, "Processing may be slow. Consider using smaller files.\n") - } - - content, err := os.ReadFile(filename) - if err != nil { - fmt.Fprintf(os.Stderr, "Error reading file: %v\n", err) - os.Exit(1) - } - source = string(content) - } else { - stat, _ := os.Stdin.Stat() - if (stat.Mode() & os.ModeCharDevice) != 0 { - _ = cmd.Help() - os.Exit(0) - } - - content, err := io.ReadAll(os.Stdin) - if err != nil { - fmt.Fprintf(os.Stderr, "Error reading stdin: %v\n", err) - os.Exit(1) - } - source = string(content) - } - - // Handle empty input - if strings.TrimSpace(source) == "" { - return - } - - // Handle Markdown rendering - if chromaMarkdown { - md := goldmark.New( - goldmark.WithExtensions( - extension.GFM, - highlighting.NewHighlighting( - highlighting.WithStyle(chromaStyle), - highlighting.WithFormatOptions( - html.WithClasses(!chromaInline), - ), - ), - ), - goldmark.WithParserOptions( - parser.WithAutoHeadingID(), - ), - goldmark.WithRendererOptions( - ghtml.WithHardWraps(), - ghtml.WithXHTML(), - ), - ) - - var buf bytes.Buffer - if err := md.Convert([]byte(source), &buf); err != nil { - fmt.Fprintf(os.Stderr, "Markdown rendering error: %v\n", err) - os.Exit(1) - } - fmt.Print(buf.String()) - return - } - - // Detect or use specified lexer - var lexer chroma.Lexer - if chromaLanguage != "" { - lexer = getCachedLexer(chromaLanguage, func() chroma.Lexer { - l := lexers.Get(chromaLanguage) - if l == nil { - fmt.Fprintf(os.Stderr, "Unknown language: %s\n", chromaLanguage) - os.Exit(1) - } - return l - }) - } else if filename != "" { - lexer = getCachedLexer("file:"+filename, func() chroma.Lexer { - return lexers.Match(filename) - }) - } - - // Try content analysis if no lexer found (limit to first 1KB for performance) - if lexer == nil { - analyzeContent := source - if len(source) > 1024 { - analyzeContent = source[:1024] - } - lexer = lexers.Analyse(analyzeContent) - } - - // Fallback to plaintext - if lexer == nil { - lexer = lexers.Fallback - } - - lexer = chroma.Coalesce(lexer) - - // Get cached style - style := getCachedStyle(chromaStyle) - - // Get cached formatter - formatter := getCachedFormatter(chromaInline, chromaLineNumbers) - - // Tokenize - iterator, err := lexer.Tokenise(nil, source) - if err != nil { - fmt.Fprintf(os.Stderr, "Tokenization error: %v\n", err) - os.Exit(1) - } - - // Format and output - if chromaLineNumbers { - var buf bytes.Buffer - if err := formatter.Format(&buf, style, iterator); err != nil { - fmt.Fprintf(os.Stderr, "Formatting error: %v\n", err) - os.Exit(1) - } - // Add spacing between line numbers - output := buf.String() - output = strings.ReplaceAll(output, "</span><span>", "</span>\u00A0\u00A0<span>") - fmt.Print(output) - } else { - if err := formatter.Format(os.Stdout, style, iterator); err != nil { - fmt.Fprintf(os.Stderr, "Formatting error: %v\n", err) - os.Exit(1) - } - } -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_clipboard.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_clipboard.go deleted file mode 100644 index 8a9b858..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_clipboard.go +++ /dev/null @@ -1,1004 +0,0 @@ -package main - -import ( - "bytes" - "context" - "encoding/base64" - "encoding/binary" - "encoding/json" - "fmt" - "image" - _ "image/gif" - _ "image/jpeg" - _ "image/png" - "io" - "net/http" - "net/url" - "os" - "os/exec" - "os/signal" - "path/filepath" - "strconv" - "strings" - "syscall" - "time" - - bolt "go.etcd.io/bbolt" - _ "golang.org/x/image/bmp" - _ "golang.org/x/image/tiff" - _ "golang.org/x/image/webp" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard" - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/spf13/cobra" -) - -var clipboardCmd = &cobra.Command{ - Use: "clipboard", - Aliases: []string{"cl"}, - Short: "Manage clipboard", - Long: "Interact with the clipboard manager", -} - -var clipCopyCmd = &cobra.Command{ - Use: "copy [text]", - Short: "Copy text to clipboard", - Long: "Copy text to clipboard. If no text provided, reads from stdin. Works without server.", - Run: runClipCopy, -} - -var ( - clipCopyForeground bool - clipCopyPasteOnce bool - clipCopyType string - clipCopyDownload bool - clipJSONOutput bool -) - -var clipPasteCmd = &cobra.Command{ - Use: "paste", - Short: "Paste text from clipboard", - Long: "Paste text from clipboard to stdout. Works without server.", - Run: runClipPaste, -} - -var clipWatchCmd = &cobra.Command{ - Use: "watch [command]", - Short: "Watch clipboard for changes", - Long: `Watch clipboard for changes and optionally execute a command. -Works like wl-paste --watch. Does not require server. - -If a command is provided, it will be executed each time the clipboard changes, -with the clipboard content piped to its stdin. - -Examples: - dms cl watch # Print clipboard changes to stdout - dms cl watch cat # Same as above - dms cl watch notify-send # Send notification on clipboard change`, - Run: runClipWatch, -} - -var clipHistoryCmd = &cobra.Command{ - Use: "history", - Short: "Show clipboard history", - Long: "Show clipboard history with previews (requires server)", - Run: runClipHistory, -} - -var clipGetCmd = &cobra.Command{ - Use: "get <id>", - Short: "Get clipboard entry by ID", - Long: "Get full clipboard entry data by ID (requires server). Use --copy to copy it to clipboard.", - Args: cobra.ExactArgs(1), - Run: runClipGet, -} - -var clipGetCopy bool - -var clipDeleteCmd = &cobra.Command{ - Use: "delete <id>", - Short: "Delete clipboard entry", - Long: "Delete a clipboard history entry by ID (requires server)", - Args: cobra.ExactArgs(1), - Run: runClipDelete, -} - -var clipClearCmd = &cobra.Command{ - Use: "clear", - Short: "Clear clipboard history", - Long: "Clear all clipboard history (requires server)", - Run: runClipClear, -} - -var clipWatchStore bool -var clipWatchMimes bool - -var clipSearchCmd = &cobra.Command{ - Use: "search [query]", - Short: "Search clipboard history", - Long: "Search clipboard history with filters (requires server)", - Run: runClipSearch, -} - -var ( - clipSearchLimit int - clipSearchOffset int - clipSearchMimeType string - clipSearchImages bool - clipSearchText bool -) - -var clipConfigCmd = &cobra.Command{ - Use: "config", - Short: "Manage clipboard config", - Long: "Get or set clipboard configuration (requires server)", -} - -var clipConfigGetCmd = &cobra.Command{ - Use: "get", - Short: "Get clipboard config", - Run: runClipConfigGet, -} - -var clipConfigSetCmd = &cobra.Command{ - Use: "set", - Short: "Set clipboard config", - Long: `Set clipboard configuration options. - -Examples: - dms cl config set --max-history 200 - dms cl config set --auto-clear-days 7 - dms cl config set --clear-at-startup`, - Run: runClipConfigSet, -} - -var ( - clipConfigMaxHistory int - clipConfigAutoClearDays int - clipConfigClearAtStartup bool - clipConfigNoClearStartup bool - clipConfigDisabled bool - clipConfigEnabled bool -) - -var clipExportCmd = &cobra.Command{ - Use: "export [file]", - Short: "Export clipboard history to JSON", - Long: "Export clipboard history to JSON file. If no file specified, writes to stdout.", - Run: runClipExport, -} - -var clipImportCmd = &cobra.Command{ - Use: "import <file>", - Short: "Import clipboard history from JSON", - Long: "Import clipboard history from JSON file exported by 'dms cl export'.", - Args: cobra.ExactArgs(1), - Run: runClipImport, -} - -var clipMigrateCmd = &cobra.Command{ - Use: "cliphist-migrate [db-path]", - Short: "Migrate from cliphist", - Long: "Migrate clipboard history from cliphist. Uses default cliphist path if not specified.", - Run: runClipMigrate, -} - -var clipMigrateDelete bool - -func init() { - clipCopyCmd.Flags().BoolVarP(&clipCopyForeground, "foreground", "f", false, "Stay in foreground instead of forking") - clipCopyCmd.Flags().BoolVarP(&clipCopyPasteOnce, "paste-once", "o", false, "Exit after first paste") - clipCopyCmd.Flags().StringVarP(&clipCopyType, "type", "t", "text/plain;charset=utf-8", "MIME type") - clipCopyCmd.Flags().BoolVarP(&clipCopyDownload, "download", "d", false, "Download URL as image and copy as file") - - clipWatchCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON") - clipHistoryCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON") - clipGetCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON") - clipGetCmd.Flags().BoolVarP(&clipGetCopy, "copy", "C", false, "Copy entry to clipboard") - - clipSearchCmd.Flags().IntVarP(&clipSearchLimit, "limit", "l", 50, "Max results") - clipSearchCmd.Flags().IntVarP(&clipSearchOffset, "offset", "o", 0, "Result offset") - clipSearchCmd.Flags().StringVarP(&clipSearchMimeType, "mime", "m", "", "Filter by MIME type") - clipSearchCmd.Flags().BoolVar(&clipSearchImages, "images", false, "Only images") - clipSearchCmd.Flags().BoolVar(&clipSearchText, "text", false, "Only text") - clipSearchCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON") - - clipConfigSetCmd.Flags().IntVar(&clipConfigMaxHistory, "max-history", 0, "Max history entries") - clipConfigSetCmd.Flags().IntVar(&clipConfigAutoClearDays, "auto-clear-days", -1, "Auto-clear entries older than N days (0 to disable)") - clipConfigSetCmd.Flags().BoolVar(&clipConfigClearAtStartup, "clear-at-startup", false, "Clear history on startup") - clipConfigSetCmd.Flags().BoolVar(&clipConfigNoClearStartup, "no-clear-at-startup", false, "Don't clear history on startup") - clipConfigSetCmd.Flags().BoolVar(&clipConfigDisabled, "disable", false, "Disable clipboard tracking") - clipConfigSetCmd.Flags().BoolVar(&clipConfigEnabled, "enable", false, "Enable clipboard tracking") - - clipWatchCmd.Flags().BoolVarP(&clipWatchStore, "store", "s", false, "Store clipboard changes to history (no server required)") - clipWatchCmd.Flags().BoolVarP(&clipWatchMimes, "mimes", "m", false, "Show all offered MIME types") - - clipMigrateCmd.Flags().BoolVar(&clipMigrateDelete, "delete", false, "Delete cliphist db after successful migration") - - clipConfigCmd.AddCommand(clipConfigGetCmd, clipConfigSetCmd) - clipboardCmd.AddCommand(clipCopyCmd, clipPasteCmd, clipWatchCmd, clipHistoryCmd, clipGetCmd, clipDeleteCmd, clipClearCmd, clipSearchCmd, clipConfigCmd, clipExportCmd, clipImportCmd, clipMigrateCmd) -} - -func runClipCopy(cmd *cobra.Command, args []string) { - var data []byte - copyFromStdin := false - - switch { - case len(args) > 0: - data = []byte(args[0]) - case clipCopyDownload || clipCopyType == "__multi__": - var err error - data, err = io.ReadAll(os.Stdin) - if err != nil { - log.Fatalf("read stdin: %v", err) - } - default: - copyFromStdin = true - } - - if clipCopyDownload { - filePath, err := downloadToTempFile(strings.TrimSpace(string(data))) - if err != nil { - log.Fatalf("download: %v", err) - } - if err := copyFileToClipboard(filePath); err != nil { - log.Fatalf("copy file: %v", err) - } - fmt.Printf("Downloaded and copied: %s\n", filePath) - return - } - - if clipCopyType == "__multi__" { - offers, err := parseMultiOffers(data) - if err != nil { - log.Fatalf("parse multi offers: %v", err) - } - if err := clipboard.CopyMulti(offers, true, clipCopyPasteOnce); err != nil { - log.Fatalf("copy multi: %v", err) - } - return - } - - if copyFromStdin { - if err := clipboard.CopyReader(os.Stdin, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil { - log.Fatalf("copy: %v", err) - } - return - } - - if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil { - log.Fatalf("copy: %v", err) - } -} - -func parseMultiOffers(data []byte) ([]clipboard.Offer, error) { - var offers []clipboard.Offer - pos := 0 - - for pos < len(data) { - mimeEnd := bytes.IndexByte(data[pos:], 0) - if mimeEnd == -1 { - break - } - mimeType := string(data[pos : pos+mimeEnd]) - pos += mimeEnd + 1 - - lenEnd := bytes.IndexByte(data[pos:], 0) - if lenEnd == -1 { - break - } - dataLen, err := strconv.Atoi(string(data[pos : pos+lenEnd])) - if err != nil { - return nil, fmt.Errorf("parse length: %w", err) - } - pos += lenEnd + 1 - - if pos+dataLen > len(data) { - return nil, fmt.Errorf("data truncated") - } - offerData := data[pos : pos+dataLen] - pos += dataLen - - offers = append(offers, clipboard.Offer{MimeType: mimeType, Data: offerData}) - } - - return offers, nil -} - -func runClipPaste(cmd *cobra.Command, args []string) { - data, _, err := clipboard.Paste() - if err != nil { - log.Fatalf("paste: %v", err) - } - os.Stdout.Write(data) -} - -func runClipWatch(cmd *cobra.Command, args []string) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - go func() { - <-sigCh - cancel() - }() - - switch { - case len(args) > 0: - if err := clipboard.Watch(ctx, func(data []byte, mimeType string) { - runCommand(args, data) - }); err != nil && err != context.Canceled { - log.Fatalf("Watch error: %v", err) - } - case clipWatchStore: - if err := clipboard.Watch(ctx, func(data []byte, mimeType string) { - if err := clipboard.Store(data, mimeType); err != nil { - log.Errorf("store: %v", err) - } - }); err != nil && err != context.Canceled { - log.Fatalf("Watch error: %v", err) - } - case clipWatchMimes: - if err := clipboard.WatchAll(ctx, func(data []byte, mimeType string, allMimes []string) { - if clipJSONOutput { - out := map[string]any{ - "data": string(data), - "mimeType": mimeType, - "mimeTypes": allMimes, - "timestamp": time.Now().Format(time.RFC3339), - "size": len(data), - } - j, _ := json.Marshal(out) - fmt.Println(string(j)) - return - } - fmt.Printf("=== Clipboard Change ===\n") - fmt.Printf("Selected: %s\n", mimeType) - fmt.Printf("All MIME types:\n") - for _, m := range allMimes { - fmt.Printf(" - %s\n", m) - } - fmt.Printf("Size: %d bytes\n\n", len(data)) - }); err != nil && err != context.Canceled { - log.Fatalf("Watch error: %v", err) - } - case clipJSONOutput: - if err := clipboard.Watch(ctx, func(data []byte, mimeType string) { - out := map[string]any{ - "data": string(data), - "mimeType": mimeType, - "timestamp": time.Now().Format(time.RFC3339), - "size": len(data), - } - j, _ := json.Marshal(out) - fmt.Println(string(j)) - }); err != nil && err != context.Canceled { - log.Fatalf("Watch error: %v", err) - } - default: - if err := clipboard.Watch(ctx, func(data []byte, mimeType string) { - os.Stdout.Write(data) - os.Stdout.WriteString("\n") - }); err != nil && err != context.Canceled { - log.Fatalf("Watch error: %v", err) - } - } -} - -func runCommand(args []string, stdin []byte) { - cmd := exec.Command(args[0], args[1:]...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - if len(stdin) == 0 { - cmd.Run() - return - } - - r, w, err := os.Pipe() - if err != nil { - cmd.Run() - return - } - - cmd.Stdin = r - go func() { - w.Write(stdin) - w.Close() - }() - cmd.Run() -} - -func runClipHistory(cmd *cobra.Command, args []string) { - req := models.Request{ - ID: 1, - Method: "clipboard.getHistory", - } - - resp, err := sendServerRequest(req) - if err != nil { - log.Fatalf("Failed to get clipboard history: %v", err) - } - - if resp.Error != "" { - log.Fatalf("Error: %s", resp.Error) - } - - if resp.Result == nil { - if clipJSONOutput { - fmt.Println("[]") - } else { - fmt.Println("No clipboard history") - } - return - } - - historyList, ok := (*resp.Result).([]any) - if !ok { - log.Fatal("Invalid response format") - } - - if clipJSONOutput { - out, _ := json.MarshalIndent(historyList, "", " ") - fmt.Println(string(out)) - return - } - - if len(historyList) == 0 { - fmt.Println("No clipboard history") - return - } - - fmt.Println("Clipboard History:") - fmt.Println() - - for _, item := range historyList { - entry, ok := item.(map[string]any) - if !ok { - continue - } - - id := uint64(entry["id"].(float64)) - preview := entry["preview"].(string) - timestamp := entry["timestamp"].(string) - isImage := entry["isImage"].(bool) - - typeStr := "text" - if isImage { - typeStr = "image" - } - - fmt.Printf("ID: %d | %s | %s\n", id, typeStr, timestamp) - fmt.Printf(" %s\n", preview) - fmt.Println() - } -} - -func runClipGet(cmd *cobra.Command, args []string) { - id, err := strconv.ParseUint(args[0], 10, 64) - if err != nil { - log.Fatalf("Invalid ID: %v", err) - } - - if clipGetCopy { - req := models.Request{ - ID: 1, - Method: "clipboard.copyEntry", - Params: map[string]any{"id": id}, - } - - resp, err := sendServerRequest(req) - if err != nil { - log.Fatalf("Failed to copy clipboard entry: %v", err) - } - if resp.Error != "" { - log.Fatalf("Error: %s", resp.Error) - } - - fmt.Printf("Copied entry %d to clipboard\n", id) - return - } - - req := models.Request{ - ID: 1, - Method: "clipboard.getEntry", - Params: map[string]any{ - "id": id, - }, - } - - resp, err := sendServerRequest(req) - if err != nil { - log.Fatalf("Failed to get clipboard entry: %v", err) - } - - if resp.Error != "" { - log.Fatalf("Error: %s", resp.Error) - } - - if resp.Result == nil { - log.Fatal("Entry not found") - } - - entry, ok := (*resp.Result).(map[string]any) - if !ok { - log.Fatal("Invalid response format") - } - - switch { - case clipJSONOutput: - output, _ := json.MarshalIndent(entry, "", " ") - fmt.Println(string(output)) - default: - if data, ok := entry["data"].(string); ok { - fmt.Print(data) - } else { - output, _ := json.MarshalIndent(entry, "", " ") - fmt.Println(string(output)) - } - } -} - -func runClipDelete(cmd *cobra.Command, args []string) { - id, err := strconv.ParseUint(args[0], 10, 64) - if err != nil { - log.Fatalf("Invalid ID: %v", err) - } - - req := models.Request{ - ID: 1, - Method: "clipboard.deleteEntry", - Params: map[string]any{ - "id": id, - }, - } - - resp, err := sendServerRequest(req) - if err != nil { - log.Fatalf("Failed to delete clipboard entry: %v", err) - } - - if resp.Error != "" { - log.Fatalf("Error: %s", resp.Error) - } - - fmt.Printf("Deleted entry %d\n", id) -} - -func runClipClear(cmd *cobra.Command, args []string) { - req := models.Request{ - ID: 1, - Method: "clipboard.clearHistory", - } - - resp, err := sendServerRequest(req) - if err != nil { - log.Fatalf("Failed to clear clipboard history: %v", err) - } - - if resp.Error != "" { - log.Fatalf("Error: %s", resp.Error) - } - - fmt.Println("Clipboard history cleared") -} - -func runClipSearch(cmd *cobra.Command, args []string) { - params := map[string]any{ - "limit": clipSearchLimit, - "offset": clipSearchOffset, - } - - if len(args) > 0 { - params["query"] = args[0] - } - if clipSearchMimeType != "" { - params["mimeType"] = clipSearchMimeType - } - if clipSearchImages { - params["isImage"] = true - } else if clipSearchText { - params["isImage"] = false - } - - req := models.Request{ - ID: 1, - Method: "clipboard.search", - Params: params, - } - - resp, err := sendServerRequest(req) - if err != nil { - log.Fatalf("Failed to search clipboard: %v", err) - } - - if resp.Error != "" { - log.Fatalf("Error: %s", resp.Error) - } - - if resp.Result == nil { - log.Fatal("No results") - } - - result, ok := (*resp.Result).(map[string]any) - if !ok { - log.Fatal("Invalid response format") - } - - if clipJSONOutput { - out, _ := json.MarshalIndent(result, "", " ") - fmt.Println(string(out)) - return - } - - entries, _ := result["entries"].([]any) - total := int(result["total"].(float64)) - hasMore := result["hasMore"].(bool) - - if len(entries) == 0 { - fmt.Println("No results found") - return - } - - fmt.Printf("Results: %d of %d\n\n", len(entries), total) - - for _, item := range entries { - entry, ok := item.(map[string]any) - if !ok { - continue - } - - id := uint64(entry["id"].(float64)) - preview := entry["preview"].(string) - timestamp := entry["timestamp"].(string) - isImage := entry["isImage"].(bool) - - typeStr := "text" - if isImage { - typeStr = "image" - } - - fmt.Printf("ID: %d | %s | %s\n", id, typeStr, timestamp) - fmt.Printf(" %s\n\n", preview) - } - - if hasMore { - fmt.Printf("Use --offset %d to see more results\n", clipSearchOffset+clipSearchLimit) - } -} - -func runClipConfigGet(cmd *cobra.Command, args []string) { - req := models.Request{ - ID: 1, - Method: "clipboard.getConfig", - } - - resp, err := sendServerRequest(req) - if err != nil { - log.Fatalf("Failed to get config: %v", err) - } - - if resp.Error != "" { - log.Fatalf("Error: %s", resp.Error) - } - - if resp.Result == nil { - log.Fatal("No config returned") - } - - cfg, ok := (*resp.Result).(map[string]any) - if !ok { - log.Fatal("Invalid response format") - } - - output, _ := json.MarshalIndent(cfg, "", " ") - fmt.Println(string(output)) -} - -func runClipConfigSet(cmd *cobra.Command, args []string) { - params := map[string]any{} - - if cmd.Flags().Changed("max-history") { - params["maxHistory"] = clipConfigMaxHistory - } - if cmd.Flags().Changed("auto-clear-days") { - params["autoClearDays"] = clipConfigAutoClearDays - } - if clipConfigClearAtStartup { - params["clearAtStartup"] = true - } - if clipConfigNoClearStartup { - params["clearAtStartup"] = false - } - if clipConfigDisabled { - params["disabled"] = true - } - if clipConfigEnabled { - params["disabled"] = false - } - - if len(params) == 0 { - fmt.Println("No config options specified") - return - } - - req := models.Request{ - ID: 1, - Method: "clipboard.setConfig", - Params: params, - } - - resp, err := sendServerRequest(req) - if err != nil { - log.Fatalf("Failed to set config: %v", err) - } - - if resp.Error != "" { - log.Fatalf("Error: %s", resp.Error) - } - - fmt.Println("Config updated") -} - -func runClipExport(cmd *cobra.Command, args []string) { - req := models.Request{ - ID: 1, - Method: "clipboard.getHistory", - } - - resp, err := sendServerRequest(req) - if err != nil { - log.Fatalf("Failed to get clipboard history: %v", err) - } - if resp.Error != "" { - log.Fatalf("Error: %s", resp.Error) - } - if resp.Result == nil { - log.Fatal("No clipboard history") - } - - out, err := json.MarshalIndent(resp.Result, "", " ") - if err != nil { - log.Fatalf("Failed to marshal: %v", err) - } - - if len(args) == 0 { - fmt.Println(string(out)) - return - } - - if err := os.WriteFile(args[0], out, 0o644); err != nil { - log.Fatalf("Failed to write file: %v", err) - } - fmt.Printf("Exported to %s\n", args[0]) -} - -func runClipImport(cmd *cobra.Command, args []string) { - data, err := os.ReadFile(args[0]) - if err != nil { - log.Fatalf("Failed to read file: %v", err) - } - - var entries []map[string]any - if err := json.Unmarshal(data, &entries); err != nil { - log.Fatalf("Failed to parse JSON: %v", err) - } - - var imported int - for _, entry := range entries { - dataStr, ok := entry["data"].(string) - if !ok { - continue - } - mimeType, _ := entry["mimeType"].(string) - if mimeType == "" { - mimeType = "text/plain" - } - - var entryData []byte - if decoded, err := base64.StdEncoding.DecodeString(dataStr); err == nil { - entryData = decoded - } else { - entryData = []byte(dataStr) - } - - if err := clipboard.Store(entryData, mimeType); err != nil { - log.Errorf("Failed to store entry: %v", err) - continue - } - imported++ - } - - fmt.Printf("Imported %d entries\n", imported) -} - -func runClipMigrate(cmd *cobra.Command, args []string) { - dbPath := getCliphistPath() - if len(args) > 0 { - dbPath = args[0] - } - - if _, err := os.Stat(dbPath); err != nil { - log.Fatalf("Cliphist db not found: %s", dbPath) - } - - db, err := bolt.Open(dbPath, 0o644, &bolt.Options{ - ReadOnly: true, - Timeout: 1 * time.Second, - }) - if err != nil { - log.Fatalf("Failed to open cliphist db: %v", err) - } - defer db.Close() - - var migrated int - err = db.View(func(tx *bolt.Tx) error { - b := tx.Bucket([]byte("b")) - if b == nil { - return fmt.Errorf("cliphist bucket not found") - } - - c := b.Cursor() - for k, v := c.First(); k != nil; k, v = c.Next() { - if len(v) == 0 { - continue - } - - mimeType := detectMimeType(v) - if err := clipboard.Store(v, mimeType); err != nil { - log.Errorf("Failed to store entry %d: %v", btoi(k), err) - continue - } - migrated++ - } - return nil - }) - if err != nil { - log.Fatalf("Migration failed: %v", err) - } - - fmt.Printf("Migrated %d entries from cliphist\n", migrated) - - if !clipMigrateDelete { - return - } - - db.Close() - if err := os.Remove(dbPath); err != nil { - log.Errorf("Failed to delete cliphist db: %v", err) - return - } - os.Remove(filepath.Dir(dbPath)) - fmt.Println("Deleted cliphist db") -} - -func getCliphistPath() string { - cacheDir, err := os.UserCacheDir() - if err != nil { - return filepath.Join(os.Getenv("HOME"), ".cache", "cliphist", "db") - } - return filepath.Join(cacheDir, "cliphist", "db") -} - -func detectMimeType(data []byte) string { - if _, _, err := image.DecodeConfig(bytes.NewReader(data)); err == nil { - return "image/png" - } - return "text/plain" -} - -func btoi(v []byte) uint64 { - return binary.BigEndian.Uint64(v) -} - -func downloadToTempFile(rawURL string) (string, error) { - if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") { - return "", fmt.Errorf("invalid URL: %s", rawURL) - } - - parsedURL, err := url.Parse(rawURL) - if err != nil { - return "", fmt.Errorf("parse URL: %w", err) - } - - ext := filepath.Ext(parsedURL.Path) - if ext == "" { - ext = ".png" - } - - client := &http.Client{Timeout: 30 * time.Second} - - var data []byte - var contentType string - var lastErr error - - for attempt := 0; attempt < 3; attempt++ { - if attempt > 0 { - time.Sleep(time.Duration(attempt) * 500 * time.Millisecond) - } - - req, err := http.NewRequest("GET", rawURL, nil) - if err != nil { - lastErr = fmt.Errorf("create request: %w", err) - continue - } - req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") - req.Header.Set("Accept", "image/*,video/*,*/*") - - resp, err := client.Do(req) - if err != nil { - lastErr = fmt.Errorf("download (attempt %d): %w", attempt+1, err) - continue - } - - if resp.StatusCode != http.StatusOK { - resp.Body.Close() - lastErr = fmt.Errorf("download failed (attempt %d): status %d", attempt+1, resp.StatusCode) - continue - } - - data, err = io.ReadAll(resp.Body) - resp.Body.Close() - if err != nil { - lastErr = fmt.Errorf("read response (attempt %d): %w", attempt+1, err) - continue - } - - contentType = resp.Header.Get("Content-Type") - if idx := strings.Index(contentType, ";"); idx != -1 { - contentType = strings.TrimSpace(contentType[:idx]) - } - - lastErr = nil - break - } - - if lastErr != nil { - return "", lastErr - } - - if len(data) == 0 { - return "", fmt.Errorf("downloaded empty file") - } - - if !strings.HasPrefix(contentType, "image/") && !strings.HasPrefix(contentType, "video/") { - if _, _, err := image.DecodeConfig(bytes.NewReader(data)); err != nil { - return "", fmt.Errorf("not a valid media file (content-type: %s)", contentType) - } - } - - cacheDir, err := os.UserCacheDir() - if err != nil { - cacheDir = "/tmp" - } - clipDir := filepath.Join(cacheDir, "dms", "clipboard") - if err := os.MkdirAll(clipDir, 0o755); err != nil { - return "", fmt.Errorf("create cache dir: %w", err) - } - - filePath := filepath.Join(clipDir, fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)) - if err := os.WriteFile(filePath, data, 0o644); err != nil { - return "", fmt.Errorf("write file: %w", err) - } - - return filePath, nil -} - -func copyFileToClipboard(filePath string) error { - req := models.Request{ - ID: 1, - Method: "clipboard.copyFile", - Params: map[string]any{"filePath": filePath}, - } - - resp, err := sendServerRequest(req) - if err != nil { - return fmt.Errorf("server request: %w", err) - } - if resp.Error != "" { - return fmt.Errorf("server error: %s", resp.Error) - } - return nil -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_colorpicker.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_colorpicker.go deleted file mode 100644 index 7c366e8..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_colorpicker.go +++ /dev/null @@ -1,139 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard" - "github.com/AvengeMedia/DankMaterialShell/core/internal/colorpicker" - "github.com/spf13/cobra" -) - -var ( - colorOutputFmt string - colorAutocopy bool - colorNotify bool - colorLowercase bool -) - -var colorCmd = &cobra.Command{ - Use: "color", - Short: "Color utilities", - Long: "Color utilities including picking colors from the screen", -} - -var colorPickCmd = &cobra.Command{ - Use: "pick", - Short: "Pick a color from the screen", - Long: `Pick a color from anywhere on your screen using an interactive color picker. - -Click on any pixel to capture its color, or press Escape to cancel. - -Output format flags (mutually exclusive, default: --hex): - --hex - Hexadecimal (#RRGGBB) - --rgb - RGB values (R G B) - --hsl - HSL values (H S% L%) - --hsv - HSV values (H S% V%) - --cmyk - CMYK values (C% M% Y% K%) - --json - JSON with all formats - -Optional: - --raw - Removes ANSI escape codes and background colors. Use this when piping to other commands - -Examples: - dms color pick # Pick color, output as hex - dms color pick --rgb # Output as RGB - dms color pick --json # Output all formats as JSON - dms color pick --hex -l # Output hex in lowercase - dms color pick -a # Auto-copy result to clipboard`, - Run: runColorPick, -} - -func init() { - colorPickCmd.Flags().Bool("hex", false, "Output as hexadecimal (#RRGGBB)") - colorPickCmd.Flags().Bool("rgb", false, "Output as RGB (R G B)") - colorPickCmd.Flags().Bool("hsl", false, "Output as HSL (H S% L%)") - colorPickCmd.Flags().Bool("hsv", false, "Output as HSV (H S% V%)") - colorPickCmd.Flags().Bool("cmyk", false, "Output as CMYK (C% M% Y% K%)") - colorPickCmd.Flags().Bool("json", false, "Output all formats as JSON") - colorPickCmd.Flags().Bool("raw", false, "Removes ANSI escape codes and background colors. Use this when piping to other commands") - colorPickCmd.Flags().StringVarP(&colorOutputFmt, "output-format", "o", "", "Custom output format template") - colorPickCmd.Flags().BoolVarP(&colorAutocopy, "autocopy", "a", false, "Copy result to clipboard") - colorPickCmd.Flags().BoolVarP(&colorLowercase, "lowercase", "l", false, "Output hex in lowercase") - - colorPickCmd.MarkFlagsMutuallyExclusive("hex", "rgb", "hsl", "hsv", "cmyk", "json") - - colorCmd.AddCommand(colorPickCmd) -} - -func runColorPick(cmd *cobra.Command, args []string) { - format := colorpicker.FormatHex // default - jsonOutput, _ := cmd.Flags().GetBool("json") - - if rgb, _ := cmd.Flags().GetBool("rgb"); rgb { - format = colorpicker.FormatRGB - } else if hsl, _ := cmd.Flags().GetBool("hsl"); hsl { - format = colorpicker.FormatHSL - } else if hsv, _ := cmd.Flags().GetBool("hsv"); hsv { - format = colorpicker.FormatHSV - } else if cmyk, _ := cmd.Flags().GetBool("cmyk"); cmyk { - format = colorpicker.FormatCMYK - } - - config := colorpicker.Config{ - Format: format, - CustomFormat: colorOutputFmt, - Lowercase: colorLowercase, - Autocopy: colorAutocopy, - Notify: colorNotify, - } - - picker := colorpicker.New(config) - color, err := picker.Run() - if err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) - } - - if color == nil { - os.Exit(0) - } - - var output string - if jsonOutput { - jsonStr, err := color.ToJSON() - if err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) - } - output = jsonStr - } else { - output = color.Format(config.Format, config.Lowercase, config.CustomFormat) - } - - if colorAutocopy { - copyToClipboard(output) - } - - if jsonOutput { - fmt.Println(output) - return - } - - if raw, _ := cmd.Flags().GetBool("raw"); raw { - fmt.Printf("%s\n", output) - return - } - - if color.IsDark() { - fmt.Printf("\033[48;2;%d;%d;%dm\033[97m %s \033[0m\n", color.R, color.G, color.B, output) - } else { - fmt.Printf("\033[48;2;%d;%d;%dm\033[30m %s \033[0m\n", color.R, color.G, color.B, output) - } -} - -func copyToClipboard(text string) { - if err := clipboard.CopyText(text); err != nil { - fmt.Fprintln(os.Stderr, "clipboard copy failed:", err) - } -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_common.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_common.go deleted file mode 100644 index 6557860..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_common.go +++ /dev/null @@ -1,530 +0,0 @@ -package main - -import ( - "fmt" - "os" - "regexp" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/internal/plugins" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server" - "github.com/spf13/cobra" -) - -var versionCmd = &cobra.Command{ - Use: "version", - Short: "Show version information", - Run: runVersion, -} - -var runCmd = &cobra.Command{ - Use: "run", - Short: "Launch quickshell with DMS configuration", - Long: "Launch quickshell with DMS configuration (qs -c dms)", - PreRunE: findConfig, - Run: func(cmd *cobra.Command, args []string) { - daemon, _ := cmd.Flags().GetBool("daemon") - session, _ := cmd.Flags().GetBool("session") - if daemon { - runShellDaemon(session) - } else { - runShellInteractive(session) - } - }, -} - -var restartCmd = &cobra.Command{ - Use: "restart", - Short: "Restart quickshell with DMS configuration", - Long: "Kill existing DMS shell processes and restart quickshell with DMS configuration", - PreRunE: findConfig, - Run: func(cmd *cobra.Command, args []string) { - restartShell() - }, -} - -var restartDetachedCmd = &cobra.Command{ - Use: "restart-detached <pid>", - Hidden: true, - Args: cobra.ExactArgs(1), - PreRunE: findConfig, - Run: func(cmd *cobra.Command, args []string) { - runDetachedRestart(args[0]) - }, -} - -var killCmd = &cobra.Command{ - Use: "kill", - Short: "Kill running DMS shell processes", - Long: "Kill all running quickshell processes with DMS configuration", - Run: func(cmd *cobra.Command, args []string) { - killShell() - }, -} - -var ipcCmd = &cobra.Command{ - Use: "ipc [target] [function] [args...]", - Short: "Send IPC commands to running DMS shell", - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - _ = findConfig(cmd, args) - return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp - }, - Run: func(cmd *cobra.Command, args []string) { - runShellIPCCommand(args) - }, -} - -func init() { - ipcCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { - _ = findConfig(cmd, args) - printIPCHelp() - }) -} - -var debugSrvCmd = &cobra.Command{ - Use: "debug-srv", - Short: "Start the debug server", - Long: "Start the Unix socket debug server for DMS", - Run: func(cmd *cobra.Command, args []string) { - if err := startDebugServer(); err != nil { - log.Fatalf("Error starting debug server: %v", err) - } - }, -} - -var pluginsCmd = &cobra.Command{ - Use: "plugins", - Short: "Manage DMS plugins", - Long: "Browse and manage DMS plugins from the registry", -} - -var pluginsBrowseCmd = &cobra.Command{ - Use: "browse", - Short: "Browse available plugins", - Long: "Browse available plugins from the DMS plugin registry", - Run: func(cmd *cobra.Command, args []string) { - if err := browsePlugins(); err != nil { - log.Fatalf("Error browsing plugins: %v", err) - } - }, -} - -var pluginsListCmd = &cobra.Command{ - Use: "list", - Short: "List installed plugins", - Long: "List all installed DMS plugins", - Run: func(cmd *cobra.Command, args []string) { - if err := listInstalledPlugins(); err != nil { - log.Fatalf("Error listing plugins: %v", err) - } - }, -} - -var pluginsInstallCmd = &cobra.Command{ - Use: "install <plugin-id>", - Short: "Install a plugin by ID", - Long: "Install a DMS plugin from the registry using its ID (e.g., 'myPlugin'). Plugin names with spaces are also supported for backward compatibility.", - Args: cobra.ExactArgs(1), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) != 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - return getAvailablePluginIDs(), cobra.ShellCompDirectiveNoFileComp - }, - Run: func(cmd *cobra.Command, args []string) { - if err := installPluginCLI(args[0]); err != nil { - log.Fatalf("Error installing plugin: %v", err) - } - }, -} - -var pluginsUninstallCmd = &cobra.Command{ - Use: "uninstall <plugin-id>", - Short: "Uninstall a plugin by ID", - Long: "Uninstall a DMS plugin using its ID (e.g., 'myPlugin'). Plugin names with spaces are also supported for backward compatibility.", - Args: cobra.ExactArgs(1), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) != 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - return getInstalledPluginIDs(), cobra.ShellCompDirectiveNoFileComp - }, - Run: func(cmd *cobra.Command, args []string) { - if err := uninstallPluginCLI(args[0]); err != nil { - log.Fatalf("Error uninstalling plugin: %v", err) - } - }, -} - -var pluginsUpdateCmd = &cobra.Command{ - Use: "update <plugin-id>", - Short: "Update a plugin by ID", - Long: "Update an installed DMS plugin using its ID (e.g., 'myPlugin'). Plugin names are also supported.", - Args: cobra.ExactArgs(1), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) != 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - return getInstalledPluginIDs(), cobra.ShellCompDirectiveNoFileComp - }, - Run: func(cmd *cobra.Command, args []string) { - if err := updatePluginCLI(args[0]); err != nil { - log.Fatalf("Error updating plugin: %v", err) - } - }, -} - -func runVersion(cmd *cobra.Command, args []string) { - fmt.Printf("%s\n", formatVersion(Version)) -} - -// Git builds: dms (git) v0.6.2-XXXX -// Stable releases: dms v0.6.2 -func formatVersion(version string) string { - // Arch/Debian/Ubuntu/OpenSUSE git format: 0.6.2+git2264.c5c5ce84 - re := regexp.MustCompile(`^([\d.]+)\+git(\d+)\.`) - if matches := re.FindStringSubmatch(version); matches != nil { - return fmt.Sprintf("dms (git) v%s-%s", matches[1], matches[2]) - } - - // Fedora COPR git format: 0.0.git.2267.d430cae9 - re = regexp.MustCompile(`^[\d.]+\.git\.(\d+)\.`) - if matches := re.FindStringSubmatch(version); matches != nil { - baseVersion := getBaseVersion() - return fmt.Sprintf("dms (git) v%s-%s", baseVersion, matches[1]) - } - - // Stable release format: 0.6.2 - re = regexp.MustCompile(`^([\d.]+)$`) - if matches := re.FindStringSubmatch(version); matches != nil { - return fmt.Sprintf("dms v%s", matches[1]) - } - - return fmt.Sprintf("dms %s", version) -} - -func getBaseVersion() string { - paths := []string{ - "/usr/share/quickshell/dms/VERSION", - "/usr/local/share/quickshell/dms/VERSION", - "/etc/xdg/quickshell/dms/VERSION", - } - - for _, path := range paths { - if content, err := os.ReadFile(path); err == nil { - ver := strings.TrimSpace(string(content)) - ver = strings.TrimPrefix(ver, "v") - if re := regexp.MustCompile(`^([\d.]+)`); re.MatchString(ver) { - if matches := re.FindStringSubmatch(ver); matches != nil { - return matches[1] - } - } - } - } - - // Fallback - return "1.0.2" -} - -func startDebugServer() error { - server.CLIVersion = Version - return server.Start(true) -} - -func browsePlugins() error { - registry, err := plugins.NewRegistry() - if err != nil { - return fmt.Errorf("failed to create registry: %w", err) - } - - manager, err := plugins.NewManager() - if err != nil { - return fmt.Errorf("failed to create manager: %w", err) - } - - fmt.Println("Fetching plugin registry...") - pluginList, err := registry.List() - if err != nil { - return fmt.Errorf("failed to list plugins: %w", err) - } - - if len(pluginList) == 0 { - fmt.Println("No plugins found in registry.") - return nil - } - - fmt.Printf("\nAvailable Plugins (%d):\n\n", len(pluginList)) - for _, plugin := range pluginList { - installed, _ := manager.IsInstalled(plugin) - installedMarker := "" - if installed { - installedMarker = " [Installed]" - } - - fmt.Printf(" %s%s\n", plugin.Name, installedMarker) - fmt.Printf(" ID: %s\n", plugin.ID) - fmt.Printf(" Category: %s\n", plugin.Category) - fmt.Printf(" Author: %s\n", plugin.Author) - fmt.Printf(" Description: %s\n", plugin.Description) - fmt.Printf(" Repository: %s\n", plugin.Repo) - if len(plugin.Capabilities) > 0 { - fmt.Printf(" Capabilities: %s\n", strings.Join(plugin.Capabilities, ", ")) - } - if len(plugin.Compositors) > 0 { - fmt.Printf(" Compositors: %s\n", strings.Join(plugin.Compositors, ", ")) - } - if len(plugin.Dependencies) > 0 { - fmt.Printf(" Dependencies: %s\n", strings.Join(plugin.Dependencies, ", ")) - } - fmt.Println() - } - - return nil -} - -func listInstalledPlugins() error { - manager, err := plugins.NewManager() - if err != nil { - return fmt.Errorf("failed to create manager: %w", err) - } - - registry, err := plugins.NewRegistry() - if err != nil { - return fmt.Errorf("failed to create registry: %w", err) - } - - installedNames, err := manager.ListInstalled() - if err != nil { - return fmt.Errorf("failed to list installed plugins: %w", err) - } - - if len(installedNames) == 0 { - fmt.Println("No plugins installed.") - return nil - } - - allPlugins, err := registry.List() - if err != nil { - return fmt.Errorf("failed to list plugins: %w", err) - } - - pluginMap := make(map[string]plugins.Plugin) - for _, p := range allPlugins { - pluginMap[p.ID] = p - } - - fmt.Printf("\nInstalled Plugins (%d):\n\n", len(installedNames)) - for _, id := range installedNames { - if plugin, ok := pluginMap[id]; ok { - fmt.Printf(" %s\n", plugin.Name) - fmt.Printf(" ID: %s\n", plugin.ID) - fmt.Printf(" Category: %s\n", plugin.Category) - fmt.Printf(" Author: %s\n", plugin.Author) - fmt.Println() - } else { - fmt.Printf(" %s (not in registry)\n\n", id) - } - } - - return nil -} - -func installPluginCLI(idOrName string) error { - registry, err := plugins.NewRegistry() - if err != nil { - return fmt.Errorf("failed to create registry: %w", err) - } - - manager, err := plugins.NewManager() - if err != nil { - return fmt.Errorf("failed to create manager: %w", err) - } - - pluginList, err := registry.List() - if err != nil { - return fmt.Errorf("failed to list plugins: %w", err) - } - - // First, try to find by ID (preferred method) - var plugin *plugins.Plugin - for _, p := range pluginList { - if p.ID == idOrName { - plugin = &p - break - } - } - - // Fallback to name for backward compatibility - if plugin == nil { - for _, p := range pluginList { - if p.Name == idOrName { - plugin = &p - break - } - } - } - - if plugin == nil { - return fmt.Errorf("plugin not found: %s", idOrName) - } - - installed, err := manager.IsInstalled(*plugin) - if err != nil { - return fmt.Errorf("failed to check install status: %w", err) - } - - if installed { - return fmt.Errorf("plugin already installed: %s", plugin.Name) - } - - fmt.Printf("Installing plugin: %s (ID: %s)\n", plugin.Name, plugin.ID) - if err := manager.Install(*plugin); err != nil { - return fmt.Errorf("failed to install plugin: %w", err) - } - - fmt.Printf("Plugin installed successfully: %s\n", plugin.Name) - return nil -} - -func getAvailablePluginIDs() []string { - registry, err := plugins.NewRegistry() - if err != nil { - return nil - } - - pluginList, err := registry.List() - if err != nil { - return nil - } - - var ids []string - for _, p := range pluginList { - ids = append(ids, p.ID) - } - return ids -} - -func getInstalledPluginIDs() []string { - manager, err := plugins.NewManager() - if err != nil { - return nil - } - - installed, err := manager.ListInstalled() - if err != nil { - return nil - } - - return installed -} - -func uninstallPluginCLI(idOrName string) error { - manager, err := plugins.NewManager() - if err != nil { - return fmt.Errorf("failed to create manager: %w", err) - } - - registry, err := plugins.NewRegistry() - if err != nil { - return fmt.Errorf("failed to create registry: %w", err) - } - - pluginList, _ := registry.List() - plugin := plugins.FindByIDOrName(idOrName, pluginList) - - if plugin != nil { - installed, err := manager.IsInstalled(*plugin) - if err != nil { - return fmt.Errorf("failed to check install status: %w", err) - } - if !installed { - return fmt.Errorf("plugin not installed: %s", plugin.Name) - } - - fmt.Printf("Uninstalling plugin: %s (ID: %s)\n", plugin.Name, plugin.ID) - if err := manager.Uninstall(*plugin); err != nil { - return fmt.Errorf("failed to uninstall plugin: %w", err) - } - fmt.Printf("Plugin uninstalled successfully: %s\n", plugin.Name) - return nil - } - - fmt.Printf("Uninstalling plugin: %s\n", idOrName) - if err := manager.UninstallByIDOrName(idOrName); err != nil { - return err - } - fmt.Printf("Plugin uninstalled successfully: %s\n", idOrName) - return nil -} - -func updatePluginCLI(idOrName string) error { - manager, err := plugins.NewManager() - if err != nil { - return fmt.Errorf("failed to create manager: %w", err) - } - - registry, err := plugins.NewRegistry() - if err != nil { - return fmt.Errorf("failed to create registry: %w", err) - } - - pluginList, _ := registry.List() - plugin := plugins.FindByIDOrName(idOrName, pluginList) - - if plugin != nil { - installed, err := manager.IsInstalled(*plugin) - if err != nil { - return fmt.Errorf("failed to check install status: %w", err) - } - if !installed { - return fmt.Errorf("plugin not installed: %s", plugin.Name) - } - - fmt.Printf("Updating plugin: %s (ID: %s)\n", plugin.Name, plugin.ID) - if err := manager.Update(*plugin); err != nil { - return fmt.Errorf("failed to update plugin: %w", err) - } - fmt.Printf("Plugin updated successfully: %s\n", plugin.Name) - return nil - } - - fmt.Printf("Updating plugin: %s\n", idOrName) - if err := manager.UpdateByIDOrName(idOrName); err != nil { - return err - } - fmt.Printf("Plugin updated successfully: %s\n", idOrName) - return nil -} - -func getCommonCommands() []*cobra.Command { - return []*cobra.Command{ - versionCmd, - runCmd, - restartCmd, - restartDetachedCmd, - killCmd, - ipcCmd, - debugSrvCmd, - pluginsCmd, - dank16Cmd, - brightnessCmd, - dpmsCmd, - keybindsCmd, - greeterCmd, - setupCmd, - colorCmd, - screenshotCmd, - notifyActionCmd, - notifyCmd, - genericNotifyActionCmd, - matugenCmd, - clipboardCmd, - chromaCmd, - doctorCmd, - configCmd, - dlCmd, - randrCmd, - blurCmd, - } -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_config.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_config.go deleted file mode 100644 index d604256..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_config.go +++ /dev/null @@ -1,318 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" - "github.com/spf13/cobra" -) - -var configCmd = &cobra.Command{ - Use: "config", - Short: "Configuration utilities", -} - -var resolveIncludeCmd = &cobra.Command{ - Use: "resolve-include <compositor> <filename>", - Short: "Check if a file is included in compositor config", - Long: "Recursively check if a file is included/sourced in compositor configuration. Returns JSON with exists and included status.", - Args: cobra.ExactArgs(2), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - switch len(args) { - case 0: - return []string{"hyprland", "niri", "mangowc"}, cobra.ShellCompDirectiveNoFileComp - case 1: - return []string{"cursor.kdl", "cursor.conf", "outputs.kdl", "outputs.conf", "binds.kdl", "binds.conf"}, cobra.ShellCompDirectiveNoFileComp - } - return nil, cobra.ShellCompDirectiveNoFileComp - }, - Run: runResolveInclude, -} - -func init() { - configCmd.AddCommand(resolveIncludeCmd) -} - -type IncludeResult struct { - Exists bool `json:"exists"` - Included bool `json:"included"` -} - -func runResolveInclude(cmd *cobra.Command, args []string) { - compositor := strings.ToLower(args[0]) - filename := args[1] - - var result IncludeResult - var err error - - switch compositor { - case "hyprland": - result, err = checkHyprlandInclude(filename) - case "niri": - result, err = checkNiriInclude(filename) - case "mangowc", "dwl", "mango": - result, err = checkMangoWCInclude(filename) - default: - log.Fatalf("Unknown compositor: %s", compositor) - } - - if err != nil { - log.Fatalf("Error checking include: %v", err) - } - - output, _ := json.Marshal(result) - fmt.Fprintln(os.Stdout, string(output)) -} - -func checkHyprlandInclude(filename string) (IncludeResult, error) { - configDir, err := utils.ExpandPath("$HOME/.config/hypr") - if err != nil { - return IncludeResult{}, err - } - - targetPath := filepath.Join(configDir, "dms", filename) - result := IncludeResult{} - - if _, err := os.Stat(targetPath); err == nil { - result.Exists = true - } - - mainConfig := filepath.Join(configDir, "hyprland.conf") - if _, err := os.Stat(mainConfig); os.IsNotExist(err) { - return result, nil - } - - processed := make(map[string]bool) - result.Included = hyprlandFindInclude(mainConfig, "dms/"+filename, processed) - return result, nil -} - -func hyprlandFindInclude(filePath, target string, processed map[string]bool) bool { - absPath, err := filepath.Abs(filePath) - if err != nil { - return false - } - - if processed[absPath] { - return false - } - processed[absPath] = true - - data, err := os.ReadFile(absPath) - if err != nil { - return false - } - - baseDir := filepath.Dir(absPath) - lines := strings.Split(string(data), "\n") - - for _, line := range lines { - trimmed := strings.TrimSpace(line) - if strings.HasPrefix(trimmed, "#") || trimmed == "" { - continue - } - - if !strings.HasPrefix(trimmed, "source") { - continue - } - - parts := strings.SplitN(trimmed, "=", 2) - if len(parts) < 2 { - continue - } - - sourcePath := strings.TrimSpace(parts[1]) - if matchesTarget(sourcePath, target) { - return true - } - - fullPath := sourcePath - if !filepath.IsAbs(sourcePath) { - fullPath = filepath.Join(baseDir, sourcePath) - } - - expanded, err := utils.ExpandPath(fullPath) - if err != nil { - continue - } - - if hyprlandFindInclude(expanded, target, processed) { - return true - } - } - - return false -} - -func checkNiriInclude(filename string) (IncludeResult, error) { - configDir, err := utils.ExpandPath("$HOME/.config/niri") - if err != nil { - return IncludeResult{}, err - } - - targetPath := filepath.Join(configDir, "dms", filename) - result := IncludeResult{} - - if _, err := os.Stat(targetPath); err == nil { - result.Exists = true - } - - mainConfig := filepath.Join(configDir, "config.kdl") - if _, err := os.Stat(mainConfig); os.IsNotExist(err) { - return result, nil - } - - processed := make(map[string]bool) - result.Included = niriFindInclude(mainConfig, "dms/"+filename, processed) - return result, nil -} - -func niriFindInclude(filePath, target string, processed map[string]bool) bool { - absPath, err := filepath.Abs(filePath) - if err != nil { - return false - } - - if processed[absPath] { - return false - } - processed[absPath] = true - - data, err := os.ReadFile(absPath) - if err != nil { - return false - } - - baseDir := filepath.Dir(absPath) - content := string(data) - - for _, line := range strings.Split(content, "\n") { - trimmed := strings.TrimSpace(line) - if strings.HasPrefix(trimmed, "//") || trimmed == "" { - continue - } - - if !strings.HasPrefix(trimmed, "include") { - continue - } - - startQuote := strings.Index(trimmed, "\"") - if startQuote == -1 { - continue - } - endQuote := strings.LastIndex(trimmed, "\"") - if endQuote <= startQuote { - continue - } - - includePath := trimmed[startQuote+1 : endQuote] - if matchesTarget(includePath, target) { - return true - } - - fullPath := includePath - if !filepath.IsAbs(includePath) { - fullPath = filepath.Join(baseDir, includePath) - } - - if niriFindInclude(fullPath, target, processed) { - return true - } - } - - return false -} - -func checkMangoWCInclude(filename string) (IncludeResult, error) { - configDir, err := utils.ExpandPath("$HOME/.config/mango") - if err != nil { - return IncludeResult{}, err - } - - targetPath := filepath.Join(configDir, "dms", filename) - result := IncludeResult{} - - if _, err := os.Stat(targetPath); err == nil { - result.Exists = true - } - - mainConfig := filepath.Join(configDir, "config.conf") - if _, err := os.Stat(mainConfig); os.IsNotExist(err) { - mainConfig = filepath.Join(configDir, "mango.conf") - } - if _, err := os.Stat(mainConfig); os.IsNotExist(err) { - return result, nil - } - - processed := make(map[string]bool) - result.Included = mangowcFindInclude(mainConfig, "dms/"+filename, processed) - return result, nil -} - -func mangowcFindInclude(filePath, target string, processed map[string]bool) bool { - absPath, err := filepath.Abs(filePath) - if err != nil { - return false - } - - if processed[absPath] { - return false - } - processed[absPath] = true - - data, err := os.ReadFile(absPath) - if err != nil { - return false - } - - baseDir := filepath.Dir(absPath) - lines := strings.Split(string(data), "\n") - - for _, line := range lines { - trimmed := strings.TrimSpace(line) - if strings.HasPrefix(trimmed, "#") || trimmed == "" { - continue - } - - if !strings.HasPrefix(trimmed, "source") { - continue - } - - parts := strings.SplitN(trimmed, "=", 2) - if len(parts) < 2 { - continue - } - - sourcePath := strings.TrimSpace(parts[1]) - if matchesTarget(sourcePath, target) { - return true - } - - fullPath := sourcePath - if !filepath.IsAbs(sourcePath) { - fullPath = filepath.Join(baseDir, sourcePath) - } - - expanded, err := utils.ExpandPath(fullPath) - if err != nil { - continue - } - - if mangowcFindInclude(expanded, target, processed) { - return true - } - } - - return false -} - -func matchesTarget(path, target string) bool { - path = strings.TrimPrefix(path, "./") - target = strings.TrimPrefix(target, "./") - return path == target || strings.HasSuffix(path, "/"+target) -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_dank16.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_dank16.go deleted file mode 100644 index 0bbbab1..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_dank16.go +++ /dev/null @@ -1,126 +0,0 @@ -package main - -import ( - "fmt" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/dank16" - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/spf13/cobra" -) - -var dank16Cmd = &cobra.Command{ - Use: "dank16 [hex_color]", - Short: "Generate Base16 color palettes", - Long: "Generate Base16 color palettes from a color with support for various output formats", - Args: cobra.MaximumNArgs(1), - Run: runDank16, -} - -func init() { - dank16Cmd.Flags().Bool("light", false, "Generate light theme variant (sets default to light)") - dank16Cmd.Flags().Bool("json", false, "Output in JSON format") - dank16Cmd.Flags().Bool("kitty", false, "Output in Kitty terminal format") - dank16Cmd.Flags().Bool("foot", false, "Output in Foot terminal format") - dank16Cmd.Flags().Bool("neovim", false, "Output in Neovim plugin format") - dank16Cmd.Flags().Bool("alacritty", false, "Output in Alacritty terminal format") - dank16Cmd.Flags().Bool("ghostty", false, "Output in Ghostty terminal format") - dank16Cmd.Flags().Bool("wezterm", false, "Output in Wezterm terminal format") - dank16Cmd.Flags().String("background", "", "Custom background color") - dank16Cmd.Flags().String("contrast", "dps", "Contrast algorithm: dps (Delta Phi Star, default) or wcag") - dank16Cmd.Flags().Bool("variants", false, "Output all variants (dark/light/default) in JSON") - dank16Cmd.Flags().String("primary-dark", "", "Primary color for dark mode (use with --variants)") - dank16Cmd.Flags().String("primary-light", "", "Primary color for light mode (use with --variants)") - _ = dank16Cmd.RegisterFlagCompletionFunc("contrast", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return []string{"dps", "wcag"}, cobra.ShellCompDirectiveNoFileComp - }) -} - -func runDank16(cmd *cobra.Command, args []string) { - isLight, _ := cmd.Flags().GetBool("light") - isJson, _ := cmd.Flags().GetBool("json") - isKitty, _ := cmd.Flags().GetBool("kitty") - isFoot, _ := cmd.Flags().GetBool("foot") - isNeovim, _ := cmd.Flags().GetBool("neovim") - isAlacritty, _ := cmd.Flags().GetBool("alacritty") - isGhostty, _ := cmd.Flags().GetBool("ghostty") - isWezterm, _ := cmd.Flags().GetBool("wezterm") - background, _ := cmd.Flags().GetString("background") - contrastAlgo, _ := cmd.Flags().GetString("contrast") - useVariants, _ := cmd.Flags().GetBool("variants") - primaryDark, _ := cmd.Flags().GetString("primary-dark") - primaryLight, _ := cmd.Flags().GetString("primary-light") - - if background != "" && !strings.HasPrefix(background, "#") { - background = "#" + background - } - if primaryDark != "" && !strings.HasPrefix(primaryDark, "#") { - primaryDark = "#" + primaryDark - } - if primaryLight != "" && !strings.HasPrefix(primaryLight, "#") { - primaryLight = "#" + primaryLight - } - - contrastAlgo = strings.ToLower(contrastAlgo) - if contrastAlgo != "dps" && contrastAlgo != "wcag" { - log.Fatalf("Invalid contrast algorithm: %s (must be 'dps' or 'wcag')", contrastAlgo) - } - - if useVariants { - if primaryDark == "" || primaryLight == "" { - if len(args) == 0 { - log.Fatalf("--variants requires either a positional color argument or both --primary-dark and --primary-light") - } - primaryColor := args[0] - if !strings.HasPrefix(primaryColor, "#") { - primaryColor = "#" + primaryColor - } - primaryDark = primaryColor - primaryLight = primaryColor - } - variantOpts := dank16.VariantOptions{ - PrimaryDark: primaryDark, - PrimaryLight: primaryLight, - Background: background, - UseDPS: contrastAlgo == "dps", - IsLightMode: isLight, - } - variantColors := dank16.GenerateVariantPalette(variantOpts) - fmt.Print(dank16.GenerateVariantJSON(variantColors)) - return - } - - if len(args) == 0 { - log.Fatalf("A color argument is required (or use --variants with --primary-dark and --primary-light)") - } - primaryColor := args[0] - if !strings.HasPrefix(primaryColor, "#") { - primaryColor = "#" + primaryColor - } - - opts := dank16.PaletteOptions{ - IsLight: isLight, - Background: background, - UseDPS: contrastAlgo == "dps", - } - - colors := dank16.GeneratePalette(primaryColor, opts) - - if isJson { - fmt.Print(dank16.GenerateJSON(colors)) - } else if isKitty { - fmt.Print(dank16.GenerateKittyTheme(colors)) - } else if isFoot { - fmt.Print(dank16.GenerateFootTheme(colors)) - } else if isAlacritty { - fmt.Print(dank16.GenerateAlacrittyTheme(colors)) - } else if isGhostty { - fmt.Print(dank16.GenerateGhosttyTheme(colors)) - } else if isWezterm { - fmt.Print(dank16.GenerateWeztermTheme(colors)) - } else if isNeovim { - fmt.Print(dank16.GenerateNeovimTheme(colors)) - } else { - fmt.Print(dank16.GenerateGhosttyTheme(colors)) - } -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_doctor.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_doctor.go deleted file mode 100644 index b182d06..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_doctor.go +++ /dev/null @@ -1,1107 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "os" - "os/exec" - "path/filepath" - "regexp" - "runtime" - "slices" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard" - "github.com/AvengeMedia/DankMaterialShell/core/internal/config" - "github.com/AvengeMedia/DankMaterialShell/core/internal/distros" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/network" - "github.com/AvengeMedia/DankMaterialShell/core/internal/tui" - "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" - "github.com/AvengeMedia/DankMaterialShell/core/internal/version" - "github.com/charmbracelet/lipgloss" - "github.com/spf13/cobra" -) - -type status string - -const ( - statusOK status = "ok" - statusWarn status = "warn" - statusError status = "error" - statusInfo status = "info" -) - -func (s status) IconStyle(styles tui.Styles) (string, lipgloss.Style) { - switch s { - case statusOK: - return "●", styles.Success - case statusWarn: - return "●", styles.Warning - case statusError: - return "●", styles.Error - default: - return "○", styles.Subtle - } -} - -type DoctorStatus struct { - Errors []checkResult - Warnings []checkResult - OK []checkResult - Info []checkResult -} - -func (ds *DoctorStatus) Add(r checkResult) { - switch r.status { - case statusError: - ds.Errors = append(ds.Errors, r) - case statusWarn: - ds.Warnings = append(ds.Warnings, r) - case statusOK: - ds.OK = append(ds.OK, r) - case statusInfo: - ds.Info = append(ds.Info, r) - } -} - -func (ds *DoctorStatus) HasIssues() bool { - return len(ds.Errors) > 0 || len(ds.Warnings) > 0 -} - -func (ds *DoctorStatus) ErrorCount() int { - return len(ds.Errors) -} - -func (ds *DoctorStatus) WarningCount() int { - return len(ds.Warnings) -} - -func (ds *DoctorStatus) OKCount() int { - return len(ds.OK) -} - -var ( - quickshellVersionRegex = regexp.MustCompile(`(?i)quickshell (\d+\.\d+\.\d+)`) - hyprlandVersionRegex = regexp.MustCompile(`v?(\d+\.\d+\.\d+)`) - niriVersionRegex = regexp.MustCompile(`niri (\d+\.\d+)`) - swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`) - riverVersionRegex = regexp.MustCompile(`river (\d+\.\d+)`) - wayfireVersionRegex = regexp.MustCompile(`wayfire (\d+\.\d+)`) - labwcVersionRegex = regexp.MustCompile(`labwc (\d+\.\d+\.\d+)`) - mangowcVersionRegex = regexp.MustCompile(`mango (\d+\.\d+\.\d+)`) -) - -var doctorCmd = &cobra.Command{ - Use: "doctor", - Short: "Diagnose DMS installation and dependencies", - Long: "Check system health, verify dependencies, and diagnose configuration issues for DMS", - Run: runDoctor, -} - -var ( - doctorVerbose bool - doctorJSON bool - doctorCopy bool -) - -func init() { - doctorCmd.Flags().BoolVarP(&doctorVerbose, "verbose", "v", false, "Show detailed output including paths and versions") - doctorCmd.Flags().BoolVarP(&doctorJSON, "json", "j", false, "Output results in JSON format") - doctorCmd.Flags().BoolVarP(&doctorCopy, "copy", "C", false, "Copy results to clipboard in GitHub-friendly format") -} - -type category int - -const ( - catSystem category = iota - catVersions - catInstallation - catCompositor - catQuickshellFeatures - catOptionalFeatures - catConfigFiles - catServices - catEnvironment -) - -func (c category) String() string { - switch c { - case catSystem: - return "System" - case catVersions: - return "Versions" - case catInstallation: - return "Installation" - case catCompositor: - return "Compositor" - case catQuickshellFeatures: - return "Quickshell Features" - case catOptionalFeatures: - return "Optional Features" - case catConfigFiles: - return "Config Files" - case catServices: - return "Services" - case catEnvironment: - return "Environment" - default: - return "Unknown" - } -} - -const ( - checkNameMaxLength = 21 - doctorDocsURL = "https://danklinux.com/docs/dankmaterialshell/cli-doctor" -) - -type checkResult struct { - category category - name string - status status - message string - details string - url string -} - -type checkResultJSON struct { - Category string `json:"category"` - Name string `json:"name"` - Status string `json:"status"` - Message string `json:"message"` - Details string `json:"details,omitempty"` - URL string `json:"url,omitempty"` -} - -type doctorOutputJSON struct { - Summary struct { - Errors int `json:"errors"` - Warnings int `json:"warnings"` - OK int `json:"ok"` - Info int `json:"info"` - } `json:"summary"` - Results []checkResultJSON `json:"results"` -} - -func (r checkResult) toJSON() checkResultJSON { - return checkResultJSON{ - Category: r.category.String(), - Name: r.name, - Status: string(r.status), - Message: r.message, - Details: r.details, - URL: r.url, - } -} - -func runDoctor(cmd *cobra.Command, args []string) { - if !doctorJSON && !doctorCopy { - printDoctorHeader() - } - - qsFeatures, qsMissingFeatures := checkQuickshellFeatures() - - results := slices.Concat( - checkSystemInfo(), - checkVersions(qsMissingFeatures), - checkDMSInstallation(), - checkWindowManagers(), - qsFeatures, - checkOptionalDependencies(), - checkConfigurationFiles(), - checkSystemdServices(), - checkEnvironmentVars(), - ) - - switch { - case doctorCopy: - text := formatResultsPlain(results) - if err := clipboard.CopyOpts([]byte(text), "text/plain;charset=utf-8", false, false); err != nil { - fmt.Fprintf(os.Stderr, "Failed to copy to clipboard: %v\n", err) - os.Exit(1) - } - fmt.Println("Doctor report copied to clipboard") - case doctorJSON: - printResultsJSON(results) - default: - printResults(results) - printSummary(results, qsMissingFeatures) - } -} - -func printDoctorHeader() { - theme := tui.TerminalTheme() - styles := tui.NewStyles(theme) - - fmt.Println(getThemedASCII()) - fmt.Println(styles.Title.Render("System Health Check")) - fmt.Println(styles.Subtle.Render("──────────────────────────────────────")) - fmt.Println() -} - -func checkSystemInfo() []checkResult { - var results []checkResult - - osInfo, err := distros.GetOSInfo() - if err != nil { - status, message, details := statusWarn, fmt.Sprintf("Unknown (%v)", err), "" - - if strings.Contains(err.Error(), "Unsupported distribution") { - osRelease := readOSRelease() - switch { - case osRelease["ID"] == "nixos": - status = statusOK - message = osRelease["PRETTY_NAME"] - if message == "" { - message = fmt.Sprintf("NixOS %s", osRelease["VERSION_ID"]) - } - details = "Supported for runtime (install via NixOS module or Flake)" - case osRelease["PRETTY_NAME"] != "": - message = fmt.Sprintf("%s (not supported by dms setup)", osRelease["PRETTY_NAME"]) - details = "DMS may work but automatic installation is not available" - } - } - - results = append(results, checkResult{catSystem, "Operating System", status, message, details, doctorDocsURL + "#operating-system"}) - } else { - status := statusOK - message := osInfo.PrettyName - if message == "" { - message = fmt.Sprintf("%s %s", osInfo.Distribution.ID, osInfo.VersionID) - } - if distros.IsUnsupportedDistro(osInfo.Distribution.ID, osInfo.VersionID) { - status = statusWarn - message += " (version may not be fully supported)" - } - results = append(results, checkResult{ - catSystem, "Operating System", status, message, - fmt.Sprintf("ID: %s, Version: %s, Arch: %s", osInfo.Distribution.ID, osInfo.VersionID, osInfo.Architecture), - doctorDocsURL + "#operating-system", - }) - } - - arch := runtime.GOARCH - archStatus := statusOK - if arch != "amd64" && arch != "arm64" { - archStatus = statusError - } - results = append(results, checkResult{catSystem, "Architecture", archStatus, arch, "", doctorDocsURL + "#architecture"}) - - waylandDisplay := os.Getenv("WAYLAND_DISPLAY") - xdgSessionType := os.Getenv("XDG_SESSION_TYPE") - - switch { - case waylandDisplay != "" || xdgSessionType == "wayland": - results = append(results, checkResult{ - catSystem, "Display Server", statusOK, "Wayland", - fmt.Sprintf("WAYLAND_DISPLAY=%s", waylandDisplay), - doctorDocsURL + "#display-server", - }) - case xdgSessionType == "x11": - results = append(results, checkResult{catSystem, "Display Server", statusError, "X11 (DMS requires Wayland)", "", doctorDocsURL + "#display-server"}) - default: - results = append(results, checkResult{ - catSystem, "Display Server", statusWarn, "Unknown (ensure you're running Wayland)", - fmt.Sprintf("XDG_SESSION_TYPE=%s", xdgSessionType), - doctorDocsURL + "#display-server", - }) - } - - return results -} - -func checkEnvironmentVars() []checkResult { - var results []checkResult - results = append(results, checkEnvVar("QT_QPA_PLATFORMTHEME")...) - results = append(results, checkEnvVar("QS_ICON_THEME")...) - return results -} - -func checkEnvVar(name string) []checkResult { - value := os.Getenv(name) - if value != "" { - return []checkResult{{catEnvironment, name, statusInfo, value, "", doctorDocsURL + "#environment-variables"}} - } - if doctorVerbose { - return []checkResult{{catEnvironment, name, statusInfo, "Not set", "", doctorDocsURL + "#environment-variables"}} - } - return nil -} - -func readOSRelease() map[string]string { - result := make(map[string]string) - data, err := os.ReadFile("/etc/os-release") - if err != nil { - return result - } - for line := range strings.SplitSeq(string(data), "\n") { - if parts := strings.SplitN(line, "=", 2); len(parts) == 2 { - result[parts[0]] = strings.Trim(parts[1], "\"") - } - } - return result -} - -func checkVersions(qsMissingFeatures bool) []checkResult { - dmsCliPath, _ := os.Executable() - dmsCliDetails := "" - if doctorVerbose { - dmsCliDetails = dmsCliPath - } - - results := []checkResult{ - {catVersions, "DMS CLI", statusOK, formatVersion(Version), dmsCliDetails, doctorDocsURL + "#dms-cli"}, - } - - qsVersion, qsStatus, qsPath := getQuickshellVersionInfo(qsMissingFeatures) - qsDetails := "" - if doctorVerbose && qsPath != "" { - qsDetails = qsPath - } - results = append(results, checkResult{catVersions, "Quickshell", qsStatus, qsVersion, qsDetails, doctorDocsURL + "#quickshell"}) - - dmsVersion, dmsPath := getDMSShellVersion() - if dmsVersion != "" { - results = append(results, checkResult{catVersions, "DMS Shell", statusOK, dmsVersion, dmsPath, doctorDocsURL + "#dms-shell"}) - } else { - results = append(results, checkResult{catVersions, "DMS Shell", statusError, "Not installed or not detected", "Run 'dms setup' to install", doctorDocsURL + "#dms-shell"}) - } - - return results -} - -func getDMSShellVersion() (version, path string) { - if err := findConfig(nil, nil); err == nil && configPath != "" { - versionFile := filepath.Join(configPath, "VERSION") - if data, err := os.ReadFile(versionFile); err == nil { - return strings.TrimSpace(string(data)), configPath - } - return "installed", configPath - } - - if dmsPath, err := config.LocateDMSConfig(); err == nil { - versionFile := filepath.Join(dmsPath, "VERSION") - if data, err := os.ReadFile(versionFile); err == nil { - return strings.TrimSpace(string(data)), dmsPath - } - return "installed", dmsPath - } - - return "", "" -} - -func getQuickshellVersionInfo(missingFeatures bool) (string, status, string) { - if !utils.CommandExists("qs") { - return "Not installed", statusError, "" - } - - qsPath, _ := exec.LookPath("qs") - - output, err := exec.Command("qs", "--version").Output() - if err != nil { - return "Installed (version check failed)", statusWarn, qsPath - } - - fullVersion := strings.TrimSpace(string(output)) - if matches := quickshellVersionRegex.FindStringSubmatch(fullVersion); len(matches) >= 2 { - if version.CompareVersions(matches[1], "0.2.0") < 0 { - return fmt.Sprintf("%s (needs >= 0.2.0)", fullVersion), statusError, qsPath - } - if missingFeatures { - return fullVersion, statusWarn, qsPath - } - return fullVersion, statusOK, qsPath - } - - return fullVersion, statusWarn, qsPath -} - -func checkDMSInstallation() []checkResult { - var results []checkResult - - dmsPath := "" - if err := findConfig(nil, nil); err == nil && configPath != "" { - dmsPath = configPath - } else if path, err := config.LocateDMSConfig(); err == nil { - dmsPath = path - } - - if dmsPath == "" { - return []checkResult{{catInstallation, "DMS Configuration", statusError, "Not found", "shell.qml not found in any config path", doctorDocsURL + "#dms-configuration"}} - } - - results = append(results, checkResult{catInstallation, "DMS Configuration", statusOK, "Found", dmsPath, doctorDocsURL + "#dms-configuration"}) - - shellQml := filepath.Join(dmsPath, "shell.qml") - if _, err := os.Stat(shellQml); err != nil { - results = append(results, checkResult{catInstallation, "shell.qml", statusError, "Missing", shellQml, doctorDocsURL + "#dms-configuration"}) - } else { - results = append(results, checkResult{catInstallation, "shell.qml", statusOK, "Present", shellQml, doctorDocsURL + "#dms-configuration"}) - } - - if doctorVerbose { - installType := "Unknown" - switch { - case strings.Contains(dmsPath, "/nix/store"): - installType = "Nix store" - case strings.Contains(dmsPath, ".local/share") || strings.Contains(dmsPath, "/usr/share"): - installType = "System package" - case strings.Contains(dmsPath, ".config"): - installType = "User config" - } - results = append(results, checkResult{catInstallation, "Install Type", statusInfo, installType, dmsPath, doctorDocsURL + "#dms-configuration"}) - } - - return results -} - -func checkWindowManagers() []checkResult { - compositors := []struct { - name, versionCmd, versionArg string - versionRegex *regexp.Regexp - commands []string - }{ - {"Hyprland", "Hyprland", "--version", hyprlandVersionRegex, []string{"hyprland", "Hyprland"}}, - {"niri", "niri", "--version", niriVersionRegex, []string{"niri"}}, - {"Sway", "sway", "--version", swayVersionRegex, []string{"sway"}}, - {"River", "river", "-version", riverVersionRegex, []string{"river"}}, - {"Wayfire", "wayfire", "--version", wayfireVersionRegex, []string{"wayfire"}}, - {"labwc", "labwc", "--version", labwcVersionRegex, []string{"labwc"}}, - {"mangowc", "mango", "-v", mangowcVersionRegex, []string{"mango"}}, - } - - var results []checkResult - foundAny := false - - for _, c := range compositors { - if !slices.ContainsFunc(c.commands, utils.CommandExists) { - continue - } - foundAny = true - var compositorPath string - for _, cmd := range c.commands { - if path, err := exec.LookPath(cmd); err == nil { - compositorPath = path - break - } - } - details := "" - if doctorVerbose && compositorPath != "" { - details = compositorPath - } - results = append(results, checkResult{ - catCompositor, c.name, statusOK, - getVersionFromCommand(c.versionCmd, c.versionArg, c.versionRegex), details, - doctorDocsURL + "#compositor-checks", - }) - } - - if !foundAny { - results = append(results, checkResult{ - catCompositor, "Compositor", statusError, - "No supported Wayland compositor found", - "Install Hyprland, niri, Sway, River, or Wayfire", - doctorDocsURL + "#compositor-checks", - }) - } - - if wm := detectRunningWM(); wm != "" { - results = append(results, checkResult{catCompositor, "Active", statusInfo, wm, "", doctorDocsURL + "#compositor"}) - } - - return results -} - -func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string { - output, err := exec.Command(cmd, arg).CombinedOutput() - if err != nil && len(output) == 0 { - return "installed" - } - - outStr := string(output) - if matches := regex.FindStringSubmatch(outStr); len(matches) > 1 { - ver := matches[1] - if strings.Contains(outStr, "git") || strings.Contains(outStr, "dirty") { - return ver + " (git)" - } - return ver - } - return strings.TrimSpace(outStr) -} - -func detectRunningWM() string { - switch { - case os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "": - return "Hyprland" - case os.Getenv("NIRI_SOCKET") != "": - return "niri" - case os.Getenv("XDG_CURRENT_DESKTOP") != "": - return os.Getenv("XDG_CURRENT_DESKTOP") - } - return "" -} - -func checkQuickshellFeatures() ([]checkResult, bool) { - if !utils.CommandExists("qs") { - return nil, false - } - - tmpDir := os.TempDir() - testScript := filepath.Join(tmpDir, "qs-feature-test.qml") - defer os.Remove(testScript) - - qmlContent := ` -import QtQuick -import Quickshell - -ShellRoot { - id: root - - property bool polkitAvailable: false - property bool idleMonitorAvailable: false - property bool idleInhibitorAvailable: false - property bool shortcutInhibitorAvailable: false - - Timer { - interval: 50 - running: true - repeat: false - onTriggered: { - try { - var polkitTest = Qt.createQmlObject( - 'import Quickshell.Services.Polkit; import QtQuick; Item {}', - root - ) - root.polkitAvailable = true - polkitTest.destroy() - } catch (e) {} - - try { - var testItem = Qt.createQmlObject( - 'import Quickshell.Wayland; import QtQuick; QtObject { ' + - 'readonly property bool hasIdleMonitor: typeof IdleMonitor !== "undefined"; ' + - 'readonly property bool hasIdleInhibitor: typeof IdleInhibitor !== "undefined"; ' + - 'readonly property bool hasShortcutInhibitor: typeof ShortcutInhibitor !== "undefined" ' + - '}', - root - ) - root.idleMonitorAvailable = testItem.hasIdleMonitor - root.idleInhibitorAvailable = testItem.hasIdleInhibitor - root.shortcutInhibitorAvailable = testItem.hasShortcutInhibitor - testItem.destroy() - } catch (e) {} - - console.warn(root.polkitAvailable ? "FEATURE:Polkit:OK" : "FEATURE:Polkit:UNAVAILABLE") - console.warn(root.idleMonitorAvailable ? "FEATURE:IdleMonitor:OK" : "FEATURE:IdleMonitor:UNAVAILABLE") - console.warn(root.idleInhibitorAvailable ? "FEATURE:IdleInhibitor:OK" : "FEATURE:IdleInhibitor:UNAVAILABLE") - console.warn(root.shortcutInhibitorAvailable ? "FEATURE:ShortcutInhibitor:OK" : "FEATURE:ShortcutInhibitor:UNAVAILABLE") - - Quickshell.execDetached(["kill", "-TERM", String(Quickshell.processId)]) - } - } -} -` - - if err := os.WriteFile(testScript, []byte(qmlContent), 0o644); err != nil { - return nil, false - } - - cmd := exec.Command("qs", "-p", testScript) - cmd.Env = append(os.Environ(), "NO_COLOR=1") - output, _ := cmd.CombinedOutput() - outputStr := string(output) - - features := []struct{ name, desc string }{ - {"Polkit", "Authentication prompts"}, - {"IdleMonitor", "Idle detection"}, - {"IdleInhibitor", "Prevent idle/sleep"}, - {"ShortcutInhibitor", "Allow shortcut management (niri)"}, - } - - var results []checkResult - missingFeatures := false - - for _, f := range features { - available := strings.Contains(outputStr, fmt.Sprintf("FEATURE:%s:OK", f.name)) - status, message := statusOK, "Available" - if !available { - status, message = statusInfo, "Not available" - missingFeatures = true - } - results = append(results, checkResult{catQuickshellFeatures, f.name, status, message, f.desc, doctorDocsURL + "#quickshell-features"}) - } - - return results, missingFeatures -} - -func checkI2CAvailability() checkResult { - ddc, err := brightness.NewDDCBackend() - if err != nil { - return checkResult{catOptionalFeatures, "I2C/DDC", statusInfo, "Not available", "External monitor brightness control", doctorDocsURL + "#optional-features"} - } - defer ddc.Close() - - devices, err := ddc.GetDevices() - if err != nil || len(devices) == 0 { - return checkResult{catOptionalFeatures, "I2C/DDC", statusInfo, "No monitors detected", "External monitor brightness control", doctorDocsURL + "#optional-features"} - } - - return checkResult{catOptionalFeatures, "I2C/DDC", statusOK, fmt.Sprintf("%d monitor(s) detected", len(devices)), "External monitor brightness control", doctorDocsURL + "#optional-features"} -} - -func checkImageFormatPlugins() []checkResult { - url := doctorDocsURL + "#optional-features" - - pluginDirs := findQtPluginDirs() - if len(pluginDirs) == 0 { - return []checkResult{ - {catOptionalFeatures, "qt6-imageformats", statusInfo, "Cannot detect (plugin dir not found)", "WebP, TIFF, JP2 support", url}, - {catOptionalFeatures, "kimageformats", statusInfo, "Cannot detect (plugin dir not found)", "AVIF, HEIF, JXL support", url}, - } - } - - type pluginCheck struct { - name string - desc string - plugins []struct{ file, format string } - } - - checks := []pluginCheck{ - { - name: "qt6-imageformats", - desc: "WebP, TIFF, GIF, JP2 support", - plugins: []struct{ file, format string }{ - {"libqwebp.so", "WebP"}, - {"libqtiff.so", "TIFF"}, - {"libqgif.so", "GIF"}, - {"libqjp2.so", "JP2"}, - {"libqicns.so", "ICNS"}, - }, - }, - { - name: "kimageformats", - desc: "AVIF, HEIF, JXL support", - plugins: []struct{ file, format string }{ - {"kimg_avif.so", "AVIF"}, - {"kimg_heif.so", "HEIF"}, - {"kimg_jxl.so", "JXL"}, - {"kimg_exr.so", "EXR"}, - }, - }, - } - - var results []checkResult - for _, c := range checks { - var found []string - var foundDirs []string - for _, pluginDir := range pluginDirs { - imageFormatsDir := filepath.Join(pluginDir, "imageformats") - for _, p := range c.plugins { - if _, err := os.Stat(filepath.Join(imageFormatsDir, p.file)); err == nil { - if !slices.Contains(found, p.format) { - found = append(found, p.format) - } - if !slices.Contains(foundDirs, imageFormatsDir) { - foundDirs = append(foundDirs, imageFormatsDir) - } - } - } - } - - var result checkResult - switch { - case len(found) == 0: - result = checkResult{catOptionalFeatures, c.name, statusWarn, "Not installed", c.desc, url} - default: - details := "" - if doctorVerbose { - details = fmt.Sprintf("Formats: %s (%s)", strings.Join(found, ", "), strings.Join(foundDirs, ":")) - } - result = checkResult{catOptionalFeatures, c.name, statusOK, fmt.Sprintf("Installed (%d formats)", len(found)), details, url} - } - results = append(results, result) - } - - return results -} - -func findQtPluginDirs() []string { - var dirs []string - - addDir := func(dir string) { - if dir != "" { - if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil { - dirs = append(dirs, dir) - } - } - } - - // Check all paths in QT_PLUGIN_PATH env var (used by NixOS and custom setups) - if envPath := os.Getenv("QT_PLUGIN_PATH"); envPath != "" { - for dir := range strings.SplitSeq(envPath, ":") { - addDir(dir) - } - } - - // Try qtpaths - for _, cmd := range []string{"qtpaths6", "qtpaths"} { - if output, err := exec.Command(cmd, "-query", "QT_INSTALL_PLUGINS").Output(); err == nil { - addDir(strings.TrimSpace(string(output))) - } - } - - // Fallback: common distro paths - for _, dir := range []string{ - "/usr/lib/qt6/plugins", - "/usr/lib64/qt6/plugins", - "/usr/lib/x86_64-linux-gnu/qt6/plugins", - "/usr/lib/aarch64-linux-gnu/qt6/plugins", - } { - addDir(dir) - } - - return dirs -} - -func detectNetworkBackend(stackResult *network.DetectResult) string { - switch stackResult.Backend { - case network.BackendNetworkManager: - return "NetworkManager" - case network.BackendIwd: - return "iwd" - case network.BackendNetworkd: - if stackResult.HasIwd { - return "iwd + systemd-networkd" - } - return "systemd-networkd" - case network.BackendConnMan: - return "ConnMan" - default: - return "" - } -} - -func getOptionalDBusStatus(busName string) (status, string) { - if utils.IsDBusServiceAvailable(busName) { - return statusOK, "Available" - } else { - return statusWarn, "Not available" - } -} - -func checkOptionalDependencies() []checkResult { - var results []checkResult - - optionalFeaturesURL := doctorDocsURL + "#optional-features" - - accountsStatus, accountsMsg := getOptionalDBusStatus("org.freedesktop.Accounts") - results = append(results, checkResult{catOptionalFeatures, "accountsservice", accountsStatus, accountsMsg, "User accounts", optionalFeaturesURL}) - - ppdStatus, ppdMsg := getOptionalDBusStatus("org.freedesktop.UPower.PowerProfiles") - results = append(results, checkResult{catOptionalFeatures, "power-profiles-daemon", ppdStatus, ppdMsg, "Power profile management", optionalFeaturesURL}) - - logindStatus, logindMsg := getOptionalDBusStatus("org.freedesktop.login1") - results = append(results, checkResult{catOptionalFeatures, "logind", logindStatus, logindMsg, "Session management", optionalFeaturesURL}) - - cupsPkHelperBus := "org.opensuse.CupsPkHelper.Mechanism" - var cupsPkStatus status - var cupsPkMsg string - switch { - case utils.IsDBusServiceAvailable(cupsPkHelperBus): - cupsPkStatus, cupsPkMsg = statusOK, "Running" - case utils.IsDBusServiceActivatable(cupsPkHelperBus): - cupsPkStatus, cupsPkMsg = statusOK, "Available" - default: - cupsPkStatus, cupsPkMsg = statusWarn, "Not available (install cups-pk-helper)" - } - results = append(results, checkResult{catOptionalFeatures, "cups-pk-helper", cupsPkStatus, cupsPkMsg, "Printer management", optionalFeaturesURL}) - - results = append(results, checkI2CAvailability()) - results = append(results, checkImageFormatPlugins()...) - - terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"} - terminals = slices.DeleteFunc(terminals, func(t string) bool { - return !utils.CommandExists(t) - }) - - if len(terminals) > 0 { - results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, strings.Join(terminals, ", "), "", optionalFeaturesURL}) - } else { - results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, foot or alacritty", optionalFeaturesURL}) - } - - networkResult, err := network.DetectNetworkStack() - networkStatus, networkMessage, networkDetails := statusOK, "Not available", "Network management" - - if err == nil && networkResult.Backend != network.BackendNone { - networkMessage = detectNetworkBackend(networkResult) - if doctorVerbose { - networkDetails = networkResult.ChosenReason - } - } else { - networkStatus = statusInfo - } - - results = append(results, checkResult{catOptionalFeatures, "Network", networkStatus, networkMessage, networkDetails, optionalFeaturesURL}) - - deps := []struct { - name, cmd, desc string - important bool - }{ - {"matugen", "matugen", "Dynamic theming", true}, - {"dgop", "dgop", "System monitoring", true}, - {"cava", "cava", "Audio visualizer", true}, - {"khal", "khal", "Calendar events", false}, - {"danksearch", "dsearch", "File search", false}, - {"fprintd", "fprintd-list", "Fingerprint auth", false}, - } - - for _, d := range deps { - found := utils.CommandExists(d.cmd) - - switch { - case found: - results = append(results, checkResult{catOptionalFeatures, d.name, statusOK, "Installed", d.desc, optionalFeaturesURL}) - case d.important: - results = append(results, checkResult{catOptionalFeatures, d.name, statusWarn, "Missing", d.desc, optionalFeaturesURL}) - default: - results = append(results, checkResult{catOptionalFeatures, d.name, statusInfo, "Not installed", d.desc, optionalFeaturesURL}) - } - } - - return results -} - -func checkConfigurationFiles() []checkResult { - configDir, _ := os.UserConfigDir() - cacheDir, _ := os.UserCacheDir() - dmsDir := "DankMaterialShell" - - configFiles := []struct{ name, path string }{ - {"settings.json", filepath.Join(configDir, dmsDir, "settings.json")}, - {"clsettings.json", filepath.Join(configDir, dmsDir, "clsettings.json")}, - {"plugin_settings.json", filepath.Join(configDir, dmsDir, "plugin_settings.json")}, - {"session.json", filepath.Join(utils.XDGStateHome(), dmsDir, "session.json")}, - {"dms-colors.json", filepath.Join(cacheDir, dmsDir, "dms-colors.json")}, - } - - var results []checkResult - for _, cf := range configFiles { - info, err := os.Stat(cf.path) - if err != nil { - results = append(results, checkResult{catConfigFiles, cf.name, statusInfo, "Not yet created", cf.path, doctorDocsURL + "#config-files"}) - continue - } - - status := statusOK - message := "Present" - if info.Mode().Perm()&0o200 == 0 { - status = statusWarn - message += " (read-only)" - } - results = append(results, checkResult{catConfigFiles, cf.name, status, message, cf.path, doctorDocsURL + "#config-files"}) - } - return results -} - -func checkSystemdServices() []checkResult { - if !utils.CommandExists("systemctl") { - return nil - } - - var results []checkResult - - dmsState := getServiceState("dms", true) - if !dmsState.exists { - results = append(results, checkResult{catServices, "dms.service", statusInfo, "Not installed", "Optional user service", doctorDocsURL + "#services"}) - } else { - status, message := statusOK, dmsState.enabled - if dmsState.active != "" { - message = fmt.Sprintf("%s, %s", dmsState.enabled, dmsState.active) - } - switch { - case dmsState.enabled == "disabled": - status, message = statusWarn, "Disabled" - case dmsState.active == "failed" || dmsState.active == "inactive": - status = statusError - } - results = append(results, checkResult{catServices, "dms.service", status, message, "", doctorDocsURL + "#services"}) - } - - greetdState := getServiceState("greetd", false) - switch { - case greetdState.exists: - status := statusOK - if greetdState.enabled == "disabled" { - status = statusInfo - } - results = append(results, checkResult{catServices, "greetd", status, greetdState.enabled, "", doctorDocsURL + "#services"}) - case doctorVerbose: - results = append(results, checkResult{catServices, "greetd", statusInfo, "Not installed", "Optional greeter service", doctorDocsURL + "#services"}) - } - - return results -} - -type serviceState struct { - exists bool - enabled string - active string -} - -func getServiceState(name string, userService bool) serviceState { - args := []string{"is-enabled", name} - if userService { - args = []string{"--user", "is-enabled", name} - } - - output, _ := exec.Command("systemctl", args...).Output() - enabled := strings.TrimSpace(string(output)) - - if enabled == "" || enabled == "not-found" { - return serviceState{} - } - - state := serviceState{exists: true, enabled: enabled} - - if userService { - output, _ = exec.Command("systemctl", "--user", "is-active", name).Output() - if active := strings.TrimSpace(string(output)); active != "" && active != "unknown" { - state.active = active - } - } - - return state -} - -func printResults(results []checkResult) { - theme := tui.TerminalTheme() - styles := tui.NewStyles(theme) - - currentCategory := category(-1) - for _, r := range results { - if r.category != currentCategory { - if currentCategory != -1 { - fmt.Println() - } - fmt.Printf(" %s\n", styles.Bold.Render(r.category.String())) - currentCategory = r.category - } - printResultLine(r, styles) - } -} - -func printResultsJSON(results []checkResult) { - var ds DoctorStatus - for _, r := range results { - ds.Add(r) - } - - output := doctorOutputJSON{} - output.Summary.Errors = ds.ErrorCount() - output.Summary.Warnings = ds.WarningCount() - output.Summary.OK = ds.OKCount() - output.Summary.Info = len(ds.Info) - - output.Results = make([]checkResultJSON, 0, len(results)) - for _, r := range results { - output.Results = append(output.Results, r.toJSON()) - } - - encoder := json.NewEncoder(os.Stdout) - encoder.SetIndent("", " ") - if err := encoder.Encode(output); err != nil { - fmt.Fprintf(os.Stderr, "Error encoding JSON: %v\n", err) - os.Exit(1) - } -} - -func printResultLine(r checkResult, styles tui.Styles) { - icon, style := r.status.IconStyle(styles) - - name := r.name - nameLen := len(name) - - if nameLen > checkNameMaxLength { - name = name[:checkNameMaxLength-1] + "…" - nameLen = checkNameMaxLength - } - dots := strings.Repeat("·", checkNameMaxLength-nameLen) - - fmt.Printf(" %s %s %s %s\n", style.Render(icon), name, styles.Subtle.Render(dots), r.message) - - if doctorVerbose && r.details != "" { - fmt.Printf(" %s\n", styles.Subtle.Render("└─ "+r.details)) - } - - if (r.status == statusError || r.status == statusWarn) && r.url != "" { - fmt.Printf(" %s\n", styles.Subtle.Render("→ "+r.url)) - } -} - -func printSummary(results []checkResult, qsMissingFeatures bool) { - theme := tui.TerminalTheme() - styles := tui.NewStyles(theme) - - var ds DoctorStatus - for _, r := range results { - ds.Add(r) - } - - fmt.Println() - fmt.Printf(" %s\n", styles.Subtle.Render("──────────────────────────────────────")) - - if !ds.HasIssues() { - fmt.Printf(" %s\n", styles.Success.Render("✓ All checks passed!")) - } else { - var parts []string - - if ds.ErrorCount() > 0 { - parts = append(parts, styles.Error.Render(fmt.Sprintf("%d error(s)", ds.ErrorCount()))) - } - if ds.WarningCount() > 0 { - parts = append(parts, styles.Warning.Render(fmt.Sprintf("%d warning(s)", ds.WarningCount()))) - } - parts = append(parts, styles.Success.Render(fmt.Sprintf("%d ok", ds.OKCount()))) - fmt.Printf(" %s\n", strings.Join(parts, ", ")) - - if qsMissingFeatures { - fmt.Println() - fmt.Printf(" %s\n", styles.Subtle.Render("→ Consider using quickshell-git for full feature support")) - } - } - fmt.Println() -} - -func formatResultsPlain(results []checkResult) string { - var sb strings.Builder - sb.WriteString("## DMS Doctor Report\n\n") - - currentCategory := category(-1) - for _, r := range results { - if r.category != currentCategory { - if currentCategory != -1 { - sb.WriteString("\n") - } - fmt.Fprintf(&sb, "**%s**\n", r.category.String()) - currentCategory = r.category - } - - fmt.Fprintf(&sb, "- [%s] %s: %s\n", r.status, r.name, r.message) - - if doctorVerbose && r.details != "" { - fmt.Fprintf(&sb, " - %s\n", r.details) - } - } - - var ds DoctorStatus - for _, r := range results { - ds.Add(r) - } - - sb.WriteString("\n---\n") - fmt.Fprintf(&sb, "**Summary:** %d error(s), %d warning(s), %d ok\n", - ds.ErrorCount(), ds.WarningCount(), ds.OKCount()) - - return sb.String() -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_download.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_download.go deleted file mode 100644 index e9caeb0..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_download.go +++ /dev/null @@ -1,99 +0,0 @@ -package main - -import ( - "context" - "fmt" - "io" - "net" - "net/http" - "os" - "path/filepath" - "time" - - "github.com/spf13/cobra" -) - -var dlOutput string -var dlUserAgent string -var dlTimeout int -var dlIPv4Only bool - -var dlCmd = &cobra.Command{ - Use: "dl <url>", - Short: "Download a URL to stdout or file", - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - if err := runDownload(args[0]); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - }, -} - -func init() { - dlCmd.Flags().StringVarP(&dlOutput, "output", "o", "", "Output file path (default: stdout)") - dlCmd.Flags().StringVar(&dlUserAgent, "user-agent", "", "Custom User-Agent header") - dlCmd.Flags().IntVar(&dlTimeout, "timeout", 10, "Request timeout in seconds") - dlCmd.Flags().BoolVarP(&dlIPv4Only, "ipv4", "4", false, "Force IPv4 only") -} - -func runDownload(url string) error { - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(dlTimeout)*time.Second) - defer cancel() - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - return fmt.Errorf("invalid request: %w", err) - } - - switch { - case dlUserAgent != "": - req.Header.Set("User-Agent", dlUserAgent) - default: - req.Header.Set("User-Agent", "DankMaterialShell/1.0 (Linux)") - } - - dialer := &net.Dialer{Timeout: 5 * time.Second} - transport := &http.Transport{DialContext: dialer.DialContext} - if dlIPv4Only { - transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { - return dialer.DialContext(ctx, "tcp4", addr) - } - } - client := &http.Client{Transport: transport} - - resp, err := client.Do(req) - if err != nil { - return fmt.Errorf("download failed: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return fmt.Errorf("HTTP %d", resp.StatusCode) - } - - if dlOutput == "" { - _, err = io.Copy(os.Stdout, resp.Body) - return err - } - - if dir := filepath.Dir(dlOutput); dir != "." { - if err := os.MkdirAll(dir, 0755); err != nil { - return fmt.Errorf("mkdir failed: %w", err) - } - } - - f, err := os.Create(dlOutput) - if err != nil { - return fmt.Errorf("create failed: %w", err) - } - defer f.Close() - - if _, err := io.Copy(f, resp.Body); err != nil { - os.Remove(dlOutput) - return fmt.Errorf("write failed: %w", err) - } - - fmt.Println(dlOutput) - return nil -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_dpms.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_dpms.go deleted file mode 100644 index 77eff1a..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_dpms.go +++ /dev/null @@ -1,105 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/spf13/cobra" -) - -var dpmsCmd = &cobra.Command{ - Use: "dpms", - Short: "Control display power management", -} - -var dpmsOnCmd = &cobra.Command{ - Use: "on [output]", - Short: "Turn display(s) on", - Args: cobra.MaximumNArgs(1), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) != 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - return getDPMSOutputs(), cobra.ShellCompDirectiveNoFileComp - }, - Run: runDPMSOn, -} - -var dpmsOffCmd = &cobra.Command{ - Use: "off [output]", - Short: "Turn display(s) off", - Args: cobra.MaximumNArgs(1), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) != 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - return getDPMSOutputs(), cobra.ShellCompDirectiveNoFileComp - }, - Run: runDPMSOff, -} - -var dpmsListCmd = &cobra.Command{ - Use: "list", - Short: "List outputs", - Args: cobra.NoArgs, - Run: runDPMSList, -} - -func init() { - dpmsCmd.AddCommand(dpmsOnCmd, dpmsOffCmd, dpmsListCmd) -} - -func runDPMSOn(cmd *cobra.Command, args []string) { - outputName := "" - if len(args) > 0 { - outputName = args[0] - } - - client, err := newDPMSClient() - if err != nil { - log.Fatalf("%v", err) - } - defer client.Close() - - if err := client.SetDPMS(outputName, true); err != nil { - log.Fatalf("%v", err) - } -} - -func runDPMSOff(cmd *cobra.Command, args []string) { - outputName := "" - if len(args) > 0 { - outputName = args[0] - } - - client, err := newDPMSClient() - if err != nil { - log.Fatalf("%v", err) - } - defer client.Close() - - if err := client.SetDPMS(outputName, false); err != nil { - log.Fatalf("%v", err) - } -} - -func getDPMSOutputs() []string { - client, err := newDPMSClient() - if err != nil { - return nil - } - defer client.Close() - return client.ListOutputs() -} - -func runDPMSList(cmd *cobra.Command, args []string) { - client, err := newDPMSClient() - if err != nil { - log.Fatalf("%v", err) - } - defer client.Close() - - for _, output := range client.ListOutputs() { - fmt.Println(output) - } -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_features.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_features.go deleted file mode 100644 index 7ec5437..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_features.go +++ /dev/null @@ -1,491 +0,0 @@ -//go:build !distro_binary - -package main - -import ( - "bufio" - "errors" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/distros" - "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" - "github.com/AvengeMedia/DankMaterialShell/core/internal/version" - "github.com/spf13/cobra" -) - -var updateCmd = &cobra.Command{ - Use: "update", - Short: "Update DankMaterialShell to the latest version", - Long: "Update DankMaterialShell to the latest version using the appropriate package manager for your distribution", - PreRunE: findConfig, - Run: func(cmd *cobra.Command, args []string) { - runUpdate() - }, -} - -var updateCheckCmd = &cobra.Command{ - Use: "check", - Short: "Check if updates are available for DankMaterialShell", - Long: "Check for available updates without performing the actual update", - Run: func(cmd *cobra.Command, args []string) { - runUpdateCheck() - }, -} - -func runUpdateCheck() { - fmt.Println("Checking for DankMaterialShell updates...") - fmt.Println() - - versionInfo, err := version.GetDMSVersionInfo() - if err != nil { - log.Fatalf("Error checking for updates: %v", err) - } - - fmt.Printf("Current version: %s\n", versionInfo.Current) - fmt.Printf("Latest version: %s\n", versionInfo.Latest) - fmt.Println() - - if versionInfo.HasUpdate { - fmt.Println("✓ Update available!") - fmt.Println() - fmt.Println("Run 'dms update' to install the latest version.") - os.Exit(0) - } else { - fmt.Println("✓ You are running the latest version.") - os.Exit(0) - } -} - -func runUpdate() { - osInfo, err := distros.GetOSInfo() - if err != nil { - log.Fatalf("Error detecting OS: %v", err) - } - - config, exists := distros.Registry[osInfo.Distribution.ID] - if !exists { - log.Fatalf("Unsupported distribution: %s", osInfo.Distribution.ID) - } - - var updateErr error - switch config.Family { - case distros.FamilyArch: - updateErr = updateArchLinux() - case distros.FamilySUSE: - updateErr = updateOtherDistros() - default: - updateErr = updateOtherDistros() - } - - if updateErr != nil { - if errors.Is(updateErr, errdefs.ErrUpdateCancelled) { - log.Info("Update cancelled.") - return - } - if errors.Is(updateErr, errdefs.ErrNoUpdateNeeded) { - return - } - log.Fatalf("Error updating DMS: %v", updateErr) - } - - log.Info("Update complete! Restarting DMS...") - restartShell() -} - -func updateArchLinux() error { - homeDir, err := os.UserHomeDir() - if err == nil { - dmsPath := filepath.Join(homeDir, ".config", "quickshell", "dms") - if _, err := os.Stat(dmsPath); err == nil { - return updateOtherDistros() - } - } - - var packageName string - var isAUR bool - if isArchPackageInstalled("dms-shell") { - packageName = "dms-shell" - } else if isArchPackageInstalled("dms-shell-git") { - packageName = "dms-shell-git" - isAUR = true - } else if isArchPackageInstalled("dms-shell-bin") { - packageName = "dms-shell-bin" - isAUR = true - } else { - fmt.Println("Info: No dms-shell package found.") - fmt.Println("Info: Falling back to git-based update method...") - return updateOtherDistros() - } - - if !isAUR { - fmt.Printf("This will update %s using pacman.\n", packageName) - if !confirmUpdate() { - return errdefs.ErrUpdateCancelled - } - - fmt.Printf("\nRunning: sudo pacman -S %s\n", packageName) - cmd := exec.Command("sudo", "pacman", "-S", "--noconfirm", packageName) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - fmt.Printf("Error: Failed to update using pacman: %v\n", err) - return err - } - - fmt.Println("dms successfully updated") - return nil - } - - var helper string - var updateCmd *exec.Cmd - - if utils.CommandExists("yay") { - helper = "yay" - updateCmd = exec.Command("yay", "-S", packageName) - } else if utils.CommandExists("paru") { - helper = "paru" - updateCmd = exec.Command("paru", "-S", packageName) - } else { - fmt.Println("Error: Neither yay nor paru found - please install an AUR helper") - fmt.Println("Info: Falling back to git-based update method...") - return updateOtherDistros() - } - - fmt.Printf("This will update DankMaterialShell using %s.\n", helper) - if !confirmUpdate() { - return errdefs.ErrUpdateCancelled - } - - fmt.Printf("\nRunning: %s -S %s\n", helper, packageName) - updateCmd.Stdout = os.Stdout - updateCmd.Stderr = os.Stderr - err = updateCmd.Run() - if err != nil { - fmt.Printf("Error: Failed to update using %s: %v\n", helper, err) - } - - fmt.Println("dms successfully updated") - return nil -} - -func updateOtherDistros() error { - homeDir, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("failed to get user home directory: %w", err) - } - - dmsPath := filepath.Join(homeDir, ".config", "quickshell", "dms") - - if _, err := os.Stat(dmsPath); os.IsNotExist(err) { - return fmt.Errorf("DMS configuration directory not found at %s", dmsPath) - } - - fmt.Printf("Found DMS configuration at %s\n", dmsPath) - - versionInfo, err := version.GetDMSVersionInfo() - if err == nil && !versionInfo.HasUpdate { - fmt.Println() - fmt.Printf("Current version: %s\n", versionInfo.Current) - fmt.Printf("Latest version: %s\n", versionInfo.Latest) - fmt.Println() - fmt.Println("✓ You are already running the latest version.") - return errdefs.ErrNoUpdateNeeded - } - - fmt.Println("\nThis will update:") - fmt.Println(" 1. The dms binary from GitHub releases") - fmt.Println(" 2. DankMaterialShell configuration using git") - if !confirmUpdate() { - return errdefs.ErrUpdateCancelled - } - - fmt.Println("\n=== Updating dms binary ===") - if err := updateDMSBinary(); err != nil { - fmt.Printf("Warning: Failed to update dms binary: %v\n", err) - fmt.Println("Continuing with shell configuration update...") - } else { - fmt.Println("dms binary successfully updated") - } - - fmt.Println("\n=== Updating DMS shell configuration ===") - - if err := os.Chdir(dmsPath); err != nil { - return fmt.Errorf("failed to change to DMS directory: %w", err) - } - - statusCmd := exec.Command("git", "status", "--porcelain") - statusOutput, _ := statusCmd.Output() - hasLocalChanges := len(strings.TrimSpace(string(statusOutput))) > 0 - - currentRefCmd := exec.Command("git", "symbolic-ref", "-q", "HEAD") - currentRefOutput, _ := currentRefCmd.Output() - onBranch := len(currentRefOutput) > 0 - - var currentTag string - var currentBranch string - - if !onBranch { - tagCmd := exec.Command("git", "describe", "--exact-match", "--tags", "HEAD") - if tagOutput, err := tagCmd.Output(); err == nil { - currentTag = strings.TrimSpace(string(tagOutput)) - } - } else { - branchCmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD") - if branchOutput, err := branchCmd.Output(); err == nil { - currentBranch = strings.TrimSpace(string(branchOutput)) - } - } - - fmt.Println("Fetching latest changes...") - fetchCmd := exec.Command("git", "fetch", "origin", "--tags", "--force") - fetchCmd.Stdout = os.Stdout - fetchCmd.Stderr = os.Stderr - if err := fetchCmd.Run(); err != nil { - return fmt.Errorf("failed to fetch changes: %w", err) - } - - if currentTag != "" { - latestTagCmd := exec.Command("git", "tag", "-l", "v*", "--sort=-version:refname") - latestTagOutput, err := latestTagCmd.Output() - if err != nil { - return fmt.Errorf("failed to get latest tag: %w", err) - } - - tags := strings.Split(strings.TrimSpace(string(latestTagOutput)), "\n") - if len(tags) == 0 || tags[0] == "" { - return fmt.Errorf("no version tags found") - } - latestTag := tags[0] - - if latestTag == currentTag { - fmt.Printf("Already on latest tag: %s\n", currentTag) - return nil - } - - fmt.Printf("Current tag: %s\n", currentTag) - fmt.Printf("Latest tag: %s\n", latestTag) - - if hasLocalChanges { - fmt.Println("\nWarning: You have local changes in your DMS configuration.") - if offerReclone(dmsPath) { - return nil - } - return errdefs.ErrUpdateCancelled - } - - fmt.Printf("Updating to %s...\n", latestTag) - checkoutCmd := exec.Command("git", "checkout", latestTag) - checkoutCmd.Stdout = os.Stdout - checkoutCmd.Stderr = os.Stderr - if err := checkoutCmd.Run(); err != nil { - fmt.Printf("Error: Failed to checkout %s: %v\n", latestTag, err) - if offerReclone(dmsPath) { - return nil - } - return fmt.Errorf("update cancelled") - } - - fmt.Printf("\nUpdate complete! Updated from %s to %s\n", currentTag, latestTag) - return nil - } - - if currentBranch == "" { - currentBranch = "master" - } - - fmt.Printf("Current branch: %s\n", currentBranch) - - if hasLocalChanges { - fmt.Println("\nWarning: You have local changes in your DMS configuration.") - if offerReclone(dmsPath) { - return nil - } - return errdefs.ErrUpdateCancelled - } - - pullCmd := exec.Command("git", "pull", "origin", currentBranch) - pullCmd.Stdout = os.Stdout - pullCmd.Stderr = os.Stderr - if err := pullCmd.Run(); err != nil { - fmt.Printf("Error: Failed to pull latest changes: %v\n", err) - if offerReclone(dmsPath) { - return nil - } - return fmt.Errorf("update cancelled") - } - - fmt.Println("\nUpdate complete!") - return nil -} - -func offerReclone(dmsPath string) bool { - fmt.Println("\nWould you like to backup and re-clone the repository? (y/N): ") - reader := bufio.NewReader(os.Stdin) - response, err := reader.ReadString('\n') - if err != nil || !strings.HasPrefix(strings.ToLower(strings.TrimSpace(response)), "y") { - return false - } - - timestamp := time.Now().Unix() - backupPath := fmt.Sprintf("%s.backup-%d", dmsPath, timestamp) - - fmt.Printf("Backing up current directory to %s...\n", backupPath) - if err := os.Rename(dmsPath, backupPath); err != nil { - fmt.Printf("Error: Failed to backup directory: %v\n", err) - return false - } - - fmt.Println("Cloning fresh copy...") - cloneCmd := exec.Command("git", "clone", "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath) - cloneCmd.Stdout = os.Stdout - cloneCmd.Stderr = os.Stderr - if err := cloneCmd.Run(); err != nil { - fmt.Printf("Error: Failed to clone repository: %v\n", err) - fmt.Printf("Restoring backup...\n") - os.Rename(backupPath, dmsPath) - return false - } - - fmt.Printf("Successfully re-cloned repository (backup at %s)\n", backupPath) - return true -} - -func confirmUpdate() bool { - fmt.Print("Do you want to proceed with the update? (y/N): ") - reader := bufio.NewReader(os.Stdin) - response, err := reader.ReadString('\n') - if err != nil { - fmt.Printf("Error reading input: %v\n", err) - return false - } - response = strings.TrimSpace(strings.ToLower(response)) - return response == "y" || response == "yes" -} - -func updateDMSBinary() error { - arch := "" - switch strings.ToLower(os.Getenv("HOSTTYPE")) { - case "x86_64", "amd64": - arch = "amd64" - case "aarch64", "arm64": - arch = "arm64" - default: - cmd := exec.Command("uname", "-m") - output, err := cmd.Output() - if err != nil { - return fmt.Errorf("failed to detect architecture: %w", err) - } - archStr := strings.TrimSpace(string(output)) - switch archStr { - case "x86_64": - arch = "amd64" - case "aarch64": - arch = "arm64" - default: - return fmt.Errorf("unsupported architecture: %s", archStr) - } - } - - fmt.Println("Fetching latest release version...") - cmd := exec.Command("curl", "-s", "https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest") - output, err := cmd.Output() - if err != nil { - return fmt.Errorf("failed to fetch latest release: %w", err) - } - - version := "" - for line := range strings.SplitSeq(string(output), "\n") { - if strings.Contains(line, "\"tag_name\"") { - parts := strings.Split(line, "\"") - if len(parts) >= 4 { - version = parts[3] - break - } - } - } - - if version == "" { - return fmt.Errorf("could not determine latest version") - } - - fmt.Printf("Latest version: %s\n", version) - - tempDir, err := os.MkdirTemp("", "dms-update-*") - if err != nil { - return fmt.Errorf("failed to create temp directory: %w", err) - } - defer os.RemoveAll(tempDir) - - binaryURL := fmt.Sprintf("https://github.com/AvengeMedia/DankMaterialShell/releases/download/%s/dms-cli-%s.gz", version, arch) - checksumURL := fmt.Sprintf("https://github.com/AvengeMedia/DankMaterialShell/releases/download/%s/dms-cli-%s.gz.sha256", version, arch) - - binaryPath := filepath.Join(tempDir, "dms.gz") - checksumPath := filepath.Join(tempDir, "dms.gz.sha256") - - fmt.Println("Downloading dms binary...") - downloadCmd := exec.Command("curl", "-L", binaryURL, "-o", binaryPath) - if err := downloadCmd.Run(); err != nil { - return fmt.Errorf("failed to download binary: %w", err) - } - - fmt.Println("Downloading checksum...") - downloadCmd = exec.Command("curl", "-L", checksumURL, "-o", checksumPath) - if err := downloadCmd.Run(); err != nil { - return fmt.Errorf("failed to download checksum: %w", err) - } - - fmt.Println("Verifying checksum...") - checksumData, err := os.ReadFile(checksumPath) - if err != nil { - return fmt.Errorf("failed to read checksum file: %w", err) - } - expectedChecksum := strings.Fields(string(checksumData))[0] - - actualCmd := exec.Command("sha256sum", binaryPath) - actualOutput, err := actualCmd.Output() - if err != nil { - return fmt.Errorf("failed to calculate checksum: %w", err) - } - actualChecksum := strings.Fields(string(actualOutput))[0] - - if expectedChecksum != actualChecksum { - return fmt.Errorf("checksum verification failed\nExpected: %s\nGot: %s", expectedChecksum, actualChecksum) - } - - fmt.Println("Decompressing binary...") - decompressCmd := exec.Command("gunzip", binaryPath) - if err := decompressCmd.Run(); err != nil { - return fmt.Errorf("failed to decompress binary: %w", err) - } - - decompressedPath := filepath.Join(tempDir, "dms") - - if err := os.Chmod(decompressedPath, 0o755); err != nil { - return fmt.Errorf("failed to make binary executable: %w", err) - } - - currentPath, err := exec.LookPath("dms") - if err != nil { - return fmt.Errorf("could not find current dms binary: %w", err) - } - - fmt.Printf("Installing to %s...\n", currentPath) - - replaceCmd := exec.Command("sudo", "install", "-m", "0755", decompressedPath, currentPath) - replaceCmd.Stdin = os.Stdin - replaceCmd.Stdout = os.Stdout - replaceCmd.Stderr = os.Stderr - if err := replaceCmd.Run(); err != nil { - return fmt.Errorf("failed to replace binary: %w", err) - } - - return nil -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_greeter.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_greeter.go deleted file mode 100644 index 1efa545..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_greeter.go +++ /dev/null @@ -1,1850 +0,0 @@ -package main - -import ( - "bufio" - "fmt" - "os" - "os/exec" - "os/user" - "path/filepath" - "strconv" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/distros" - "github.com/AvengeMedia/DankMaterialShell/core/internal/greeter" - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam" - "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" - "github.com/spf13/cobra" - "golang.org/x/text/cases" - "golang.org/x/text/language" -) - -var greeterCmd = &cobra.Command{ - Use: "greeter", - Short: "Manage DMS greeter", - Long: "Manage DMS greeter (greetd)", -} - -var ( - greeterConfigSyncFn = greeter.SyncDMSConfigs - sharedAuthSyncFn = sharedpam.SyncAuthConfig -) - -var greeterInstallCmd = &cobra.Command{ - Use: "install", - Short: "Install and configure DMS greeter", - Long: "Install greetd and configure it to use DMS as the greeter interface", - PreRunE: requireMutableSystemCommand, - Run: func(cmd *cobra.Command, args []string) { - 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) - } - }, -} - -var greeterSyncCmd = &cobra.Command{ - Use: "sync", - Short: "Sync DMS theme and settings with greeter", - Long: "Synchronize your current user's DMS theme, settings, and wallpaper configuration with the login greeter screen", - Run: func(cmd *cobra.Command, args []string) { - yes, _ := cmd.Flags().GetBool("yes") - auth, _ := cmd.Flags().GetBool("auth") - local, _ := cmd.Flags().GetBool("local") - term, _ := cmd.Flags().GetBool("terminal") - if term { - if err := syncInTerminal(yes, auth, local); err != nil { - log.Fatalf("Error launching sync in terminal: %v", err) - } - return - } - if err := syncGreeter(yes, auth, local); err != nil { - log.Fatalf("Error syncing greeter: %v", err) - } - }, -} - -func init() { - greeterSyncCmd.Flags().BoolP("yes", "y", false, "Non-interactive mode: skip prompts, use defaults (for UI)") - greeterSyncCmd.Flags().BoolP("terminal", "t", false, "Run sync in a new terminal (for entering sudo password); terminal auto-closes when done") - greeterSyncCmd.Flags().BoolP("auth", "a", false, "Configure PAM for fingerprint and U2F (adds both if modules exist); overrides UI toggles") - greeterSyncCmd.Flags().BoolP("local", "l", false, "Developer mode: force greetd config to use a local DMS checkout path") -} - -var greeterEnableCmd = &cobra.Command{ - Use: "enable", - Short: "Enable DMS greeter in greetd config", - Long: "Configure greetd to use DMS as the greeter", - PreRunE: requireMutableSystemCommand, - Run: func(cmd *cobra.Command, args []string) { - 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) - } - }, -} - -var greeterStatusCmd = &cobra.Command{ - Use: "status", - Short: "Check greeter sync status", - Long: "Check the status of greeter installation and configuration sync", - Run: func(cmd *cobra.Command, args []string) { - if err := checkGreeterStatus(); err != nil { - log.Fatalf("Error checking greeter status: %v", err) - } - }, -} - -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 syncGreeterConfigsAndAuth(dmsPath, compositor string, logFunc func(string), options sharedpam.SyncAuthOptions, beforeAuth func()) error { - if err := greeterConfigSyncFn(dmsPath, compositor, logFunc, ""); err != nil { - return err - } - if beforeAuth != nil { - beforeAuth() - } - return sharedAuthSyncFn(logFunc, "", options) -} - -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 - } - - greeter.TryInstallGreeterPackage(logFunc, "") - if isPackageOnlyGreeterDistro() && !greeter.IsGreeterPackaged() { - return fmt.Errorf("dms-greeter must be installed from distro packages on this distribution. %s", packageInstallHint()) - } - if greeter.IsGreeterPackaged() && greeter.HasLegacyLocalGreeterWrapper() { - 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 isGreeterEnabled() { - fmt.Print("\nGreeter is already installed and configured. Re-run to re-sync settings and permissions? [Y/n]: ") - var response string - fmt.Scanln(&response) - response = strings.TrimSpace(strings.ToLower(response)) - if response == "n" || response == "no" { - fmt.Println("Run 'dms greeter sync' to re-sync theme and settings at any time.") - return nil - } - fmt.Println() - } - - fmt.Println("\nDetecting DMS installation...") - dmsPath, err := greeter.DetectDMSPath() - if err != nil { - return err - } - fmt.Printf("✓ Found DMS at: %s\n", dmsPath) - - fmt.Println("\nDetecting installed compositors...") - compositors := greeter.DetectCompositors() - if len(compositors) == 0 { - return fmt.Errorf("no supported compositors found (niri or Hyprland required)") - } - - var selectedCompositor string - if len(compositors) == 1 { - selectedCompositor = compositors[0] - fmt.Printf("✓ Found compositor: %s\n", selectedCompositor) - } else { - var err error - selectedCompositor, err = greeter.PromptCompositorChoice(compositors) - if err != nil { - return err - } - fmt.Printf("✓ Selected compositor: %s\n", selectedCompositor) - } - - fmt.Println("\nSetting up dms-greeter group and permissions...") - if err := greeter.SetupDMSGroup(logFunc, ""); err != nil { - return err - } - - fmt.Println("\nCopying greeter files...") - if err := greeter.CopyGreeterFiles(dmsPath, selectedCompositor, logFunc, ""); err != nil { - return err - } - - if greeter.IsAppArmorEnabled() { - 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...") - greeterPathForConfig := "" - if !greeter.IsGreeterPackaged() { - greeterPathForConfig = dmsPath - } - if err := greeter.ConfigureGreetd(greeterPathForConfig, selectedCompositor, logFunc, ""); err != nil { - return err - } - - fmt.Println("\nSynchronizing DMS configurations...") - if err := syncGreeterConfigsAndAuth(dmsPath, selectedCompositor, logFunc, sharedpam.SyncAuthOptions{}, func() { - fmt.Println("\nConfiguring authentication...") - }); err != nil { - return err - } - - if err := ensureGraphicalTarget(); err != nil { - return err - } - - if err := handleConflictingDisplayManagers(); err != nil { - return err - } - - if err := ensureGreetdEnabled(); err != nil { - return err - } - - fmt.Println("\n=== Installation Complete ===") - fmt.Println("\nTo start the greeter now, run:") - fmt.Println(" sudo systemctl start greetd") - fmt.Println("\nOr reboot to see the greeter at next boot.") - - return nil -} - -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 !nonInteractive { - fmt.Print("\nThis will:\n • Stop and disable greetd\n • Remove the DMS-managed greeter auth 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 - } - } - - fmt.Println("\nDisabling greetd...") - 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 disabled") - } - - fmt.Println("\nRemoving DMS authentication configuration...") - if err := sharedpam.RemoveManagedGreeterPamBlock(logFunc, ""); err != nil { - fmt.Printf(" ⚠ PAM cleanup failed: %v\n", err) - } - - 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("\nReboot to complete the uninstallation and switch to your previous display manager.") - fmt.Println("To re-enable DMS greeter at any time, run: dms greeter enable") - - return nil -} - -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 - }{ - {"gnome-terminal", []string{"--", "bash", "-c", shellCmd}}, - {"konsole", []string{"-e", "bash", "-c", shellCmd}}, - {"xfce4-terminal", []string{"-e", "bash -c \"" + strings.ReplaceAll(shellCmd, `"`, `\"`) + "\""}}, - {"ghostty", []string{"-e", "bash", "-c", shellCmd}}, - {"wezterm", []string{"start", "--", "bash", "-c", shellCmd}}, - {"alacritty", []string{"-e", "bash", "-c", shellCmd}}, - {"kitty", []string{"bash", "-c", shellCmd}}, - {"xterm", []string{"-e", "bash -c \"" + strings.ReplaceAll(shellCmd, `"`, `\"`) + "\""}}, - } - for _, t := range terminals { - if _, err := exec.LookPath(t.name); err != nil { - continue - } - cmd := exec.Command(t.name, t.args...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - return err - } - 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 resolveLocalWrapperShell() (string, error) { - for _, shellName := range []string{"bash", "sh"} { - shellPath, err := exec.LookPath(shellName) - if err == nil { - return shellPath, nil - } - } - return "", fmt.Errorf("could not find bash or sh in PATH for local greeter wrapper") -} - -func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error { - if !nonInteractive { - fmt.Println("=== DMS Greeter Sync ===") - fmt.Println() - } - - logFunc := func(msg string) { - fmt.Println(msg) - } - - if !nonInteractive { - fmt.Println("Detecting DMS installation...") - } - var dmsPath string - var err error - if local { - dmsPath, err = resolveLocalDMSPath() - if err != nil { - return err - } - if !nonInteractive { - fmt.Printf("✓ Using local DMS path: %s\n", dmsPath) - } - } else { - dmsPath, err = greeter.DetectDMSPath() - if err != nil { - return err - } - if !nonInteractive { - fmt.Printf("✓ Found DMS at: %s\n", dmsPath) - } - } - - if !isGreeterEnabled() { - if nonInteractive { - return fmt.Errorf("greeter is not enabled; run 'dms greeter install' or 'dms greeter enable' first") - } - fmt.Println("\n⚠ DMS greeter is not enabled in greetd config.") - fmt.Print("Would you like to enable it now? (Y/n): ") - - var response string - fmt.Scanln(&response) - response = strings.ToLower(strings.TrimSpace(response)) - - if response != "n" && response != "no" { - if err := enableGreeter(false); err != nil { - return err - } - } else { - return fmt.Errorf("greeter must be enabled before syncing") - } - } - - if greeter.IsGreeterPackaged() && greeter.HasLegacyLocalGreeterWrapper() { - 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") - } - - cacheDir := greeter.GreeterCacheDir - if _, err := os.Stat(cacheDir); os.IsNotExist(err) { - logFunc("Cache directory not found — attempting to create it...") - } - - greeterGroup := greeter.DetectGreeterGroup() - greeterGroupExists := utils.HasGroup(greeterGroup) - if greeterGroupExists { - currentUser, err := user.Current() - if err != nil { - return fmt.Errorf("failed to get current user: %w", err) - } - - groupsCmd := exec.Command("groups", currentUser.Username) - groupsOutput, err := groupsCmd.Output() - if err != nil { - return fmt.Errorf("failed to check groups: %w", err) - } - - inGreeterGroup := strings.Contains(string(groupsOutput), greeterGroup) - if !inGreeterGroup { - if nonInteractive { - logFunc(fmt.Sprintf("⚠ Not yet in %s group — will be added during sync (logout/login required to take effect).", greeterGroup)) - } else { - fmt.Printf("\n⚠ Warning: You are not in the %s group.\n", greeterGroup) - fmt.Printf("Would you like to add your user to the %s group? (Y/n): ", greeterGroup) - - var response string - fmt.Scanln(&response) - response = strings.ToLower(strings.TrimSpace(response)) - - if response != "n" && response != "no" { - fmt.Printf("\nAdding user to %s group...\n", greeterGroup) - addUserCmd := exec.Command("sudo", "usermod", "-aG", greeterGroup, currentUser.Username) - addUserCmd.Stdout = os.Stdout - addUserCmd.Stderr = os.Stderr - if err := addUserCmd.Run(); err != nil { - return fmt.Errorf("failed to add user to %s group: %w", greeterGroup, err) - } - fmt.Printf("✓ User added to %s group\n", greeterGroup) - fmt.Println("⚠ You will need to log out and back in for the group change to take effect") - } else { - return fmt.Errorf("aborted: user must be in the greeter group before syncing") - } - } - } - } - - compositor := detectConfiguredCompositor() - if compositor == "" { - compositors := greeter.DetectCompositors() - switch len(compositors) { - case 0: - return fmt.Errorf("no supported compositors found") - case 1: - compositor = compositors[0] - if !nonInteractive { - fmt.Printf("✓ Using compositor: %s\n", compositor) - } - default: - if nonInteractive { - compositor = compositors[0] - break - } - var err error - compositor, err = promptCompositorChoice(compositors) - if err != nil { - return err - } - fmt.Printf("✓ Selected compositor: %s\n", compositor) - } - } else if !nonInteractive { - fmt.Printf("✓ Detected compositor from config: %s\n", compositor) - } - - if local { - localWrapperScript := filepath.Join(dmsPath, "Modules", "Greetd", "assets", "dms-greeter") - restoreWrapperOverride := func() {} - if info, statErr := os.Stat(localWrapperScript); statErr == nil && !info.IsDir() { - wrapperShell, shellErr := resolveLocalWrapperShell() - if shellErr != nil { - return shellErr - } - previousWrapperOverride, hadWrapperOverride := os.LookupEnv("DMS_GREETER_WRAPPER_CMD") - wrapperCmdOverride := wrapperShell + " " + localWrapperScript - _ = os.Setenv("DMS_GREETER_WRAPPER_CMD", wrapperCmdOverride) - restoreWrapperOverride = func() { - if hadWrapperOverride { - _ = os.Setenv("DMS_GREETER_WRAPPER_CMD", previousWrapperOverride) - } else { - _ = os.Unsetenv("DMS_GREETER_WRAPPER_CMD") - } - } - if !nonInteractive { - fmt.Printf("✓ Using local greeter wrapper script: %s\n", localWrapperScript) - } - } else if !nonInteractive { - fmt.Printf("ℹ Local wrapper script not found at %s; using system wrapper.\n", localWrapperScript) - } - - fmt.Println("\nUpdating greetd command to use local DMS path...") - err := greeter.ConfigureGreetd(dmsPath, compositor, logFunc, "") - restoreWrapperOverride() - if err != nil { - return fmt.Errorf("failed to apply local greeter path: %w", err) - } - if !nonInteractive { - fmt.Println("ℹ Local mode applies both DMS path override (-p) and local wrapper behavior when available.") - } - } else { - greeterPathForConfig := "" - if !greeter.IsGreeterPackaged() { - greeterPathForConfig = dmsPath - } - fmt.Println("\nUpdating greetd command...") - if err := greeter.ConfigureGreetd(greeterPathForConfig, compositor, logFunc, ""); err != nil { - return fmt.Errorf("failed to update greetd command: %w", err) - } - } - - fmt.Println("\nSetting up permissions and ACLs...") - greeter.RemediateStaleACLs(logFunc, "") - greeter.RemediateStaleAppArmor(logFunc, "") - if err := greeter.SetupDMSGroup(logFunc, ""); err != nil { - return err - } - if err := greeter.EnsureGreeterCacheDir(logFunc, ""); err != nil { - return fmt.Errorf("failed to ensure greeter cache directory at %s: %w\nRun: sudo mkdir -p %s && sudo chown root:%s %s && sudo chmod 2770 %s", cacheDir, err, cacheDir, greeterGroup, cacheDir, cacheDir) - } - - fmt.Println("\nSynchronizing DMS configurations...") - if err := syncGreeterConfigsAndAuth(dmsPath, compositor, logFunc, sharedpam.SyncAuthOptions{ - ForceGreeterAuth: forceAuth, - }, func() { - fmt.Println("\nConfiguring authentication...") - }); err != nil { - return err - } - - if greeter.IsAppArmorEnabled() { - 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.") - fmt.Println("Shared authentication settings were also checked and reconciled where needed.") - if forceAuth { - fmt.Println("Authentication has been configured for fingerprint and U2F (where modules exist).") - } - fmt.Println("The changes will be visible on the next login screen.") - - return nil -} - -func hasDmsShellQml(dir string) bool { - info, err := os.Stat(filepath.Join(dir, "shell.qml")) - return err == nil && !info.IsDir() -} - -func resolveDMSLocalCandidate(path string) (string, bool) { - if path == "" { - return "", false - } - if hasDmsShellQml(path) { - abs, err := filepath.Abs(path) - if err != nil { - return path, true - } - return abs, true - } - - quickshellPath := filepath.Join(path, "quickshell") - if hasDmsShellQml(quickshellPath) { - abs, err := filepath.Abs(quickshellPath) - if err != nil { - return quickshellPath, true - } - return abs, true - } - - return "", false -} - -func resolveLocalDMSPath() (string, error) { - if override := strings.TrimSpace(os.Getenv("DMS_LOCAL_PATH")); override != "" { - if resolved, ok := resolveDMSLocalCandidate(override); ok { - return resolved, nil - } - return "", fmt.Errorf("DMS_LOCAL_PATH is set but does not point to a valid DMS quickshell path: %s", override) - } - - wd, err := os.Getwd() - if err != nil { - return "", fmt.Errorf("failed to get current directory: %w", err) - } - - dir := wd - for { - if resolved, ok := resolveDMSLocalCandidate(dir); ok { - return resolved, nil - } - - parent := filepath.Dir(dir) - if parent == dir { - break - } - dir = parent - } - - homeDir, err := os.UserHomeDir() - if err == nil && homeDir != "" { - for _, candidate := range []string{ - filepath.Join(homeDir, "dms"), - filepath.Join(homeDir, "DankMaterialShell"), - filepath.Join(homeDir, "dankmaterialshell"), - filepath.Join(homeDir, "projects", "dms"), - filepath.Join(homeDir, "src", "dms"), - } { - if resolved, ok := resolveDMSLocalCandidate(candidate); ok { - return resolved, nil - } - } - - if entries, readErr := os.ReadDir(homeDir); readErr == nil { - for _, entry := range entries { - if !entry.IsDir() { - continue - } - name := strings.ToLower(entry.Name()) - if !strings.Contains(name, "dms") && !strings.Contains(name, "dank") { - continue - } - if resolved, ok := resolveDMSLocalCandidate(filepath.Join(homeDir, entry.Name())); ok { - return resolved, nil - } - } - } - } - - return "", fmt.Errorf("could not locate a local DMS checkout from %s; run from repo root or set DMS_LOCAL_PATH=/absolute/path/to/repo", wd) -} - -func disableDisplayManager(dmName string) (bool, error) { - state, err := getSystemdServiceState(dmName) - if err != nil { - return false, fmt.Errorf("failed to check %s state: %w", dmName, err) - } - - if !state.Exists { - return false, nil - } - - fmt.Printf("\nChecking %s...\n", dmName) - fmt.Printf(" Current state: enabled=%s\n", state.EnabledState) - - actionTaken := false - - if state.NeedsDisable { - var disableCmd *exec.Cmd - var actionVerb string - - if state.EnabledState == "static" { - fmt.Printf(" Masking %s (static service cannot be disabled)...\n", dmName) - disableCmd = exec.Command("sudo", "systemctl", "mask", dmName) - actionVerb = "masked" - } else { - fmt.Printf(" Disabling %s...\n", dmName) - disableCmd = exec.Command("sudo", "systemctl", "disable", dmName) - actionVerb = "disabled" - } - - disableCmd.Stdout = os.Stdout - disableCmd.Stderr = os.Stderr - if err := disableCmd.Run(); err != nil { - return actionTaken, fmt.Errorf("failed to disable/mask %s: %w", dmName, err) - } - - enabledState, shouldDisable, verifyErr := checkSystemdServiceEnabled(dmName) - if verifyErr != nil { - fmt.Printf(" ⚠ Warning: Could not verify %s was %s: %v\n", dmName, actionVerb, verifyErr) - } else if shouldDisable { - return actionTaken, fmt.Errorf("%s is still in state '%s' after %s operation", dmName, enabledState, actionVerb) - } else { - fmt.Printf(" ✓ %s %s (now: %s)\n", cases.Title(language.English).String(actionVerb), dmName, enabledState) - } - - actionTaken = true - } else { - if state.EnabledState == "masked" || state.EnabledState == "masked-runtime" { - fmt.Printf(" ✓ %s is already masked\n", dmName) - } else { - fmt.Printf(" ✓ %s is already disabled\n", dmName) - } - } - - return actionTaken, nil -} - -func ensureGreetdEnabled() error { - fmt.Println("\nChecking greetd service status...") - - state, err := getSystemdServiceState("greetd") - if err != nil { - return fmt.Errorf("failed to check greetd state: %w", err) - } - - if !state.Exists { - return fmt.Errorf("greetd service not found. Please install greetd first") - } - - fmt.Printf(" Current state: %s\n", state.EnabledState) - - if state.EnabledState == "masked" || state.EnabledState == "masked-runtime" { - fmt.Println(" Unmasking greetd...") - unmaskCmd := exec.Command("sudo", "systemctl", "unmask", "greetd") - unmaskCmd.Stdout = os.Stdout - unmaskCmd.Stderr = os.Stderr - if err := unmaskCmd.Run(); err != nil { - return fmt.Errorf("failed to unmask greetd: %w", err) - } - fmt.Println(" ✓ Unmasked greetd") - } - - if state.EnabledState == "enabled" || state.EnabledState == "enabled-runtime" { - fmt.Println(" Reasserting greetd as active display manager...") - } else { - fmt.Println(" Enabling greetd service...") - } - - enableCmd := exec.Command("sudo", "systemctl", "enable", "--force", "greetd") - enableCmd.Stdout = os.Stdout - enableCmd.Stderr = os.Stderr - if err := enableCmd.Run(); err != nil { - return fmt.Errorf("failed to enable greetd: %w", err) - } - - enabledState, _, verifyErr := checkSystemdServiceEnabled("greetd") - if verifyErr != nil { - fmt.Printf(" ⚠ Warning: Could not verify greetd enabled state: %v\n", verifyErr) - } else { - switch enabledState { - case "enabled", "enabled-runtime", "static", "indirect", "alias": - fmt.Printf(" ✓ greetd enabled (state: %s)\n", enabledState) - default: - return fmt.Errorf("greetd is still in state '%s' after enable operation", enabledState) - } - } - - return nil -} - -func ensureGraphicalTarget() error { - getDefaultCmd := exec.Command("systemctl", "get-default") - currentTarget, err := getDefaultCmd.Output() - if err != nil { - fmt.Println("⚠ Warning: Could not detect current default systemd target") - return nil - } - - currentTargetStr := strings.TrimSpace(string(currentTarget)) - if currentTargetStr != "graphical.target" { - fmt.Printf("\nSetting graphical.target as default (current: %s)...\n", currentTargetStr) - setDefaultCmd := exec.Command("sudo", "systemctl", "set-default", "graphical.target") - setDefaultCmd.Stdout = os.Stdout - setDefaultCmd.Stderr = os.Stderr - if err := setDefaultCmd.Run(); err != nil { - fmt.Println("⚠ Warning: Failed to set graphical.target as default") - fmt.Println(" Greeter may not start on boot. Run manually:") - fmt.Println(" sudo systemctl set-default graphical.target") - return nil - } - fmt.Println("✓ Set graphical.target as default") - } else { - fmt.Println("✓ Default target already set to graphical.target") - } - - return nil -} - -func handleConflictingDisplayManagers() error { - fmt.Println("\n=== Checking for Conflicting Display Managers ===") - - conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"} - - disabledAny := false - var errors []string - - for _, dm := range conflictingDMs { - actionTaken, err := disableDisplayManager(dm) - if err != nil { - errMsg := fmt.Sprintf("Failed to handle %s: %v", dm, err) - errors = append(errors, errMsg) - fmt.Printf(" ⚠⚠⚠ ERROR: %s\n", errMsg) - continue - } - if actionTaken { - disabledAny = true - } - } - - if len(errors) > 0 { - fmt.Println("\n╔════════════════════════════════════════════════════════════╗") - fmt.Println("║ ⚠⚠⚠ ERRORS OCCURRED ⚠⚠⚠ ║") - fmt.Println("╚════════════════════════════════════════════════════════════╝") - fmt.Println("\nSome display managers could not be disabled:") - for _, err := range errors { - fmt.Printf(" ✗ %s\n", err) - } - fmt.Println("\nThis may prevent greetd from starting properly.") - fmt.Println("You may need to manually disable them before greetd will work.") - fmt.Println("\nManual commands to try:") - for _, dm := range conflictingDMs { - fmt.Printf(" sudo systemctl disable %s\n", dm) - fmt.Printf(" sudo systemctl mask %s\n", dm) - } - fmt.Print("\nContinue with greeter enablement anyway? (Y/n): ") - - var response string - fmt.Scanln(&response) - response = strings.ToLower(strings.TrimSpace(response)) - - if response == "n" || response == "no" { - return fmt.Errorf("aborted due to display manager conflicts") - } - fmt.Println("\nContinuing despite errors...") - } - - if !disabledAny && len(errors) == 0 { - fmt.Println("\n✓ No conflicting display managers found") - } else if disabledAny && len(errors) == 0 { - fmt.Println("\n✓ Successfully handled all conflicting display managers") - } - - return nil -} - -func enableGreeter(nonInteractive bool) error { - fmt.Println("=== DMS Greeter Enable ===") - fmt.Println() - - configPath := "/etc/greetd/config.toml" - if _, err := os.Stat(configPath); os.IsNotExist(err) { - return fmt.Errorf("greetd config not found at %s\nPlease install greetd first", configPath) - } else if err != nil { - return fmt.Errorf("failed to access greetd config at %s: %w", configPath, err) - } - - if greeter.IsGreeterPackaged() && greeter.HasLegacyLocalGreeterWrapper() { - 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") - } - - configAlreadyCorrect := isGreeterEnabled() - configuredCompositor := detectConfiguredCompositor() - - logFunc := func(msg string) { - fmt.Println(msg) - } - greeterGroup := greeter.DetectGreeterGroup() - - if configAlreadyCorrect { - fmt.Println("✓ Greeter is already configured with dms-greeter") - if configuredCompositor != "" { - fmt.Printf("✓ Configured compositor: %s\n", configuredCompositor) - } - - fmt.Println("\nSetting up dms-greeter group and permissions...") - if err := greeter.SetupDMSGroup(logFunc, ""); err != nil { - return err - } - if err := greeter.EnsureGreeterCacheDir(logFunc, ""); err != nil { - fmt.Printf("⚠ Could not ensure cache directory: %v\n Run: sudo mkdir -p %s && sudo chown root:%s %s && sudo chmod 2770 %s\n", err, greeter.GreeterCacheDir, greeterGroup, greeter.GreeterCacheDir, greeter.GreeterCacheDir) - } - - if err := ensureGraphicalTarget(); err != nil { - return err - } - - if err := handleConflictingDisplayManagers(); err != nil { - return err - } - - if err := ensureGreetdEnabled(); err != nil { - return err - } - - fmt.Println("\n=== Enable Complete ===") - fmt.Println("\nGreeter configuration verified and system state corrected.") - fmt.Println("To start the greeter now, run:") - fmt.Println(" sudo systemctl start greetd") - fmt.Println("\nOr reboot to see the greeter at boot time.") - - 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() - - if utils.CommandExists("sway") { - compositors = append(compositors, "sway") - } - - if len(compositors) == 0 { - return fmt.Errorf("no supported compositors found (niri, Hyprland, or sway required)") - } - - var selectedCompositor string - if len(compositors) == 1 { - selectedCompositor = compositors[0] - fmt.Printf("✓ Found compositor: %s\n", selectedCompositor) - } else { - var err error - selectedCompositor, err = promptCompositorChoice(compositors) - if err != nil { - return err - } - fmt.Printf("✓ Selected compositor: %s\n", selectedCompositor) - } - - greeterPathForConfig := "" - if !greeter.IsGreeterPackaged() { - dmsPath, err := greeter.DetectDMSPath() - if err != nil { - return fmt.Errorf("failed to detect DMS path for manual greeter configuration: %w", err) - } - greeterPathForConfig = dmsPath - } - if err := greeter.ConfigureGreetd(greeterPathForConfig, selectedCompositor, logFunc, ""); err != nil { - return fmt.Errorf("failed to configure greetd: %w", err) - } - - fmt.Println("\nSetting up dms-greeter group and permissions...") - if err := greeter.SetupDMSGroup(logFunc, ""); err != nil { - return err - } - if err := greeter.EnsureGreeterCacheDir(logFunc, ""); err != nil { - fmt.Printf("⚠ Could not ensure cache directory: %v\n Run: sudo mkdir -p %s && sudo chown root:%s %s && sudo chmod 2770 %s\n", err, greeter.GreeterCacheDir, greeterGroup, greeter.GreeterCacheDir, greeter.GreeterCacheDir) - } - - if greeter.IsAppArmorEnabled() { - if err := greeter.InstallAppArmorProfile(logFunc, ""); err != nil { - logFunc(fmt.Sprintf("⚠ AppArmor profile setup failed: %v", err)) - } - } - - if err := ensureGraphicalTarget(); err != nil { - return err - } - - if err := handleConflictingDisplayManagers(); err != nil { - return err - } - - if err := ensureGreetdEnabled(); err != nil { - return err - } - - fmt.Println("\n=== Enable Complete ===") - fmt.Println("\nTo start the greeter now, run:") - fmt.Println(" sudo systemctl start greetd") - fmt.Println("\nOr reboot to see the greeter at boot time.") - - return nil -} - -func isGreeterEnabled() bool { - command := readDefaultSessionCommand("/etc/greetd/config.toml") - return command != "" && strings.Contains(command, "dms-greeter") -} - -func detectConfiguredCompositor() string { - command := strings.ToLower(readDefaultSessionCommand("/etc/greetd/config.toml")) - switch { - case strings.Contains(command, "--command niri"): - return "niri" - case strings.Contains(command, "--command hyprland"): - return "hyprland" - case strings.Contains(command, "--command sway"): - return "sway" - } - return "" -} - -func stripTomlComment(line string) string { - trimmed := strings.TrimSpace(line) - if idx := strings.Index(trimmed, "#"); idx >= 0 { - return strings.TrimSpace(trimmed[:idx]) - } - return trimmed -} - -func parseTomlSection(line string) (string, bool) { - trimmed := stripTomlComment(line) - if len(trimmed) < 3 || !strings.HasPrefix(trimmed, "[") || !strings.HasSuffix(trimmed, "]") { - return "", false - } - return strings.TrimSpace(trimmed[1 : len(trimmed)-1]), true -} - -func readDefaultSessionCommand(configPath string) string { - data, err := os.ReadFile(configPath) - if err != nil { - return "" - } - - inDefaultSession := false - for line := range strings.SplitSeq(string(data), "\n") { - if section, ok := parseTomlSection(line); ok { - inDefaultSession = section == "default_session" - continue - } - - if !inDefaultSession { - continue - } - - trimmed := stripTomlComment(line) - if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") { - continue - } - - parts := strings.SplitN(trimmed, "=", 2) - if len(parts) != 2 { - continue - } - - command := strings.Trim(strings.TrimSpace(parts[1]), `"`) - if command != "" { - return command - } - } - - return "" -} - -func extractGreeterCacheDirFromCommand(command string) string { - if command == "" { - return greeter.GreeterCacheDir - } - tokens := strings.Fields(command) - for i := 0; i < len(tokens); i++ { - token := strings.Trim(tokens[i], "\"") - if token == "--cache-dir" && i+1 < len(tokens) { - return strings.Trim(tokens[i+1], "\"") - } - if strings.HasPrefix(token, "--cache-dir=") { - value := strings.TrimPrefix(token, "--cache-dir=") - value = strings.Trim(value, "\"") - if value != "" { - return value - } - } - } - return greeter.GreeterCacheDir -} - -func extractGreeterWrapperFromCommand(command string) string { - if command == "" { - return "" - } - tokens := strings.Fields(command) - if len(tokens) == 0 { - return "" - } - wrapper := strings.Trim(tokens[0], "\"") - if wrapper == "" { - return "" - } - if len(tokens) > 1 { - next := strings.Trim(tokens[1], "\"") - if next != "" && (filepath.Base(wrapper) == "bash" || filepath.Base(wrapper) == "sh") && strings.Contains(filepath.Base(next), "dms-greeter") { - return fmt.Sprintf("%s (script: %s)", wrapper, next) - } - } - return wrapper -} - -func extractGreeterPathOverrideFromCommand(command string) string { - if command == "" { - return "" - } - tokens := strings.Fields(command) - for i := 0; i < len(tokens); i++ { - token := strings.Trim(tokens[i], "\"") - if (token == "-p" || token == "--path") && i+1 < len(tokens) { - return strings.Trim(tokens[i+1], "\"") - } - if strings.HasPrefix(token, "--path=") { - value := strings.TrimPrefix(token, "--path=") - value = strings.Trim(value, "\"") - if value != "" { - return value - } - } - } - return "" -} - -func parseManagedGreeterPamAuth(pamText string) (managed bool, fingerprint bool, u2f bool, legacy bool) { - return sharedpam.ParseManagedGreeterPamAuth(pamText) -} - -func packageInstallHint() string { - osInfo, err := distros.GetOSInfo() - if err != nil { - return "Install package: dms-greeter" - } - config, exists := distros.Registry[osInfo.Distribution.ID] - if !exists { - return "Install package: dms-greeter" - } - - switch config.Family { - case distros.FamilyDebian: - return "Install with 'sudo apt install dms-greeter' (requires DankLinux OBS repo — see https://danklinux.com/docs/dankgreeter/installation#debian)" - case distros.FamilySUSE: - return "Install with 'sudo zypper install dms-greeter' (requires DankLinux OBS repo — see https://danklinux.com/docs/dankgreeter/installation#opensuse)" - case distros.FamilyUbuntu: - return "Install with 'sudo apt install dms-greeter' (requires ppa:avengemedia/danklinux: sudo add-apt-repository ppa:avengemedia/danklinux)" - case distros.FamilyFedora: - return "Install with 'sudo dnf install dms-greeter' (requires COPR: sudo dnf copr enable avengemedia/danklinux)" - case distros.FamilyArch: - return "Install from AUR with 'paru -S greetd-dms-greeter-git' or 'yay -S greetd-dms-greeter-git'" - default: - return "Run 'dms greeter install' to install greeter" - } -} - -func systemPamManagerRemediationHint() string { - osInfo, err := distros.GetOSInfo() - if err != nil { - return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login." - } - config, exists := distros.Registry[osInfo.Distribution.ID] - if !exists { - return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login." - } - - switch config.Family { - case distros.FamilyFedora: - return "Disable it in authselect to force password-only greeter login." - case distros.FamilyDebian, distros.FamilyUbuntu: - return "Disable it in pam-auth-update to force password-only greeter login." - default: - return "Disable it in your distro PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login." - } -} - -func isPackageOnlyGreeterDistro() bool { - osInfo, err := distros.GetOSInfo() - if err != nil { - return false - } - config, exists := distros.Registry[osInfo.Distribution.ID] - if !exists { - return false - } - return config.Family == distros.FamilyDebian || - config.Family == distros.FamilySUSE || - config.Family == distros.FamilyUbuntu || - config.Family == distros.FamilyFedora || - config.Family == distros.FamilyArch -} - -func promptCompositorChoice(compositors []string) (string, error) { - fmt.Println("\nMultiple compositors detected:") - for i, comp := range compositors { - fmt.Printf("%d) %s\n", i+1, comp) - } - - var response string - fmt.Print("Choose compositor for greeter: ") - fmt.Scanln(&response) - response = strings.TrimSpace(response) - - choice := 0 - fmt.Sscanf(response, "%d", &choice) - - if choice < 1 || choice > len(compositors) { - return "", fmt.Errorf("invalid choice") - } - - return compositors[choice-1], nil -} - -func checkGreeterStatus() error { - fmt.Println("=== DMS Greeter Status ===") - fmt.Println() - - homeDir, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("failed to get user home directory: %w", err) - } - - currentUser, err := user.Current() - if err != nil { - return fmt.Errorf("failed to get current user: %w", err) - } - - configPath := "/etc/greetd/config.toml" - configuredCommand := "" - allGood := true - fmt.Println("Greeter Configuration:") - if _, err := os.ReadFile(configPath); err == nil { - configuredCommand = readDefaultSessionCommand(configPath) - if configuredCommand != "" && strings.Contains(configuredCommand, "dms-greeter") { - fmt.Println(" ✓ Greeter is enabled") - if wrapper := extractGreeterWrapperFromCommand(configuredCommand); wrapper != "" { - fmt.Printf(" Wrapper: %s\n", wrapper) - } - if pathOverride := extractGreeterPathOverrideFromCommand(configuredCommand); pathOverride != "" { - fmt.Printf(" DMS path override: %s\n", pathOverride) - } - - compositor := detectConfiguredCompositor() - switch compositor { - case "niri": - fmt.Println(" Compositor: niri") - case "hyprland": - fmt.Println(" Compositor: Hyprland") - case "sway": - fmt.Println(" Compositor: sway") - default: - fmt.Println(" Compositor: unknown") - } - } else { - fmt.Println(" ✗ Greeter is NOT enabled") - fmt.Println(" Run 'dms greeter enable' to enable it, or use the Activate button in Settings → Greeter, then Sync.") - allGood = false - } - } else { - fmt.Println(" ✗ Greeter config not found") - fmt.Printf(" %s\n", packageInstallHint()) - allGood = false - } - - fmt.Println("\nGroup Membership:") - groupsCmd := exec.Command("groups", currentUser.Username) - groupsOutput, err := groupsCmd.Output() - if err != nil { - return fmt.Errorf("failed to check groups: %w", err) - } - - greeterGroup := greeter.DetectGreeterGroup() - inGreeterGroup := strings.Contains(string(groupsOutput), greeterGroup) - if inGreeterGroup { - fmt.Printf(" ✓ User is in %s group\n", greeterGroup) - } else { - fmt.Printf(" ✗ User is NOT in %s group\n", greeterGroup) - fmt.Println(" Run 'dms greeter sync' to set up group membership and permissions") - } - - cacheDir := extractGreeterCacheDirFromCommand(configuredCommand) - fmt.Println("\nGreeter Cache Directory:") - fmt.Printf(" Effective cache dir: %s\n", cacheDir) - if cacheDir != greeter.GreeterCacheDir { - fmt.Printf(" ⚠ Non-default cache dir detected (default: %s)\n", greeter.GreeterCacheDir) - } - if stat, err := os.Stat(cacheDir); err == nil && stat.IsDir() { - fmt.Printf(" ✓ %s exists\n", cacheDir) - requiredSubdirs := []string{".local/state", ".local/share", ".cache"} - missingSubdirs := false - for _, sub := range requiredSubdirs { - subPath := filepath.Join(cacheDir, sub) - if _, err := os.Stat(subPath); os.IsNotExist(err) { - fmt.Printf(" ⚠ Missing required subdir: %s\n", subPath) - missingSubdirs = true - } - } - if missingSubdirs { - fmt.Println(" Run 'dms greeter sync' to initialize the cache directory structure.") - allGood = false - } - } else { - fmt.Printf(" ✗ %s not found\n", cacheDir) - fmt.Printf(" %s\n", packageInstallHint()) - return nil - } - - fmt.Println("\nConfiguration Symlinks:") - colorSyncInfo, colorSyncErr := greeter.ResolveGreeterColorSyncInfo(homeDir) - if colorSyncErr != nil { - fmt.Printf(" ✗ Failed to resolve expected greeter color source: %v\n", colorSyncErr) - allGood = false - colorSyncInfo = greeter.GreeterColorSyncInfo{ - SourcePath: filepath.Join(homeDir, ".cache", "DankMaterialShell", "dms-colors.json"), - } - } - - colorThemeDesc := "Color theme" - if colorSyncInfo.UsesDynamicWallpaperOverride { - colorThemeDesc = "Color theme (greeter wallpaper override)" - } - - symlinks := []struct { - source string - target string - desc string - }{ - { - source: filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"), - target: filepath.Join(cacheDir, "settings.json"), - desc: "Settings", - }, - { - source: filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"), - target: filepath.Join(cacheDir, "session.json"), - desc: "Session state", - }, - { - source: colorSyncInfo.SourcePath, - target: filepath.Join(cacheDir, "colors.json"), - desc: colorThemeDesc, - }, - } - - for _, link := range symlinks { - targetInfo, err := os.Lstat(link.target) - if err != nil { - fmt.Printf(" ✗ %s: symlink not found at %s\n", link.desc, link.target) - allGood = false - continue - } - - if targetInfo.Mode()&os.ModeSymlink == 0 { - fmt.Printf(" ✗ %s: %s is not a symlink\n", link.desc, link.target) - allGood = false - continue - } - - linkDest, err := os.Readlink(link.target) - if err != nil { - fmt.Printf(" ✗ %s: failed to read symlink\n", link.desc) - allGood = false - continue - } - - if linkDest != link.source { - fmt.Printf(" ✗ %s: symlink points to wrong location\n", link.desc) - fmt.Printf(" Expected: %s\n", link.source) - fmt.Printf(" Got: %s\n", linkDest) - allGood = false - continue - } - - if _, err := os.Stat(link.source); os.IsNotExist(err) { - fmt.Printf(" ⚠ %s: symlink OK, but source file doesn't exist yet\n", link.desc) - fmt.Printf(" Will be created when you run DMS\n") - continue - } - - fmt.Printf(" ✓ %s: synced correctly\n", link.desc) - } - - if colorSyncInfo.UsesDynamicWallpaperOverride { - fmt.Printf(" ℹ Dynamic theme uses greeter override colors from %s\n", colorSyncInfo.SourcePath) - } - - fmt.Println("\nGreeter Wallpaper Override:") - overridePath := filepath.Join(cacheDir, "greeter_wallpaper_override.jpg") - if stat, err := os.Stat(overridePath); err == nil && !stat.IsDir() { - fmt.Printf(" ✓ Override file present: %s\n", overridePath) - } else if os.IsNotExist(err) { - fmt.Println(" ℹ Override file not present (desktop/session wallpaper fallback in effect)") - } else if err != nil { - fmt.Printf(" ✗ Could not inspect override file: %v\n", err) - allGood = false - } else { - fmt.Printf(" ✗ Override path is not a regular file: %s\n", overridePath) - allGood = false - } - - fmt.Println("\nGreeter PAM Authentication (DMS-managed block):") - if greeter.IsNixOS() { - fmt.Println(" ℹ NixOS detected: PAM is managed by NixOS modules.") - fmt.Println(" Configure fingerprint/U2F via your greetd NixOS module (security.pam.services.greetd).") - fmt.Println() - if allGood && inGreeterGroup { - fmt.Println("✓ All checks passed! Greeter is properly configured.") - } else if !allGood { - fmt.Println("⚠ Some issues detected. Run 'dms greeter sync' to repair configuration.") - } else if !inGreeterGroup { - fmt.Printf("⚠ User is not in %s group. Run 'dms greeter sync' after adding group membership.\n", greeterGroup) - } - return nil - } - greetdPamPath := "/etc/pam.d/greetd" - pamData, err := os.ReadFile(greetdPamPath) - if err != nil { - fmt.Printf(" ✗ Failed to read %s: %v\n", greetdPamPath, err) - allGood = false - } else { - managed, managedFprint, managedU2f, legacyManaged := parseManagedGreeterPamAuth(string(pamData)) - if managed { - fmt.Println(" ✓ Managed auth block present") - if managedFprint { - fmt.Println(" - fingerprint: enabled") - } else { - fmt.Println(" - fingerprint: disabled") - } - if managedU2f { - fmt.Println(" - security key (U2F): enabled") - } else { - fmt.Println(" - security key (U2F): disabled") - } - } else { - fmt.Println(" ℹ No managed auth block present (DMS-managed fingerprint/U2F lines are disabled)") - } - if legacyManaged { - fmt.Println(" ⚠ Legacy unmanaged DMS PAM lines detected. Run 'dms auth sync' to normalize.") - allGood = false - } - enableFprintToggle, enableU2fToggle := false, false - if enableFprint, enableU2f, settingsErr := sharedpam.ReadGreeterAuthToggles(homeDir); settingsErr == nil { - enableFprintToggle = enableFprint - enableU2fToggle = enableU2f - } else { - fmt.Printf(" ℹ Could not read greeter auth toggles from settings: %v\n", settingsErr) - } - - includedFprintFile := sharedpam.DetectIncludedPamModule(string(pamData), "pam_fprintd.so") - includedU2fFile := sharedpam.DetectIncludedPamModule(string(pamData), "pam_u2f.so") - fprintAvailableForCurrentUser := sharedpam.FingerprintAuthAvailableForCurrentUser() - - if managedFprint && includedFprintFile != "" { - fmt.Printf(" ⚠ pam_fprintd found in both DMS managed block and %s.\n", includedFprintFile) - fmt.Println(" Double fingerprint auth detected — run 'dms auth sync' to resolve.") - allGood = false - } - if managedU2f && includedU2fFile != "" { - fmt.Printf(" ⚠ pam_u2f found in both DMS managed block and %s.\n", includedU2fFile) - fmt.Println(" Double security-key auth detected — run 'dms auth sync' to resolve.") - allGood = false - } - - if includedFprintFile != "" && !managedFprint { - if enableFprintToggle { - fmt.Printf(" ℹ Fingerprint auth is enabled via included %s.\n", includedFprintFile) - if fprintAvailableForCurrentUser { - fmt.Println(" DMS toggle is enabled, and effective auth is coming from the included PAM stack.") - } else { - fmt.Println(" No enrolled fingerprints detected for the current user; password auth remains the effective path.") - } - } else { - if fprintAvailableForCurrentUser { - fmt.Printf(" ℹ Fingerprint auth is active via included %s while DMS fingerprint toggle is off.\n", includedFprintFile) - fmt.Println(" Password login will work but may be delayed while the fingerprint module runs first.") - fmt.Printf(" To eliminate the delay, %s\n", systemPamManagerRemediationHint()) - } else { - fmt.Printf(" ℹ pam_fprintd is present via included %s, but no enrolled fingerprints were detected for user %s.\n", includedFprintFile, currentUser.Username) - fmt.Println(" Password auth remains the effective login path.") - } - } - } - if includedU2fFile != "" && !managedU2f { - if enableU2fToggle { - fmt.Printf(" ℹ Security-key auth is enabled via included %s.\n", includedU2fFile) - fmt.Println(" DMS toggle is enabled, but effective auth is coming from the included PAM stack.") - } else { - fmt.Printf(" ⚠ Security-key auth is active via included %s while DMS security-key toggle is off.\n", includedU2fFile) - fmt.Printf(" %s\n", systemPamManagerRemediationHint()) - } - } - } - - fmt.Println("\nSecurity (AppArmor):") - if !greeter.IsAppArmorEnabled() { - 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.") - } else if !allGood { - fmt.Println("⚠ Some issues detected. Run 'dms greeter sync' to repair configuration.") - } else if !inGreeterGroup { - fmt.Printf("⚠ User is not in %s group. Run 'dms greeter sync' after adding group membership.\n", greeterGroup) - } - - return 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 "" for a named AppArmor profile. -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 "" -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_greeter_test.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_greeter_test.go deleted file mode 100644 index 775149f..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_greeter_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package main - -import ( - "errors" - "reflect" - "testing" - - sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam" -) - -func TestSyncGreeterConfigsAndAuthDelegatesSharedAuth(t *testing.T) { - origGreeterConfigSyncFn := greeterConfigSyncFn - origSharedAuthSyncFn := sharedAuthSyncFn - t.Cleanup(func() { - greeterConfigSyncFn = origGreeterConfigSyncFn - sharedAuthSyncFn = origSharedAuthSyncFn - }) - - var calls []string - greeterConfigSyncFn = func(dmsPath, compositor string, logFunc func(string), sudoPassword string) error { - if dmsPath != "/tmp/dms" { - t.Fatalf("unexpected dmsPath %q", dmsPath) - } - if compositor != "niri" { - t.Fatalf("unexpected compositor %q", compositor) - } - if sudoPassword != "" { - t.Fatalf("expected empty sudoPassword, got %q", sudoPassword) - } - calls = append(calls, "configs") - return nil - } - - var gotOptions sharedpam.SyncAuthOptions - sharedAuthSyncFn = func(logFunc func(string), sudoPassword string, options sharedpam.SyncAuthOptions) error { - if sudoPassword != "" { - t.Fatalf("expected empty sudoPassword, got %q", sudoPassword) - } - gotOptions = options - calls = append(calls, "auth") - return nil - } - - err := syncGreeterConfigsAndAuth("/tmp/dms", "niri", func(string) {}, sharedpam.SyncAuthOptions{ - ForceGreeterAuth: true, - }, func() { - calls = append(calls, "before-auth") - }) - if err != nil { - t.Fatalf("syncGreeterConfigsAndAuth returned error: %v", err) - } - - wantCalls := []string{"configs", "before-auth", "auth"} - if !reflect.DeepEqual(calls, wantCalls) { - t.Fatalf("call order = %v, want %v", calls, wantCalls) - } - if !gotOptions.ForceGreeterAuth { - t.Fatalf("expected ForceGreeterAuth to be true, got %+v", gotOptions) - } -} - -func TestSyncGreeterConfigsAndAuthStopsOnConfigError(t *testing.T) { - origGreeterConfigSyncFn := greeterConfigSyncFn - origSharedAuthSyncFn := sharedAuthSyncFn - t.Cleanup(func() { - greeterConfigSyncFn = origGreeterConfigSyncFn - sharedAuthSyncFn = origSharedAuthSyncFn - }) - - greeterConfigSyncFn = func(string, string, func(string), string) error { - return errors.New("config sync failed") - } - - authCalled := false - sharedAuthSyncFn = func(func(string), string, sharedpam.SyncAuthOptions) error { - authCalled = true - return nil - } - - err := syncGreeterConfigsAndAuth("/tmp/dms", "niri", func(string) {}, sharedpam.SyncAuthOptions{}, nil) - if err == nil || err.Error() != "config sync failed" { - t.Fatalf("expected config sync error, got %v", err) - } - if authCalled { - t.Fatal("expected auth sync not to run after config sync failure") - } -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_keybinds.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_keybinds.go deleted file mode 100644 index cdb15ac..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_keybinds.go +++ /dev/null @@ -1,265 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" - "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds/providers" - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/spf13/cobra" -) - -var keybindsCmd = &cobra.Command{ - Use: "keybinds", - Aliases: []string{"cheatsheet", "chsht"}, - Short: "Manage keybinds and cheatsheets", - Long: "Display and manage keybinds and cheatsheets for various applications", -} - -var keybindsListCmd = &cobra.Command{ - Use: "list", - Short: "List available providers", - Long: "List all available keybind/cheatsheet providers", - Run: runKeybindsList, -} - -var keybindsShowCmd = &cobra.Command{ - Use: "show <provider>", - Short: "Show keybinds for a provider", - Long: "Display keybinds/cheatsheet for the specified provider", - Args: cobra.ExactArgs(1), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) != 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - registry := keybinds.GetDefaultRegistry() - return registry.List(), cobra.ShellCompDirectiveNoFileComp - }, - Run: runKeybindsShow, -} - -var keybindsSetCmd = &cobra.Command{ - Use: "set <provider> <key> <action>", - Short: "Set a keybind override", - Long: "Create or update a keybind override for the specified provider", - Args: cobra.ExactArgs(3), - Run: runKeybindsSet, -} - -var keybindsRemoveCmd = &cobra.Command{ - Use: "remove <provider> <key>", - Short: "Remove a keybind override", - Long: "Remove a keybind override from the specified provider", - Args: cobra.ExactArgs(2), - Run: runKeybindsRemove, -} - -func init() { - keybindsListCmd.Flags().BoolP("json", "j", false, "Output as JSON") - keybindsShowCmd.Flags().String("path", "", "Override config path for the provider") - keybindsSetCmd.Flags().String("desc", "", "Description for hotkey overlay") - keybindsSetCmd.Flags().Bool("allow-when-locked", false, "Allow when screen is locked") - keybindsSetCmd.Flags().Int("cooldown-ms", 0, "Cooldown in milliseconds") - keybindsSetCmd.Flags().Bool("no-repeat", false, "Disable key repeat") - keybindsSetCmd.Flags().Bool("no-inhibiting", false, "Keep bind active when shortcuts are inhibited (allow-inhibiting=false)") - keybindsSetCmd.Flags().String("replace-key", "", "Original key to replace (removes old key)") - keybindsSetCmd.Flags().String("flags", "", "Hyprland bind flags (e.g., 'e' for repeat, 'l' for locked, 'r' for release)") - - keybindsCmd.AddCommand(keybindsListCmd) - keybindsCmd.AddCommand(keybindsShowCmd) - keybindsCmd.AddCommand(keybindsSetCmd) - keybindsCmd.AddCommand(keybindsRemoveCmd) - - keybinds.SetJSONProviderFactory(func(filePath string) (keybinds.Provider, error) { - return providers.NewJSONFileProvider(filePath) - }) - - initializeProviders() -} - -func initializeProviders() { - registry := keybinds.GetDefaultRegistry() - - hyprlandProvider := providers.NewHyprlandProvider("") - if err := registry.Register(hyprlandProvider); err != nil { - log.Warnf("Failed to register Hyprland provider: %v", err) - } - - mangowcProvider := providers.NewMangoWCProvider("") - if err := registry.Register(mangowcProvider); err != nil { - log.Warnf("Failed to register MangoWC provider: %v", err) - } - - configDir, _ := os.UserConfigDir() - - if configDir != "" { - scrollProvider := providers.NewSwayProvider(filepath.Join(configDir, "scroll")) - if err := registry.Register(scrollProvider); err != nil { - log.Warnf("Failed to register Scroll provider: %v", err) - } - } - - miracleProvider := providers.NewMiracleProvider("") - if err := registry.Register(miracleProvider); err != nil { - log.Warnf("Failed to register Miracle WM provider: %v", err) - } - - if configDir != "" { - swayProvider := providers.NewSwayProvider(filepath.Join(configDir, "sway")) - if err := registry.Register(swayProvider); err != nil { - log.Warnf("Failed to register Sway provider: %v", err) - } - } - - niriProvider := providers.NewNiriProvider("") - if err := registry.Register(niriProvider); err != nil { - log.Warnf("Failed to register Niri provider: %v", err) - } - - config := keybinds.DefaultDiscoveryConfig() - if err := keybinds.AutoDiscoverProviders(registry, config); err != nil { - log.Warnf("Failed to auto-discover providers: %v", err) - } -} - -func runKeybindsList(cmd *cobra.Command, _ []string) { - providerList := keybinds.GetDefaultRegistry().List() - asJSON, _ := cmd.Flags().GetBool("json") - - if asJSON { - output, _ := json.Marshal(providerList) - fmt.Fprintln(os.Stdout, string(output)) - return - } - - if len(providerList) == 0 { - fmt.Fprintln(os.Stdout, "No providers available") - return - } - - fmt.Fprintln(os.Stdout, "Available providers:") - for _, name := range providerList { - fmt.Fprintf(os.Stdout, " - %s\n", name) - } -} - -func makeProviderWithPath(name, path string) keybinds.Provider { - switch name { - case "hyprland": - return providers.NewHyprlandProvider(path) - case "mangowc": - return providers.NewMangoWCProvider(path) - case "sway": - return providers.NewSwayProvider(path) - case "scroll": - return providers.NewSwayProvider(path) - case "miracle": - return providers.NewMiracleProvider(path) - case "niri": - return providers.NewNiriProvider(path) - default: - return nil - } -} - -func printCheatSheet(provider keybinds.Provider) { - sheet, err := provider.GetCheatSheet() - if err != nil { - log.Fatalf("Error getting cheatsheet: %v", err) - } - output, err := json.MarshalIndent(sheet, "", " ") - if err != nil { - log.Fatalf("Error generating JSON: %v", err) - } - fmt.Fprintln(os.Stdout, string(output)) -} - -func runKeybindsShow(cmd *cobra.Command, args []string) { - providerName := args[0] - customPath, _ := cmd.Flags().GetString("path") - - if customPath != "" { - provider := makeProviderWithPath(providerName, customPath) - if provider == nil { - log.Fatalf("Provider %s does not support custom path", providerName) - } - printCheatSheet(provider) - return - } - - provider, err := keybinds.GetDefaultRegistry().Get(providerName) - if err != nil { - log.Fatalf("Error: %v", err) - } - printCheatSheet(provider) -} - -func getWritableProvider(name string) keybinds.WritableProvider { - provider, err := keybinds.GetDefaultRegistry().Get(name) - if err != nil { - log.Fatalf("Error: %v", err) - } - writable, ok := provider.(keybinds.WritableProvider) - if !ok { - log.Fatalf("Provider %s does not support writing keybinds", name) - } - return writable -} - -func runKeybindsSet(cmd *cobra.Command, args []string) { - providerName, key, action := args[0], args[1], args[2] - writable := getWritableProvider(providerName) - - if replaceKey, _ := cmd.Flags().GetString("replace-key"); replaceKey != "" && replaceKey != key { - _ = writable.RemoveBind(replaceKey) - } - - options := make(map[string]any) - if v, _ := cmd.Flags().GetBool("allow-when-locked"); v { - options["allow-when-locked"] = true - } - if v, _ := cmd.Flags().GetInt("cooldown-ms"); v > 0 { - options["cooldown-ms"] = v - } - if v, _ := cmd.Flags().GetBool("no-repeat"); v { - options["repeat"] = false - } - if v, _ := cmd.Flags().GetBool("no-inhibiting"); v { - options["allow-inhibiting"] = false - } - if v, _ := cmd.Flags().GetString("flags"); v != "" { - options["flags"] = v - } - - desc, _ := cmd.Flags().GetString("desc") - if err := writable.SetBind(key, action, desc, options); err != nil { - log.Fatalf("Error setting keybind: %v", err) - } - - output, _ := json.MarshalIndent(map[string]any{ - "success": true, - "key": key, - "action": action, - "path": writable.GetOverridePath(), - }, "", " ") - fmt.Fprintln(os.Stdout, string(output)) -} - -func runKeybindsRemove(_ *cobra.Command, args []string) { - providerName, key := args[0], args[1] - writable := getWritableProvider(providerName) - - if err := writable.RemoveBind(key); err != nil { - log.Fatalf("Error removing keybind: %v", err) - } - - output, _ := json.MarshalIndent(map[string]any{ - "success": true, - "key": key, - "removed": true, - }, "", " ") - fmt.Fprintln(os.Stdout, string(output)) -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_matugen.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_matugen.go deleted file mode 100644 index 566f360..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_matugen.go +++ /dev/null @@ -1,202 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "os" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/internal/matugen" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/spf13/cobra" -) - -var matugenCmd = &cobra.Command{ - Use: "matugen", - Short: "Generate Material Design themes", - Long: "Generate Material Design themes using matugen with dank16 color integration", -} - -var matugenGenerateCmd = &cobra.Command{ - Use: "generate", - Short: "Generate theme synchronously", - Run: runMatugenGenerate, -} - -var matugenQueueCmd = &cobra.Command{ - Use: "queue", - Short: "Queue theme generation (uses socket if available)", - Run: runMatugenQueue, -} - -var matugenCheckCmd = &cobra.Command{ - Use: "check", - Short: "Check which template apps are detected", - Run: runMatugenCheck, -} - -func init() { - matugenCmd.AddCommand(matugenGenerateCmd) - matugenCmd.AddCommand(matugenQueueCmd) - matugenCmd.AddCommand(matugenCheckCmd) - - for _, cmd := range []*cobra.Command{matugenGenerateCmd, matugenQueueCmd} { - cmd.Flags().String("state-dir", "", "State directory for cache files") - cmd.Flags().String("shell-dir", "", "DMS shell installation directory") - cmd.Flags().String("config-dir", "", "User config directory") - cmd.Flags().String("kind", "image", "Source type: image or hex") - cmd.Flags().String("value", "", "Wallpaper path or hex color") - cmd.Flags().String("mode", "dark", "Color mode: dark or light") - cmd.Flags().String("icon-theme", "System Default", "Icon theme name") - cmd.Flags().String("matugen-type", "scheme-tonal-spot", "Matugen scheme type") - cmd.Flags().Bool("run-user-templates", true, "Run user matugen templates") - cmd.Flags().String("stock-colors", "", "Stock theme colors JSON") - cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal") - cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant") - cmd.Flags().String("skip-templates", "", "Comma-separated list of templates to skip") - cmd.Flags().Float64("contrast", 0, "Contrast value from -1 to 1 (0 = standard)") - } - - matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion") - matugenQueueCmd.Flags().Duration("timeout", 90*time.Second, "Timeout for waiting") -} - -func buildMatugenOptions(cmd *cobra.Command) matugen.Options { - stateDir, _ := cmd.Flags().GetString("state-dir") - shellDir, _ := cmd.Flags().GetString("shell-dir") - configDir, _ := cmd.Flags().GetString("config-dir") - kind, _ := cmd.Flags().GetString("kind") - value, _ := cmd.Flags().GetString("value") - mode, _ := cmd.Flags().GetString("mode") - iconTheme, _ := cmd.Flags().GetString("icon-theme") - matugenType, _ := cmd.Flags().GetString("matugen-type") - runUserTemplates, _ := cmd.Flags().GetBool("run-user-templates") - stockColors, _ := cmd.Flags().GetString("stock-colors") - syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal") - terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark") - skipTemplates, _ := cmd.Flags().GetString("skip-templates") - contrast, _ := cmd.Flags().GetFloat64("contrast") - - return matugen.Options{ - StateDir: stateDir, - ShellDir: shellDir, - ConfigDir: configDir, - Kind: kind, - Value: value, - Mode: matugen.ColorMode(mode), - IconTheme: iconTheme, - MatugenType: matugenType, - Contrast: contrast, - RunUserTemplates: runUserTemplates, - StockColors: stockColors, - SyncModeWithPortal: syncModeWithPortal, - TerminalsAlwaysDark: terminalsAlwaysDark, - SkipTemplates: skipTemplates, - } -} - -func runMatugenGenerate(cmd *cobra.Command, args []string) { - opts := buildMatugenOptions(cmd) - err := matugen.Run(opts) - switch { - case errors.Is(err, matugen.ErrNoChanges): - os.Exit(2) - case err != nil: - log.Fatalf("Theme generation failed: %v", err) - } -} - -func runMatugenQueue(cmd *cobra.Command, args []string) { - opts := buildMatugenOptions(cmd) - wait, _ := cmd.Flags().GetBool("wait") - timeout, _ := cmd.Flags().GetDuration("timeout") - - request := models.Request{ - ID: 1, - Method: "matugen.queue", - Params: map[string]any{ - "stateDir": opts.StateDir, - "shellDir": opts.ShellDir, - "configDir": opts.ConfigDir, - "kind": opts.Kind, - "value": opts.Value, - "mode": opts.Mode, - "iconTheme": opts.IconTheme, - "matugenType": opts.MatugenType, - "runUserTemplates": opts.RunUserTemplates, - "stockColors": opts.StockColors, - "syncModeWithPortal": opts.SyncModeWithPortal, - "terminalsAlwaysDark": opts.TerminalsAlwaysDark, - "skipTemplates": opts.SkipTemplates, - "contrast": opts.Contrast, - "wait": wait, - }, - } - - if !wait { - if err := sendServerRequestFireAndForget(request); err != nil { - log.Info("Server unavailable, running synchronously") - err := matugen.Run(opts) - switch { - case errors.Is(err, matugen.ErrNoChanges): - os.Exit(2) - case err != nil: - log.Fatalf("Theme generation failed: %v", err) - } - return - } - fmt.Println("Theme generation queued") - return - } - - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - - resultCh := make(chan error, 1) - go func() { - resp, ok := tryServerRequest(request) - if !ok { - log.Info("Server unavailable, running synchronously") - err := matugen.Run(opts) - switch { - case errors.Is(err, matugen.ErrNoChanges): - resultCh <- matugen.ErrNoChanges - case err != nil: - resultCh <- err - default: - resultCh <- nil - } - return - } - if resp.Error != "" { - resultCh <- fmt.Errorf("server error: %s", resp.Error) - return - } - resultCh <- nil - }() - - select { - case err := <-resultCh: - switch { - case errors.Is(err, matugen.ErrNoChanges): - os.Exit(2) - case err != nil: - log.Fatalf("Theme generation failed: %v", err) - } - fmt.Println("Theme generation completed") - case <-ctx.Done(): - log.Fatalf("Timeout waiting for theme generation") - } -} - -func runMatugenCheck(cmd *cobra.Command, args []string) { - checks := matugen.CheckTemplates(nil) - data, err := json.Marshal(checks) - if err != nil { - log.Fatalf("Failed to marshal check results: %v", err) - } - fmt.Println(string(data)) -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_notify.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_notify.go deleted file mode 100644 index e04b60f..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_notify.go +++ /dev/null @@ -1,68 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/notify" - "github.com/spf13/cobra" -) - -var ( - notifyAppName string - notifyIcon string - notifyFile string - notifyTimeout int -) - -var notifyCmd = &cobra.Command{ - Use: "notify <summary> [body]", - Short: "Send a desktop notification", - Long: `Send a desktop notification with optional actions. - -If --file is provided, the notification will have "Open" and "Open Folder" actions. - -Examples: - dms notify "Hello" "World" - dms notify "File received" "photo.jpg" --file ~/Downloads/photo.jpg --icon smartphone - dms notify "Download complete" --file ~/Downloads/file.zip --app "My App"`, - Args: cobra.MinimumNArgs(1), - Run: runNotify, -} - -var genericNotifyActionCmd = &cobra.Command{ - Use: "notify-action-generic", - Hidden: true, - Run: func(cmd *cobra.Command, args []string) { - notify.RunActionListener(args) - }, -} - -func init() { - notifyCmd.Flags().StringVar(¬ifyAppName, "app", "DMS", "Application name") - notifyCmd.Flags().StringVar(¬ifyIcon, "icon", "", "Icon name or path") - notifyCmd.Flags().StringVar(¬ifyFile, "file", "", "File path (enables Open/Open Folder actions)") - notifyCmd.Flags().IntVar(¬ifyTimeout, "timeout", 5000, "Timeout in milliseconds") -} - -func runNotify(cmd *cobra.Command, args []string) { - summary := args[0] - body := "" - if len(args) > 1 { - body = args[1] - } - - n := notify.Notification{ - AppName: notifyAppName, - Icon: notifyIcon, - Summary: summary, - Body: body, - FilePath: notifyFile, - Timeout: int32(notifyTimeout), - } - - if err := notify.Send(n); err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) - } -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_open.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_open.go deleted file mode 100644 index 8cc16cf..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_open.go +++ /dev/null @@ -1,205 +0,0 @@ -package main - -import ( - "fmt" - "mime" - "net/url" - "os" - "path/filepath" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" - "github.com/spf13/cobra" -) - -var ( - openMimeType string - openCategories []string - openRequestType string -) - -var openCmd = &cobra.Command{ - Use: "open [target]", - Short: "Open a file, URL, or resource with an application picker", - Long: `Open a target (URL, file, or other resource) using the DMS application picker. -By default, this opens URLs with the browser picker. You can customize the behavior -with flags to handle different MIME types or application categories. - -Examples: - dms open https://example.com # Open URL with browser picker - dms open file.pdf --mime application/pdf # Open PDF with compatible apps - dms open document.odt --category Office # Open with office applications - dms open --mime image/png image.png # Open image with image viewers`, - Args: cobra.ExactArgs(1), - Run: func(cmd *cobra.Command, args []string) { - runOpen(args[0]) - }, -} - -func init() { - rootCmd.AddCommand(openCmd) - openCmd.Flags().StringVar(&openMimeType, "mime", "", "MIME type for filtering applications") - openCmd.Flags().StringSliceVar(&openCategories, "category", []string{}, "Application categories to filter (e.g., WebBrowser, Office, Graphics)") - openCmd.Flags().StringVar(&openRequestType, "type", "url", "Request type (url, file, or custom)") - _ = openCmd.RegisterFlagCompletionFunc("type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return []string{"url", "file", "custom"}, cobra.ShellCompDirectiveNoFileComp - }) -} - -// mimeTypeToCategories maps MIME types to desktop file categories -func mimeTypeToCategories(mimeType string) []string { - // Split MIME type to get the main type - parts := strings.Split(mimeType, "/") - if len(parts) < 1 { - return nil - } - - mainType := parts[0] - - switch mainType { - case "image": - return []string{"Graphics", "Viewer"} - case "video": - return []string{"Video", "AudioVideo"} - case "audio": - return []string{"Audio", "AudioVideo"} - case "text": - if strings.Contains(mimeType, "html") { - return []string{"WebBrowser"} - } - return []string{"TextEditor", "Office"} - case "application": - if strings.Contains(mimeType, "pdf") { - return []string{"Office", "Viewer"} - } - if strings.Contains(mimeType, "document") || strings.Contains(mimeType, "spreadsheet") || - strings.Contains(mimeType, "presentation") || strings.Contains(mimeType, "msword") || - strings.Contains(mimeType, "ms-excel") || strings.Contains(mimeType, "ms-powerpoint") || - strings.Contains(mimeType, "opendocument") { - return []string{"Office"} - } - if strings.Contains(mimeType, "zip") || strings.Contains(mimeType, "tar") || - strings.Contains(mimeType, "gzip") || strings.Contains(mimeType, "compress") { - return []string{"Archiving", "Utility"} - } - return []string{"Office", "Viewer"} - } - - return nil -} - -func runOpen(target string) { - // Parse file:// URIs to extract the actual file path - actualTarget := target - detectedMimeType := openMimeType - detectedCategories := openCategories - detectedRequestType := openRequestType - - log.Infof("Processing target: %s", target) - - if parsedURL, err := url.Parse(target); err == nil && parsedURL.Scheme == "file" { - // Extract file path from file:// URI and convert to absolute path - actualTarget = parsedURL.Path - if absPath, err := filepath.Abs(actualTarget); err == nil { - actualTarget = absPath - } - - if detectedRequestType == "url" || detectedRequestType == "" { - detectedRequestType = "file" - } - - log.Infof("Detected file:// URI, extracted absolute path: %s", actualTarget) - - // Auto-detect MIME type if not provided - if detectedMimeType == "" { - ext := filepath.Ext(actualTarget) - if ext != "" { - detectedMimeType = mime.TypeByExtension(ext) - log.Infof("Detected MIME type from extension %s: %s", ext, detectedMimeType) - } - } - - // Auto-detect categories based on MIME type if not provided - if len(detectedCategories) == 0 && detectedMimeType != "" { - detectedCategories = mimeTypeToCategories(detectedMimeType) - log.Infof("Detected categories from MIME type: %v", detectedCategories) - } - } else if strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") { - // Handle HTTP(S) URLs - if detectedRequestType == "" { - detectedRequestType = "url" - } - log.Infof("Detected HTTP(S) URL") - } else if strings.HasPrefix(target, "dms://") { - // Handle DMS internal URLs (theme/plugin install, etc.) - if detectedRequestType == "" { - detectedRequestType = "url" - } - log.Infof("Detected DMS internal URL") - } else if _, err := os.Stat(target); err == nil { - // Handle local file paths directly (not file:// URIs) - // Convert to absolute path - if absPath, err := filepath.Abs(target); err == nil { - actualTarget = absPath - } - - if detectedRequestType == "url" || detectedRequestType == "" { - detectedRequestType = "file" - } - - log.Infof("Detected local file path, converted to absolute: %s", actualTarget) - - // Auto-detect MIME type if not provided - if detectedMimeType == "" { - ext := filepath.Ext(actualTarget) - if ext != "" { - detectedMimeType = mime.TypeByExtension(ext) - log.Infof("Detected MIME type from extension %s: %s", ext, detectedMimeType) - } - } - - // Auto-detect categories based on MIME type if not provided - if len(detectedCategories) == 0 && detectedMimeType != "" { - detectedCategories = mimeTypeToCategories(detectedMimeType) - log.Infof("Detected categories from MIME type: %v", detectedCategories) - } - } - - params := map[string]any{ - "target": actualTarget, - } - - if detectedMimeType != "" { - params["mimeType"] = detectedMimeType - } - - if len(detectedCategories) > 0 { - params["categories"] = detectedCategories - } - - if detectedRequestType != "" { - params["requestType"] = detectedRequestType - } - - method := "apppicker.open" - if detectedMimeType == "" && len(detectedCategories) == 0 && (strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") || strings.HasPrefix(target, "dms://")) { - method = "browser.open" - params["url"] = target - } - - req := models.Request{ - ID: 1, - Method: method, - Params: params, - } - - log.Infof("Sending request - Method: %s, Params: %+v", method, params) - - if err := sendServerRequestFireAndForget(req); err != nil { - fmt.Println("DMS is not running. Please start DMS first.") - os.Exit(1) - } - - log.Infof("Request sent successfully") -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_randr.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_randr.go deleted file mode 100644 index fa0c990..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_randr.go +++ /dev/null @@ -1,58 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/spf13/cobra" -) - -var randrCmd = &cobra.Command{ - Use: "randr", - Short: "Query output display information", - Long: "Query Wayland compositor for output names, scales, resolutions and refresh rates via zwlr-output-management", - Run: runRandr, -} - -func init() { - randrCmd.Flags().Bool("json", false, "Output in JSON format") -} - -type randrJSON struct { - Outputs []randrOutput `json:"outputs"` -} - -func runRandr(cmd *cobra.Command, args []string) { - outputs, err := queryRandr() - if err != nil { - log.Fatalf("%v", err) - } - - jsonFlag, _ := cmd.Flags().GetBool("json") - - if jsonFlag { - data, err := json.Marshal(randrJSON{Outputs: outputs}) - if err != nil { - log.Fatalf("failed to marshal JSON: %v", err) - } - fmt.Println(string(data)) - return - } - - for i, out := range outputs { - if i > 0 { - fmt.Println() - } - status := "enabled" - if !out.Enabled { - status = "disabled" - } - fmt.Printf("%s (%s)\n", out.Name, status) - fmt.Printf(" Scale: %.4g\n", out.Scale) - fmt.Printf(" Resolution: %dx%d\n", out.Width, out.Height) - if out.Refresh > 0 { - fmt.Printf(" Refresh: %.2f Hz\n", float64(out.Refresh)/1000.0) - } - } -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_root.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_root.go deleted file mode 100644 index b263ae6..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_root.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/config" - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/spf13/cobra" -) - -var customConfigPath string -var configPath string - -var rootCmd = &cobra.Command{ - Use: "dms", - Short: "dms CLI", - Long: "dms is the DankMaterialShell management CLI and backend server.", -} - -func init() { - rootCmd.PersistentFlags().StringVarP(&customConfigPath, "config", "c", "", "Specify a custom path to the DMS config directory") -} - -func findConfig(cmd *cobra.Command, args []string) error { - if customConfigPath != "" { - log.Debug("Custom config path provided via -c flag: %s", customConfigPath) - shellPath := filepath.Join(customConfigPath, "shell.qml") - - info, statErr := os.Stat(shellPath) - - if statErr == nil && !info.IsDir() { - configPath = customConfigPath - log.Debug("Using config from: %s", configPath) - return nil - } - - if statErr != nil { - return fmt.Errorf("custom config path error: %w", statErr) - } - - return fmt.Errorf("path is a directory, not a file: %s", shellPath) - } - - configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path") - if data, readErr := os.ReadFile(configStateFile); readErr == nil { - if len(getAllDMSPIDs()) == 0 { - os.Remove(configStateFile) - } else { - statePath := strings.TrimSpace(string(data)) - shellPath := filepath.Join(statePath, "shell.qml") - - if info, statErr := os.Stat(shellPath); statErr == nil && !info.IsDir() { - log.Debug("Using config from active session state file: %s", statePath) - configPath = statePath - log.Debug("Using config from: %s", configPath) - return nil - } - os.Remove(configStateFile) - } - } - - log.Debug("No custom path or active session, searching default XDG locations...") - var err error - configPath, err = config.LocateDMSConfig() - if err != nil { - return err - } - - log.Debug("Using config from: %s", configPath) - return nil -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_screenshot.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_screenshot.go deleted file mode 100644 index 2875019..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_screenshot.go +++ /dev/null @@ -1,422 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard" - "github.com/AvengeMedia/DankMaterialShell/core/internal/screenshot" - "github.com/spf13/cobra" -) - -var ( - ssOutputName string - ssCursor string - ssFormat string - ssQuality int - ssOutputDir string - ssFilename string - ssNoClipboard bool - ssNoFile bool - ssNoNotify bool - ssNoConfirm bool - ssReset bool - ssStdout bool -) - -var screenshotCmd = &cobra.Command{ - Use: "screenshot", - Short: "Capture screenshots", - Long: `Capture screenshots from Wayland displays. - -Modes: - region - Select a region interactively (default) - full - Capture the focused output - all - Capture all outputs combined - output - Capture a specific output by name - window - Capture the focused window (Hyprland/DWL) - last - Capture the last selected region - -Output format (--format): - png - PNG format (default) - jpg/jpeg - JPEG format - ppm - PPM format - -Examples: - dms screenshot # Region select, save file + clipboard - dms screenshot full # Full screen of focused output - dms screenshot all # All screens combined - dms screenshot output -o DP-1 # Specific output - dms screenshot window # Focused window (Hyprland) - dms screenshot last # Last region (pre-selected) - dms screenshot --reset # Reset last region pre-selection - dms screenshot --no-clipboard # Save file only - dms screenshot --no-file # Clipboard only - dms screenshot --no-confirm # Region capture on mouse release - dms screenshot --cursor=on # Include cursor - dms screenshot -f jpg -q 85 # JPEG with quality 85`, -} - -var ssRegionCmd = &cobra.Command{ - Use: "region", - Short: "Select a region interactively", - Run: runScreenshotRegion, -} - -var ssFullCmd = &cobra.Command{ - Use: "full", - Short: "Capture the focused output", - Run: runScreenshotFull, -} - -var ssAllCmd = &cobra.Command{ - Use: "all", - Short: "Capture all outputs combined", - Run: runScreenshotAll, -} - -var ssOutputCmd = &cobra.Command{ - Use: "output", - Short: "Capture a specific output", - Run: runScreenshotOutput, -} - -var ssLastCmd = &cobra.Command{ - Use: "last", - Short: "Capture the last selected region", - Long: `Capture the previously selected region without interactive selection. -If no previous region exists, falls back to interactive selection.`, - Run: runScreenshotLast, -} - -var ssWindowCmd = &cobra.Command{ - Use: "window", - Short: "Capture the focused window", - Long: `Capture the currently focused window. Supported on Hyprland and DWL.`, - Run: runScreenshotWindow, -} - -var ssListCmd = &cobra.Command{ - Use: "list", - Short: "List available outputs", - Run: runScreenshotList, -} - -var notifyActionCmd = &cobra.Command{ - Use: "notify-action", - Hidden: true, - Run: func(cmd *cobra.Command, args []string) { - screenshot.RunNotifyActionListener(args) - }, -} - -func init() { - screenshotCmd.PersistentFlags().StringVarP(&ssOutputName, "output", "o", "", "Output name for 'output' mode") - screenshotCmd.PersistentFlags().StringVar(&ssCursor, "cursor", "off", "Include cursor in screenshot (on/off)") - screenshotCmd.PersistentFlags().StringVarP(&ssFormat, "format", "f", "png", "Output format (png, jpg, ppm)") - screenshotCmd.PersistentFlags().IntVarP(&ssQuality, "quality", "q", 90, "JPEG quality (1-100)") - screenshotCmd.PersistentFlags().StringVarP(&ssOutputDir, "dir", "d", "", "Output directory") - screenshotCmd.PersistentFlags().StringVar(&ssFilename, "filename", "", "Output filename (auto-generated if empty)") - screenshotCmd.PersistentFlags().BoolVar(&ssNoClipboard, "no-clipboard", false, "Don't copy to clipboard") - screenshotCmd.PersistentFlags().BoolVar(&ssNoFile, "no-file", false, "Don't save to file") - screenshotCmd.PersistentFlags().BoolVar(&ssNoNotify, "no-notify", false, "Don't show notification") - screenshotCmd.PersistentFlags().BoolVar(&ssNoConfirm, "no-confirm", false, "Region mode: capture on mouse release without Enter/Space confirmation") - screenshotCmd.PersistentFlags().BoolVar(&ssReset, "reset", false, "Reset saved last-region preselection before capturing") - screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)") - - screenshotCmd.AddCommand(ssRegionCmd) - screenshotCmd.AddCommand(ssFullCmd) - screenshotCmd.AddCommand(ssAllCmd) - screenshotCmd.AddCommand(ssOutputCmd) - screenshotCmd.AddCommand(ssLastCmd) - screenshotCmd.AddCommand(ssWindowCmd) - screenshotCmd.AddCommand(ssListCmd) - - screenshotCmd.Run = runScreenshotRegion -} - -func getScreenshotConfig(mode screenshot.Mode) screenshot.Config { - config := screenshot.DefaultConfig() - config.Mode = mode - config.OutputName = ssOutputName - if strings.EqualFold(ssCursor, "on") { - config.Cursor = screenshot.CursorOn - } - config.Clipboard = !ssNoClipboard - config.SaveFile = !ssNoFile - config.Notify = !ssNoNotify - config.NoConfirm = ssNoConfirm - config.Reset = ssReset - config.Stdout = ssStdout - - if ssOutputDir != "" { - config.OutputDir = ssOutputDir - } - if ssFilename != "" { - config.Filename = ssFilename - } - - switch strings.ToLower(ssFormat) { - case "jpg", "jpeg": - config.Format = screenshot.FormatJPEG - case "ppm": - config.Format = screenshot.FormatPPM - default: - config.Format = screenshot.FormatPNG - } - - if ssQuality < 1 { - ssQuality = 1 - } - if ssQuality > 100 { - ssQuality = 100 - } - config.Quality = ssQuality - - return config -} - -func runScreenshot(config screenshot.Config) { - sc := screenshot.New(config) - result, err := sc.Run() - if err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) - } - - if result == nil { - os.Exit(0) - } - - defer result.Buffer.Close() - - if result.YInverted { - result.Buffer.FlipVertical() - } - - if config.Stdout { - if err := writeImageToStdout(result.Buffer, config.Format, config.Quality, result.Format); err != nil { - fmt.Fprintf(os.Stderr, "Error writing to stdout: %v\n", err) - os.Exit(1) - } - return - } - - var filePath string - - if config.SaveFile { - outputDir := config.OutputDir - if outputDir == "" { - outputDir = screenshot.GetOutputDir() - } - - filename := config.Filename - if filename == "" { - filename = screenshot.GenerateFilename(config.Format) - } - - filePath = filepath.Join(outputDir, filename) - if err := screenshot.WriteToFileWithFormat(result.Buffer, filePath, config.Format, config.Quality, result.Format); err != nil { - fmt.Fprintf(os.Stderr, "Error writing file: %v\n", err) - os.Exit(1) - } - fmt.Println(filePath) - } - - if config.Clipboard { - if err := copyImageToClipboard(result.Buffer, config.Format, config.Quality, result.Format); err != nil { - fmt.Fprintf(os.Stderr, "Error copying to clipboard: %v\n", err) - os.Exit(1) - } - if !config.SaveFile { - fmt.Println("Copied to clipboard") - } - } - - if config.Notify { - thumbData, thumbW, thumbH := bufferToRGBThumbnail(result.Buffer, 256, result.Format) - screenshot.SendNotification(screenshot.NotifyResult{ - FilePath: filePath, - Clipboard: config.Clipboard, - ImageData: thumbData, - Width: thumbW, - Height: thumbH, - }) - } -} - -func copyImageToClipboard(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error { - var mimeType string - var data bytes.Buffer - - img := screenshot.BufferToImageWithFormat(buf, pixelFormat) - - switch format { - case screenshot.FormatJPEG: - mimeType = "image/jpeg" - if err := screenshot.EncodeJPEG(&data, img, quality); err != nil { - return err - } - default: - mimeType = "image/png" - if err := screenshot.EncodePNG(&data, img); err != nil { - return err - } - } - - return clipboard.Copy(data.Bytes(), mimeType) -} - -func writeImageToStdout(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error { - img := screenshot.BufferToImageWithFormat(buf, pixelFormat) - - switch format { - case screenshot.FormatJPEG: - return screenshot.EncodeJPEG(os.Stdout, img, quality) - default: - return screenshot.EncodePNG(os.Stdout, img) - } -} - -func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat uint32) ([]byte, int, int) { - srcW, srcH := buf.Width, buf.Height - scale := 1.0 - if srcW > maxSize || srcH > maxSize { - if srcW > srcH { - scale = float64(maxSize) / float64(srcW) - } else { - scale = float64(maxSize) / float64(srcH) - } - } - - dstW := int(float64(srcW) * scale) - dstH := int(float64(srcH) * scale) - if dstW < 1 { - dstW = 1 - } - if dstH < 1 { - dstH = 1 - } - - data := buf.Data() - rgb := make([]byte, dstW*dstH*3) - - var swapRB bool - switch pixelFormat { - case uint32(screenshot.FormatABGR8888), uint32(screenshot.FormatXBGR8888): - swapRB = false - default: - swapRB = true - } - - for y := 0; y < dstH; y++ { - srcY := int(float64(y) / scale) - if srcY >= srcH { - srcY = srcH - 1 - } - for x := 0; x < dstW; x++ { - srcX := int(float64(x) / scale) - if srcX >= srcW { - srcX = srcW - 1 - } - si := srcY*buf.Stride + srcX*4 - di := (y*dstW + x) * 3 - if si+3 >= len(data) { - continue - } - if swapRB { - rgb[di+0] = data[si+2] - rgb[di+1] = data[si+1] - rgb[di+2] = data[si+0] - } else { - rgb[di+0] = data[si+0] - rgb[di+1] = data[si+1] - rgb[di+2] = data[si+2] - } - } - } - return rgb, dstW, dstH -} - -func runScreenshotRegion(cmd *cobra.Command, args []string) { - config := getScreenshotConfig(screenshot.ModeRegion) - runScreenshot(config) -} - -func runScreenshotFull(cmd *cobra.Command, args []string) { - config := getScreenshotConfig(screenshot.ModeFullScreen) - runScreenshot(config) -} - -func runScreenshotAll(cmd *cobra.Command, args []string) { - config := getScreenshotConfig(screenshot.ModeAllScreens) - runScreenshot(config) -} - -func runScreenshotOutput(cmd *cobra.Command, args []string) { - if ssOutputName == "" && len(args) > 0 { - ssOutputName = args[0] - } - if ssOutputName == "" { - fmt.Fprintln(os.Stderr, "Error: output name required (use -o or provide as argument)") - os.Exit(1) - } - config := getScreenshotConfig(screenshot.ModeOutput) - runScreenshot(config) -} - -func runScreenshotLast(cmd *cobra.Command, args []string) { - config := getScreenshotConfig(screenshot.ModeLastRegion) - runScreenshot(config) -} - -func runScreenshotWindow(cmd *cobra.Command, args []string) { - config := getScreenshotConfig(screenshot.ModeWindow) - runScreenshot(config) -} - -func runScreenshotList(cmd *cobra.Command, args []string) { - outputs, err := screenshot.ListOutputs() - if err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) - } - - for _, o := range outputs { - scaleStr := fmt.Sprintf("%.2f", o.FractionalScale) - if o.FractionalScale == float64(int(o.FractionalScale)) { - scaleStr = fmt.Sprintf("%d", int(o.FractionalScale)) - } - - transformStr := transformName(o.Transform) - - fmt.Printf("%s: %dx%d+%d+%d scale=%s transform=%s\n", - o.Name, o.Width, o.Height, o.X, o.Y, scaleStr, transformStr) - } -} - -func transformName(t int32) string { - switch t { - case 0: - return "normal" - case 1: - return "90" - case 2: - return "180" - case 3: - return "270" - case 4: - return "flipped" - case 5: - return "flipped-90" - case 6: - return "flipped-180" - case 7: - return "flipped-270" - default: - return fmt.Sprintf("%d", t) - } -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_setup.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_setup.go deleted file mode 100644 index 0f4a58d..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_setup.go +++ /dev/null @@ -1,436 +0,0 @@ -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", - PersistentPreRunE: requireMutableSystemCommand, - 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 -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_windowrules.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_windowrules.go deleted file mode 100644 index 0883811..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_windowrules.go +++ /dev/null @@ -1,338 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "os" - "strings" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" - "github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules" - "github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules/providers" - "github.com/spf13/cobra" -) - -var windowrulesCmd = &cobra.Command{ - Use: "windowrules", - Short: "Manage window rules", -} - -var windowrulesListCmd = &cobra.Command{ - Use: "list [compositor]", - Short: "List all window rules", - Long: "List all window rules from compositor config file. Returns JSON with rules and DMS status.", - Args: cobra.MaximumNArgs(1), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) == 0 { - return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp - } - return nil, cobra.ShellCompDirectiveNoFileComp - }, - Run: runWindowrulesList, -} - -var windowrulesAddCmd = &cobra.Command{ - Use: "add <compositor> '<json>'", - Short: "Add a window rule to DMS file", - Long: "Add a new window rule to the DMS-managed rules file.", - Args: cobra.ExactArgs(2), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) == 0 { - // ! disabled hyprland return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp - return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp - } - return nil, cobra.ShellCompDirectiveNoFileComp - }, - Run: runWindowrulesAdd, -} - -var windowrulesUpdateCmd = &cobra.Command{ - Use: "update <compositor> <id> '<json>'", - Short: "Update a window rule in DMS file", - Long: "Update an existing window rule in the DMS-managed rules file.", - Args: cobra.ExactArgs(3), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) == 0 { - return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp - } - return nil, cobra.ShellCompDirectiveNoFileComp - }, - Run: runWindowrulesUpdate, -} - -var windowrulesRemoveCmd = &cobra.Command{ - Use: "remove <compositor> <id>", - Short: "Remove a window rule from DMS file", - Long: "Remove a window rule from the DMS-managed rules file.", - Args: cobra.ExactArgs(2), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) == 0 { - return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp - } - return nil, cobra.ShellCompDirectiveNoFileComp - }, - Run: runWindowrulesRemove, -} - -var windowrulesReorderCmd = &cobra.Command{ - Use: "reorder <compositor> '<json-array-of-ids>'", - Short: "Reorder window rules in DMS file", - Long: "Reorder window rules by providing a JSON array of rule IDs in the desired order.", - Args: cobra.ExactArgs(2), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) == 0 { - return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp - } - return nil, cobra.ShellCompDirectiveNoFileComp - }, - Run: runWindowrulesReorder, -} - -func init() { - configCmd.AddCommand(windowrulesCmd) - windowrulesCmd.AddCommand(windowrulesListCmd) - windowrulesCmd.AddCommand(windowrulesAddCmd) - windowrulesCmd.AddCommand(windowrulesUpdateCmd) - windowrulesCmd.AddCommand(windowrulesRemoveCmd) - windowrulesCmd.AddCommand(windowrulesReorderCmd) -} - -type WindowRulesListResult struct { - Rules []windowrules.WindowRule `json:"rules"` - DMSStatus *windowrules.DMSRulesStatus `json:"dmsStatus,omitempty"` -} - -type WindowRuleWriteResult struct { - Success bool `json:"success"` - ID string `json:"id,omitempty"` - Path string `json:"path,omitempty"` - Error string `json:"error,omitempty"` -} - -func getCompositor(args []string) string { - if len(args) > 0 { - return strings.ToLower(args[0]) - } - if os.Getenv("NIRI_SOCKET") != "" { - return "niri" - } - // if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "" { - // return "hyprland" - // } - return "" -} - -func writeRuleError(errMsg string) { - result := WindowRuleWriteResult{Success: false, Error: errMsg} - output, _ := json.Marshal(result) - fmt.Fprintln(os.Stdout, string(output)) - os.Exit(1) -} - -func writeRuleSuccess(id, path string) { - result := WindowRuleWriteResult{Success: true, ID: id, Path: path} - output, _ := json.Marshal(result) - fmt.Fprintln(os.Stdout, string(output)) -} - -func runWindowrulesList(cmd *cobra.Command, args []string) { - compositor := getCompositor(args) - if compositor == "" { - log.Fatalf("Could not detect compositor. Please specify: hyprland or niri") - } - - var result WindowRulesListResult - - switch compositor { - case "niri": - configDir, err := utils.ExpandPath("$HOME/.config/niri") - if err != nil { - log.Fatalf("Failed to expand niri config path: %v", err) - } - - parseResult, err := providers.ParseNiriWindowRules(configDir) - if err != nil { - log.Fatalf("Failed to parse niri window rules: %v", err) - } - - allRules := providers.ConvertNiriRulesToWindowRules(parseResult.Rules) - - provider := providers.NewNiriWritableProvider(configDir) - dmsRulesPath := provider.GetOverridePath() - dmsRules, _ := provider.LoadDMSRules() - - dmsRuleMap := make(map[int]windowrules.WindowRule) - for i, dr := range dmsRules { - dmsRuleMap[i] = dr - } - - dmsIdx := 0 - for i, r := range allRules { - if r.Source == dmsRulesPath { - if dmr, ok := dmsRuleMap[dmsIdx]; ok { - allRules[i].ID = dmr.ID - allRules[i].Name = dmr.Name - } - dmsIdx++ - } - } - - result.Rules = allRules - result.DMSStatus = parseResult.DMSStatus - - case "hyprland": - log.Fatalf("Hyprland support is currently disabled.") // ! disabled hyprland - configDir, err := utils.ExpandPath("$HOME/.config/hypr") - if err != nil { - log.Fatalf("Failed to expand hyprland config path: %v", err) - } - - parseResult, err := providers.ParseHyprlandWindowRules(configDir) - if err != nil { - log.Fatalf("Failed to parse hyprland window rules: %v", err) - } - - allRules := providers.ConvertHyprlandRulesToWindowRules(parseResult.Rules) - - provider := providers.NewHyprlandWritableProvider(configDir) - dmsRulesPath := provider.GetOverridePath() - dmsRules, _ := provider.LoadDMSRules() - - dmsRuleMap := make(map[int]windowrules.WindowRule) - for i, dr := range dmsRules { - dmsRuleMap[i] = dr - } - - dmsIdx := 0 - for i, r := range allRules { - if r.Source == dmsRulesPath { - if dmr, ok := dmsRuleMap[dmsIdx]; ok { - allRules[i].ID = dmr.ID - allRules[i].Name = dmr.Name - } - dmsIdx++ - } - } - - result.Rules = allRules - result.DMSStatus = parseResult.DMSStatus - - default: - log.Fatalf("Unknown compositor: %s", compositor) - } - - output, _ := json.Marshal(result) - fmt.Fprintln(os.Stdout, string(output)) -} - -func runWindowrulesAdd(cmd *cobra.Command, args []string) { - compositor := strings.ToLower(args[0]) - ruleJSON := args[1] - - var rule windowrules.WindowRule - if err := json.Unmarshal([]byte(ruleJSON), &rule); err != nil { - writeRuleError(fmt.Sprintf("Invalid JSON: %v", err)) - } - - if rule.ID == "" { - rule.ID = generateRuleID() - } - rule.Enabled = true - - provider := getWindowRulesProvider(compositor) - if provider == nil { - writeRuleError(fmt.Sprintf("Unknown compositor: %s", compositor)) - } - - if err := provider.SetRule(rule); err != nil { - writeRuleError(err.Error()) - } - - writeRuleSuccess(rule.ID, provider.GetOverridePath()) -} - -func runWindowrulesUpdate(cmd *cobra.Command, args []string) { - compositor := strings.ToLower(args[0]) - ruleID := args[1] - ruleJSON := args[2] - - var rule windowrules.WindowRule - if err := json.Unmarshal([]byte(ruleJSON), &rule); err != nil { - writeRuleError(fmt.Sprintf("Invalid JSON: %v", err)) - } - - rule.ID = ruleID - - provider := getWindowRulesProvider(compositor) - if provider == nil { - writeRuleError(fmt.Sprintf("Unknown compositor: %s", compositor)) - } - - if err := provider.SetRule(rule); err != nil { - writeRuleError(err.Error()) - } - - writeRuleSuccess(rule.ID, provider.GetOverridePath()) -} - -func runWindowrulesRemove(cmd *cobra.Command, args []string) { - compositor := strings.ToLower(args[0]) - ruleID := args[1] - - provider := getWindowRulesProvider(compositor) - if provider == nil { - writeRuleError(fmt.Sprintf("Unknown compositor: %s", compositor)) - } - - if err := provider.RemoveRule(ruleID); err != nil { - writeRuleError(err.Error()) - } - - writeRuleSuccess(ruleID, provider.GetOverridePath()) -} - -func runWindowrulesReorder(cmd *cobra.Command, args []string) { - compositor := strings.ToLower(args[0]) - idsJSON := args[1] - - var ids []string - if err := json.Unmarshal([]byte(idsJSON), &ids); err != nil { - writeRuleError(fmt.Sprintf("Invalid JSON array: %v", err)) - } - - provider := getWindowRulesProvider(compositor) - if provider == nil { - writeRuleError(fmt.Sprintf("Unknown compositor: %s", compositor)) - } - - if err := provider.ReorderRules(ids); err != nil { - writeRuleError(err.Error()) - } - - writeRuleSuccess("", provider.GetOverridePath()) -} - -func getWindowRulesProvider(compositor string) windowrules.WritableProvider { - switch compositor { - case "niri": - configDir, err := utils.ExpandPath("$HOME/.config/niri") - if err != nil { - return nil - } - return providers.NewNiriWritableProvider(configDir) - case "hyprland": - configDir, err := utils.ExpandPath("$HOME/.config/hypr") - if err != nil { - return nil - } - return providers.NewHyprlandWritableProvider(configDir) - default: - return nil - } -} - -func generateRuleID() string { - return fmt.Sprintf("wr_%d", time.Now().UnixNano()) -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/dpms_client.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/dpms_client.go deleted file mode 100644 index 98f66d9..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/dpms_client.go +++ /dev/null @@ -1,339 +0,0 @@ -package main - -import ( - "fmt" - "sync" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_power" - wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" -) - -type cmd struct { - fn func() - done chan error -} - -type dpmsClient struct { - display *wlclient.Display - ctx *wlclient.Context - powerMgr *wlr_output_power.ZwlrOutputPowerManagerV1 - outputs map[string]*outputState - mu sync.Mutex - syncRound int - done bool - err error - cmdq chan cmd - stopChan chan struct{} - wg sync.WaitGroup -} - -type outputState struct { - wlOutput *wlclient.Output - powerCtrl *wlr_output_power.ZwlrOutputPowerV1 - name string - mode uint32 - failed bool - waitCh chan struct{} - wantMode *uint32 -} - -func (c *dpmsClient) post(fn func()) { - done := make(chan error, 1) - select { - case c.cmdq <- cmd{fn: fn, done: done}: - <-done - case <-c.stopChan: - } -} - -func (c *dpmsClient) waylandActor() { - defer c.wg.Done() - for { - select { - case <-c.stopChan: - return - case cmd := <-c.cmdq: - cmd.fn() - close(cmd.done) - } - } -} - -func newDPMSClient() (*dpmsClient, error) { - display, err := wlclient.Connect("") - if err != nil { - return nil, fmt.Errorf("failed to connect to Wayland: %w", err) - } - - c := &dpmsClient{ - display: display, - ctx: display.Context(), - outputs: make(map[string]*outputState), - cmdq: make(chan cmd, 128), - stopChan: make(chan struct{}), - } - - c.wg.Add(1) - go c.waylandActor() - - registry, err := display.GetRegistry() - if err != nil { - display.Context().Close() - return nil, fmt.Errorf("failed to get registry: %w", err) - } - - registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) { - switch e.Interface { - case wlr_output_power.ZwlrOutputPowerManagerV1InterfaceName: - powerMgr := wlr_output_power.NewZwlrOutputPowerManagerV1(c.ctx) - version := min(e.Version, 1) - if err := registry.Bind(e.Name, e.Interface, version, powerMgr); err == nil { - c.powerMgr = powerMgr - } - - case "wl_output": - output := wlclient.NewOutput(c.ctx) - version := min(e.Version, 4) - if err := registry.Bind(e.Name, e.Interface, version, output); err == nil { - outputID := fmt.Sprintf("output-%d", output.ID()) - state := &outputState{ - wlOutput: output, - name: outputID, - } - - c.mu.Lock() - c.outputs[outputID] = state - c.mu.Unlock() - - output.SetNameHandler(func(ev wlclient.OutputNameEvent) { - c.mu.Lock() - delete(c.outputs, state.name) - state.name = ev.Name - c.outputs[ev.Name] = state - c.mu.Unlock() - }) - } - } - }) - - syncCallback, err := display.Sync() - if err != nil { - c.Close() - return nil, fmt.Errorf("failed to sync display: %w", err) - } - syncCallback.SetDoneHandler(func(e wlclient.CallbackDoneEvent) { - c.handleSync() - }) - - for !c.done { - if err := c.ctx.Dispatch(); err != nil { - c.Close() - return nil, fmt.Errorf("dispatch error: %w", err) - } - } - - if c.err != nil { - c.Close() - return nil, c.err - } - - return c, nil -} - -func (c *dpmsClient) handleSync() { - c.syncRound++ - - switch c.syncRound { - case 1: - if c.powerMgr == nil { - c.err = fmt.Errorf("wlr-output-power-management protocol not supported by compositor") - c.done = true - return - } - - c.mu.Lock() - for _, state := range c.outputs { - powerCtrl, err := c.powerMgr.GetOutputPower(state.wlOutput) - if err != nil { - continue - } - state.powerCtrl = powerCtrl - - powerCtrl.SetModeHandler(func(e wlr_output_power.ZwlrOutputPowerV1ModeEvent) { - c.mu.Lock() - defer c.mu.Unlock() - if state.powerCtrl == nil { - return - } - state.mode = e.Mode - if state.wantMode != nil && e.Mode == *state.wantMode && state.waitCh != nil { - close(state.waitCh) - state.wantMode = nil - } - }) - - powerCtrl.SetFailedHandler(func(e wlr_output_power.ZwlrOutputPowerV1FailedEvent) { - c.mu.Lock() - defer c.mu.Unlock() - if state.powerCtrl == nil { - return - } - state.failed = true - if state.waitCh != nil { - close(state.waitCh) - state.wantMode = nil - } - }) - } - c.mu.Unlock() - - syncCallback, err := c.display.Sync() - if err != nil { - c.err = fmt.Errorf("failed to sync display: %w", err) - c.done = true - return - } - syncCallback.SetDoneHandler(func(e wlclient.CallbackDoneEvent) { - c.handleSync() - }) - - default: - c.done = true - } -} - -func (c *dpmsClient) ListOutputs() []string { - c.mu.Lock() - defer c.mu.Unlock() - - names := make([]string, 0, len(c.outputs)) - for name := range c.outputs { - names = append(names, name) - } - return names -} - -func (c *dpmsClient) SetDPMS(outputName string, on bool) error { - var mode uint32 - if on { - mode = uint32(wlr_output_power.ZwlrOutputPowerV1ModeOn) - } else { - mode = uint32(wlr_output_power.ZwlrOutputPowerV1ModeOff) - } - - var setErr error - c.post(func() { - c.mu.Lock() - var waitStates []*outputState - - if outputName == "" || outputName == "all" { - if len(c.outputs) == 0 { - c.mu.Unlock() - setErr = fmt.Errorf("no outputs found") - return - } - - for _, state := range c.outputs { - if state.powerCtrl == nil { - continue - } - state.wantMode = &mode - state.waitCh = make(chan struct{}) - state.failed = false - waitStates = append(waitStates, state) - state.powerCtrl.SetMode(mode) - } - } else { - state, ok := c.outputs[outputName] - if !ok { - c.mu.Unlock() - setErr = fmt.Errorf("output not found: %s", outputName) - return - } - if state.powerCtrl == nil { - c.mu.Unlock() - setErr = fmt.Errorf("output %s has nil powerCtrl", outputName) - return - } - state.wantMode = &mode - state.waitCh = make(chan struct{}) - state.failed = false - waitStates = append(waitStates, state) - state.powerCtrl.SetMode(mode) - } - c.mu.Unlock() - - deadline := time.Now().Add(10 * time.Second) - - for _, state := range waitStates { - c.mu.Lock() - ch := state.waitCh - c.mu.Unlock() - - done := false - for !done { - if err := c.ctx.Dispatch(); err != nil { - setErr = fmt.Errorf("dispatch error: %w", err) - return - } - - select { - case <-ch: - c.mu.Lock() - if state.failed { - setErr = fmt.Errorf("compositor reported failed for %s", state.name) - c.mu.Unlock() - return - } - c.mu.Unlock() - done = true - default: - if time.Now().After(deadline) { - setErr = fmt.Errorf("timeout waiting for mode change on %s", state.name) - return - } - time.Sleep(10 * time.Millisecond) - } - } - } - - c.mu.Lock() - for _, state := range waitStates { - if state.powerCtrl != nil { - state.powerCtrl.Destroy() - state.powerCtrl = nil - } - } - c.mu.Unlock() - - c.display.Roundtrip() - }) - - return setErr -} - -func (c *dpmsClient) Close() { - close(c.stopChan) - c.wg.Wait() - - c.mu.Lock() - defer c.mu.Unlock() - - for _, state := range c.outputs { - if state.powerCtrl != nil { - state.powerCtrl.Destroy() - } - } - c.outputs = nil - - if c.powerMgr != nil { - c.powerMgr.Destroy() - c.powerMgr = nil - } - - if c.display != nil { - c.ctx.Close() - c.display = nil - } -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/immutable_policy.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/immutable_policy.go deleted file mode 100644 index 5529c9f..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/immutable_policy.go +++ /dev/null @@ -1,271 +0,0 @@ -package main - -import ( - "bufio" - _ "embed" - "encoding/json" - "fmt" - "os" - "strings" - "sync" - - "github.com/spf13/cobra" -) - -const ( - cliPolicyPackagedPath = "/usr/share/dms/cli-policy.json" - cliPolicyAdminPath = "/etc/dms/cli-policy.json" -) - -var ( - immutablePolicyOnce sync.Once - immutablePolicy immutableCommandPolicy - immutablePolicyErr error -) - -//go:embed assets/cli-policy.default.json -var defaultCLIPolicyJSON []byte - -type immutableCommandPolicy struct { - ImmutableSystem bool - ImmutableReason string - BlockedCommands []string - Message string -} - -type cliPolicyFile struct { - PolicyVersion int `json:"policy_version"` - ImmutableSystem *bool `json:"immutable_system"` - BlockedCommands *[]string `json:"blocked_commands"` - Message *string `json:"message"` -} - -func normalizeCommandSpec(raw string) string { - normalized := strings.ToLower(strings.TrimSpace(raw)) - normalized = strings.TrimPrefix(normalized, "dms ") - return strings.Join(strings.Fields(normalized), " ") -} - -func normalizeBlockedCommands(raw []string) []string { - normalized := make([]string, 0, len(raw)) - seen := make(map[string]bool) - - for _, cmd := range raw { - spec := normalizeCommandSpec(cmd) - if spec == "" || seen[spec] { - continue - } - seen[spec] = true - normalized = append(normalized, spec) - } - - return normalized -} - -func commandBlockedByPolicy(commandPath string, blocked []string) bool { - normalizedPath := normalizeCommandSpec(commandPath) - if normalizedPath == "" { - return false - } - - for _, entry := range blocked { - spec := normalizeCommandSpec(entry) - if spec == "" { - continue - } - if normalizedPath == spec || strings.HasPrefix(normalizedPath, spec+" ") { - return true - } - } - - return false -} - -func loadPolicyFile(path string) (*cliPolicyFile, error) { - data, err := os.ReadFile(path) - if err != nil { - if os.IsNotExist(err) { - return nil, nil - } - return nil, fmt.Errorf("failed to read %s: %w", path, err) - } - - var policy cliPolicyFile - if err := json.Unmarshal(data, &policy); err != nil { - return nil, fmt.Errorf("failed to parse %s: %w", path, err) - } - - return &policy, nil -} - -func mergePolicyFile(base *immutableCommandPolicy, path string) error { - policyFile, err := loadPolicyFile(path) - if err != nil { - return err - } - if policyFile == nil { - return nil - } - - if policyFile.ImmutableSystem != nil { - base.ImmutableSystem = *policyFile.ImmutableSystem - } - if policyFile.BlockedCommands != nil { - base.BlockedCommands = normalizeBlockedCommands(*policyFile.BlockedCommands) - } - if policyFile.Message != nil { - msg := strings.TrimSpace(*policyFile.Message) - if msg != "" { - base.Message = msg - } - } - - return nil -} - -func readOSReleaseMap(path string) map[string]string { - values := make(map[string]string) - - file, err := os.Open(path) - if err != nil { - return values - } - defer file.Close() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - parts := strings.SplitN(line, "=", 2) - if len(parts) != 2 { - continue - } - key := strings.ToUpper(strings.TrimSpace(parts[0])) - value := strings.Trim(strings.TrimSpace(parts[1]), "\"") - values[key] = strings.ToLower(value) - } - - return values -} - -func hasAnyToken(text string, tokens ...string) bool { - if text == "" { - return false - } - for _, token := range tokens { - if strings.Contains(text, token) { - return true - } - } - return false -} - -func detectImmutableSystem() (bool, string) { - if _, err := os.Stat("/run/ostree-booted"); err == nil { - return true, "/run/ostree-booted is present" - } - - osRelease := readOSReleaseMap("/etc/os-release") - if len(osRelease) == 0 { - return false, "" - } - - id := osRelease["ID"] - idLike := osRelease["ID_LIKE"] - variantID := osRelease["VARIANT_ID"] - name := osRelease["NAME"] - prettyName := osRelease["PRETTY_NAME"] - - immutableIDs := map[string]bool{ - "bluefin": true, - "bazzite": true, - "silverblue": true, - "kinoite": true, - "sericea": true, - "onyx": true, - "aurora": true, - "fedora-iot": true, - "fedora-coreos": true, - } - if immutableIDs[id] { - return true, "os-release ID=" + id - } - - markers := []string{"silverblue", "kinoite", "sericea", "onyx", "bazzite", "bluefin", "aurora", "ostree", "atomic"} - if hasAnyToken(variantID, markers...) { - return true, "os-release VARIANT_ID=" + variantID - } - if hasAnyToken(idLike, "ostree", "rpm-ostree") { - return true, "os-release ID_LIKE=" + idLike - } - if hasAnyToken(name, markers...) || hasAnyToken(prettyName, markers...) { - return true, "os-release identifies an atomic/ostree variant" - } - - return false, "" -} - -func getImmutablePolicy() (*immutableCommandPolicy, error) { - immutablePolicyOnce.Do(func() { - detectedImmutable, reason := detectImmutableSystem() - immutablePolicy = immutableCommandPolicy{ - ImmutableSystem: detectedImmutable, - ImmutableReason: reason, - BlockedCommands: []string{"greeter install", "greeter enable", "setup"}, - Message: "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes.", - } - - var defaultPolicy cliPolicyFile - if err := json.Unmarshal(defaultCLIPolicyJSON, &defaultPolicy); err != nil { - immutablePolicyErr = fmt.Errorf("failed to parse embedded default CLI policy: %w", err) - return - } - if defaultPolicy.BlockedCommands != nil { - immutablePolicy.BlockedCommands = normalizeBlockedCommands(*defaultPolicy.BlockedCommands) - } - if defaultPolicy.Message != nil { - msg := strings.TrimSpace(*defaultPolicy.Message) - if msg != "" { - immutablePolicy.Message = msg - } - } - - if err := mergePolicyFile(&immutablePolicy, cliPolicyPackagedPath); err != nil { - immutablePolicyErr = err - return - } - if err := mergePolicyFile(&immutablePolicy, cliPolicyAdminPath); err != nil { - immutablePolicyErr = err - return - } - }) - - if immutablePolicyErr != nil { - return nil, immutablePolicyErr - } - return &immutablePolicy, nil -} - -func requireMutableSystemCommand(cmd *cobra.Command, _ []string) error { - policy, err := getImmutablePolicy() - if err != nil { - return err - } - if !policy.ImmutableSystem { - return nil - } - - commandPath := normalizeCommandSpec(cmd.CommandPath()) - if !commandBlockedByPolicy(commandPath, policy.BlockedCommands) { - return nil - } - - reason := "" - if policy.ImmutableReason != "" { - reason = "Detected immutable system: " + policy.ImmutableReason + "\n" - } - - return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath) -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/main.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/main.go deleted file mode 100644 index c818148..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/main.go +++ /dev/null @@ -1,43 +0,0 @@ -//go:build !distro_binary - -package main - -import ( - "os" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard" - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" -) - -var Version = "dev" - -func init() { - 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") - - greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd) - authCmd.AddCommand(authSyncCmd) - setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) - updateCmd.AddCommand(updateCheckCmd) - pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) - rootCmd.AddCommand(getCommonCommands()...) - - rootCmd.AddCommand(authCmd) - rootCmd.AddCommand(updateCmd) - - rootCmd.SetHelpTemplate(getHelpTemplate()) -} - -func main() { - clipboard.MaybeServeAndExit() - - if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) { - log.Fatal("This program should not be run as root. Exiting.") - } - - if err := rootCmd.Execute(); err != nil { - log.Fatal(err) - } -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/main_distro.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/main_distro.go deleted file mode 100644 index 2b544ff..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/main_distro.go +++ /dev/null @@ -1,40 +0,0 @@ -//go:build distro_binary - -package main - -import ( - "os" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard" - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" -) - -var Version = "dev" - -func init() { - 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") - - greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd) - authCmd.AddCommand(authSyncCmd) - setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd) - pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd) - rootCmd.AddCommand(getCommonCommands()...) - rootCmd.AddCommand(authCmd) - - rootCmd.SetHelpTemplate(getHelpTemplate()) -} - -func main() { - clipboard.MaybeServeAndExit() - - if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) { - log.Fatal("This program should not be run as root. Exiting.") - } - - if err := rootCmd.Execute(); err != nil { - log.Fatal(err) - } -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/randr_client.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/randr_client.go deleted file mode 100644 index 1a149ca..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/randr_client.go +++ /dev/null @@ -1,172 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management" - wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client" -) - -type randrOutput struct { - Name string `json:"name"` - Scale float64 `json:"scale"` - Width int32 `json:"width"` - Height int32 `json:"height"` - Refresh int32 `json:"refresh"` - Enabled bool `json:"enabled"` -} - -type randrHead struct { - name string - enabled bool - scale float64 - currentModeID uint32 - modeIDs []uint32 -} - -type randrMode struct { - width int32 - height int32 - refresh int32 -} - -type randrClient struct { - display *wlclient.Display - ctx *wlclient.Context - manager *wlr_output_management.ZwlrOutputManagerV1 - heads map[uint32]*randrHead - modes map[uint32]*randrMode - done bool - err error -} - -func queryRandr() ([]randrOutput, error) { - display, err := wlclient.Connect("") - if err != nil { - return nil, fmt.Errorf("failed to connect to Wayland: %w", err) - } - - c := &randrClient{ - display: display, - ctx: display.Context(), - heads: make(map[uint32]*randrHead), - modes: make(map[uint32]*randrMode), - } - defer c.ctx.Close() - - registry, err := display.GetRegistry() - if err != nil { - return nil, fmt.Errorf("failed to get registry: %w", err) - } - - registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) { - if e.Interface == wlr_output_management.ZwlrOutputManagerV1InterfaceName { - mgr := wlr_output_management.NewZwlrOutputManagerV1(c.ctx) - version := min(e.Version, 4) - - mgr.SetHeadHandler(func(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) { - c.handleHead(e) - }) - - mgr.SetDoneHandler(func(e wlr_output_management.ZwlrOutputManagerV1DoneEvent) { - c.done = true - }) - - if err := registry.Bind(e.Name, e.Interface, version, mgr); err == nil { - c.manager = mgr - } - } - }) - - // First roundtrip: discover globals and bind manager - syncCallback, err := display.Sync() - if err != nil { - return nil, fmt.Errorf("failed to sync display: %w", err) - } - syncCallback.SetDoneHandler(func(e wlclient.CallbackDoneEvent) { - if c.manager == nil { - c.err = fmt.Errorf("zwlr_output_manager_v1 protocol not supported by compositor") - c.done = true - } - // Otherwise wait for manager's DoneHandler - }) - - for !c.done { - if err := c.ctx.Dispatch(); err != nil { - return nil, fmt.Errorf("dispatch error: %w", err) - } - } - - if c.err != nil { - return nil, c.err - } - - return c.buildOutputs(), nil -} - -func (c *randrClient) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) { - handle := e.Head - headID := handle.ID() - - head := &randrHead{ - modeIDs: make([]uint32, 0), - } - c.heads[headID] = head - - handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) { - head.name = e.Name - }) - - handle.SetEnabledHandler(func(e wlr_output_management.ZwlrOutputHeadV1EnabledEvent) { - head.enabled = e.Enabled != 0 - }) - - handle.SetScaleHandler(func(e wlr_output_management.ZwlrOutputHeadV1ScaleEvent) { - head.scale = e.Scale - }) - - handle.SetCurrentModeHandler(func(e wlr_output_management.ZwlrOutputHeadV1CurrentModeEvent) { - head.currentModeID = e.Mode.ID() - }) - - handle.SetModeHandler(func(e wlr_output_management.ZwlrOutputHeadV1ModeEvent) { - modeHandle := e.Mode - modeID := modeHandle.ID() - - head.modeIDs = append(head.modeIDs, modeID) - - mode := &randrMode{} - c.modes[modeID] = mode - - modeHandle.SetSizeHandler(func(e wlr_output_management.ZwlrOutputModeV1SizeEvent) { - mode.width = e.Width - mode.height = e.Height - }) - - modeHandle.SetRefreshHandler(func(e wlr_output_management.ZwlrOutputModeV1RefreshEvent) { - mode.refresh = e.Refresh - }) - }) -} - -func (c *randrClient) buildOutputs() []randrOutput { - outputs := make([]randrOutput, 0, len(c.heads)) - - for _, head := range c.heads { - out := randrOutput{ - Name: head.name, - Scale: head.scale, - Enabled: head.enabled, - } - - if mode, ok := c.modes[head.currentModeID]; ok { - out.Width = mode.width - out.Height = mode.height - out.Refresh = mode.refresh - } - - outputs = append(outputs, out) - } - - return outputs -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/server_client.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/server_client.go deleted file mode 100644 index 871d21d..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/server_client.go +++ /dev/null @@ -1,114 +0,0 @@ -package main - -import ( - "bufio" - "encoding/json" - "fmt" - "net" - "os" - "path/filepath" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/server" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" -) - -func sendServerRequest(req models.Request) (*models.Response[any], error) { - socketPath := getServerSocketPath() - - conn, err := net.Dial("unix", socketPath) - if err != nil { - return nil, fmt.Errorf("failed to connect to server (is it running?): %w", err) - } - defer conn.Close() - - scanner := bufio.NewScanner(conn) - scanner.Scan() // discard initial capabilities message - - reqData, err := json.Marshal(req) - if err != nil { - return nil, fmt.Errorf("failed to marshal request: %w", err) - } - - if _, err := conn.Write(reqData); err != nil { - return nil, fmt.Errorf("failed to write request: %w", err) - } - - if _, err := conn.Write([]byte("\n")); err != nil { - return nil, fmt.Errorf("failed to write newline: %w", err) - } - - if !scanner.Scan() { - return nil, fmt.Errorf("failed to read response") - } - - var resp models.Response[any] - if err := json.Unmarshal(scanner.Bytes(), &resp); err != nil { - return nil, fmt.Errorf("failed to unmarshal response: %w", err) - } - - return &resp, nil -} - -// sendServerRequestFireAndForget sends a request without waiting for a response. -// Useful for commands that trigger UI or async operations. -func sendServerRequestFireAndForget(req models.Request) error { - socketPath := getServerSocketPath() - - conn, err := net.Dial("unix", socketPath) - if err != nil { - return fmt.Errorf("failed to connect to server (is it running?): %w", err) - } - defer conn.Close() - - scanner := bufio.NewScanner(conn) - scanner.Scan() // discard initial capabilities message - - reqData, err := json.Marshal(req) - if err != nil { - return fmt.Errorf("failed to marshal request: %w", err) - } - - if _, err := conn.Write(reqData); err != nil { - return fmt.Errorf("failed to write request: %w", err) - } - - if _, err := conn.Write([]byte("\n")); err != nil { - return fmt.Errorf("failed to write newline: %w", err) - } - - return nil -} - -// tryServerRequest attempts to send a request but returns false if server unavailable. -// Does not log errors - caller can decide what to do on failure. -func tryServerRequest(req models.Request) (*models.Response[any], bool) { - resp, err := sendServerRequest(req) - if err != nil { - return nil, false - } - return resp, true -} - -func getServerSocketPath() string { - runtimeDir := os.Getenv("XDG_RUNTIME_DIR") - if runtimeDir == "" { - runtimeDir = os.TempDir() - } - - entries, err := os.ReadDir(runtimeDir) - if err != nil { - return filepath.Join(runtimeDir, "danklinux.sock") - } - - for _, entry := range entries { - name := entry.Name() - if name == "danklinux.sock" { - return filepath.Join(runtimeDir, name) - } - if len(name) > 10 && name[:10] == "danklinux-" && filepath.Ext(name) == ".sock" { - return filepath.Join(runtimeDir, name) - } - } - - return server.GetSocketPath() -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/shell.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/shell.go deleted file mode 100644 index 7ce2003..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/shell.go +++ /dev/null @@ -1,739 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - "os/exec" - "os/signal" - "path/filepath" - "slices" - "strconv" - "strings" - "sync" - "syscall" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/log" - "github.com/AvengeMedia/DankMaterialShell/core/internal/server" -) - -type ipcTargets map[string]map[string][]string - -// getProcessExitCode returns the exit code from a ProcessState. -// For normal exits, returns the exit code directly. -// For signal termination, returns 128 + signal number (Unix convention). -func getProcessExitCode(state *os.ProcessState) int { - if state == nil { - return 1 - } - if code := state.ExitCode(); code != -1 { - return code - } - // Process was killed by signal - extract signal number - if status, ok := state.Sys().(syscall.WaitStatus); ok { - if status.Signaled() { - return 128 + int(status.Signal()) - } - } - return 1 -} - -var isSessionManaged bool - -func execDetachedRestart(targetPID int) { - selfPath, err := os.Executable() - if err != nil { - return - } - - cmd := exec.Command(selfPath, "restart-detached", strconv.Itoa(targetPID)) - cmd.SysProcAttr = &syscall.SysProcAttr{ - Setsid: true, - } - cmd.Start() -} - -func runDetachedRestart(targetPIDStr string) { - targetPID, err := strconv.Atoi(targetPIDStr) - if err != nil { - return - } - - time.Sleep(200 * time.Millisecond) - - proc, err := os.FindProcess(targetPID) - if err == nil { - proc.Signal(syscall.SIGTERM) - } - - time.Sleep(500 * time.Millisecond) - - killShell() - runShellDaemon(false) -} - -func getRuntimeDir() string { - if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" { - return runtime - } - return os.TempDir() -} - -func hasSystemdRun() bool { - _, err := exec.LookPath("systemd-run") - return err == nil -} - -func getPIDFilePath() string { - return filepath.Join(getRuntimeDir(), fmt.Sprintf("danklinux-%d.pid", os.Getpid())) -} - -func writePIDFile(childPID int) error { - pidFile := getPIDFilePath() - return os.WriteFile(pidFile, []byte(strconv.Itoa(childPID)), 0o644) -} - -func removePIDFile() { - pidFile := getPIDFilePath() - os.Remove(pidFile) -} - -func getAllDMSPIDs() []int { - dir := getRuntimeDir() - entries, err := os.ReadDir(dir) - if err != nil { - return nil - } - - var pids []int - - for _, entry := range entries { - if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".pid") { - continue - } - - pidFile := filepath.Join(dir, entry.Name()) - data, err := os.ReadFile(pidFile) - if err != nil { - continue - } - - childPID, err := strconv.Atoi(strings.TrimSpace(string(data))) - if err != nil { - os.Remove(pidFile) - continue - } - - proc, err := os.FindProcess(childPID) - if err != nil { - os.Remove(pidFile) - continue - } - - if err := proc.Signal(syscall.Signal(0)); err != nil { - os.Remove(pidFile) - continue - } - - pids = append(pids, childPID) - - parentPIDStr := strings.TrimPrefix(entry.Name(), "danklinux-") - parentPIDStr = strings.TrimSuffix(parentPIDStr, ".pid") - if parentPID, err := strconv.Atoi(parentPIDStr); err == nil { - if parentProc, err := os.FindProcess(parentPID); err == nil { - if err := parentProc.Signal(syscall.Signal(0)); err == nil { - pids = append(pids, parentPID) - } - } - } - } - - return pids -} - -func runShellInteractive(session bool) { - isSessionManaged = session - go printASCII() - fmt.Fprintf(os.Stderr, "dms %s\n", Version) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - socketPath := server.GetSocketPath() - - configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path") - if err := os.WriteFile(configStateFile, []byte(configPath), 0o644); err != nil { - log.Warnf("Failed to write config state file: %v", err) - } - defer os.Remove(configStateFile) - - errChan := make(chan error, 2) - - go func() { - defer func() { - if r := recover(); r != nil { - errChan <- fmt.Errorf("server panic: %v", r) - } - }() - server.CLIVersion = Version - if err := server.Start(false); err != nil { - errChan <- fmt.Errorf("server error: %w", err) - } - }() - - log.Infof("Spawning quickshell with -p %s", configPath) - - cmd := exec.CommandContext(ctx, "qs", "-p", configPath) - cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath) - if os.Getenv("QT_LOGGING_RULES") == "" { - if qtRules := log.GetQtLoggingRules(); qtRules != "" { - cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules) - } - } - - // ! TODO - remove when QS 0.3 is up and we can use the pragma - cmd.Env = append(cmd.Env, "QS_APP_ID=com.danklinux.dms") - - if isSessionManaged && hasSystemdRun() { - cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope") - } - - homeDir, err := os.UserHomeDir() - if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" { - if !strings.HasPrefix(configPath, homeDir) { - cmd.Env = append(cmd.Env, "DMS_DISABLE_HOT_RELOAD=1") - } - } - - if os.Getenv("QT_QPA_PLATFORMTHEME") == "" { - cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME=gtk3") - } - if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") == "" { - cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3") - } - if os.Getenv("QT_QPA_PLATFORM") == "" { - cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb") - } - - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - if err := cmd.Start(); err != nil { - log.Fatalf("Error starting quickshell: %v", err) - } - - // Write PID file for the quickshell child process - if err := writePIDFile(cmd.Process.Pid); err != nil { - log.Warnf("Failed to write PID file: %v", err) - } - defer removePIDFile() - - defer func() { - if cmd.Process != nil { - cmd.Process.Signal(syscall.SIGTERM) - } - }() - - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1) - - go func() { - if err := cmd.Wait(); err != nil { - errChan <- fmt.Errorf("quickshell exited: %w", err) - } else { - errChan <- fmt.Errorf("quickshell exited") - } - }() - - for { - select { - case sig := <-sigChan: - if sig == syscall.SIGUSR1 { - if isSessionManaged { - log.Infof("Received SIGUSR1, exiting for systemd restart...") - cancel() - cmd.Process.Signal(syscall.SIGTERM) - os.Remove(socketPath) - os.Exit(1) - } - log.Infof("Received SIGUSR1, spawning detached restart process...") - execDetachedRestart(os.Getpid()) - return - } - - // Check if qs already crashed before we got SIGTERM (systemd sends SIGTERM when D-Bus name is released) - select { - case <-errChan: - cancel() - os.Remove(socketPath) - os.Exit(getProcessExitCode(cmd.ProcessState)) - case <-time.After(500 * time.Millisecond): - } - - log.Infof("\nReceived signal %v, shutting down...", sig) - cancel() - cmd.Process.Signal(syscall.SIGTERM) - os.Remove(socketPath) - return - - case err := <-errChan: - log.Error(err) - cancel() - if cmd.Process != nil { - cmd.Process.Signal(syscall.SIGTERM) - } - os.Remove(socketPath) - os.Exit(getProcessExitCode(cmd.ProcessState)) - } - } -} - -func restartShell() { - pids := getAllDMSPIDs() - - if len(pids) == 0 { - log.Info("No running DMS shell instances found. Starting daemon...") - runShellDaemon(false) - return - } - - currentPid := os.Getpid() - uniquePids := make(map[int]bool) - - for _, pid := range pids { - if pid != currentPid { - uniquePids[pid] = true - } - } - - for pid := range uniquePids { - proc, err := os.FindProcess(pid) - if err != nil { - log.Errorf("Error finding process %d: %v", pid, err) - continue - } - - if err := proc.Signal(syscall.Signal(0)); err != nil { - continue - } - - if err := proc.Signal(syscall.SIGUSR1); err != nil { - log.Errorf("Error sending SIGUSR1 to process %d: %v", pid, err) - } else { - log.Infof("Sent SIGUSR1 to DMS process with PID %d", pid) - } - } -} - -func killShell() { - pids := getAllDMSPIDs() - - if len(pids) == 0 { - log.Info("No running DMS shell instances found.") - return - } - - currentPid := os.Getpid() - uniquePids := make(map[int]bool) - - for _, pid := range pids { - if pid != currentPid { - uniquePids[pid] = true - } - } - - for pid := range uniquePids { - proc, err := os.FindProcess(pid) - if err != nil { - log.Errorf("Error finding process %d: %v", pid, err) - continue - } - - if err := proc.Signal(syscall.Signal(0)); err != nil { - continue - } - - if err := proc.Kill(); err != nil { - log.Errorf("Error killing process %d: %v", pid, err) - } else { - log.Infof("Killed DMS process with PID %d", pid) - } - } - - dir := getRuntimeDir() - entries, err := os.ReadDir(dir) - if err != nil { - return - } - - for _, entry := range entries { - if strings.HasPrefix(entry.Name(), "danklinux-") && strings.HasSuffix(entry.Name(), ".pid") { - pidFile := filepath.Join(dir, entry.Name()) - os.Remove(pidFile) - } - } -} - -func runShellDaemon(session bool) { - isSessionManaged = session - isDaemonChild := slices.Contains(os.Args, "--daemon-child") - - if !isDaemonChild { - fmt.Fprintf(os.Stderr, "dms %s\n", Version) - - cmd := exec.Command(os.Args[0], "run", "-d", "--daemon-child") - cmd.Env = os.Environ() - - cmd.SysProcAttr = &syscall.SysProcAttr{ - Setsid: true, - } - - if err := cmd.Start(); err != nil { - log.Fatalf("Error starting daemon: %v", err) - } - - log.Infof("DMS shell daemon started (PID: %d)", cmd.Process.Pid) - return - } - - fmt.Fprintf(os.Stderr, "dms %s\n", Version) - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - socketPath := server.GetSocketPath() - - configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path") - if err := os.WriteFile(configStateFile, []byte(configPath), 0o644); err != nil { - log.Warnf("Failed to write config state file: %v", err) - } - defer os.Remove(configStateFile) - - errChan := make(chan error, 2) - - go func() { - defer func() { - if r := recover(); r != nil { - errChan <- fmt.Errorf("server panic: %v", r) - } - }() - server.CLIVersion = Version - if err := server.Start(false); err != nil { - errChan <- fmt.Errorf("server error: %w", err) - } - }() - - log.Infof("Spawning quickshell with -p %s", configPath) - - cmd := exec.CommandContext(ctx, "qs", "-p", configPath) - cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath) - if os.Getenv("QT_LOGGING_RULES") == "" { - if qtRules := log.GetQtLoggingRules(); qtRules != "" { - cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules) - } - } - - // ! TODO - remove when QS 0.3 is up and we can use the pragma - cmd.Env = append(cmd.Env, "QS_APP_ID=com.danklinux.dms") - - if isSessionManaged && hasSystemdRun() { - cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope") - } - - homeDir, err := os.UserHomeDir() - if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" { - if !strings.HasPrefix(configPath, homeDir) { - cmd.Env = append(cmd.Env, "DMS_DISABLE_HOT_RELOAD=1") - } - } - - if os.Getenv("QT_QPA_PLATFORMTHEME") == "" { - cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME=gtk3") - } - if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") == "" { - cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3") - } - if os.Getenv("QT_QPA_PLATFORM") == "" { - cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb") - } - - devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0) - if err != nil { - log.Fatalf("Error opening /dev/null: %v", err) - } - defer devNull.Close() - - cmd.Stdin = devNull - cmd.Stdout = devNull - cmd.Stderr = devNull - - if err := cmd.Start(); err != nil { - log.Fatalf("Error starting daemon: %v", err) - } - - // Write PID file for the quickshell child process - if err := writePIDFile(cmd.Process.Pid); err != nil { - log.Warnf("Failed to write PID file: %v", err) - } - defer removePIDFile() - - defer func() { - if cmd.Process != nil { - cmd.Process.Signal(syscall.SIGTERM) - } - }() - - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1) - - go func() { - if err := cmd.Wait(); err != nil { - errChan <- fmt.Errorf("quickshell exited: %w", err) - } else { - errChan <- fmt.Errorf("quickshell exited") - } - }() - - for { - select { - case sig := <-sigChan: - if sig == syscall.SIGUSR1 { - if isSessionManaged { - log.Infof("Received SIGUSR1, exiting for systemd restart...") - cancel() - cmd.Process.Signal(syscall.SIGTERM) - os.Remove(socketPath) - os.Exit(1) - } - log.Infof("Received SIGUSR1, spawning detached restart process...") - execDetachedRestart(os.Getpid()) - return - } - - // Check if qs already crashed before we got SIGTERM (systemd sends SIGTERM when D-Bus name is released) - select { - case <-errChan: - cancel() - os.Remove(socketPath) - os.Exit(getProcessExitCode(cmd.ProcessState)) - case <-time.After(500 * time.Millisecond): - } - - cancel() - cmd.Process.Signal(syscall.SIGTERM) - os.Remove(socketPath) - return - - case <-errChan: - cancel() - if cmd.Process != nil { - cmd.Process.Signal(syscall.SIGTERM) - } - os.Remove(socketPath) - os.Exit(getProcessExitCode(cmd.ProcessState)) - } - } -} - -var qsHasAnyDisplay = sync.OnceValue(func() bool { - out, err := exec.Command("qs", "ipc", "--help").Output() - if err != nil { - return false - } - return strings.Contains(string(out), "--any-display") -}) - -func parseTargetsFromIPCShowOutput(output string) ipcTargets { - targets := make(ipcTargets) - var currentTarget string - for line := range strings.SplitSeq(output, "\n") { - if after, ok := strings.CutPrefix(line, "target "); ok { - currentTarget = strings.TrimSpace(after) - targets[currentTarget] = make(map[string][]string) - } - if strings.HasPrefix(line, " function") && currentTarget != "" { - argsList := []string{} - currentFunc := strings.TrimPrefix(line, " function ") - funcDef := strings.SplitN(currentFunc, "(", 2) - argList := strings.SplitN(funcDef[1], ")", 2)[0] - args := strings.Split(argList, ",") - if len(args) > 0 && strings.TrimSpace(args[0]) != "" { - argsList = append(argsList, funcDef[0]) - for _, arg := range args { - argName := strings.SplitN(strings.TrimSpace(arg), ":", 2)[0] - argsList = append(argsList, argName) - } - targets[currentTarget][funcDef[0]] = argsList - } else { - targets[currentTarget][funcDef[0]] = make([]string, 0) - } - } - } - return targets -} - -func getShellIPCCompletions(args []string, _ string) []string { - cmdArgs := []string{"ipc"} - if qsHasAnyDisplay() { - cmdArgs = append(cmdArgs, "--any-display") - } - cmdArgs = append(cmdArgs, "-p", configPath, "show") - cmd := exec.Command("qs", cmdArgs...) - var targets ipcTargets - - if output, err := cmd.Output(); err == nil { - targets = parseTargetsFromIPCShowOutput(string(output)) - } else { - log.Debugf("Error getting IPC show output for completions: %v", err) - return nil - } - - if len(args) > 0 && args[0] == "call" { - args = args[1:] - } - - if len(args) == 0 { - targetNames := make([]string, 0) - targetNames = append(targetNames, "call") - for k := range targets { - targetNames = append(targetNames, k) - } - return targetNames - } - if len(args) == 1 { - if targetFuncs, ok := targets[args[0]]; ok { - funcNames := make([]string, 0) - for k := range targetFuncs { - funcNames = append(funcNames, k) - } - return funcNames - } - return nil - } - if len(args) <= len(targets[args[0]]) { - funcArgs := targets[args[0]][args[1]] - if len(funcArgs) >= len(args) { - return []string{fmt.Sprintf("[%s]", funcArgs[len(args)-1])} - } - } - - return nil -} - -func getFirstDMSPID() (int, bool) { - dir := getRuntimeDir() - entries, err := os.ReadDir(dir) - if err != nil { - return 0, false - } - - for _, entry := range entries { - if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".pid") { - continue - } - - data, err := os.ReadFile(filepath.Join(dir, entry.Name())) - if err != nil { - continue - } - - pid, err := strconv.Atoi(strings.TrimSpace(string(data))) - if err != nil { - continue - } - - proc, err := os.FindProcess(pid) - if err != nil { - continue - } - - if proc.Signal(syscall.Signal(0)) != nil { - continue - } - - return pid, true - } - - return 0, false -} - -func runShellIPCCommand(args []string) { - if len(args) == 0 { - printIPCHelp() - return - } - - if args[0] != "call" { - args = append([]string{"call"}, args...) - } - - cmdArgs := []string{"ipc"} - - switch pid, ok := getFirstDMSPID(); { - case ok: - cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid)) - default: - if err := findConfig(nil, nil); err != nil { - log.Fatalf("Error finding config: %v", err) - } - // ! TODO - remove check when QS 0.3 is released - if qsHasAnyDisplay() { - cmdArgs = append(cmdArgs, "--any-display") - } - cmdArgs = append(cmdArgs, "-p", configPath) - } - - cmdArgs = append(cmdArgs, args...) - cmd := exec.Command("qs", cmdArgs...) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - if err := cmd.Run(); err != nil { - log.Fatalf("Error running IPC command: %v", err) - } -} - -func printIPCHelp() { - fmt.Println("Usage: dms ipc <target> <function> [args...]") - fmt.Println() - - cmdArgs := []string{"ipc"} - if qsHasAnyDisplay() { - cmdArgs = append(cmdArgs, "--any-display") - } - cmdArgs = append(cmdArgs, "-p", configPath, "show") - cmd := exec.Command("qs", cmdArgs...) - - output, err := cmd.Output() - if err != nil { - fmt.Println("Could not retrieve available IPC targets (is DMS running?)") - return - } - - targets := parseTargetsFromIPCShowOutput(string(output)) - if len(targets) == 0 { - fmt.Println("No IPC targets available") - return - } - - fmt.Println("Targets:") - - targetNames := make([]string, 0, len(targets)) - for name := range targets { - targetNames = append(targetNames, name) - } - slices.Sort(targetNames) - - for _, targetName := range targetNames { - funcs := targets[targetName] - funcNames := make([]string, 0, len(funcs)) - for fn := range funcs { - funcNames = append(funcNames, fn) - } - slices.Sort(funcNames) - fmt.Printf(" %-16s %s\n", targetName, strings.Join(funcNames, ", ")) - } -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/ui.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/ui.go deleted file mode 100644 index c0b50e6..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/ui.go +++ /dev/null @@ -1,53 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/tui" - "github.com/charmbracelet/lipgloss" -) - -func printASCII() { - fmt.Print(getThemedASCII()) -} - -func getThemedASCII() string { - theme := tui.TerminalTheme() - - logo := ` -██████╗ █████╗ ███╗ ██╗██╗ ██╗ -██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝ -██║ ██║███████║██╔██╗ ██║█████╔╝ -██║ ██║██╔══██║██║╚██╗██║██╔═██╗ -██████╔╝██║ ██║██║ ╚████║██║ ██╗ -╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝` - - style := lipgloss.NewStyle(). - Foreground(lipgloss.Color(theme.Primary)). - Bold(true) - - return style.Render(logo) + "\n" -} - -func getHelpTemplate() string { - return getThemedASCII() + ` -{{.Long}} - -Usage: - {{.UseLine}}{{if .HasAvailableSubCommands}} - -Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} - {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} - -Flags: -{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} - -Global Flags: -{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} - -Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} - {{rpad .Name .NamePadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} - -Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} -` -} diff --git a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/utils.go b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/utils.go deleted file mode 100644 index 9459e41..0000000 --- a/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/utils.go +++ /dev/null @@ -1,89 +0,0 @@ -package main - -import ( - "fmt" - "os/exec" - "slices" - "strings" -) - -// isReadOnlyCommand returns true if the CLI args indicate a command that is -// safe to run as root (e.g. shell completion, help). -func isReadOnlyCommand(args []string) bool { - for _, arg := range args[1:] { - if strings.HasPrefix(arg, "-") { - continue - } - switch arg { - case "completion", "help", "__complete": - return true - } - return false - } - return false -} - -func isArchPackageInstalled(packageName string) bool { - cmd := exec.Command("pacman", "-Q", packageName) - err := cmd.Run() - return err == nil -} - -type systemdServiceState struct { - Name string - EnabledState string - NeedsDisable bool - Exists bool -} - -// checkSystemdServiceEnabled returns (state, should_disable, error) for a systemd service -func checkSystemdServiceEnabled(serviceName string) (string, bool, error) { - cmd := exec.Command("systemctl", "is-enabled", serviceName) - output, err := cmd.Output() - - stateStr := strings.TrimSpace(string(output)) - - if err != nil { - knownStates := []string{"disabled", "masked", "masked-runtime", "not-found", "enabled", "enabled-runtime", "static", "indirect", "alias"} - isKnownState := slices.Contains(knownStates, stateStr) - - if !isKnownState { - return stateStr, false, fmt.Errorf("systemctl is-enabled failed: %w (output: %s)", err, stateStr) - } - } - - shouldDisable := false - switch stateStr { - case "enabled", "enabled-runtime", "static", "indirect", "alias": - shouldDisable = true - case "disabled", "masked", "masked-runtime", "not-found": - shouldDisable = false - default: - shouldDisable = true - } - - return stateStr, shouldDisable, nil -} - -func getSystemdServiceState(serviceName string) (*systemdServiceState, error) { - state := &systemdServiceState{ - Name: serviceName, - Exists: false, - } - - enabledState, needsDisable, err := checkSystemdServiceEnabled(serviceName) - if err != nil { - return nil, fmt.Errorf("failed to check enabled state: %w", err) - } - - state.EnabledState = enabledState - state.NeedsDisable = needsDisable - - if enabledState == "not-found" { - state.Exists = false - return state, nil - } - - state.Exists = true - return state, nil -} |