mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-05 21:15:38 -05:00
* feat: add browser picker for opening URLs - Introduce a QML modal allowing users to select a web browser to open a given URL. - Add a CLI command `dms open <url>` that sends a `browser.open` request to the DMS server. - Implement server‑side Browser manager, request handling, and subscription handling to propagate open events to clients. - Extend router and server initialization to register the new “browser” capability and include it in advertised capabilities. - Expose `openUrlRequested` signal in DMSService.qml and connect it to the modal for seamless UI activation. - Add a desktop entry for the Browser Picker and update the active subscriptions list to include the browser service. * fix(browser-picker): resolve QML errors in BrowserPickerModal and DMSShell * fix(browser-picker): fix socket discovery in dms open command * feat: add keyboard navigation and dynamic model to browser picker - Replace the static browsers array with a ListModel built from AppSearchService, ensuring robust iteration and future‑proofing of the browser list. - Introduce keyboard navigation (arrow keys and Enter) using selectedIndex and gridColumns, allowing users to select a browser without a mouse. - Reset URL, selected index, and navigation flag when the modal closes to avoid stale state. - Redesign the grid layout to compute cell width from columns, improve focus handling, and use AppLauncherGridDelegate for a consistent UI. - Enhance delegate behavior to update selection on hover and reset keyboard navigation state appropriately. * feat: add searchable list/grid view to browser picker - Introduce view mode setting (list or grid) saved in SettingsData for persistent user preference - Add search field with real‑time filtering to quickly locate a browser by name - Sort browsers by usage frequency from AppUsageHistoryData, falling back to alphabetical order - Provide UI toggle buttons to switch between list and grid layouts, updating the stored setting - Adjust keyboard navigation logic to support both layouts and improve focus handling - Refine modal dimensions and header layout for better visual consistency - Record launched browser usage to keep usage rankings up‑to‑date. * feat(browser-picker): improve UX with search, view persistence, and usage tracking Enhance BrowserPickerModal to match AppLauncher design and functionality: UI/UX Improvements: - Add search bar with DankTextField for filtering browsers - Move view mode switcher (list/grid) to header next to title - Persist view mode preference to SettingsData.browserPickerViewMode - Match AppLauncher dimensions (520x500) - Add proper spacing between list items - Improve URL display with truncation (single line, elide middle) - Remove redundant close button Functionality: - Implement separate browser usage tracking in SettingsData.browserUsageHistory - Sort browsers by most recently used (independent from app launcher stats) - Add keyboard navigation auto-scrolling for list and grid views - Track usage count, last used timestamp, and browser name - Filter browsers by search query Technical: - Add ensureVisible() functions to DankListView and DankGridView - Store browser usage with count, lastUsed, and name fields - Update browser list reactively on search query changes * feat(browser-picker): use appLauncherGridColumns setting for grid layout Make browser picker grid view respect the same column setting as the app launcher for consistent UI across both components. * refactor: make browser picker extensible for any MIME type/category Refactor browser picker into a generic, reusable application picker system that can handle any MIME type or application category, similar to Junction. This addresses the maintainer feedback about making the functionality "as re-usable as possible." Frontend (QML): - Create generic AppPickerModal component (~450 lines) - Configurable filtering by application categories - Customizable title, view modes, and usage tracking - Emits applicationSelected signal for flexibility - Refactor BrowserPickerModal as thin wrapper (473 → 46 lines) - Demonstrates how to create specialized pickers - Maintains all existing browser picker functionality Backend (Go): - Rename browser package to apppicker for clarity - Enhance event model to support: - MIME types (for future file associations) - Application categories (WebBrowser, Office, Graphics, etc.) - Request types (url, file, custom) - Maintain backward compatibility with browser.open method - Add new apppicker.open method for generic usage CLI: - Rename commands_browser.go to commands_open.go - Add extensibility flags: --mime/-m: Filter by MIME type --category/-c: Filter by category (repeatable) --type/-t: Specify request type - Examples: dms open file.pdf --category Office dms open image.png --category Graphics DMSService: - Add appPickerRequested signal for generic events - Smart routing between URL and generic app picker events - Fully backward compatible Benefits: - Easy to create new pickers (~15 lines of wrapper code) - Foundation for universal file handling system - Consistent UX across all picker types - Ready for MIME type associations Future extensions: - PDF picker, image viewer picker, text editor picker - Default application management - File association UI in settings - Multi-MIME type desktop file integration * fix(cli): remove all shorthands from open command flags for consistency Remove shorthands from --mime, --category, and --type flags to maintain consistency and avoid conflicts with global flags. Flags now (all long-form only): - --category: Application categories - --mime: MIME type - --type: Request type Global flags still available: - --config, -c: Config directory path * style: apply gofmt formatting to apppicker files Fix formatting issues caught by CI: - Align struct field spacing in OpenEvent - Align variable declaration spacing - Fix Args field alignment in cobra.Command * feat(apppicker): add generic file opener with auto MIME detection Implements Junction-style generic file opening capabilities: **Backend (Go):** - Enhanced CLI to parse file:// URIs and extract file paths - Auto-detect MIME types from file extensions using Go's mime package - Auto-map MIME types to desktop categories: - Images → Graphics, Viewer - Videos → Video, AudioVideo - Audio → Audio, AudioVideo - Text → TextEditor, Office (or WebBrowser for HTML) - PDFs → Office, Viewer - Office docs → Office - Archives → Archiving, Utility - Added debug logging to CLI and server handler for troubleshooting **Frontend (QML):** - Added generic AppPickerModal (filePickerModal) for file selection - Connected to DMSService.appPickerRequested signal - Implemented onApplicationSelected handler with desktop entry field code support: - %f/%F for file paths - %u/%U for file:// URIs - Fallback to appending path if no field codes - Separate usage tracking: filePickerUsageHistory **Desktop Integration:** - Updated dms-open.desktop to handle x-scheme-handler/file - Changed category from Network;WebBrowser to Utility (more generic) - Added text/html to MIME types **Usage:** Set DMS as default for specific MIME types in ~/.config/mimeapps.list: text/plain=dms-open.desktop image/png=dms-open.desktop application/pdf=dms-open.desktop Then use: xdg-open file.txt xdg-open image.png dms open document.pdf The picker will show appropriate apps based on auto-detected categories. Related to #815 * fix: resolve relative path handling by converting to absolute paths - Convert file:// URIs to absolute filesystem paths for reliable file resolution - Convert plain local file arguments to absolute paths to ensure consistent processing - Update log messages to display absolute paths, improving traceability - Retain request type detection while using absolute path extensions for MIME type inference * feat(app-picker): add Tab key view toggle and fix targetData binding - Add Tab key to toggle between grid and list views for better keyboard UX - Fix bug where targetData binding broke after first modal close - Removed targetData reset from onDialogClosed - Parent components (BrowserPickerModal, filePickerModal) now manage targetData - Fixes issue where URL/file path disappeared on subsequent opens * fix(app-picker): properly escape URLs and file paths for shell execution - Add shellEscape() function to wrap arguments in single quotes - Prevents shell interpretation of special characters (&, ?, =, spaces, etc.) - Fixes bug where URLs with query parameters were truncated at first & - Example: http://localhost:36275/vnc.html?autoconnect=true&reconnect=true now properly passes the full URL instead of cutting at first & - Applied to both BrowserPickerModal (URLs) and filePickerModal (file paths) * fix: check error return from InitializeAppPickerManager
1397 lines
38 KiB
Go
1397 lines
38 KiB
Go
package server
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"syscall"
|
|
"time"
|
|
|
|
"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/cups"
|
|
"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/loginctl"
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
|
"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 = 22
|
|
|
|
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 interface{} `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 wlContext *wlcontext.SharedContext
|
|
|
|
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 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")
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
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 := req.Params["services"].([]interface{}); 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("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("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
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
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 wlContext != nil {
|
|
wlContext.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\": <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.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("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("")
|
|
}
|
|
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
|
|
}
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
if err := InitializeLoginctlManager(); err != nil {
|
|
log.Warnf("Loginctl manager unavailable: %v", err)
|
|
} else {
|
|
notifyCapabilityChange()
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
if err := InitializeFreedeskManager(); err != nil {
|
|
log.Warnf("Freedesktop manager unavailable: %v", err)
|
|
} else if freedesktopManager != nil {
|
|
freedesktopManager.NotifySubscribers()
|
|
notifyCapabilityChange()
|
|
}
|
|
}()
|
|
|
|
if err := InitializeWaylandManager(); err != nil {
|
|
log.Warnf("Wayland manager unavailable: %v", err)
|
|
}
|
|
|
|
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()
|
|
}
|
|
}()
|
|
|
|
if wlContext != nil {
|
|
wlContext.Start()
|
|
log.Info("Wayland event dispatcher started")
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|