diff --git a/app/demo/document.go b/app/demo/document.go new file mode 100644 index 0000000..9d0b97b --- /dev/null +++ b/app/demo/document.go @@ -0,0 +1,5 @@ +package main + +import "syscall/js" + +var document = js.Global().Get("document") diff --git a/app/demo/index.html b/app/demo/index.html new file mode 100644 index 0000000..ca18c54 --- /dev/null +++ b/app/demo/index.html @@ -0,0 +1,74 @@ + + + + + + + + + +
+ + +
+
+
+ +
+

+    
+ + + + diff --git a/app/demo/log.go b/app/demo/log.go new file mode 100644 index 0000000..cf3b9fe --- /dev/null +++ b/app/demo/log.go @@ -0,0 +1,42 @@ +package main + +import "syscall/js" + +type LogWriter struct { + Element js.Value + Container js.Value +} + +func (pw *LogWriter) ScrollToBottom() { + scrollHeight := pw.Container.Get("scrollHeight").Int() + clientHeight := pw.Container.Get("clientHeight").Int() + pw.Container.Set("scrollTop", scrollHeight-clientHeight) +} + +func (pw *LogWriter) IsScrolledToBottom() bool { + scrollHeight := pw.Container.Get("scrollHeight").Int() + clientHeight := pw.Container.Get("clientHeight").Int() + scrollTop := pw.Container.Get("scrollTop").Int() + return scrollHeight-clientHeight <= scrollTop+25 +} + +func (pw *LogWriter) Write(p []byte) (n int, err error) { + if !pw.Element.IsUndefined() { + currentText := pw.Element.Get("textContent").String() + newText := currentText + string(p) + pw.Element.Set("textContent", newText) + if pw.IsScrolledToBottom() { + pw.ScrollToBottom() + } + } + return len(p), nil +} + +func NewLogWriter(id string) *LogWriter { + element := document.Call("getElementById", id) + container := element.Get("parentElement") + return &LogWriter{ + Element: element, + Container: container, + } +} diff --git a/app/demo/main.go b/app/demo/main.go new file mode 100644 index 0000000..929abf7 --- /dev/null +++ b/app/demo/main.go @@ -0,0 +1,121 @@ +package main + +import ( + "context" + "fmt" + "log/slog" + + "github.com/jwetzell/showbridge-go" + "github.com/jwetzell/showbridge-go/internal/config" +) + +func main() { + + slog.SetLogLoggerLevel(slog.LevelDebug) + slog.SetDefault(slog.New(slog.NewTextHandler(NewLogWriter("logs"), &slog.HandlerOptions{ + Level: slog.LevelDebug, + }))) + + router, moduleConfigErrors, routeConfigErrors := showbridge.NewRouter(config.Config{ + Api: config.ApiConfig{ + Enabled: false, + Port: 0, + }, + Modules: []config.ModuleConfig{ + { + Id: "timer", + Type: "time.interval", + Params: map[string]any{ + "duration": 1000, + }, + }, + { + Id: "button1", + Type: "web.onclick", + Params: map[string]any{ + "id": "button1", + }, + }, + { + Id: "button2", + Type: "web.onclick", + Params: map[string]any{ + "id": "button2", + }, + }, + }, + Routes: []config.RouteConfig{ + { + Input: "timer", + Processors: []config.ProcessorConfig{ + { + Type: "debug.log", + }, + }, + }, + { + Input: "button1", + Processors: []config.ProcessorConfig{ + { + Type: "string.create", + Params: map[string]any{ + "template": "{{.Payload.UnixMilli}}", + }, + }, + { + Type: "debug.log", + }, + { + Type: "web.set", + Params: map[string]any{ + "id": "output1", + "property": "innerText", + "value": "Button1 Pressed @ {{.Payload}}", + }, + }, + }, + }, + { + Input: "button2", + Processors: []config.ProcessorConfig{ + { + Type: "string.create", + Params: map[string]any{ + "template": "{{.Payload.UnixMilli}}", + }, + }, + { + Type: "debug.log", + }, + { + Type: "web.set", + Params: map[string]any{ + "id": "output2", + "property": "innerText", + "value": "Button2 Pressed @ {{.Payload}}", + }, + }, + }, + }, + }, + }) + + if len(moduleConfigErrors) > 0 { + for _, err := range moduleConfigErrors { + println("Module config error:", err.Error) + } + } + + if len(routeConfigErrors) > 0 { + for _, err := range routeConfigErrors { + println("Route config error:", err.Error) + } + } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { + router.Start(ctx) + fmt.Println("router stopped") + }() + <-ctx.Done() +} diff --git a/internal/module/db-sqlite.go b/internal/module/db-sqlite.go index 1be8beb..ac2d1e0 100644 --- a/internal/module/db-sqlite.go +++ b/internal/module/db-sqlite.go @@ -1,3 +1,5 @@ +//go:build !js + package module import ( diff --git a/internal/module/http-server.go b/internal/module/http-server.go index ddb3d36..360b736 100644 --- a/internal/module/http-server.go +++ b/internal/module/http-server.go @@ -1,3 +1,5 @@ +//go:build !js + package module import ( diff --git a/internal/module/mqtt-client.go b/internal/module/mqtt-client.go index 31b8c5e..8853777 100644 --- a/internal/module/mqtt-client.go +++ b/internal/module/mqtt-client.go @@ -1,3 +1,5 @@ +//go:build !js + package module import ( diff --git a/internal/module/nats-client.go b/internal/module/nats-client.go index 20e869b..ff3109a 100644 --- a/internal/module/nats-client.go +++ b/internal/module/nats-client.go @@ -1,3 +1,5 @@ +//go:build !js + package module import ( diff --git a/internal/module/nats-server.go b/internal/module/nats-server.go index d1b714a..c99aa01 100644 --- a/internal/module/nats-server.go +++ b/internal/module/nats-server.go @@ -1,3 +1,5 @@ +//go:build !js + package module import ( diff --git a/internal/module/psn-client.go b/internal/module/psn-client.go index ebcec91..68a9937 100644 --- a/internal/module/psn-client.go +++ b/internal/module/psn-client.go @@ -1,3 +1,5 @@ +//go:build !js + package module import ( diff --git a/internal/module/redis-client.go b/internal/module/redis-client.go index 2fafc98..b02e213 100644 --- a/internal/module/redis-client.go +++ b/internal/module/redis-client.go @@ -1,3 +1,5 @@ +//go:build !js + package module import ( diff --git a/internal/module/sip-call-server.go b/internal/module/sip-call-server.go index 5c93aaf..d619f7b 100644 --- a/internal/module/sip-call-server.go +++ b/internal/module/sip-call-server.go @@ -1,3 +1,5 @@ +//go:build !js + package module import ( diff --git a/internal/module/sip-dtmf-server.go b/internal/module/sip-dtmf-server.go index e562d3f..4d19f69 100644 --- a/internal/module/sip-dtmf-server.go +++ b/internal/module/sip-dtmf-server.go @@ -1,3 +1,5 @@ +//go:build !js + package module import ( diff --git a/internal/module/tcp-client.go b/internal/module/tcp-client.go index 4e759fd..affc165 100644 --- a/internal/module/tcp-client.go +++ b/internal/module/tcp-client.go @@ -1,3 +1,5 @@ +//go:build !js + package module import ( diff --git a/internal/module/tcp-server.go b/internal/module/tcp-server.go index a3e40a7..e85c808 100644 --- a/internal/module/tcp-server.go +++ b/internal/module/tcp-server.go @@ -1,3 +1,5 @@ +//go:build !js + package module import ( diff --git a/internal/module/time-timer.go b/internal/module/time-timer.go index f765013..51dc650 100644 --- a/internal/module/time-timer.go +++ b/internal/module/time-timer.go @@ -64,7 +64,7 @@ func (t *TimeTimer) Start(ctx context.Context) error { router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO) if !ok { - return errors.New("net.tcp.client unable to get router from context") + return errors.New("time.timer unable to get router from context") } t.router = router moduleContext, cancel := context.WithCancel(ctx) diff --git a/internal/module/udp-client.go b/internal/module/udp-client.go index 29e4bd9..3e6553d 100644 --- a/internal/module/udp-client.go +++ b/internal/module/udp-client.go @@ -1,3 +1,5 @@ +//go:build !js + package module import ( diff --git a/internal/module/udp-multicast.go b/internal/module/udp-multicast.go index 0353ce0..e8e9a19 100644 --- a/internal/module/udp-multicast.go +++ b/internal/module/udp-multicast.go @@ -1,3 +1,5 @@ +//go:build !js + package module import ( diff --git a/internal/module/udp-server.go b/internal/module/udp-server.go index 3815784..b9c80a7 100644 --- a/internal/module/udp-server.go +++ b/internal/module/udp-server.go @@ -1,3 +1,5 @@ +//go:build !js + package module import ( diff --git a/internal/module/web-onclick.go b/internal/module/web-onclick.go new file mode 100644 index 0000000..58d3283 --- /dev/null +++ b/internal/module/web-onclick.go @@ -0,0 +1,91 @@ +//go:build js + +package module + +import ( + "context" + "errors" + "fmt" + "log/slog" + "syscall/js" + "time" + + "github.com/google/jsonschema-go/jsonschema" + "github.com/jwetzell/showbridge-go/internal/common" + "github.com/jwetzell/showbridge-go/internal/config" +) + +type WebOnClick struct { + config config.ModuleConfig + ctx context.Context + router common.RouteIO + logger *slog.Logger + ElementId string +} + +func init() { + RegisterModule(ModuleRegistration{ + Type: "web.onclick", + Title: "On Click", + ParamsSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "id": { + Title: "Element ID", + Type: "string", + Description: "ID of the HTML element to attach the click listener to", + }, + }, + Required: []string{"duration"}, + AdditionalProperties: nil, + }, + New: func(config config.ModuleConfig) (common.Module, error) { + params := config.Params + + idString, err := params.GetString("id") + if err != nil { + return nil, fmt.Errorf("web.onclick id error: %w", err) + } + + return &WebOnClick{ElementId: idString, config: config, logger: CreateLogger(config)}, nil + }, + }) +} + +func (woc *WebOnClick) Id() string { + return woc.config.Id +} + +func (woc *WebOnClick) Type() string { + return woc.config.Type +} + +func (woc *WebOnClick) Start(ctx context.Context) error { + woc.logger.Debug("running") + router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO) + + if !ok { + return errors.New("net.tcp.client unable to get router from context") + } + woc.router = router + woc.ctx = ctx + + element := js.Global().Get("document").Call("getElementById", woc.ElementId) + + if element.IsNull() || element.IsUndefined() { + return fmt.Errorf("web.onclick unable to find element with id: %s", woc.ElementId) + } + + element.Set("onclick", js.FuncOf(func(js.Value, []js.Value) interface{} { + if woc.router != nil { + woc.router.HandleInput(woc.ctx, woc.Id(), time.Now()) + } + return nil + })) + + <-ctx.Done() + return nil +} + +func (woc *WebOnClick) Stop() { +} diff --git a/internal/processor/artnet-packet-decode.go b/internal/processor/artnet-packet-decode.go index 692f422..f2abf9f 100644 --- a/internal/processor/artnet-packet-decode.go +++ b/internal/processor/artnet-packet-decode.go @@ -1,3 +1,5 @@ +//go:build !js + package processor import ( diff --git a/internal/processor/artnet-packet-encode.go b/internal/processor/artnet-packet-encode.go index 1a22ce7..0970d7d 100644 --- a/internal/processor/artnet-packet-encode.go +++ b/internal/processor/artnet-packet-encode.go @@ -1,3 +1,5 @@ +//go:build !js + package processor import ( diff --git a/internal/processor/http-request-do.go b/internal/processor/http-request-do.go index 1b89d98..a51f5d6 100644 --- a/internal/processor/http-request-do.go +++ b/internal/processor/http-request-do.go @@ -21,6 +21,11 @@ type HTTPRequestDo struct { URL *template.Template } +type HTTPResponse struct { + Status int + Body []byte +} + func (hrd *HTTPRequestDo) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) { templateData := wrappedPayload diff --git a/internal/processor/http-response-create.go b/internal/processor/http-response-create.go index 2a720f8..03f1fb0 100644 --- a/internal/processor/http-response-create.go +++ b/internal/processor/http-response-create.go @@ -1,3 +1,5 @@ +//go:build !js + package processor import ( @@ -17,11 +19,6 @@ type HTTPResponseCreate struct { config config.ProcessorConfig } -type HTTPResponse struct { - Status int - Body []byte -} - func (hrc *HTTPResponseCreate) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) { templateData := wrappedPayload diff --git a/internal/processor/mqtt-message-create.go b/internal/processor/mqtt-message-create.go index b0bf7cf..359c158 100644 --- a/internal/processor/mqtt-message-create.go +++ b/internal/processor/mqtt-message-create.go @@ -1,3 +1,5 @@ +//go:build !js + package processor import ( diff --git a/internal/processor/nats-message-create.go b/internal/processor/nats-message-create.go index c7210ae..0ca993f 100644 --- a/internal/processor/nats-message-create.go +++ b/internal/processor/nats-message-create.go @@ -1,3 +1,5 @@ +//go:build !js + package processor import ( diff --git a/internal/processor/script-js.go b/internal/processor/script-js.go index bbd35f1..7262dc0 100644 --- a/internal/processor/script-js.go +++ b/internal/processor/script-js.go @@ -1,3 +1,5 @@ +//go:build !js + package processor import ( diff --git a/internal/processor/script-wasm.go b/internal/processor/script-wasm.go index daaa76f..bc81111 100644 --- a/internal/processor/script-wasm.go +++ b/internal/processor/script-wasm.go @@ -1,3 +1,5 @@ +//go:build !js + package processor import ( diff --git a/internal/processor/sip-response-audio-create.go b/internal/processor/sip-response-audio-create.go index 91c1dd6..5f92f67 100644 --- a/internal/processor/sip-response-audio-create.go +++ b/internal/processor/sip-response-audio-create.go @@ -1,3 +1,5 @@ +//go:build !js + package processor import ( diff --git a/internal/processor/sip-response-dtmf-create.go b/internal/processor/sip-response-dtmf-create.go index a836bf3..3b48f74 100644 --- a/internal/processor/sip-response-dtmf-create.go +++ b/internal/processor/sip-response-dtmf-create.go @@ -1,3 +1,5 @@ +//go:build !js + package processor import ( diff --git a/internal/processor/web-set.go b/internal/processor/web-set.go new file mode 100644 index 0000000..258f5a4 --- /dev/null +++ b/internal/processor/web-set.go @@ -0,0 +1,102 @@ +//go:build js + +package processor + +import ( + "bytes" + "context" + "fmt" + "html/template" + "log/slog" + "syscall/js" + + "github.com/google/jsonschema-go/jsonschema" + "github.com/jwetzell/showbridge-go/internal/common" + "github.com/jwetzell/showbridge-go/internal/config" +) + +type WebSet struct { + config config.ProcessorConfig + ModuleId string + ElementId string + Property string + Value *template.Template + logger *slog.Logger +} + +func (kvs *WebSet) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) { + + element := js.Global().Get("document").Call("getElementById", kvs.ElementId) + + if element.IsNull() || element.IsUndefined() { + wrappedPayload.End = true + return wrappedPayload, fmt.Errorf("web.set unable to find element with id: %s", kvs.ElementId) + } + + var valueBuffer bytes.Buffer + err := kvs.Value.Execute(&valueBuffer, wrappedPayload) + + if err != nil { + wrappedPayload.End = true + return wrappedPayload, err + } + + element.Set(kvs.Property, valueBuffer.String()) + return wrappedPayload, nil +} + +func (kvs *WebSet) Type() string { + return kvs.config.Type +} + +func init() { + RegisterProcessor(ProcessorRegistration{ + Type: "web.set", + Title: "Set Web Element Property", + ParamsSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "id": { + Title: "Element ID", + Type: "string", + }, + "property": { + Title: "Property", + Type: "string", + }, + "value": { + Title: "Value", + Type: "string", + }, + }, + Required: []string{"id", "property", "value"}, + AdditionalProperties: nil, + }, + New: func(config config.ProcessorConfig) (Processor, error) { + + params := config.Params + + idString, err := params.GetString("id") + if err != nil { + return nil, fmt.Errorf("web.set id error: %w", err) + } + + propertyString, err := params.GetString("property") + if err != nil { + return nil, fmt.Errorf("web.set property error: %w", err) + } + + valueString, err := params.GetString("value") + if err != nil { + return nil, fmt.Errorf("web.set value error: %w", err) + } + valueTemplate, err := template.New("template").Parse(valueString) + + if err != nil { + return nil, err + } + + return &WebSet{config: config, ElementId: idString, Property: propertyString, Value: valueTemplate, logger: slog.Default().With("component", "processor", "type", config.Type)}, nil + }, + }) +}