summaryrefslogtreecommitdiff
path: root/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms
diff options
context:
space:
mode:
authornippy <you@example.com>2026-04-18 13:49:56 +0200
committernippy <you@example.com>2026-04-18 13:49:56 +0200
commit5d94c0a7d44a2255b81815a52a7056a94a39842d (patch)
tree759bdea9645c6a62f9e1e4c001f7d81cccd120d2 /raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms
parente79cdf210b267f21a186255ce1a4d50029439d54 (diff)
downloadRaveOS-PKGBUILD-5d94c0a7d44a2255b81815a52a7056a94a39842d.tar.gz
RaveOS-PKGBUILD-5d94c0a7d44a2255b81815a52a7056a94a39842d.zip
update Raveos themes
Diffstat (limited to 'raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms')
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/assets/cli-policy.default.json10
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_auth.go76
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_blur.go40
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_brightness.go311
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_chroma.go300
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_clipboard.go1004
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_colorpicker.go139
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_common.go530
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_config.go318
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_dank16.go126
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_doctor.go1107
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_download.go99
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_dpms.go105
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_features.go491
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_greeter.go1850
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_greeter_test.go87
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_keybinds.go265
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_matugen.go202
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_notify.go68
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_open.go205
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_randr.go58
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_root.go74
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_screenshot.go422
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_setup.go436
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_windowrules.go338
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/dpms_client.go339
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/immutable_policy.go271
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/main.go43
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/main_distro.go40
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/randr_client.go172
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/server_client.go114
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/shell.go739
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/ui.go53
-rw-r--r--raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/utils.go89
34 files changed, 10521 insertions, 0 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
new file mode 100644
index 0000000..de9b8ad
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/assets/cli-policy.default.json
@@ -0,0 +1,10 @@
+{
+ "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
new file mode 100644
index 0000000..727700e
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_auth.go
@@ -0,0 +1,76 @@
+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
new file mode 100644
index 0000000..d01eb93
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_blur.go
@@ -0,0 +1,40 @@
+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
new file mode 100644
index 0000000..0f73493
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_brightness.go
@@ -0,0 +1,311 @@
+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
new file mode 100644
index 0000000..4945f33
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_chroma.go
@@ -0,0 +1,300 @@
+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
new file mode 100644
index 0000000..8a9b858
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_clipboard.go
@@ -0,0 +1,1004 @@
+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
new file mode 100644
index 0000000..7c366e8
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_colorpicker.go
@@ -0,0 +1,139 @@
+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
new file mode 100644
index 0000000..6557860
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_common.go
@@ -0,0 +1,530 @@
+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
new file mode 100644
index 0000000..d604256
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_config.go
@@ -0,0 +1,318 @@
+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
new file mode 100644
index 0000000..0bbbab1
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_dank16.go
@@ -0,0 +1,126 @@
+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
new file mode 100644
index 0000000..b182d06
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_doctor.go
@@ -0,0 +1,1107 @@
+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
new file mode 100644
index 0000000..e9caeb0
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_download.go
@@ -0,0 +1,99 @@
+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
new file mode 100644
index 0000000..77eff1a
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_dpms.go
@@ -0,0 +1,105 @@
+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
new file mode 100644
index 0000000..7ec5437
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_features.go
@@ -0,0 +1,491 @@
+//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
new file mode 100644
index 0000000..1efa545
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_greeter.go
@@ -0,0 +1,1850 @@
+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
new file mode 100644
index 0000000..775149f
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_greeter_test.go
@@ -0,0 +1,87 @@
+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
new file mode 100644
index 0000000..cdb15ac
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_keybinds.go
@@ -0,0 +1,265 @@
+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
new file mode 100644
index 0000000..566f360
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_matugen.go
@@ -0,0 +1,202 @@
+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
new file mode 100644
index 0000000..e04b60f
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_notify.go
@@ -0,0 +1,68 @@
+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(&notifyAppName, "app", "DMS", "Application name")
+ notifyCmd.Flags().StringVar(&notifyIcon, "icon", "", "Icon name or path")
+ notifyCmd.Flags().StringVar(&notifyFile, "file", "", "File path (enables Open/Open Folder actions)")
+ notifyCmd.Flags().IntVar(&notifyTimeout, "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
new file mode 100644
index 0000000..8cc16cf
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_open.go
@@ -0,0 +1,205 @@
+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
new file mode 100644
index 0000000..fa0c990
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_randr.go
@@ -0,0 +1,58 @@
+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
new file mode 100644
index 0000000..b263ae6
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_root.go
@@ -0,0 +1,74 @@
+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
new file mode 100644
index 0000000..2875019
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_screenshot.go
@@ -0,0 +1,422 @@
+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
new file mode 100644
index 0000000..0f4a58d
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_setup.go
@@ -0,0 +1,436 @@
+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
new file mode 100644
index 0000000..0883811
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/commands_windowrules.go
@@ -0,0 +1,338 @@
+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
new file mode 100644
index 0000000..98f66d9
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/dpms_client.go
@@ -0,0 +1,339 @@
+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
new file mode 100644
index 0000000..5529c9f
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/immutable_policy.go
@@ -0,0 +1,271 @@
+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
new file mode 100644
index 0000000..c818148
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/main.go
@@ -0,0 +1,43 @@
+//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
new file mode 100644
index 0000000..2b544ff
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/main_distro.go
@@ -0,0 +1,40 @@
+//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
new file mode 100644
index 0000000..1a149ca
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/randr_client.go
@@ -0,0 +1,172 @@
+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
new file mode 100644
index 0000000..871d21d
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/server_client.go
@@ -0,0 +1,114 @@
+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
new file mode 100644
index 0000000..7ce2003
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/shell.go
@@ -0,0 +1,739 @@
+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
new file mode 100644
index 0000000..c0b50e6
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/ui.go
@@ -0,0 +1,53 @@
+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
new file mode 100644
index 0000000..9459e41
--- /dev/null
+++ b/raveos-theme/hyprland/theme-data/DankMaterialShell/core/cmd/dms/utils.go
@@ -0,0 +1,89 @@
+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
+}