summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/DankMaterialShell/core/cmd/dms/shell.go
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/cmd/dms/shell.go')
-rw-r--r--raveos-hyprland-theme/theme-data/DankMaterialShell/core/cmd/dms/shell.go739
1 files changed, 739 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/cmd/dms/shell.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/cmd/dms/shell.go
new file mode 100644
index 0000000..7ce2003
--- /dev/null
+++ b/raveos-hyprland-theme/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, ", "))
+ }
+}