From 70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 Mon Sep 17 00:00:00 2001 From: AlexanderCurl Date: Sat, 18 Apr 2026 17:46:06 +0100 Subject: Replaced file structures for themes in the correct format --- .../core/internal/server/server.go | 1758 ++++++++++++++++++++ 1 file changed, 1758 insertions(+) create mode 100644 raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/server.go (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/server.go') diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/server.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/server.go new file mode 100644 index 0000000..58acb62 --- /dev/null +++ b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/server/server.go @@ -0,0 +1,1758 @@ +package server + +import ( + "bufio" + "encoding/json" + "fmt" + "net" + "os" + "path/filepath" + "strconv" + "strings" + "sync" + "sync/atomic" + "syscall" + "time" + + "github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation" + "github.com/AvengeMedia/DankMaterialShell/core/internal/log" + "github.com/AvengeMedia/DankMaterialShell/core/internal/server/apppicker" + "github.com/AvengeMedia/DankMaterialShell/core/internal/server/bluez" + "github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness" + "github.com/AvengeMedia/DankMaterialShell/core/internal/server/clipboard" + "github.com/AvengeMedia/DankMaterialShell/core/internal/server/cups" + serverDbus "github.com/AvengeMedia/DankMaterialShell/core/internal/server/dbus" + "github.com/AvengeMedia/DankMaterialShell/core/internal/server/dwl" + "github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev" + "github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace" + "github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop" + "github.com/AvengeMedia/DankMaterialShell/core/internal/server/location" + "github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl" + "github.com/AvengeMedia/DankMaterialShell/core/internal/server/models" + "github.com/AvengeMedia/DankMaterialShell/core/internal/server/network" + "github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode" + "github.com/AvengeMedia/DankMaterialShell/core/internal/server/trayrecovery" + "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland" + "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext" + "github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlroutput" + "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" +) + +const APIVersion = 24 + +var CLIVersion = "dev" + +type Capabilities struct { + Capabilities []string `json:"capabilities"` +} + +type ServerInfo struct { + APIVersion int `json:"apiVersion"` + CLIVersion string `json:"cliVersion,omitempty"` + Capabilities []string `json:"capabilities"` +} + +type ServiceEvent struct { + Service string `json:"service"` + Data any `json:"data"` +} + +var networkManager *network.Manager +var loginctlManager *loginctl.Manager +var freedesktopManager *freedesktop.Manager +var waylandManager *wayland.Manager +var bluezManager *bluez.Manager +var appPickerManager *apppicker.Manager +var cupsManager *cups.Manager +var dwlManager *dwl.Manager +var extWorkspaceManager *extworkspace.Manager +var brightnessManager *brightness.Manager +var wlrOutputManager *wlroutput.Manager +var evdevManager *evdev.Manager +var clipboardManager *clipboard.Manager +var dbusManager *serverDbus.Manager +var wlContext *wlcontext.SharedContext +var themeModeManager *thememode.Manager +var trayRecoveryManager *trayrecovery.Manager +var locationManager *location.Manager +var geoClientInstance geolocation.Client + +const dbusClientID = "dms-dbus-client" + +var capabilitySubscribers syncmap.Map[string, chan ServerInfo] +var cupsSubscribers syncmap.Map[string, bool] +var cupsSubscriberCount atomic.Int32 +var extWorkspaceAvailable atomic.Bool +var extWorkspaceInitMutex sync.Mutex + +func getSocketDir() string { + if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" { + return runtime + } + + if os.Getuid() == 0 { + if _, err := os.Stat("/run"); err == nil { + return "/run/dankdots" + } + return "/var/run/dankdots" + } + + return os.TempDir() +} + +func GetSocketPath() string { + return filepath.Join(getSocketDir(), fmt.Sprintf("danklinux-%d.sock", os.Getpid())) +} + +func FindSocket() (string, error) { + dir := getSocketDir() + entries, err := os.ReadDir(dir) + if err != nil { + return "", err + } + + for _, entry := range entries { + if strings.HasPrefix(entry.Name(), "danklinux-") && strings.HasSuffix(entry.Name(), ".sock") { + return filepath.Join(dir, entry.Name()), nil + } + } + return "", fmt.Errorf("no dms socket found") +} + +func cleanupStaleSockets() { + dir := getSocketDir() + entries, err := os.ReadDir(dir) + if err != nil { + return + } + + for _, entry := range entries { + if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".sock") { + continue + } + + pidStr := strings.TrimPrefix(entry.Name(), "danklinux-") + pidStr = strings.TrimSuffix(pidStr, ".sock") + pid, err := strconv.Atoi(pidStr) + if err != nil { + continue + } + + process, err := os.FindProcess(pid) + if err != nil { + socketPath := filepath.Join(dir, entry.Name()) + os.Remove(socketPath) + log.Debugf("Removed stale socket: %s", socketPath) + continue + } + + err = process.Signal(syscall.Signal(0)) + if err != nil { + socketPath := filepath.Join(dir, entry.Name()) + os.Remove(socketPath) + log.Debugf("Removed stale socket: %s", socketPath) + } + } +} + +func InitializeNetworkManager() error { + manager, err := network.NewManager() + if err != nil { + log.Warnf("Failed to initialize network manager: %v", err) + return err + } + + networkManager = manager + + log.Info("Network manager initialized") + return nil +} + +func InitializeLoginctlManager() error { + manager, err := loginctl.NewManager() + if err != nil { + log.Warnf("Failed to initialize loginctl manager: %v", err) + return err + } + + loginctlManager = manager + + log.Info("Loginctl manager initialized") + return nil +} + +func InitializeFreedeskManager() error { + manager, err := freedesktop.NewManager() + if err != nil { + log.Warnf("Failed to initialize freedesktop manager: %v", err) + return err + } + + freedesktopManager = manager + + log.Info("Freedesktop manager initialized") + return nil +} + +func InitializeWaylandManager() error { + log.Info("Attempting to initialize Wayland gamma control...") + + if wlContext == nil { + ctx, err := wlcontext.New() + if err != nil { + log.Errorf("Failed to create shared Wayland context: %v", err) + return err + } + wlContext = ctx + } + + config := wayland.DefaultConfig() + manager, err := wayland.NewManager(wlContext.Display(), config) + if err != nil { + log.Errorf("Failed to initialize wayland manager: %v", err) + return err + } + + waylandManager = manager + + log.Info("Wayland gamma control initialized successfully") + return nil +} + +func InitializeBluezManager() error { + manager, err := bluez.NewManager() + if err != nil { + log.Warnf("Failed to initialize bluez manager: %v", err) + return err + } + + bluezManager = manager + + log.Info("Bluez manager initialized") + return nil +} + +func InitializeAppPickerManager() error { + manager := apppicker.NewManager() + appPickerManager = manager + log.Info("AppPicker manager initialized") + return nil +} + +func InitializeCupsManager() error { + manager, err := cups.NewManager() + if err != nil { + log.Warnf("Failed to initialize cups manager: %v", err) + return err + } + + cupsManager = manager + + log.Info("CUPS manager initialized") + return nil +} + +func InitializeDwlManager() error { + log.Info("Attempting to initialize DWL IPC...") + + if wlContext == nil { + ctx, err := wlcontext.New() + if err != nil { + log.Errorf("Failed to create shared Wayland context: %v", err) + return err + } + wlContext = ctx + } + + manager, err := dwl.NewManager(wlContext.Display()) + if err != nil { + log.Debug("Failed to initialize dwl manager: %v", err) + return err + } + + dwlManager = manager + + log.Info("DWL IPC initialized successfully") + return nil +} + +func InitializeBrightnessManager() error { + manager, err := brightness.NewManager() + if err != nil { + log.Warnf("Failed to initialize brightness manager: %v", err) + return err + } + + brightnessManager = manager + + log.Info("Brightness manager initialized") + return nil +} + +func InitializeExtWorkspaceManager() error { + log.Info("Attempting to initialize ExtWorkspace...") + + if wlContext == nil { + ctx, err := wlcontext.New() + if err != nil { + log.Errorf("Failed to create shared Wayland context: %v", err) + return err + } + wlContext = ctx + } + + manager, err := extworkspace.NewManager(wlContext.Display()) + if err != nil { + log.Debug("Failed to initialize extworkspace manager: %v", err) + return err + } + + extWorkspaceManager = manager + + log.Info("ExtWorkspace initialized successfully") + return nil +} + +func InitializeWlrOutputManager() error { + log.Info("Attempting to initialize WlrOutput management...") + + if wlContext == nil { + ctx, err := wlcontext.New() + if err != nil { + log.Errorf("Failed to create shared Wayland context: %v", err) + return err + } + wlContext = ctx + } + + manager, err := wlroutput.NewManager(wlContext.Display()) + if err != nil { + log.Debug("Failed to initialize wlroutput manager: %v", err) + return err + } + + wlrOutputManager = manager + + log.Info("WlrOutput management initialized successfully") + return nil +} + +func InitializeEvdevManager() error { + manager, err := evdev.InitializeManager() + if err != nil { + log.Warnf("Failed to initialize evdev manager: %v", err) + return err + } + + evdevManager = manager + + log.Info("Evdev manager initialized") + return nil +} + +func InitializeClipboardManager() error { + log.Info("Attempting to initialize clipboard manager...") + + if wlContext == nil { + ctx, err := wlcontext.New() + if err != nil { + log.Errorf("Failed to create shared Wayland context: %v", err) + return err + } + wlContext = ctx + } + + config := clipboard.LoadConfig() + manager, err := clipboard.NewManager(wlContext, config) + if err != nil { + log.Errorf("Failed to initialize clipboard manager: %v", err) + return err + } + + clipboardManager = manager + + log.Info("Clipboard manager initialized successfully") + return nil +} + +func InitializeDbusManager() error { + manager, err := serverDbus.NewManager() + if err != nil { + log.Warnf("Failed to initialize dbus manager: %v", err) + return err + } + + dbusManager = manager + + log.Info("DBus manager initialized") + return nil +} + +func InitializeThemeModeManager() error { + manager := thememode.NewManager() + themeModeManager = manager + + log.Info("Theme mode automation manager initialized") + return nil +} + +func InitializeTrayRecoveryManager() error { + manager, err := trayrecovery.NewManager() + if err != nil { + return err + } + + trayRecoveryManager = manager + + log.Info("TrayRecovery manager initialized") + return nil +} + +func InitializeLocationManager(geoClient geolocation.Client) error { + manager, err := location.NewManager(geoClient) + if err != nil { + log.Warnf("Failed to initialize location manager: %v", err) + return err + } + + locationManager = manager + + log.Info("Location manager initialized") + return nil +} + +func handleConnection(conn net.Conn) { + defer conn.Close() + + caps := getCapabilities() + capsData, _ := json.Marshal(caps) + conn.Write(capsData) + conn.Write([]byte("\n")) + scanner := bufio.NewScanner(conn) + for scanner.Scan() { + line := scanner.Bytes() + + var req models.Request + if err := json.Unmarshal(line, &req); err != nil { + log.Warnf("handleConnection: Failed to unmarshal JSON: %v, line: %s", err, string(line)) + models.RespondError(conn, 0, "invalid json") + continue + } + + go RouteRequest(conn, req) + } +} + +func getCapabilities() Capabilities { + caps := []string{"plugins"} + + if networkManager != nil { + caps = append(caps, "network") + } + + if loginctlManager != nil { + caps = append(caps, "loginctl") + } + + if freedesktopManager != nil { + caps = append(caps, "freedesktop") + } + + if waylandManager != nil { + caps = append(caps, "gamma") + } + + if bluezManager != nil { + caps = append(caps, "bluetooth") + } + + if appPickerManager != nil { + caps = append(caps, "browser") + } + + if cupsManager != nil { + caps = append(caps, "cups") + } + + if dwlManager != nil { + caps = append(caps, "dwl") + } + + if extWorkspaceAvailable.Load() { + caps = append(caps, "extworkspace") + } + + if brightnessManager != nil { + caps = append(caps, "brightness") + } + + if wlrOutputManager != nil { + caps = append(caps, "wlroutput") + } + + if evdevManager != nil { + caps = append(caps, "evdev") + } + + if clipboardManager != nil { + caps = append(caps, "clipboard") + } + + if themeModeManager != nil { + caps = append(caps, "theme.auto") + } + + if dbusManager != nil { + caps = append(caps, "dbus") + } + + return Capabilities{Capabilities: caps} +} + +func getServerInfo() ServerInfo { + caps := []string{"plugins"} + + if networkManager != nil { + caps = append(caps, "network") + } + + if loginctlManager != nil { + caps = append(caps, "loginctl") + } + + if freedesktopManager != nil { + caps = append(caps, "freedesktop") + } + + if waylandManager != nil { + caps = append(caps, "gamma") + } + + if bluezManager != nil { + caps = append(caps, "bluetooth") + } + + if appPickerManager != nil { + caps = append(caps, "browser") + } + + if cupsManager != nil { + caps = append(caps, "cups") + } + + if dwlManager != nil { + caps = append(caps, "dwl") + } + + if extWorkspaceAvailable.Load() { + caps = append(caps, "extworkspace") + } + + if brightnessManager != nil { + caps = append(caps, "brightness") + } + + if wlrOutputManager != nil { + caps = append(caps, "wlroutput") + } + + if evdevManager != nil { + caps = append(caps, "evdev") + } + + if clipboardManager != nil { + caps = append(caps, "clipboard") + } + + if themeModeManager != nil { + caps = append(caps, "theme.auto") + } + + if locationManager != nil { + caps = append(caps, "location") + } + + if dbusManager != nil { + caps = append(caps, "dbus") + } + + return ServerInfo{ + APIVersion: APIVersion, + CLIVersion: CLIVersion, + Capabilities: caps, + } +} + +func notifyCapabilityChange() { + info := getServerInfo() + capabilitySubscribers.Range(func(key string, ch chan ServerInfo) bool { + select { + case ch <- info: + default: + } + return true + }) +} + +func handleSubscribe(conn net.Conn, req models.Request) { + clientID := fmt.Sprintf("meta-client-%p", conn) + + var services []string + if servicesParam, ok := models.Get[[]any](req, "services"); ok { + for _, s := range servicesParam { + if str, ok := s.(string); ok { + services = append(services, str) + } + } + } + + if len(services) == 0 { + services = []string{"all"} + } + + subscribeAll := false + for _, s := range services { + if s == "all" { + subscribeAll = true + break + } + } + + var wg sync.WaitGroup + eventChan := make(chan ServiceEvent, 256) + stopChan := make(chan struct{}) + + capChan := make(chan ServerInfo, 64) + capabilitySubscribers.Store(clientID+"-capabilities", capChan) + + wg.Add(1) + go func() { + defer wg.Done() + defer capabilitySubscribers.Delete(clientID + "-capabilities") + + for { + select { + case info, ok := <-capChan: + if !ok { + return + } + select { + case eventChan <- ServiceEvent{Service: "server", Data: info}: + case <-stopChan: + return + } + case <-stopChan: + return + } + } + }() + + shouldSubscribe := func(service string) bool { + if subscribeAll { + return true + } + for _, s := range services { + if s == service { + return true + } + } + return false + } + + if shouldSubscribe("network") && networkManager != nil { + wg.Add(1) + netChan := networkManager.Subscribe(clientID + "-network") + go func() { + defer wg.Done() + defer networkManager.Unsubscribe(clientID + "-network") + + initialState := networkManager.GetState() + select { + case eventChan <- ServiceEvent{Service: "network", Data: initialState}: + case <-stopChan: + return + } + + for { + select { + case state, ok := <-netChan: + if !ok { + return + } + select { + case eventChan <- ServiceEvent{Service: "network", Data: state}: + case <-stopChan: + return + } + case <-stopChan: + return + } + } + }() + } + + if shouldSubscribe("network.credentials") && networkManager != nil { + wg.Add(1) + credChan := networkManager.SubscribeCredentials(clientID + "-credentials") + go func() { + defer wg.Done() + defer networkManager.UnsubscribeCredentials(clientID + "-credentials") + + for { + select { + case prompt, ok := <-credChan: + if !ok { + return + } + select { + case eventChan <- ServiceEvent{Service: "network.credentials", Data: prompt}: + case <-stopChan: + return + } + case <-stopChan: + return + } + } + }() + } + + if shouldSubscribe("loginctl") && loginctlManager != nil { + wg.Add(1) + loginChan := loginctlManager.Subscribe(clientID + "-loginctl") + go func() { + defer wg.Done() + defer loginctlManager.Unsubscribe(clientID + "-loginctl") + + initialState := loginctlManager.GetState() + select { + case eventChan <- ServiceEvent{Service: "loginctl", Data: initialState}: + case <-stopChan: + return + } + + for { + select { + case state, ok := <-loginChan: + if !ok { + return + } + select { + case eventChan <- ServiceEvent{Service: "loginctl", Data: state}: + case <-stopChan: + return + } + case <-stopChan: + return + } + } + }() + } + + if shouldSubscribe("freedesktop") && freedesktopManager != nil { + wg.Add(1) + freedesktopChan := freedesktopManager.Subscribe(clientID + "-freedesktop") + go func() { + defer wg.Done() + defer freedesktopManager.Unsubscribe(clientID + "-freedesktop") + + initialState := freedesktopManager.GetState() + select { + case eventChan <- ServiceEvent{Service: "freedesktop", Data: initialState}: + case <-stopChan: + return + } + + for { + select { + case state, ok := <-freedesktopChan: + if !ok { + return + } + select { + case eventChan <- ServiceEvent{Service: "freedesktop", Data: state}: + case <-stopChan: + return + } + case <-stopChan: + return + } + } + }() + } + + if shouldSubscribe("freedesktop.screensaver") && freedesktopManager != nil { + wg.Add(1) + screensaverChan := freedesktopManager.SubscribeScreensaver(clientID + "-screensaver") + go func() { + defer wg.Done() + defer freedesktopManager.UnsubscribeScreensaver(clientID + "-screensaver") + + initialState := freedesktopManager.GetScreensaverState() + select { + case eventChan <- ServiceEvent{Service: "freedesktop.screensaver", Data: initialState}: + case <-stopChan: + return + } + + for { + select { + case state, ok := <-screensaverChan: + if !ok { + return + } + select { + case eventChan <- ServiceEvent{Service: "freedesktop.screensaver", Data: state}: + case <-stopChan: + return + } + case <-stopChan: + return + } + } + }() + } + + if shouldSubscribe("gamma") && waylandManager != nil { + wg.Add(1) + waylandChan := waylandManager.Subscribe(clientID + "-gamma") + go func() { + defer wg.Done() + defer waylandManager.Unsubscribe(clientID + "-gamma") + + initialState := waylandManager.GetState() + select { + case eventChan <- ServiceEvent{Service: "gamma", Data: initialState}: + case <-stopChan: + return + } + + for { + select { + case state, ok := <-waylandChan: + if !ok { + return + } + select { + case eventChan <- ServiceEvent{Service: "gamma", Data: state}: + case <-stopChan: + return + } + case <-stopChan: + return + } + } + }() + } + + if shouldSubscribe("theme.auto") && themeModeManager != nil { + wg.Add(1) + themeAutoChan := themeModeManager.Subscribe(clientID + "-theme-auto") + go func() { + defer wg.Done() + defer themeModeManager.Unsubscribe(clientID + "-theme-auto") + + initialState := themeModeManager.GetState() + select { + case eventChan <- ServiceEvent{Service: "theme.auto", Data: initialState}: + case <-stopChan: + return + } + + for { + select { + case state, ok := <-themeAutoChan: + if !ok { + return + } + select { + case eventChan <- ServiceEvent{Service: "theme.auto", Data: state}: + case <-stopChan: + return + } + case <-stopChan: + return + } + } + }() + } + + if shouldSubscribe("bluetooth") && bluezManager != nil { + wg.Add(1) + bluezChan := bluezManager.Subscribe(clientID + "-bluetooth") + go func() { + defer wg.Done() + defer bluezManager.Unsubscribe(clientID + "-bluetooth") + + initialState := bluezManager.GetState() + select { + case eventChan <- ServiceEvent{Service: "bluetooth", Data: initialState}: + case <-stopChan: + return + } + + for { + select { + case state, ok := <-bluezChan: + if !ok { + return + } + select { + case eventChan <- ServiceEvent{Service: "bluetooth", Data: state}: + case <-stopChan: + return + } + case <-stopChan: + return + } + } + }() + } + + if shouldSubscribe("bluetooth.pairing") && bluezManager != nil { + wg.Add(1) + pairingChan := bluezManager.SubscribePairing(clientID + "-pairing") + go func() { + defer wg.Done() + defer bluezManager.UnsubscribePairing(clientID + "-pairing") + + for { + select { + case prompt, ok := <-pairingChan: + if !ok { + return + } + select { + case eventChan <- ServiceEvent{Service: "bluetooth.pairing", Data: prompt}: + case <-stopChan: + return + } + case <-stopChan: + return + } + } + }() + } + + if shouldSubscribe("browser") && appPickerManager != nil { + wg.Add(1) + appPickerChan := appPickerManager.Subscribe(clientID + "-browser") + go func() { + defer wg.Done() + defer appPickerManager.Unsubscribe(clientID + "-browser") + + for { + select { + case event, ok := <-appPickerChan: + if !ok { + return + } + select { + case eventChan <- ServiceEvent{Service: "browser.open_requested", Data: event}: + case <-stopChan: + return + } + case <-stopChan: + return + } + } + }() + } + + if shouldSubscribe("cups") { + cupsSubscribers.Store(clientID+"-cups", true) + count := cupsSubscriberCount.Add(1) + + if count == 1 { + if err := InitializeCupsManager(); err != nil { + log.Warnf("Failed to initialize CUPS manager for subscription: %v", err) + } else { + notifyCapabilityChange() + } + } + + if cupsManager != nil { + wg.Add(1) + cupsChan := cupsManager.Subscribe(clientID + "-cups") + go func() { + defer wg.Done() + defer func() { + cupsManager.Unsubscribe(clientID + "-cups") + cupsSubscribers.Delete(clientID + "-cups") + count := cupsSubscriberCount.Add(-1) + + if count == 0 { + log.Info("Last CUPS subscriber disconnected, shutting down CUPS manager") + if cupsManager != nil { + cupsManager.Close() + cupsManager = nil + notifyCapabilityChange() + } + } + }() + + initialState := cupsManager.GetState() + select { + case eventChan <- ServiceEvent{Service: "cups", Data: initialState}: + case <-stopChan: + return + } + + for { + select { + case state, ok := <-cupsChan: + if !ok { + return + } + select { + case eventChan <- ServiceEvent{Service: "cups", Data: state}: + case <-stopChan: + return + } + case <-stopChan: + return + } + } + }() + } + } + + if shouldSubscribe("dwl") && dwlManager != nil { + wg.Add(1) + dwlChan := dwlManager.Subscribe(clientID + "-dwl") + go func() { + defer wg.Done() + defer dwlManager.Unsubscribe(clientID + "-dwl") + + initialState := dwlManager.GetState() + select { + case eventChan <- ServiceEvent{Service: "dwl", Data: initialState}: + case <-stopChan: + return + } + + for { + select { + case state, ok := <-dwlChan: + if !ok { + return + } + select { + case eventChan <- ServiceEvent{Service: "dwl", Data: state}: + case <-stopChan: + return + } + case <-stopChan: + return + } + } + }() + } + + if shouldSubscribe("extworkspace") { + if extWorkspaceManager == nil && extWorkspaceAvailable.Load() { + extWorkspaceInitMutex.Lock() + if extWorkspaceManager == nil { + if err := InitializeExtWorkspaceManager(); err != nil { + log.Warnf("Failed to initialize ExtWorkspace manager for subscription: %v", err) + } + } + extWorkspaceInitMutex.Unlock() + } + + if extWorkspaceManager != nil { + wg.Add(1) + extWorkspaceChan := extWorkspaceManager.Subscribe(clientID + "-extworkspace") + go func() { + defer wg.Done() + defer extWorkspaceManager.Unsubscribe(clientID + "-extworkspace") + + initialState := extWorkspaceManager.GetState() + select { + case eventChan <- ServiceEvent{Service: "extworkspace", Data: initialState}: + case <-stopChan: + return + } + + for { + select { + case state, ok := <-extWorkspaceChan: + if !ok { + return + } + select { + case eventChan <- ServiceEvent{Service: "extworkspace", Data: state}: + case <-stopChan: + return + } + case <-stopChan: + return + } + } + }() + } + } + + if shouldSubscribe("brightness") && brightnessManager != nil { + wg.Add(2) + brightnessStateChan := brightnessManager.Subscribe(clientID + "-brightness-state") + brightnessUpdateChan := brightnessManager.SubscribeUpdates(clientID + "-brightness-updates") + + go func() { + defer wg.Done() + defer brightnessManager.Unsubscribe(clientID + "-brightness-state") + + initialState := brightnessManager.GetState() + select { + case eventChan <- ServiceEvent{Service: "brightness", Data: initialState}: + case <-stopChan: + return + } + + for { + select { + case state, ok := <-brightnessStateChan: + if !ok { + return + } + select { + case eventChan <- ServiceEvent{Service: "brightness", Data: state}: + case <-stopChan: + return + } + case <-stopChan: + return + } + } + }() + + go func() { + defer wg.Done() + defer brightnessManager.UnsubscribeUpdates(clientID + "-brightness-updates") + + for { + select { + case update, ok := <-brightnessUpdateChan: + if !ok { + return + } + select { + case eventChan <- ServiceEvent{Service: "brightness.update", Data: update}: + case <-stopChan: + return + } + case <-stopChan: + return + } + } + }() + } + + if shouldSubscribe("wlroutput") && wlrOutputManager != nil { + wg.Add(1) + wlrOutputChan := wlrOutputManager.Subscribe(clientID + "-wlroutput") + go func() { + defer wg.Done() + defer wlrOutputManager.Unsubscribe(clientID + "-wlroutput") + + initialState := wlrOutputManager.GetState() + select { + case eventChan <- ServiceEvent{Service: "wlroutput", Data: initialState}: + case <-stopChan: + return + } + + for { + select { + case state, ok := <-wlrOutputChan: + if !ok { + return + } + select { + case eventChan <- ServiceEvent{Service: "wlroutput", Data: state}: + case <-stopChan: + return + } + case <-stopChan: + return + } + } + }() + } + + if shouldSubscribe("evdev") && evdevManager != nil { + wg.Add(1) + evdevChan := evdevManager.Subscribe(clientID + "-evdev") + go func() { + defer wg.Done() + defer evdevManager.Unsubscribe(clientID + "-evdev") + + initialState := evdevManager.GetState() + select { + case eventChan <- ServiceEvent{Service: "evdev", Data: initialState}: + case <-stopChan: + return + } + + for { + select { + case state, ok := <-evdevChan: + if !ok { + return + } + select { + case eventChan <- ServiceEvent{Service: "evdev", Data: state}: + case <-stopChan: + return + } + case <-stopChan: + return + } + } + }() + } + + if shouldSubscribe("clipboard") && clipboardManager != nil { + wg.Add(1) + clipboardChan := clipboardManager.Subscribe(clientID + "-clipboard") + go func() { + defer wg.Done() + defer clipboardManager.Unsubscribe(clientID + "-clipboard") + + initialState := clipboardManager.GetState() + select { + case eventChan <- ServiceEvent{Service: "clipboard", Data: initialState}: + case <-stopChan: + return + } + + for { + select { + case state, ok := <-clipboardChan: + if !ok { + return + } + select { + case eventChan <- ServiceEvent{Service: "clipboard", Data: state}: + case <-stopChan: + return + } + case <-stopChan: + return + } + } + }() + } + + if shouldSubscribe("dbus") && dbusManager != nil { + wg.Add(1) + dbusChan := dbusManager.SubscribeSignals(dbusClientID) + go func() { + defer wg.Done() + defer dbusManager.UnsubscribeSignals(dbusClientID) + + for { + select { + case event, ok := <-dbusChan: + if !ok { + return + } + select { + case eventChan <- ServiceEvent{Service: "dbus", Data: event}: + case <-stopChan: + return + } + case <-stopChan: + return + } + } + }() + } + + go func() { + wg.Wait() + close(eventChan) + }() + + info := getServerInfo() + if err := json.NewEncoder(conn).Encode(models.Response[ServiceEvent]{ + ID: req.ID, + Result: &ServiceEvent{Service: "server", Data: info}, + }); err != nil { + close(stopChan) + return + } + + for event := range eventChan { + if err := json.NewEncoder(conn).Encode(models.Response[ServiceEvent]{ + ID: req.ID, + Result: &event, + }); err != nil { + close(stopChan) + return + } + } +} + +func cleanupManagers() { + if networkManager != nil { + networkManager.Close() + } + if loginctlManager != nil { + loginctlManager.Close() + } + if freedesktopManager != nil { + freedesktopManager.Close() + } + if waylandManager != nil { + waylandManager.Close() + } + if bluezManager != nil { + bluezManager.Close() + } + if appPickerManager != nil { + appPickerManager.Close() + } + if cupsManager != nil { + cupsManager.Close() + } + if dwlManager != nil { + dwlManager.Close() + } + if extWorkspaceManager != nil { + extWorkspaceManager.Close() + } + if brightnessManager != nil { + brightnessManager.Close() + } + if wlrOutputManager != nil { + wlrOutputManager.Close() + } + if evdevManager != nil { + evdevManager.Close() + } + if clipboardManager != nil { + clipboardManager.Close() + } + if dbusManager != nil { + dbusManager.Close() + } + if themeModeManager != nil { + themeModeManager.Close() + } + if trayRecoveryManager != nil { + trayRecoveryManager.Close() + } + if wlContext != nil { + wlContext.Close() + } + if locationManager != nil { + locationManager.Close() + } + if geoClientInstance != nil { + geoClientInstance.Close() + } +} + +func Start(printDocs bool) error { + cleanupStaleSockets() + + socketPath := GetSocketPath() + os.Remove(socketPath) + + listener, err := net.Listen("unix", socketPath) + if err != nil { + return err + } + defer listener.Close() + defer cleanupManagers() + + log.Infof("DMS API Server listening on: %s", socketPath) + log.Infof("API Version: %d", APIVersion) + log.Info("Protocol: JSON over Unix socket") + log.Info("Request format: {\"id\": , \"method\": \"...\", \"params\": {...}}") + log.Info("Response format: {\"id\": , \"result\": {...}} or {\"id\": , \"error\": \"...\"}") + log.Info("") + if printDocs { + log.Info("Available methods:") + log.Info(" ping - Test connection") + log.Info(" getServerInfo - Get server info (API version and capabilities)") + log.Info(" subscribe - Subscribe to multiple services (params: services [default: all])") + log.Info("Plugins:") + log.Info(" plugins.list - List all plugins") + log.Info(" plugins.listInstalled - List installed plugins") + log.Info(" plugins.install - Install plugin (params: name)") + log.Info(" plugins.uninstall - Uninstall plugin (params: name)") + log.Info(" plugins.update - Update plugin (params: name)") + log.Info(" plugins.search - Search plugins (params: query, category?, compositor?, capability?)") + log.Info("Network:") + log.Info(" network.getState - Get current network state") + log.Info(" network.wifi.scan - Scan for WiFi networks (params: device?)") + log.Info(" network.wifi.networks - Get WiFi network list") + log.Info(" network.wifi.connect - Connect to WiFi (params: ssid, password?, username?, device?, eapMethod?, phase2Auth?, caCertPath?, clientCertPath?, privateKeyPath?, useSystemCACerts?)") + log.Info(" network.wifi.disconnect - Disconnect WiFi (params: device?)") + log.Info(" network.wifi.forget - Forget network (params: ssid)") + log.Info(" network.wifi.toggle - Toggle WiFi radio") + log.Info(" network.wifi.enable - Enable WiFi") + log.Info(" network.wifi.disable - Disable WiFi") + log.Info(" network.wifi.setAutoconnect - Set network autoconnect (params: ssid, autoconnect)") + log.Info(" network.ethernet.connect - Connect Ethernet") + log.Info(" network.ethernet.connect.config - Connect Ethernet to a specific configuration") + log.Info(" network.ethernet.disconnect - Disconnect Ethernet") + log.Info(" network.vpn.profiles - List VPN profiles") + log.Info(" network.vpn.active - List active VPN connections") + log.Info(" network.vpn.connect - Connect VPN (params: uuidOrName|name|uuid, singleActive?)") + log.Info(" network.vpn.disconnect - Disconnect VPN (params: uuidOrName|name|uuid)") + log.Info(" network.vpn.disconnectAll - Disconnect all VPNs") + log.Info(" network.vpn.clearCredentials - Clear saved VPN credentials (params: uuidOrName|name|uuid)") + log.Info(" network.vpn.plugins - List available VPN plugins") + log.Info(" network.vpn.import - Import VPN from file (params: file|path, name?)") + log.Info(" network.vpn.getConfig - Get VPN configuration (params: uuid|name|uuidOrName)") + log.Info(" network.vpn.updateConfig - Update VPN configuration (params: uuid, name?, autoconnect?, data?)") + log.Info(" network.vpn.delete - Delete VPN connection (params: uuid|name|uuidOrName)") + log.Info(" network.preference.set - Set preference (params: preference [auto|wifi|ethernet])") + log.Info(" network.info - Get network info (params: ssid)") + log.Info(" network.credentials.submit - Submit credentials for prompt (params: token, secrets, save?)") + log.Info(" network.credentials.cancel - Cancel credential prompt (params: token)") + log.Info(" network.subscribe - Subscribe to network state changes (streaming)") + log.Info("Loginctl:") + log.Info(" loginctl.getState - Get current session state") + log.Info(" loginctl.lock - Lock session") + log.Info(" loginctl.unlock - Unlock session") + log.Info(" loginctl.activate - Activate session") + log.Info(" loginctl.setIdleHint - Set idle hint (params: idle)") + log.Info(" loginctl.setLockBeforeSuspend - Set lock before suspend (params: enabled)") + log.Info(" loginctl.setSleepInhibitorEnabled - Enable/disable sleep inhibitor (params: enabled)") + log.Info(" loginctl.lockerReady - Signal locker UI is ready (releases sleep inhibitor)") + log.Info(" loginctl.terminate - Terminate session") + log.Info(" loginctl.subscribe - Subscribe to session state changes (streaming)") + log.Info("Freedesktop:") + log.Info(" freedesktop.getState - Get accounts & settings state") + log.Info(" freedesktop.accounts.setIconFile - Set profile icon (params: path)") + log.Info(" freedesktop.accounts.setRealName - Set real name (params: name)") + log.Info(" freedesktop.accounts.setEmail - Set email (params: email)") + log.Info(" freedesktop.accounts.setLanguage - Set language (params: language)") + log.Info(" freedesktop.accounts.setLocation - Set location (params: location)") + log.Info(" freedesktop.accounts.getUserIconFile - Get user icon (params: username)") + log.Info(" freedesktop.settings.getColorScheme - Get color scheme") + log.Info(" freedesktop.settings.setIconTheme - Set icon theme (params: iconTheme)") + log.Info("Wayland:") + log.Info(" wayland.gamma.getState - Get current gamma control state") + log.Info(" wayland.gamma.setTemperature - Set temperature range (params: low, high)") + log.Info(" wayland.gamma.setLocation - Set location (params: latitude, longitude)") + log.Info(" wayland.gamma.setManualTimes - Set manual times (params: sunrise, sunset)") + log.Info(" wayland.gamma.setGamma - Set gamma value (params: gamma)") + log.Info(" wayland.gamma.setEnabled - Enable/disable gamma control (params: enabled)") + log.Info(" wayland.gamma.subscribe - Subscribe to gamma state changes (streaming)") + log.Info("Theme automation:") + log.Info(" theme.auto.getState - Get current theme automation state") + log.Info(" theme.auto.setEnabled - Enable/disable theme automation (params: enabled)") + log.Info(" theme.auto.setMode - Set automation mode (params: mode [time|location])") + log.Info(" theme.auto.setSchedule - Set time schedule (params: startHour, startMinute, endHour, endMinute)") + log.Info(" theme.auto.setLocation - Set location (params: latitude, longitude)") + log.Info(" theme.auto.setUseIPLocation - Use IP location (params: use)") + log.Info(" theme.auto.trigger - Trigger immediate re-evaluation") + log.Info(" theme.auto.subscribe - Subscribe to theme automation state changes (streaming)") + log.Info("Bluetooth:") + log.Info(" bluetooth.getState - Get current bluetooth state") + log.Info(" bluetooth.startDiscovery - Start device discovery") + log.Info(" bluetooth.stopDiscovery - Stop device discovery") + log.Info(" bluetooth.setPowered - Set adapter power state (params: powered)") + log.Info(" bluetooth.pair - Pair with device (params: device)") + log.Info(" bluetooth.connect - Connect to device (params: device)") + log.Info(" bluetooth.disconnect - Disconnect from device (params: device)") + log.Info(" bluetooth.remove - Remove/unpair device (params: device)") + log.Info(" bluetooth.trust - Trust device (params: device)") + log.Info(" bluetooth.untrust - Untrust device (params: device)") + log.Info(" bluetooth.pairing.submit - Submit pairing response (params: token, secrets?, accept?)") + log.Info(" bluetooth.pairing.cancel - Cancel pairing prompt (params: token)") + log.Info(" bluetooth.subscribe - Subscribe to bluetooth state changes (streaming)") + log.Info("CUPS:") + log.Info(" cups.getPrinters - Get printers list") + log.Info(" cups.getJobs - Get non-completed jobs list (params: printerName)") + log.Info(" cups.pausePrinter - Pause printer (params: printerName)") + log.Info(" cups.resumePrinter - Resume printer (params: printerName)") + log.Info(" cups.cancelJob - Cancel job (params: printerName, jobID)") + log.Info(" cups.purgeJobs - Cancel all jobs (params: printerName)") + log.Info("DWL:") + log.Info(" dwl.getState - Get current dwl state (tags, windows, layouts, keyboard)") + log.Info(" dwl.setTags - Set active tags (params: output, tagmask, toggleTagset)") + log.Info(" dwl.setClientTags - Set focused client tags (params: output, andTags, xorTags)") + log.Info(" dwl.setLayout - Set layout (params: output, index)") + log.Info(" dwl.subscribe - Subscribe to dwl state changes (streaming)") + log.Info(" Output state includes:") + log.Info(" - tags : Tag states (active, clients, focused)") + log.Info(" - layoutSymbol : Current layout name") + log.Info(" - title : Focused window title") + log.Info(" - appId : Focused window app ID") + log.Info(" - kbLayout : Current keyboard layout") + log.Info(" - keymode : Current keybind mode") + log.Info("ExtWorkspace:") + log.Info(" extworkspace.getState - Get current workspace state (groups, workspaces)") + log.Info(" extworkspace.activateWorkspace - Activate workspace (params: groupID, workspaceID)") + log.Info(" extworkspace.deactivateWorkspace - Deactivate workspace (params: groupID, workspaceID)") + log.Info(" extworkspace.removeWorkspace - Remove workspace (params: groupID, workspaceID)") + log.Info(" extworkspace.createWorkspace - Create workspace (params: groupID, name)") + log.Info(" extworkspace.subscribe - Subscribe to workspace state changes (streaming)") + log.Info("Brightness:") + log.Info(" brightness.getState - Get current brightness state for all devices") + log.Info(" brightness.setBrightness - Set device brightness (params: device, percent)") + log.Info(" brightness.increment - Increment device brightness (params: device, step?)") + log.Info(" brightness.decrement - Decrement device brightness (params: device, step?)") + log.Info(" brightness.rescan - Rescan for brightness devices (e.g., after plugging in monitor)") + log.Info(" brightness.subscribe - Subscribe to brightness state changes (streaming)") + log.Info(" Subscription events:") + log.Info(" - brightness : Full device list (on rescan, DDC discovery, device changes)") + log.Info(" - brightness.update: Single device update (on brightness change for efficiency)") + log.Info("WlrOutput:") + log.Info(" wlroutput.getState - Get current output configuration state") + log.Info(" wlroutput.applyConfiguration - Apply output configuration (params: heads)") + log.Info(" wlroutput.testConfiguration - Test output configuration without applying (params: heads)") + log.Info(" wlroutput.subscribe - Subscribe to output state changes (streaming)") + log.Info(" Head configuration params:") + log.Info(" - name : Output name (required)") + log.Info(" - enabled : Enable/disable output (required)") + log.Info(" - modeId : Mode ID from available modes (optional)") + log.Info(" - customMode : Custom mode {width, height, refresh} (optional)") + log.Info(" - position : Position {x, y} (optional)") + log.Info(" - transform : Transform value (optional)") + log.Info(" - scale : Scale value (optional)") + log.Info(" - adaptiveSync : Adaptive sync state (optional)") + log.Info("Evdev:") + log.Info(" evdev.getState - Get current evdev state (caps lock)") + log.Info(" evdev.subscribe - Subscribe to evdev state changes (streaming)") + log.Info("Clipboard:") + log.Info(" clipboard.getState - Get clipboard state (enabled, history, current)") + log.Info(" clipboard.getHistory - Get clipboard history with previews") + log.Info(" clipboard.getEntry - Get full entry by ID (params: id)") + log.Info(" clipboard.deleteEntry - Delete entry by ID (params: id)") + log.Info(" clipboard.clearHistory - Clear all clipboard history") + log.Info(" clipboard.copy - Copy text to clipboard (params: text)") + log.Info(" clipboard.paste - Get current clipboard text") + log.Info(" clipboard.search - Search history (params: query?, mimeType?, isImage?, limit?, offset?, before?, after?)") + log.Info(" clipboard.getConfig - Get clipboard configuration") + log.Info(" clipboard.setConfig - Set configuration (params: maxHistory?, maxEntrySize?, autoClearDays?, clearAtStartup?)") + log.Info(" clipboard.subscribe - Subscribe to clipboard state changes (streaming)") + log.Info("Location:") + log.Info(" location.getState - Get current location state") + log.Info(" location.subscribe - Subscribe to location changes (streaming)") + log.Info("") + } + log.Info("Initializing managers...") + log.Info("") + + go func() { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + + if err := InitializeNetworkManager(); err != nil { + log.Warnf("Network manager unavailable: %v", err) + } else { + notifyCapabilityChange() + return + } + + for range ticker.C { + if networkManager != nil { + return + } + if err := InitializeNetworkManager(); err == nil { + log.Info("Network manager initialized") + notifyCapabilityChange() + return + } + } + }() + + loginctlReady := make(chan struct{}) + freedesktopReady := make(chan struct{}) + + go func() { + defer close(loginctlReady) + if err := InitializeLoginctlManager(); err != nil { + log.Warnf("Loginctl manager unavailable: %v", err) + } else { + notifyCapabilityChange() + } + }() + + go func() { + defer close(freedesktopReady) + if err := InitializeFreedeskManager(); err != nil { + log.Warnf("Freedesktop manager unavailable: %v", err) + } else if freedesktopManager != nil { + freedesktopManager.NotifySubscribers() + notifyCapabilityChange() + } + }() + + // Bridge loginctl lock state to the freedesktop/gnome screensaver + // ActiveChanged signal so apps like Bitwarden can detect screen lock. + go func() { + <-loginctlReady + <-freedesktopReady + + if loginctlManager == nil || freedesktopManager == nil { + return + } + + ch := loginctlManager.Subscribe("dms-lock-bridge") + defer loginctlManager.Unsubscribe("dms-lock-bridge") + + initial := loginctlManager.GetState() + lastLocked := initial.Locked + freedesktopManager.SetScreenLockActive(lastLocked) + + for state := range ch { + if state.Locked != lastLocked { + lastLocked = state.Locked + freedesktopManager.SetScreenLockActive(lastLocked) + } + } + }() + + if err := InitializeWaylandManager(); err != nil { + log.Warnf("Wayland manager unavailable: %v", err) + } + + if err := InitializeThemeModeManager(); err != nil { + log.Warnf("Theme mode manager unavailable: %v", err) + } else { + notifyCapabilityChange() + go func() { + <-loginctlReady + if loginctlManager == nil { + return + } + themeModeManager.WatchLoginctl(loginctlManager) + }() + } + + go func() { + <-loginctlReady + if loginctlManager == nil { + return + } + if err := InitializeTrayRecoveryManager(); err != nil { + log.Warnf("TrayRecovery manager unavailable: %v", err) + } else { + trayRecoveryManager.WatchLoginctl(loginctlManager) + } + }() + + go func() { + geoClient := geolocation.NewClient() + geoClientInstance = geoClient + + if waylandManager != nil { + waylandManager.SetGeoClient(geoClient) + } + if themeModeManager != nil { + themeModeManager.SetGeoClient(geoClient) + } + + if err := InitializeLocationManager(geoClient); err != nil { + log.Warnf("Location manager unavailable: %v", err) + } else { + notifyCapabilityChange() + } + }() + + go func() { + if err := InitializeBluezManager(); err != nil { + log.Warnf("Bluez manager unavailable: %v", err) + } else { + notifyCapabilityChange() + } + }() + + if err := InitializeAppPickerManager(); err != nil { + log.Debugf("AppPicker manager unavailable: %v", err) + } + + if err := InitializeDwlManager(); err != nil { + log.Debugf("DWL manager unavailable: %v", err) + } + + if extworkspace.CheckCapability() { + extWorkspaceAvailable.Store(true) + log.Info("ExtWorkspace capability detected and will be available on subscription") + } else { + log.Debug("ExtWorkspace capability not available") + extWorkspaceAvailable.Store(false) + } + + if err := InitializeWlrOutputManager(); err != nil { + log.Debugf("WlrOutput manager unavailable: %v", err) + } + + fatalErrChan := make(chan error, 1) + if wlrOutputManager != nil { + go func() { + err := <-wlrOutputManager.FatalError() + fatalErrChan <- fmt.Errorf("WlrOutput fatal error: %w", err) + }() + } + if wlContext != nil { + go func() { + err := <-wlContext.FatalError() + fatalErrChan <- fmt.Errorf("wayland context fatal error: %w", err) + }() + } + + go func() { + if err := InitializeBrightnessManager(); err != nil { + log.Warnf("Brightness manager unavailable: %v", err) + } else { + notifyCapabilityChange() + } + }() + + go func() { + if err := InitializeEvdevManager(); err != nil { + log.Debugf("Evdev manager unavailable: %v", err) + } else { + notifyCapabilityChange() + } + }() + + go func() { + if err := InitializeClipboardManager(); err != nil { + log.Warnf("Clipboard manager unavailable: %v", err) + } + if wlContext != nil { + wlContext.Start() + log.Info("Wayland event dispatcher started") + } + }() + + go func() { + if err := InitializeDbusManager(); err != nil { + log.Warnf("DBus manager unavailable: %v", err) + } else { + notifyCapabilityChange() + } + }() + + log.Info("") + log.Infof("Ready! Capabilities: %v", getCapabilities().Capabilities) + + listenerErrChan := make(chan error, 1) + + go func() { + for { + conn, err := listener.Accept() + if err != nil { + listenerErrChan <- err + return + } + go handleConnection(conn) + } + }() + + select { + case err := <-listenerErrChan: + return err + case err := <-fatalErrChan: + return err + } +} -- cgit v1.3