From 5650bad796484af52039fa1f3dee479037ee64f0 Mon Sep 17 00:00:00 2001 From: Andrew Davis <1709934+Savid@users.noreply.github.com> Date: Sat, 29 Mar 2025 18:05:07 +1000 Subject: [PATCH] add jsonrpc keyboard macro get/set --- config.go | 61 ++++++++++++++++++++++++++++++++++ jsonrpc.go | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+) diff --git a/config.go b/config.go index 642f113..be85018 100644 --- a/config.go +++ b/config.go @@ -14,6 +14,65 @@ type WakeOnLanDevice struct { MacAddress string `json:"macAddress"` } +// Constants for keyboard macro limits +const ( + MaxMacrosPerDevice = 25 + MaxStepsPerMacro = 10 + MaxKeysPerStep = 10 + MinStepDelay = 50 + MaxStepDelay = 2000 +) + +type KeyboardMacroStep struct { + Keys []string `json:"keys"` + Modifiers []string `json:"modifiers"` + Delay int `json:"delay"` +} + +func (s *KeyboardMacroStep) Validate() error { + if len(s.Keys) > MaxKeysPerStep { + return fmt.Errorf("too many keys in step (max %d)", MaxKeysPerStep) + } + + if s.Delay < MinStepDelay { + s.Delay = MinStepDelay + } else if s.Delay > MaxStepDelay { + s.Delay = MaxStepDelay + } + + return nil +} + +type KeyboardMacro struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Steps []KeyboardMacroStep `json:"steps"` + SortOrder int `json:"sortOrder,omitempty"` +} + +func (m *KeyboardMacro) Validate() error { + if m.Name == "" { + return fmt.Errorf("macro name cannot be empty") + } + + if len(m.Steps) == 0 { + return fmt.Errorf("macro must have at least one step") + } + + if len(m.Steps) > MaxStepsPerMacro { + m.Steps = m.Steps[:MaxStepsPerMacro] + } + + for i := range m.Steps { + if err := m.Steps[i].Validate(); err != nil { + return fmt.Errorf("invalid step %d: %w", i+1, err) + } + } + + return nil +} + type Config struct { CloudURL string `json:"cloud_url"` CloudAppURL string `json:"cloud_app_url"` @@ -26,6 +85,7 @@ type Config struct { LocalAuthToken string `json:"local_auth_token"` LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"` + KeyboardMacros []KeyboardMacro `json:"keyboard_macros"` EdidString string `json:"hdmi_edid_string"` ActiveExtension string `json:"active_extension"` DisplayMaxBrightness int `json:"display_max_brightness"` @@ -43,6 +103,7 @@ var defaultConfig = &Config{ CloudAppURL: "https://app.jetkvm.com", AutoUpdateEnabled: true, // Set a default value ActiveExtension: "", + KeyboardMacros: []KeyboardMacro{}, DisplayMaxBrightness: 64, DisplayDimAfterSec: 120, // 2 minutes DisplayOffAfterSec: 1800, // 30 minutes diff --git a/jsonrpc.go b/jsonrpc.go index 64935e1..c5dbcf4 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -792,6 +792,101 @@ func rpcSetScrollSensitivity(sensitivity string) error { return nil } +func getKeyboardMacros() (interface{}, error) { + macros := make([]KeyboardMacro, len(config.KeyboardMacros)) + copy(macros, config.KeyboardMacros) + + return macros, nil +} + +type KeyboardMacrosParams struct { + Macros []interface{} `json:"macros"` +} + +func setKeyboardMacros(params KeyboardMacrosParams) (interface{}, error) { + if params.Macros == nil { + return nil, fmt.Errorf("missing or invalid macros parameter") + } + + newMacros := make([]KeyboardMacro, 0, len(params.Macros)) + + for i, item := range params.Macros { + macroMap, ok := item.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid macro at index %d", i) + } + + id, _ := macroMap["id"].(string) + if id == "" { + id = fmt.Sprintf("macro-%d", time.Now().UnixNano()) + } + + name, _ := macroMap["name"].(string) + description, _ := macroMap["description"].(string) + + sortOrder := i + 1 + if sortOrderFloat, ok := macroMap["sortOrder"].(float64); ok { + sortOrder = int(sortOrderFloat) + } + + steps := []KeyboardMacroStep{} + if stepsArray, ok := macroMap["steps"].([]interface{}); ok { + for _, stepItem := range stepsArray { + stepMap, ok := stepItem.(map[string]interface{}) + if !ok { + continue + } + + step := KeyboardMacroStep{} + + if keysArray, ok := stepMap["keys"].([]interface{}); ok { + for _, k := range keysArray { + if keyStr, ok := k.(string); ok { + step.Keys = append(step.Keys, keyStr) + } + } + } + + if modsArray, ok := stepMap["modifiers"].([]interface{}); ok { + for _, m := range modsArray { + if modStr, ok := m.(string); ok { + step.Modifiers = append(step.Modifiers, modStr) + } + } + } + + if delay, ok := stepMap["delay"].(float64); ok { + step.Delay = int(delay) + } + + steps = append(steps, step) + } + } + + macro := KeyboardMacro{ + ID: id, + Name: name, + Description: description, + Steps: steps, + SortOrder: sortOrder, + } + + if err := macro.Validate(); err != nil { + return nil, fmt.Errorf("invalid macro at index %d: %w", i, err) + } + + newMacros = append(newMacros, macro) + } + + config.KeyboardMacros = newMacros + + if err := SaveConfig(); err != nil { + return nil, err + } + + return nil, nil +} + var rpcHandlers = map[string]RPCHandler{ "ping": {Func: rpcPing}, "getDeviceID": {Func: rpcGetDeviceID}, @@ -857,4 +952,6 @@ var rpcHandlers = map[string]RPCHandler{ "setCloudUrl": {Func: rpcSetCloudUrl, Params: []string{"apiUrl", "appUrl"}}, "getScrollSensitivity": {Func: rpcGetScrollSensitivity}, "setScrollSensitivity": {Func: rpcSetScrollSensitivity, Params: []string{"sensitivity"}}, + "getKeyboardMacros": {Func: getKeyboardMacros}, + "setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}}, }