mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-27 21:47:50 -04:00
* feat(clipboard): clear only the filtered entries With a search query or type filter active, the clear button deleted every unpinned entry regardless of what the list was showing, so pruning a subset meant deleting entries one by one from the context menu. When the recents list is filtered, the button now clears exactly the entries it shows and the modal stays open on that filter; with no filter the previous behaviour is untouched. A filter that matches nothing does nothing at all, rather than falling back to clearing everything. The tooltip says "Clear Filtered" while a filter is active, and the confirmation names the count. Deleting one id at a time would have cost a round trip and a full state broadcast per entry, so this adds clipboard.deleteEntries: one bolt transaction, one state update, and it skips pinned entries server-side so a bulk delete can never take out saved items. Fixes #3198 * fix(clipboard): add translator context and list deleteEntries in IPC help Review follow-up. The two strings this branch adds carried no translator context: the "Clear Filtered" tooltip in ClipboardHeader and the confirmation body for a filtered clear in ClipboardHistoryContent. Both now say where they appear, and the second one names what %1 is, per CONTRIBUTING. "Clear All" on the same line is deliberately left alone. It is an existing shared term with four other call sites, so a context there would attach a clipboard-specific comment to a term the printer tab also uses. clipboard.deleteEntries was handled but absent from the IPC method listing that `dms debug-srv` prints, which left it undiscoverable right next to clipboard.deleteEntry. Catalogs untouched. * chore: re-trigger CI The lint-and-test failure on21cefbfis an unrelated flake: TestStoreEntry_CorruptDBReturnsErrorNotPanic died with SIGBUS in the bbolt mmap. That test comes from master (c68b7e5) and passes there; this branch only adds TestDeleteEntries_*/TestHandleDeleteEntries_* and touches manager.go additively. * clipboard: survive mmap faults from corrupted db pages --------- Co-authored-by: bbedward <[email protected]>
1761 lines
48 KiB
Go
1761 lines
48 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"runtime/debug"
|
|
"slices"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
|
|
"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/evdev"
|
|
"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/notifyactions"
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/sysupdate"
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/tailscale"
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/thememode"
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/trayrecovery"
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wallpaper"
|
|
"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/dankgo/ipc"
|
|
"github.com/AvengeMedia/dankgo/paths"
|
|
"github.com/AvengeMedia/dankgo/syncmap"
|
|
)
|
|
|
|
const APIVersion = 32
|
|
|
|
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 tailscaleManager *tailscale.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 wallpaperManager *wallpaper.Manager
|
|
var trayRecoveryManager *trayrecovery.Manager
|
|
var locationManager *location.Manager
|
|
var sysUpdateManager *sysupdate.Manager
|
|
var notifyActionsManager *notifyactions.Manager
|
|
var geoClientInstance geolocation.Client
|
|
|
|
const dbusClientID = "dms-dbus-client"
|
|
|
|
var capabilitySubscribers syncmap.Map[string, chan ServerInfo]
|
|
var cupsMu sync.Mutex
|
|
var cupsSubscriberCount int
|
|
var cupsEverAvailable bool
|
|
|
|
var appPaths = paths.New("danklinux")
|
|
|
|
func GetSocketPath() string {
|
|
return appPaths.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
|
|
matugen.SetColorSchemeEchoHook(manager.ExpectColorSchemeEcho)
|
|
|
|
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
|
|
}
|
|
|
|
// Reports whether the cups capability was newly gained, not whether a manager was created.
|
|
func initializeCupsManagerLocked() (bool, error) {
|
|
if cupsManager != nil {
|
|
return false, nil
|
|
}
|
|
manager, err := cups.NewManager()
|
|
if err != nil {
|
|
log.Warnf("Failed to initialize cups manager: %v", err)
|
|
return false, err
|
|
}
|
|
cupsManager = manager
|
|
log.Info("CUPS manager initialized")
|
|
gained := !cupsEverAvailable
|
|
cupsEverAvailable = true
|
|
return gained, nil
|
|
}
|
|
|
|
func ensureCupsManager() (*cups.Manager, error) {
|
|
cupsMu.Lock()
|
|
gained, err := initializeCupsManagerLocked()
|
|
mgr := cupsManager
|
|
cupsMu.Unlock()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if gained {
|
|
notifyCapabilityChange()
|
|
}
|
|
return mgr, nil
|
|
}
|
|
|
|
// Sticky: the manager is torn down when nobody is subscribed, but CUPS itself stays available.
|
|
func cupsAvailable() bool {
|
|
cupsMu.Lock()
|
|
defer cupsMu.Unlock()
|
|
return cupsEverAvailable
|
|
}
|
|
|
|
func releaseCupsSubscriber() {
|
|
cupsMu.Lock()
|
|
if cupsSubscriberCount > 0 {
|
|
cupsSubscriberCount--
|
|
}
|
|
var mgr *cups.Manager
|
|
if cupsSubscriberCount == 0 && cupsManager != nil {
|
|
mgr = cupsManager
|
|
cupsManager = nil
|
|
}
|
|
cupsMu.Unlock()
|
|
if mgr == nil {
|
|
return
|
|
}
|
|
log.Info("Last CUPS subscriber disconnected, shutting down CUPS manager")
|
|
mgr.Close()
|
|
}
|
|
|
|
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 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 InitializeWallpaperManager() error {
|
|
wallpaperManager = wallpaper.NewManager()
|
|
|
|
log.Info("Wallpaper rotation scheduler initialized")
|
|
return nil
|
|
}
|
|
|
|
func InitializeTrayRecoveryManager() error {
|
|
manager, err := trayrecovery.NewManager()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
trayRecoveryManager = manager
|
|
manager.WatchWatcherOwner()
|
|
|
|
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 InitializeNotifyActionsManager() error {
|
|
manager, err := notifyactions.NewManager()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
notifyActionsManager = manager
|
|
log.Info("Notification action manager initialized")
|
|
return nil
|
|
}
|
|
|
|
func InitializeSysUpdateManager() error {
|
|
manager, err := sysupdate.NewManager()
|
|
if err != nil {
|
|
log.Warnf("Failed to initialize sysupdate manager: %v", err)
|
|
return err
|
|
}
|
|
|
|
sysUpdateManager = manager
|
|
|
|
log.Info("Sysupdate manager initialized")
|
|
return nil
|
|
}
|
|
|
|
func routeHandler(_ context.Context, conn *models.Conn, req ipc.Request, _ *ipc.Subscriber) {
|
|
routeRequestRecovered(conn, models.Request(req))
|
|
}
|
|
|
|
func subscribeHandler(_ context.Context, conn *models.Conn, req ipc.Request, _ *ipc.Subscriber) {
|
|
switch req.Method {
|
|
case "subscribe":
|
|
routeRequestRecovered(conn, models.Request(req))
|
|
default:
|
|
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
|
}
|
|
}
|
|
|
|
// routeRequestRecovered keeps a panicking handler from taking down the whole daemon
|
|
func routeRequestRecovered(conn *models.Conn, req models.Request) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
log.Errorf("RouteRequest panic recovered: method=%s panic=%v\n%s", req.Method, r, debug.Stack())
|
|
models.RespondError(conn, req.ID, "internal server error")
|
|
}
|
|
}()
|
|
RouteRequest(conn, req)
|
|
}
|
|
|
|
func getCapabilities() Capabilities {
|
|
caps := []string{"plugins", "dgop"}
|
|
|
|
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 cupsAvailable() {
|
|
caps = append(caps, "cups")
|
|
}
|
|
|
|
if tailscaleManager != nil && tailscaleManager.IsAvailable() {
|
|
caps = append(caps, "tailscale")
|
|
}
|
|
|
|
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 wallpaperManager != nil {
|
|
caps = append(caps, "wallpaper")
|
|
}
|
|
|
|
if locationManager != nil {
|
|
caps = append(caps, "location")
|
|
}
|
|
|
|
if dbusManager != nil {
|
|
caps = append(caps, "dbus")
|
|
}
|
|
|
|
if sysUpdateManager != nil {
|
|
caps = append(caps, "sysupdate")
|
|
}
|
|
|
|
return Capabilities{Capabilities: caps}
|
|
}
|
|
|
|
func getServerInfo() ServerInfo {
|
|
return ServerInfo{
|
|
APIVersion: APIVersion,
|
|
CLIVersion: CLIVersion,
|
|
Capabilities: getCapabilities().Capabilities,
|
|
}
|
|
}
|
|
|
|
func notifyCapabilityChange() {
|
|
info := getServerInfo()
|
|
capabilitySubscribers.Range(func(key string, ch chan ServerInfo) bool {
|
|
select {
|
|
case ch <- info:
|
|
default:
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
func handleSubscribe(conn *models.Conn, req models.Request) {
|
|
clientID := fmt.Sprintf("meta-client-%p", conn)
|
|
|
|
dbusClient := dbusClientID
|
|
if id, ok := models.Get[string](req, "clientId"); ok && id != "" {
|
|
dbusClient = id
|
|
}
|
|
|
|
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 := slices.Contains(services, "all")
|
|
|
|
var wg sync.WaitGroup
|
|
eventChan := make(chan ServiceEvent, 256)
|
|
stopChan := make(chan struct{})
|
|
|
|
capChan := make(chan ServerInfo, 64)
|
|
capabilitySubscribers.Store(clientID+"-capabilities", capChan)
|
|
|
|
wg.Go(func() {
|
|
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
|
|
}
|
|
return slices.Contains(services, service)
|
|
}
|
|
|
|
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("wallpaper") && wallpaperManager != nil {
|
|
wg.Add(1)
|
|
wallpaperChan := wallpaperManager.Subscribe(clientID + "-wallpaper")
|
|
go func() {
|
|
defer wg.Done()
|
|
defer wallpaperManager.Unsubscribe(clientID + "-wallpaper")
|
|
|
|
initialState := wallpaperManager.GetState()
|
|
select {
|
|
case eventChan <- ServiceEvent{Service: "wallpaper", Data: initialState}:
|
|
case <-stopChan:
|
|
return
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case state, ok := <-wallpaperChan:
|
|
if !ok {
|
|
return
|
|
}
|
|
select {
|
|
case eventChan <- ServiceEvent{Service: "wallpaper", 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") {
|
|
cupsMu.Lock()
|
|
cupsSubscriberCount++
|
|
gained, err := initializeCupsManagerLocked()
|
|
mgr := cupsManager
|
|
cupsMu.Unlock()
|
|
|
|
if err != nil {
|
|
log.Warnf("Failed to initialize CUPS manager for subscription: %v", err)
|
|
} else if gained {
|
|
notifyCapabilityChange()
|
|
}
|
|
|
|
if mgr == nil {
|
|
releaseCupsSubscriber()
|
|
} else {
|
|
wg.Add(1)
|
|
cupsChan := mgr.Subscribe(clientID + "-cups")
|
|
go func() {
|
|
defer wg.Done()
|
|
defer func() {
|
|
mgr.Unsubscribe(clientID + "-cups")
|
|
releaseCupsSubscriber()
|
|
}()
|
|
|
|
initialState := mgr.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("tailscale") && tailscaleManager != nil && tailscaleManager.IsAvailable() {
|
|
wg.Add(1)
|
|
tailscaleChan := tailscaleManager.Subscribe(clientID + "-tailscale")
|
|
go func() {
|
|
defer wg.Done()
|
|
defer tailscaleManager.Unsubscribe(clientID + "-tailscale")
|
|
|
|
initialState := tailscaleManager.GetState()
|
|
select {
|
|
case eventChan <- ServiceEvent{Service: "tailscale", Data: initialState}:
|
|
case <-stopChan:
|
|
return
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case state, ok := <-tailscaleChan:
|
|
if !ok {
|
|
return
|
|
}
|
|
select {
|
|
case eventChan <- ServiceEvent{Service: "tailscale", 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("location") && locationManager != nil {
|
|
wg.Add(1)
|
|
locationChan := locationManager.Subscribe(clientID + "-location")
|
|
go func() {
|
|
defer wg.Done()
|
|
defer locationManager.Unsubscribe(clientID + "-location")
|
|
|
|
initialState := locationManager.GetState()
|
|
select {
|
|
case eventChan <- ServiceEvent{Service: "location", Data: initialState}:
|
|
case <-stopChan:
|
|
return
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case state, ok := <-locationChan:
|
|
if !ok {
|
|
return
|
|
}
|
|
select {
|
|
case eventChan <- ServiceEvent{Service: "location", Data: state}:
|
|
case <-stopChan:
|
|
return
|
|
}
|
|
case <-stopChan:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
if shouldSubscribe("sysupdate") && sysUpdateManager != nil {
|
|
wg.Add(1)
|
|
sysupdateChan := sysUpdateManager.Subscribe(clientID + "-sysupdate")
|
|
go func() {
|
|
defer wg.Done()
|
|
defer sysUpdateManager.Unsubscribe(clientID + "-sysupdate")
|
|
|
|
initialState := sysUpdateManager.GetState()
|
|
select {
|
|
case eventChan <- ServiceEvent{Service: "sysupdate", Data: initialState}:
|
|
case <-stopChan:
|
|
return
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case state, ok := <-sysupdateChan:
|
|
if !ok {
|
|
return
|
|
}
|
|
select {
|
|
case eventChan <- ServiceEvent{Service: "sysupdate", Data: state}:
|
|
case <-stopChan:
|
|
return
|
|
}
|
|
case <-stopChan:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
if shouldSubscribe("dbus") && dbusManager != nil {
|
|
wg.Add(1)
|
|
dbusChan := dbusManager.SubscribeSignals(dbusClient)
|
|
go func() {
|
|
defer wg.Done()
|
|
defer dbusManager.UnsubscribeSignals(dbusClient)
|
|
|
|
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 := conn.WriteResponse(models.Response[ServiceEvent]{
|
|
ID: req.ID,
|
|
Result: &ServiceEvent{Service: "server", Data: info},
|
|
}); err != nil {
|
|
close(stopChan)
|
|
return
|
|
}
|
|
|
|
for event := range eventChan {
|
|
if err := conn.WriteResponse(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()
|
|
}
|
|
cupsMu.Lock()
|
|
if cupsManager != nil {
|
|
cupsManager.Close()
|
|
cupsManager = nil
|
|
}
|
|
cupsMu.Unlock()
|
|
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 notifyActionsManager != nil {
|
|
notifyActionsManager.Close()
|
|
}
|
|
if themeModeManager != nil {
|
|
themeModeManager.Close()
|
|
}
|
|
if wallpaperManager != nil {
|
|
wallpaperManager.Close()
|
|
}
|
|
if trayRecoveryManager != nil {
|
|
trayRecoveryManager.Close()
|
|
}
|
|
if wlContext != nil {
|
|
wlContext.Close()
|
|
}
|
|
if locationManager != nil {
|
|
locationManager.Close()
|
|
}
|
|
if sysUpdateManager != nil {
|
|
sysUpdateManager.Close()
|
|
}
|
|
if geoClientInstance != nil {
|
|
geoClientInstance.Close()
|
|
}
|
|
if tailscaleManager != nil {
|
|
tailscaleManager.Close()
|
|
}
|
|
}
|
|
|
|
type Server struct {
|
|
ipc *ipc.Server
|
|
}
|
|
|
|
func New() *Server {
|
|
return &Server{ipc: ipc.NewServer(ipc.Config{
|
|
AppName: appPaths.Name,
|
|
APIVersion: APIVersion,
|
|
CapabilitiesFunc: func() []string { return getCapabilities().Capabilities },
|
|
MaxLineSize: 64 * 1024 * 1024, // large clipboard payloads
|
|
SubscribeHandler: subscribeHandler,
|
|
}, routeHandler)}
|
|
}
|
|
|
|
func (s *Server) Listen() error { return s.ipc.Listen() }
|
|
|
|
func (s *Server) SocketPath() string { return s.ipc.SocketPath() }
|
|
|
|
func (s *Server) Close() {
|
|
s.ipc.Close()
|
|
}
|
|
|
|
func Start(printDocs bool) error {
|
|
s := New()
|
|
if err := s.Listen(); err != nil {
|
|
return err
|
|
}
|
|
return s.Serve(printDocs)
|
|
}
|
|
|
|
func (s *Server) Serve(printDocs bool) error {
|
|
defer s.ipc.Close()
|
|
defer cleanupManagers()
|
|
|
|
tuneRuntime()
|
|
startPprof()
|
|
|
|
// Tailscale manager always starts — reconnects internally via WatchIPNBus.
|
|
// The capability is only advertised once tailscaled is reachable; the
|
|
// callback wakes capability subscribers so QML clients see it transition.
|
|
tailscaleManager = tailscale.NewManager("")
|
|
tailscaleManager.SetAvailabilityCallback(func(bool) {
|
|
notifyCapabilityChange()
|
|
})
|
|
|
|
log.Infof("DMS API Server listening on: %s", s.ipc.SocketPath())
|
|
log.Infof("API Version: %d", APIVersion)
|
|
log.Info("Protocol: JSON over Unix socket")
|
|
log.Info("Request format: {\"id\": <any>, \"method\": \"...\", \"params\": {...}}")
|
|
log.Info("Response format: {\"id\": <any>, \"result\": {...}} or {\"id\": <any>, \"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.cellular.connect - Connect Cellular")
|
|
log.Info(" network.cellular.connect.config - Connect Cellular to a specific configuration (params: uuid)")
|
|
log.Info(" network.cellular.disconnect - Disconnect Cellular (params: device?)")
|
|
log.Info(" network.cellular.toggle - Toggle Cellular radio")
|
|
log.Info(" network.cellular.enable - Enable Cellular")
|
|
log.Info(" network.cellular.disable - Disable Cellular")
|
|
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|cellular])")
|
|
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, durationMinutes)")
|
|
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 (params: adapter?)")
|
|
log.Info(" bluetooth.stopDiscovery - Stop device discovery (params: adapter?)")
|
|
log.Info(" bluetooth.setPowered - Set adapter power state (params: powered, adapter?)")
|
|
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("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.deleteEntries - Delete entries by ID, skipping pinned ones (params: ids)")
|
|
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("Notify:")
|
|
log.Info(" notify.watchAction - Open a file when a notification action fires (params: id, path)")
|
|
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() {
|
|
switch err := InitializeNetworkManager(); {
|
|
case err == nil:
|
|
notifyCapabilityChange()
|
|
return
|
|
case errors.Is(err, network.ErrNoNetworkBackend):
|
|
log.Warn("No supported network backend present; skipping retries")
|
|
return
|
|
default:
|
|
log.Warnf("Network manager unavailable, will retry: %v", err)
|
|
}
|
|
|
|
ticker := time.NewTicker(30 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for range 10 {
|
|
<-ticker.C
|
|
if networkManager != nil {
|
|
return
|
|
}
|
|
switch err := InitializeNetworkManager(); {
|
|
case err == nil:
|
|
log.Info("Network manager initialized")
|
|
notifyCapabilityChange()
|
|
return
|
|
case errors.Is(err, network.ErrNoNetworkBackend):
|
|
log.Warn("No supported network backend present; stopping retries")
|
|
return
|
|
}
|
|
}
|
|
log.Warn("Network manager still unavailable after retries; giving up")
|
|
}()
|
|
|
|
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)
|
|
}()
|
|
}
|
|
|
|
if err := InitializeWallpaperManager(); err != nil {
|
|
log.Warnf("Wallpaper scheduler unavailable: %v", err)
|
|
} else {
|
|
notifyCapabilityChange()
|
|
go func() {
|
|
<-loginctlReady
|
|
if loginctlManager == nil {
|
|
return
|
|
}
|
|
wallpaperManager.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() {
|
|
for {
|
|
err := InitializeBluezManager()
|
|
if err == nil {
|
|
notifyCapabilityChange()
|
|
return
|
|
}
|
|
log.Warnf("Bluez manager unavailable: %v", err)
|
|
if !errors.Is(err, bluez.ErrNoAdapter) {
|
|
return
|
|
}
|
|
if err := bluez.WaitForAdapter(); err != nil {
|
|
log.Warnf("Bluetooth adapter watch failed: %v", err)
|
|
return
|
|
}
|
|
log.Info("Bluetooth adapter appeared, initializing bluez manager")
|
|
}
|
|
}()
|
|
|
|
if err := InitializeAppPickerManager(); err != nil {
|
|
log.Debugf("AppPicker manager unavailable: %v", err)
|
|
}
|
|
|
|
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()
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
if err := InitializeNotifyActionsManager(); err != nil {
|
|
log.Warnf("Notification action manager unavailable: %v", err)
|
|
}
|
|
}()
|
|
|
|
if err := InitializeSysUpdateManager(); err != nil {
|
|
log.Warnf("Sysupdate manager unavailable: %v", err)
|
|
}
|
|
|
|
log.Info("")
|
|
log.Infof("Ready! Capabilities: %v", getCapabilities().Capabilities)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
serveErrChan := make(chan error, 1)
|
|
go func() {
|
|
serveErrChan <- s.ipc.Serve(ctx)
|
|
}()
|
|
|
|
select {
|
|
case err := <-serveErrChan:
|
|
return err
|
|
case err := <-fatalErrChan:
|
|
return err
|
|
}
|
|
}
|