mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
working on vdom implementation, other fixes (#136)
This commit is contained in:
parent
e6f60ff210
commit
6c2ef6cb99
@ -3,47 +3,53 @@
|
|||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
type WaveAppStyle struct {
|
import (
|
||||||
BackgroundColor string `json:"backgroundColor,omitempty"`
|
"context"
|
||||||
Color string `json:"color,omitempty"`
|
"fmt"
|
||||||
Border string `json:"border,omitempty"`
|
"log"
|
||||||
FontSize string `json:"fontSize,omitempty"`
|
|
||||||
FontFamily string `json:"fontFamily,omitempty"`
|
|
||||||
FontWeight string `json:"fontWeight,omitempty"`
|
|
||||||
FontStyle string `json:"fontStyle,omitempty"`
|
|
||||||
TextDecoration string `json:"textDecoration,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type WaveAppMouseEvent struct {
|
"github.com/wavetermdev/thenextwave/pkg/vdom"
|
||||||
TargetId string `json:"targetid"`
|
"github.com/wavetermdev/thenextwave/pkg/wshutil"
|
||||||
}
|
)
|
||||||
|
|
||||||
type WaveAppChangeEvent struct {
|
func Page(ctx context.Context, props map[string]any) any {
|
||||||
TargetId string `json:"targetid"`
|
clicked, setClicked := vdom.UseState(ctx, false)
|
||||||
Value string `json:"value"`
|
var clickedDiv *vdom.Elem
|
||||||
}
|
if clicked {
|
||||||
|
clickedDiv = vdom.Bind(`<div>clicked</div>`, nil)
|
||||||
type WaveAppElement struct {
|
|
||||||
WaveId string `json:"waveid"`
|
|
||||||
Elem string `json:"elem"`
|
|
||||||
Props map[string]any `json:"props,omitempty"`
|
|
||||||
Handlers map[string]string `json:"handlers,omitempty"`
|
|
||||||
Children []*WaveAppElement `json:"children,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *WaveAppElement) AddChild(child *WaveAppElement) {
|
|
||||||
e.Children = append(e.Children, child)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *WaveAppElement) Style() *WaveAppStyle {
|
|
||||||
style, ok := e.Props["style"].(*WaveAppStyle)
|
|
||||||
if !ok {
|
|
||||||
style := &WaveAppStyle{}
|
|
||||||
e.Props["style"] = style
|
|
||||||
}
|
}
|
||||||
return style
|
clickFn := func() {
|
||||||
|
log.Printf("run clickFn\n")
|
||||||
|
setClicked(true)
|
||||||
|
}
|
||||||
|
return vdom.Bind(
|
||||||
|
`
|
||||||
|
<div>
|
||||||
|
<h1>hello world</h1>
|
||||||
|
<Button onClick="#bind:clickFn">hello</Button>
|
||||||
|
<bind key="clickedDiv"/>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
map[string]any{"clickFn": clickFn, "clickedDiv": clickedDiv},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Button(ctx context.Context, props map[string]any) any {
|
||||||
|
ref := vdom.UseRef(ctx, nil)
|
||||||
|
clName, setClName := vdom.UseState(ctx, "button")
|
||||||
|
vdom.UseEffect(ctx, func() func() {
|
||||||
|
fmt.Printf("Button useEffect\n")
|
||||||
|
setClName("button mounted")
|
||||||
|
return nil
|
||||||
|
}, nil)
|
||||||
|
return vdom.Bind(`
|
||||||
|
<div className="#bind:clName" ref="#bind:ref" onClick="#bind:onClick">
|
||||||
|
<bind key="children"/>
|
||||||
|
</div>
|
||||||
|
`, map[string]any{"clName": clName, "ref": ref, "onClick": props["onClick"], "children": props["children"]})
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
wshutil.SetTermRawModeAndInstallShutdownHandlers(true)
|
||||||
|
defer wshutil.RestoreTermState()
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
||||||
|
"github.com/wavetermdev/thenextwave/pkg/wshutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
var deleteBlockCmd = &cobra.Command{
|
var deleteBlockCmd = &cobra.Command{
|
||||||
@ -32,7 +33,7 @@ func deleteBlockRun(cmd *cobra.Command, args []string) {
|
|||||||
fmt.Printf("%v\n", err)
|
fmt.Printf("%v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setTermRawMode()
|
wshutil.SetTermRawModeAndInstallShutdownHandlers(true)
|
||||||
fullORef, err := resolveSimpleId(oref)
|
fullORef, err := resolveSimpleId(oref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("error resolving oref: %v\r\n", err)
|
fmt.Printf("error resolving oref: %v\r\n", err)
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wshrpc/wshclient"
|
"github.com/wavetermdev/thenextwave/pkg/wshrpc/wshclient"
|
||||||
|
"github.com/wavetermdev/thenextwave/pkg/wshutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
var getMetaCmd = &cobra.Command{
|
var getMetaCmd = &cobra.Command{
|
||||||
@ -37,7 +38,7 @@ func getMetaRun(cmd *cobra.Command, args []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setTermRawMode()
|
wshutil.SetTermRawModeAndInstallShutdownHandlers(true)
|
||||||
fullORef, err := resolveSimpleId(oref)
|
fullORef, err := resolveSimpleId(oref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("error resolving oref: %v\r\n", err)
|
fmt.Printf("error resolving oref: %v\r\n", err)
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/wavetermdev/thenextwave/pkg/wshutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -20,20 +21,20 @@ var htmlCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func htmlRun(cmd *cobra.Command, args []string) {
|
func htmlRun(cmd *cobra.Command, args []string) {
|
||||||
defer doShutdown("normal exit", 0)
|
defer wshutil.DoShutdown("normal exit", 0, true)
|
||||||
setTermHtmlMode()
|
setTermHtmlMode()
|
||||||
for {
|
for {
|
||||||
var buf [1]byte
|
var buf [1]byte
|
||||||
_, err := WrappedStdin.Read(buf[:])
|
_, err := WrappedStdin.Read(buf[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
doShutdown(fmt.Sprintf("stdin closed/error (%v)", err), 1)
|
wshutil.DoShutdown(fmt.Sprintf("stdin closed/error (%v)", err), 1, true)
|
||||||
}
|
}
|
||||||
if buf[0] == 0x03 {
|
if buf[0] == 0x03 {
|
||||||
doShutdown("read Ctrl-C from stdin", 1)
|
wshutil.DoShutdown("read Ctrl-C from stdin", 1, true)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if buf[0] == 'x' {
|
if buf[0] == 'x' {
|
||||||
doShutdown("read 'x' from stdin", 0)
|
wshutil.DoShutdown("read 'x' from stdin", 0, true)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
57
cmd/wsh/cmd/wshcmd-readfile.go
Normal file
57
cmd/wsh/cmd/wshcmd-readfile.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
||||||
|
"github.com/wavetermdev/thenextwave/pkg/wshrpc/wshclient"
|
||||||
|
"github.com/wavetermdev/thenextwave/pkg/wshutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
var readFileCmd = &cobra.Command{
|
||||||
|
Use: "readfile",
|
||||||
|
Short: "read a blockfile",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
Run: runReadFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(readFileCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runReadFile(cmd *cobra.Command, args []string) {
|
||||||
|
oref := args[0]
|
||||||
|
if oref == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "oref is required\r\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := validateEasyORef(oref)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wshutil.SetTermRawModeAndInstallShutdownHandlers(true)
|
||||||
|
fullORef, err := resolveSimpleId(oref)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error resolving oref: %v\r\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp64, err := wshclient.ReadFile(RpcClient, wshrpc.CommandFileData{ZoneId: fullORef.OID, FileName: args[1]}, &wshrpc.WshRpcCommandOpts{Timeout: 5000})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error reading file: %v\r\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := base64.StdEncoding.DecodeString(resp64)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "error decoding file: %v\r\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Print(string(resp))
|
||||||
|
}
|
@ -8,11 +8,8 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@ -21,7 +18,6 @@ import (
|
|||||||
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wshrpc/wshclient"
|
"github.com/wavetermdev/thenextwave/pkg/wshrpc/wshclient"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wshutil"
|
"github.com/wavetermdev/thenextwave/pkg/wshutil"
|
||||||
"golang.org/x/term"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -32,85 +28,37 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
var shutdownOnce sync.Once
|
|
||||||
var origTermState *term.State
|
|
||||||
var madeRaw bool
|
|
||||||
var usingHtmlMode bool
|
var usingHtmlMode bool
|
||||||
var shutdownSignalHandlersInstalled bool
|
|
||||||
var WrappedStdin io.Reader
|
var WrappedStdin io.Reader
|
||||||
var RpcClient *wshutil.WshRpc
|
var RpcClient *wshutil.WshRpc
|
||||||
|
|
||||||
func doShutdown(reason string, exitCode int) {
|
func extraShutdownFn() {
|
||||||
shutdownOnce.Do(func() {
|
if usingHtmlMode {
|
||||||
defer os.Exit(exitCode)
|
cmd := &wshrpc.CommandSetMetaData{
|
||||||
if reason != "" {
|
Meta: map[string]any{"term:mode": nil},
|
||||||
log.Printf("shutting down: %s\r\n", reason)
|
|
||||||
}
|
}
|
||||||
if usingHtmlMode {
|
RpcClient.SendCommand(wshrpc.Command_SetMeta, cmd)
|
||||||
cmd := &wshrpc.CommandSetMetaData{
|
time.Sleep(10 * time.Millisecond)
|
||||||
Meta: map[string]any{"term:mode": nil},
|
}
|
||||||
}
|
|
||||||
RpcClient.SendCommand(wshrpc.Command_SetMeta, cmd)
|
|
||||||
time.Sleep(10 * time.Millisecond)
|
|
||||||
}
|
|
||||||
if origTermState != nil {
|
|
||||||
term.Restore(int(os.Stdin.Fd()), origTermState)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns the wrapped stdin and a new rpc client (that wraps the stdin input and stdout output)
|
// returns the wrapped stdin and a new rpc client (that wraps the stdin input and stdout output)
|
||||||
func setupRpcClient(handlerFn wshutil.CommandHandlerFnType) {
|
func setupRpcClient(handlerFn wshutil.CommandHandlerFnType) {
|
||||||
log.Printf("setup rpc client\r\n")
|
log.Printf("setup rpc client\r\n")
|
||||||
messageCh := make(chan []byte, 32)
|
RpcClient, WrappedStdin = wshutil.SetupTerminalRpcClient(handlerFn)
|
||||||
outputCh := make(chan []byte, 32)
|
|
||||||
ptyBuf := wshutil.MakePtyBuffer(wshutil.WaveServerOSCPrefix, os.Stdin, messageCh)
|
|
||||||
rpcClient := wshutil.MakeWshRpc(messageCh, outputCh, wshutil.RpcContext{}, handlerFn)
|
|
||||||
go func() {
|
|
||||||
for msg := range outputCh {
|
|
||||||
barr := wshutil.EncodeWaveOSCBytes(wshutil.WaveOSC, msg)
|
|
||||||
os.Stdout.Write(barr)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
WrappedStdin = ptyBuf
|
|
||||||
RpcClient = rpcClient
|
|
||||||
}
|
|
||||||
|
|
||||||
func setTermRawMode() {
|
|
||||||
if madeRaw {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
origState, err := term.MakeRaw(int(os.Stdin.Fd()))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error setting raw mode: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
origTermState = origState
|
|
||||||
madeRaw = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func setTermHtmlMode() {
|
func setTermHtmlMode() {
|
||||||
installShutdownSignalHandlers()
|
wshutil.SetExtraShutdownFunc(extraShutdownFn)
|
||||||
setTermRawMode()
|
wshutil.SetTermRawModeAndInstallShutdownHandlers(true)
|
||||||
cmd := &wshrpc.CommandSetMetaData{
|
cmd := &wshrpc.CommandSetMetaData{
|
||||||
Meta: map[string]any{"term:mode": "html"},
|
Meta: map[string]any{"term:mode": "html"},
|
||||||
}
|
}
|
||||||
RpcClient.SendCommand(wshrpc.Command_SetMeta, cmd)
|
err := RpcClient.SendCommand(wshrpc.Command_SetMeta, cmd)
|
||||||
usingHtmlMode = true
|
if err != nil {
|
||||||
}
|
fmt.Fprintf(os.Stderr, "Error setting html mode: %v\r\n", err)
|
||||||
|
|
||||||
func installShutdownSignalHandlers() {
|
|
||||||
if shutdownSignalHandlersInstalled {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
sigCh := make(chan os.Signal, 1)
|
usingHtmlMode = true
|
||||||
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGINT)
|
|
||||||
go func() {
|
|
||||||
for sig := range sigCh {
|
|
||||||
doShutdown(fmt.Sprintf("got signal %v", sig), 1)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var oidRe = regexp.MustCompile(`^[0-9a-f]{8}$`)
|
var oidRe = regexp.MustCompile(`^[0-9a-f]{8}$`)
|
||||||
@ -162,7 +110,7 @@ func resolveSimpleId(id string) (*waveobj.ORef, error) {
|
|||||||
|
|
||||||
// Execute executes the root command.
|
// Execute executes the root command.
|
||||||
func Execute() error {
|
func Execute() error {
|
||||||
defer doShutdown("", 0)
|
defer wshutil.DoShutdown("", 0, false)
|
||||||
setupRpcClient(nil)
|
setupRpcClient(nil)
|
||||||
return rootCmd.Execute()
|
return rootCmd.Execute()
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
||||||
|
"github.com/wavetermdev/thenextwave/pkg/wshutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
var setMetaCmd = &cobra.Command{
|
var setMetaCmd = &cobra.Command{
|
||||||
@ -74,7 +75,7 @@ func setMetaRun(cmd *cobra.Command, args []string) {
|
|||||||
fmt.Printf("%v\n", err)
|
fmt.Printf("%v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setTermRawMode()
|
wshutil.SetTermRawModeAndInstallShutdownHandlers(true)
|
||||||
fullORef, err := resolveSimpleId(oref)
|
fullORef, err := resolveSimpleId(oref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("error resolving oref: %v\n", err)
|
fmt.Printf("error resolving oref: %v\n", err)
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
||||||
|
"github.com/wavetermdev/thenextwave/pkg/wshutil"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wstore"
|
"github.com/wavetermdev/thenextwave/pkg/wstore"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -43,7 +44,7 @@ func viewRun(cmd *cobra.Command, args []string) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("error getting file info: %v\n", err)
|
log.Printf("error getting file info: %v\n", err)
|
||||||
}
|
}
|
||||||
setTermRawMode()
|
wshutil.SetTermRawModeAndInstallShutdownHandlers(true)
|
||||||
viewWshCmd := &wshrpc.CommandCreateBlockData{
|
viewWshCmd := &wshrpc.CommandCreateBlockData{
|
||||||
BlockDef: &wstore.BlockDef{
|
BlockDef: &wstore.BlockDef{
|
||||||
View: "preview",
|
View: "preview",
|
||||||
|
@ -28,7 +28,7 @@ class WshServerType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// command "file:append" [call]
|
// command "file:append" [call]
|
||||||
AppendFileCommand(data: CommandAppendFileData, opts?: WshRpcCommandOpts): Promise<void> {
|
AppendFileCommand(data: CommandFileData, opts?: WshRpcCommandOpts): Promise<void> {
|
||||||
return WOS.wshServerRpcHelper_call("file:append", data, opts);
|
return WOS.wshServerRpcHelper_call("file:append", data, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,6 +37,16 @@ class WshServerType {
|
|||||||
return WOS.wshServerRpcHelper_call("file:appendijson", data, opts);
|
return WOS.wshServerRpcHelper_call("file:appendijson", data, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// command "file:read" [call]
|
||||||
|
ReadFile(data: CommandFileData, opts?: WshRpcCommandOpts): Promise<string> {
|
||||||
|
return WOS.wshServerRpcHelper_call("file:read", data, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// command "file:write" [call]
|
||||||
|
WriteFile(data: CommandFileData, opts?: WshRpcCommandOpts): Promise<void> {
|
||||||
|
return WOS.wshServerRpcHelper_call("file:write", data, opts);
|
||||||
|
}
|
||||||
|
|
||||||
// command "getmeta" [call]
|
// command "getmeta" [call]
|
||||||
GetMetaCommand(data: CommandGetMetaData, opts?: WshRpcCommandOpts): Promise<MetaType> {
|
GetMetaCommand(data: CommandGetMetaData, opts?: WshRpcCommandOpts): Promise<MetaType> {
|
||||||
return WOS.wshServerRpcHelper_call("getmeta", data, opts);
|
return WOS.wshServerRpcHelper_call("getmeta", data, opts);
|
||||||
|
@ -9,11 +9,11 @@ import clsx from "clsx";
|
|||||||
import { produce } from "immer";
|
import { produce } from "immer";
|
||||||
import * as jotai from "jotai";
|
import * as jotai from "jotai";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { IJsonView } from "./ijson";
|
|
||||||
import { TermStickers } from "./termsticker";
|
import { TermStickers } from "./termsticker";
|
||||||
import { TermWrap } from "./termwrap";
|
import { TermWrap } from "./termwrap";
|
||||||
|
|
||||||
import { WshServer } from "@/app/store/wshserver";
|
import { WshServer } from "@/app/store/wshserver";
|
||||||
|
import { VDomView } from "@/app/view/term/vdom";
|
||||||
import "public/xterm.css";
|
import "public/xterm.css";
|
||||||
import "./term.less";
|
import "./term.less";
|
||||||
|
|
||||||
@ -100,16 +100,24 @@ type InitialLoadDataType = {
|
|||||||
heldData: Uint8Array[];
|
heldData: Uint8Array[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const IJSONConst = {
|
function vdomText(text: string): VDomElem {
|
||||||
|
return {
|
||||||
|
tag: "#text",
|
||||||
|
text: text,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const testVDom: VDomElem = {
|
||||||
|
id: "testid1",
|
||||||
tag: "div",
|
tag: "div",
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
tag: "h1",
|
tag: "h1",
|
||||||
children: ["Hello World"],
|
children: [vdomText("Hello World")],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: "p",
|
tag: "p",
|
||||||
children: ["This is a paragraph"],
|
children: [vdomText("This is a paragraph (from VDOM)")],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@ -343,7 +351,7 @@ const TerminalView = ({ blockId, model }: { blockId: string; model: TermViewMode
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div key="htmlElemContent" className="term-htmlelem-content">
|
<div key="htmlElemContent" className="term-htmlelem-content">
|
||||||
<IJsonView rootNode={IJSONConst} />
|
<VDomView rootNode={testVDom} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
129
frontend/app/view/term/vdom.tsx
Normal file
129
frontend/app/view/term/vdom.tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
const AllowedTags: { [tagName: string]: boolean } = {
|
||||||
|
div: true,
|
||||||
|
b: true,
|
||||||
|
i: true,
|
||||||
|
p: true,
|
||||||
|
s: true,
|
||||||
|
span: true,
|
||||||
|
a: true,
|
||||||
|
img: true,
|
||||||
|
h1: true,
|
||||||
|
h2: true,
|
||||||
|
h3: true,
|
||||||
|
h4: true,
|
||||||
|
h5: true,
|
||||||
|
h6: true,
|
||||||
|
ul: true,
|
||||||
|
ol: true,
|
||||||
|
li: true,
|
||||||
|
input: true,
|
||||||
|
button: true,
|
||||||
|
textarea: true,
|
||||||
|
select: true,
|
||||||
|
option: true,
|
||||||
|
form: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
function convertVDomFunc(fnDecl: VDomFuncType, compId: string, propName: string): (e: any) => void {
|
||||||
|
return (e: any) => {
|
||||||
|
if ((propName == "onKeyDown" || propName == "onKeyDownCapture") && fnDecl["#keys"]) {
|
||||||
|
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||||
|
for (let keyDesc of fnDecl["#keys"]) {
|
||||||
|
if (checkKeyPressed(waveEvent, keyDesc)) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
callFunc(e, compId, propName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (fnDecl["#preventDefault"]) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
if (fnDecl["#stopPropagation"]) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
callFunc(e, compId, propName);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertElemToTag(elem: VDomElem): JSX.Element | string {
|
||||||
|
if (elem == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (elem.tag == "#text") {
|
||||||
|
return elem.text;
|
||||||
|
}
|
||||||
|
return React.createElement(VDomTag, { elem: elem, key: elem.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
function isObject(v: any): boolean {
|
||||||
|
return v != null && !Array.isArray(v) && typeof v === "object";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isArray(v: any): boolean {
|
||||||
|
return Array.isArray(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
function callFunc(e: any, compId: string, propName: string) {
|
||||||
|
console.log("callfunc", compId, propName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRefFunc(elem: any, ref: VDomRefType) {
|
||||||
|
console.log("updateref", ref["#ref"], elem);
|
||||||
|
}
|
||||||
|
|
||||||
|
function VDomTag({ elem }: { elem: VDomElem }) {
|
||||||
|
if (!AllowedTags[elem.tag]) {
|
||||||
|
return <div>{"Invalid Tag <" + elem.tag + ">"}</div>;
|
||||||
|
}
|
||||||
|
let props = {};
|
||||||
|
for (let key in elem.props) {
|
||||||
|
let val = elem.props[key];
|
||||||
|
if (val == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (key == "ref") {
|
||||||
|
if (val == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isObject(val) && "#ref" in val) {
|
||||||
|
props[key] = (elem: HTMLElement) => {
|
||||||
|
updateRefFunc(elem, val);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (isObject(val) && "#func" in val) {
|
||||||
|
props[key] = convertVDomFunc(val, elem.id, key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let childrenComps: (string | JSX.Element)[] = [];
|
||||||
|
if (elem.children) {
|
||||||
|
for (let child of elem.children) {
|
||||||
|
if (child == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
childrenComps.push(convertElemToTag(child));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (elem.tag == "#fragment") {
|
||||||
|
return childrenComps;
|
||||||
|
}
|
||||||
|
return React.createElement(elem.tag, props, childrenComps);
|
||||||
|
}
|
||||||
|
|
||||||
|
function VDomView({ rootNode }: { rootNode: VDomElem }) {
|
||||||
|
let rtn = convertElemToTag(rootNode);
|
||||||
|
return <div className="vdom">{rtn}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { VDomView };
|
37
frontend/types/gotypes.d.ts
vendored
37
frontend/types/gotypes.d.ts
vendored
@ -55,13 +55,6 @@ declare global {
|
|||||||
meta: MetaType;
|
meta: MetaType;
|
||||||
};
|
};
|
||||||
|
|
||||||
// wshrpc.CommandAppendFileData
|
|
||||||
type CommandAppendFileData = {
|
|
||||||
zoneid: string;
|
|
||||||
filename: string;
|
|
||||||
data64: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// wshrpc.CommandAppendIJsonData
|
// wshrpc.CommandAppendIJsonData
|
||||||
type CommandAppendIJsonData = {
|
type CommandAppendIJsonData = {
|
||||||
zoneid: string;
|
zoneid: string;
|
||||||
@ -100,6 +93,13 @@ declare global {
|
|||||||
blockid: string;
|
blockid: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// wshrpc.CommandFileData
|
||||||
|
type CommandFileData = {
|
||||||
|
zoneid: string;
|
||||||
|
filename: string;
|
||||||
|
data64?: string;
|
||||||
|
};
|
||||||
|
|
||||||
// wshrpc.CommandGetMetaData
|
// wshrpc.CommandGetMetaData
|
||||||
type CommandGetMetaData = {
|
type CommandGetMetaData = {
|
||||||
oref: ORef;
|
oref: ORef;
|
||||||
@ -297,6 +297,29 @@ declare global {
|
|||||||
checkboxstat?: boolean;
|
checkboxstat?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// vdom.Elem
|
||||||
|
type VDomElem = {
|
||||||
|
id?: string;
|
||||||
|
tag: string;
|
||||||
|
props?: MetaType;
|
||||||
|
children?: VDomElem[];
|
||||||
|
text?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// vdom.VDomFuncType
|
||||||
|
type VDomFuncType = {
|
||||||
|
#func: string;
|
||||||
|
#stopPropagation?: boolean;
|
||||||
|
#preventDefault?: boolean;
|
||||||
|
#keys?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// vdom.VDomRefType
|
||||||
|
type VDomRefType = {
|
||||||
|
#ref: string;
|
||||||
|
current: any;
|
||||||
|
};
|
||||||
|
|
||||||
type WSCommandType = {
|
type WSCommandType = {
|
||||||
wscommand: string;
|
wscommand: string;
|
||||||
} & ( SetBlockTermSizeWSCommand | BlockInputWSCommand | WSRpcCommand );
|
} & ( SetBlockTermSizeWSCommand | BlockInputWSCommand | WSRpcCommand );
|
||||||
|
6
go.mod
6
go.mod
@ -1,8 +1,6 @@
|
|||||||
module github.com/wavetermdev/thenextwave
|
module github.com/wavetermdev/thenextwave
|
||||||
|
|
||||||
go 1.22
|
go 1.22.4
|
||||||
|
|
||||||
toolchain go1.22.1
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alexflint/go-filemutex v1.3.0
|
github.com/alexflint/go-filemutex v1.3.0
|
||||||
@ -18,6 +16,7 @@ require (
|
|||||||
github.com/mattn/go-sqlite3 v1.14.22
|
github.com/mattn/go-sqlite3 v1.14.22
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
github.com/sawka/txwrap v0.2.0
|
github.com/sawka/txwrap v0.2.0
|
||||||
|
github.com/wavetermdev/htmltoken v0.1.0
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
github.com/wavetermdev/waveterm/wavesrv v0.0.0-20240508181017-d07068c09d94
|
github.com/wavetermdev/waveterm/wavesrv v0.0.0-20240508181017-d07068c09d94
|
||||||
golang.org/x/crypto v0.25.0
|
golang.org/x/crypto v0.25.0
|
||||||
@ -32,6 +31,7 @@ require (
|
|||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
github.com/stretchr/testify v1.8.4 // indirect
|
github.com/stretchr/testify v1.8.4 // indirect
|
||||||
go.uber.org/atomic v1.7.0 // indirect
|
go.uber.org/atomic v1.7.0 // indirect
|
||||||
|
golang.org/x/net v0.27.0 // indirect
|
||||||
golang.org/x/sys v0.22.0 // indirect
|
golang.org/x/sys v0.22.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
4
go.sum
4
go.sum
@ -53,6 +53,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
|||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/wavetermdev/htmltoken v0.1.0 h1:RMdA9zTfnYa5jRC4RRG3XNoV5NOP8EDxpaVPjuVz//Q=
|
||||||
|
github.com/wavetermdev/htmltoken v0.1.0/go.mod h1:5FM0XV6zNYiNza2iaTcFGj+hnMtgqumFHO31Z8euquk=
|
||||||
github.com/wavetermdev/ssh_config v0.0.0-20240306041034-17e2087ebde2 h1:onqZrJVap1sm15AiIGTfWzdr6cEF0KdtddeuuOVhzyY=
|
github.com/wavetermdev/ssh_config v0.0.0-20240306041034-17e2087ebde2 h1:onqZrJVap1sm15AiIGTfWzdr6cEF0KdtddeuuOVhzyY=
|
||||||
github.com/wavetermdev/ssh_config v0.0.0-20240306041034-17e2087ebde2/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
github.com/wavetermdev/ssh_config v0.0.0-20240306041034-17e2087ebde2/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||||
github.com/wavetermdev/waveterm/wavesrv v0.0.0-20240508181017-d07068c09d94 h1:/SPCxd4KHlS4eRTreYEXWFRr8WfRFBcChlV5cgkaO58=
|
github.com/wavetermdev/waveterm/wavesrv v0.0.0-20240508181017-d07068c09d94 h1:/SPCxd4KHlS4eRTreYEXWFRr8WfRFBcChlV5cgkaO58=
|
||||||
@ -61,6 +63,8 @@ go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
|||||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||||
|
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||||
|
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
@ -233,9 +233,14 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta map[str
|
|||||||
return shellProcErr
|
return shellProcErr
|
||||||
}
|
}
|
||||||
var cmdStr string
|
var cmdStr string
|
||||||
var cmdOpts shellexec.CommandOptsType
|
cmdOpts := shellexec.CommandOptsType{
|
||||||
|
Env: make(map[string]string),
|
||||||
|
}
|
||||||
|
// temporary for blockid (will switch to a JWT at some point)
|
||||||
|
cmdOpts.Env["LC_WAVETERM_BLOCKID"] = bc.BlockId
|
||||||
if bc.ControllerType == BlockController_Shell {
|
if bc.ControllerType == BlockController_Shell {
|
||||||
cmdOpts = shellexec.CommandOptsType{Interactive: true, Login: true}
|
cmdOpts.Interactive = true
|
||||||
|
cmdOpts.Login = true
|
||||||
} else if bc.ControllerType == BlockController_Cmd {
|
} else if bc.ControllerType == BlockController_Cmd {
|
||||||
if _, ok := blockMeta["cmd"].(string); ok {
|
if _, ok := blockMeta["cmd"].(string); ok {
|
||||||
cmdStr = blockMeta["cmd"].(string)
|
cmdStr = blockMeta["cmd"].(string)
|
||||||
@ -260,7 +265,6 @@ func (bc *BlockController) DoRunShellCommand(rc *RunShellOpts, blockMeta map[str
|
|||||||
}
|
}
|
||||||
if _, ok := blockMeta["cmd:env"].(map[string]any); ok {
|
if _, ok := blockMeta["cmd:env"].(map[string]any); ok {
|
||||||
cmdEnv := blockMeta["cmd:env"].(map[string]any)
|
cmdEnv := blockMeta["cmd:env"].(map[string]any)
|
||||||
cmdOpts.Env = make(map[string]string)
|
|
||||||
for k, v := range cmdEnv {
|
for k, v := range cmdEnv {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
continue
|
continue
|
||||||
|
@ -33,8 +33,9 @@ type WSEventType struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
FileOp_Append = "append"
|
FileOp_Append = "append"
|
||||||
FileOp_Truncate = "truncate"
|
FileOp_Truncate = "truncate"
|
||||||
|
FileOp_Invalidate = "invalidate"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WSFileEventData struct {
|
type WSFileEventData struct {
|
||||||
|
@ -176,6 +176,11 @@ func StartRemoteShellProc(termSize TermSize, cmdStr string, cmdOpts CommandOptsT
|
|||||||
session.Stdin = cmdTty
|
session.Stdin = cmdTty
|
||||||
session.Stdout = cmdTty
|
session.Stdout = cmdTty
|
||||||
session.Stderr = cmdTty
|
session.Stderr = cmdTty
|
||||||
|
for envKey, envVal := range cmdOpts.Env {
|
||||||
|
// note these might fail depending on server settings, but we still try
|
||||||
|
session.Setenv(envKey, envVal)
|
||||||
|
}
|
||||||
|
|
||||||
session.RequestPty("xterm-256color", termSize.Rows, termSize.Cols, nil)
|
session.RequestPty("xterm-256color", termSize.Rows, termSize.Cols, nil)
|
||||||
|
|
||||||
sessionWrap := SessionWrap{session, cmdCombined, cmdTty}
|
sessionWrap := SessionWrap{session, cmdCombined, cmdTty}
|
||||||
@ -216,6 +221,7 @@ func StartShellProc(termSize TermSize, cmdStr string, cmdOpts CommandOptsType) (
|
|||||||
envToAdd["LANG"] = wavebase.DetermineLang()
|
envToAdd["LANG"] = wavebase.DetermineLang()
|
||||||
}
|
}
|
||||||
shellutil.UpdateCmdEnv(ecmd, envToAdd)
|
shellutil.UpdateCmdEnv(ecmd, envToAdd)
|
||||||
|
shellutil.UpdateCmdEnv(ecmd, cmdOpts.Env)
|
||||||
cmdPty, cmdTty, err := pty.Open()
|
cmdPty, cmdTty, err := pty.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("opening new pty: %w", err)
|
return nil, fmt.Errorf("opening new pty: %w", err)
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/wavetermdev/thenextwave/pkg/service"
|
"github.com/wavetermdev/thenextwave/pkg/service"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta"
|
"github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/userinput"
|
"github.com/wavetermdev/thenextwave/pkg/userinput"
|
||||||
|
"github.com/wavetermdev/thenextwave/pkg/vdom"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/waveobj"
|
"github.com/wavetermdev/thenextwave/pkg/waveobj"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/wconfig"
|
"github.com/wavetermdev/thenextwave/pkg/wconfig"
|
||||||
"github.com/wavetermdev/thenextwave/pkg/web/webcmd"
|
"github.com/wavetermdev/thenextwave/pkg/web/webcmd"
|
||||||
@ -41,6 +42,9 @@ var ExtraTypes = []any{
|
|||||||
wshutil.RpcMessage{},
|
wshutil.RpcMessage{},
|
||||||
wshrpc.WshServerCommandMeta{},
|
wshrpc.WshServerCommandMeta{},
|
||||||
userinput.UserInputRequest{},
|
userinput.UserInputRequest{},
|
||||||
|
vdom.Elem{},
|
||||||
|
vdom.VDomFuncType{},
|
||||||
|
vdom.VDomRefType{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// add extra type unions to generate here
|
// add extra type unions to generate here
|
||||||
@ -149,6 +153,7 @@ func TypeToTSType(t reflect.Type, tsTypesMap map[reflect.Type]string) (string, [
|
|||||||
|
|
||||||
var tsRenameMap = map[string]string{
|
var tsRenameMap = map[string]string{
|
||||||
"Window": "WaveWindow",
|
"Window": "WaveWindow",
|
||||||
|
"Elem": "VDomElem",
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateTSTypeInternal(rtype reflect.Type, tsTypesMap map[reflect.Type]string) (string, []reflect.Type) {
|
func generateTSTypeInternal(rtype reflect.Type, tsTypesMap map[reflect.Type]string) (string, []reflect.Type) {
|
||||||
|
270
pkg/vdom/vdom.go
Normal file
270
pkg/vdom/vdom.go
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package vdom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReactNode types = nil | string | Elem
|
||||||
|
|
||||||
|
const TextTag = "#text"
|
||||||
|
const FragmentTag = "#fragment"
|
||||||
|
|
||||||
|
const ChildrenPropKey = "children"
|
||||||
|
const KeyPropKey = "key"
|
||||||
|
|
||||||
|
// doubles as VDOM structure
|
||||||
|
type Elem struct {
|
||||||
|
Id string `json:"id,omitempty"` // used for vdom
|
||||||
|
Tag string `json:"tag"`
|
||||||
|
Props map[string]any `json:"props,omitempty"`
|
||||||
|
Children []Elem `json:"children,omitempty"`
|
||||||
|
Text string `json:"text,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VDomRefType struct {
|
||||||
|
RefId string `json:"#ref"`
|
||||||
|
Current any `json:"current"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// can be used to set preventDefault/stopPropagation
|
||||||
|
type VDomFuncType struct {
|
||||||
|
Fn any `json:"-"` // the actual function to call (called via reflection)
|
||||||
|
FuncType string `json:"#func"`
|
||||||
|
StopPropagation bool `json:"#stopPropagation,omitempty"`
|
||||||
|
PreventDefault bool `json:"#preventDefault,omitempty"`
|
||||||
|
Keys []string `json:"#keys,omitempty"` // special for keyDown events a list of keys to "capture"
|
||||||
|
}
|
||||||
|
|
||||||
|
// generic hook structure
|
||||||
|
type Hook struct {
|
||||||
|
Init bool // is initialized
|
||||||
|
Idx int // index in the hook array
|
||||||
|
Fn func() func() // for useEffect
|
||||||
|
UnmountFn func() // for useEffect
|
||||||
|
Val any // for useState, useMemo, useRef
|
||||||
|
Deps []any
|
||||||
|
}
|
||||||
|
|
||||||
|
type CFunc = func(ctx context.Context, props map[string]any) any
|
||||||
|
|
||||||
|
func (e *Elem) Key() string {
|
||||||
|
keyVal, ok := e.Props[KeyPropKey]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
keyStr, ok := keyVal.(string)
|
||||||
|
if ok {
|
||||||
|
return keyStr
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func TextElem(text string) Elem {
|
||||||
|
return Elem{Tag: TextTag, Text: text}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeProps(props *map[string]any, newProps map[string]any) {
|
||||||
|
if *props == nil {
|
||||||
|
*props = make(map[string]any)
|
||||||
|
}
|
||||||
|
for k, v := range newProps {
|
||||||
|
if v == nil {
|
||||||
|
delete(*props, k)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
(*props)[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func E(tag string, parts ...any) *Elem {
|
||||||
|
rtn := &Elem{Tag: tag}
|
||||||
|
for _, part := range parts {
|
||||||
|
if part == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
props, ok := part.(map[string]any)
|
||||||
|
if ok {
|
||||||
|
mergeProps(&rtn.Props, props)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
elems := partToElems(part)
|
||||||
|
rtn.Children = append(rtn.Children, elems...)
|
||||||
|
}
|
||||||
|
return rtn
|
||||||
|
}
|
||||||
|
|
||||||
|
func P(propName string, propVal any) map[string]any {
|
||||||
|
return map[string]any{propName: propVal}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHookFromCtx(ctx context.Context) (*VDomContextVal, *Hook) {
|
||||||
|
vc := getRenderContext(ctx)
|
||||||
|
if vc == nil {
|
||||||
|
panic("UseState must be called within a component (no context)")
|
||||||
|
}
|
||||||
|
if vc.Comp == nil {
|
||||||
|
panic("UseState must be called within a component (vc.Comp is nil)")
|
||||||
|
}
|
||||||
|
for len(vc.Comp.Hooks) <= vc.HookIdx {
|
||||||
|
vc.Comp.Hooks = append(vc.Comp.Hooks, &Hook{Idx: len(vc.Comp.Hooks)})
|
||||||
|
}
|
||||||
|
hookVal := vc.Comp.Hooks[vc.HookIdx]
|
||||||
|
vc.HookIdx++
|
||||||
|
return vc, hookVal
|
||||||
|
}
|
||||||
|
|
||||||
|
func UseState[T any](ctx context.Context, initialVal T) (T, func(T)) {
|
||||||
|
vc, hookVal := getHookFromCtx(ctx)
|
||||||
|
if !hookVal.Init {
|
||||||
|
hookVal.Init = true
|
||||||
|
hookVal.Val = initialVal
|
||||||
|
}
|
||||||
|
var rtnVal T
|
||||||
|
rtnVal, ok := hookVal.Val.(T)
|
||||||
|
if !ok {
|
||||||
|
panic("UseState hook value is not a state (possible out of order or conditional hooks)")
|
||||||
|
}
|
||||||
|
setVal := func(newVal T) {
|
||||||
|
hookVal.Val = newVal
|
||||||
|
vc.Root.AddRenderWork(vc.Comp.Id)
|
||||||
|
}
|
||||||
|
return rtnVal, setVal
|
||||||
|
}
|
||||||
|
|
||||||
|
func UseRef(ctx context.Context, initialVal any) *VDomRefType {
|
||||||
|
vc, hookVal := getHookFromCtx(ctx)
|
||||||
|
if !hookVal.Init {
|
||||||
|
hookVal.Init = true
|
||||||
|
refId := vc.Comp.Id + ":" + strconv.Itoa(hookVal.Idx)
|
||||||
|
hookVal.Val = &VDomRefType{RefId: refId, Current: initialVal}
|
||||||
|
}
|
||||||
|
refVal, ok := hookVal.Val.(*VDomRefType)
|
||||||
|
if !ok {
|
||||||
|
panic("UseRef hook value is not a ref (possible out of order or conditional hooks)")
|
||||||
|
}
|
||||||
|
return refVal
|
||||||
|
}
|
||||||
|
|
||||||
|
func UseId(ctx context.Context) string {
|
||||||
|
vc := getRenderContext(ctx)
|
||||||
|
if vc == nil {
|
||||||
|
panic("UseId must be called within a component (no context)")
|
||||||
|
}
|
||||||
|
return vc.Comp.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
func depsEqual(deps1 []any, deps2 []any) bool {
|
||||||
|
if len(deps1) != len(deps2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range deps1 {
|
||||||
|
if deps1[i] != deps2[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func UseEffect(ctx context.Context, fn func() func(), deps []any) {
|
||||||
|
// note UseEffect never actually runs anything, it just queues the effect to run later
|
||||||
|
vc, hookVal := getHookFromCtx(ctx)
|
||||||
|
if !hookVal.Init {
|
||||||
|
hookVal.Init = true
|
||||||
|
hookVal.Fn = fn
|
||||||
|
hookVal.Deps = deps
|
||||||
|
vc.Root.AddEffectWork(vc.Comp.Id, hookVal.Idx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if depsEqual(hookVal.Deps, deps) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hookVal.Fn = fn
|
||||||
|
hookVal.Deps = deps
|
||||||
|
vc.Root.AddEffectWork(vc.Comp.Id, hookVal.Idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func numToString[T any](value T) (string, bool) {
|
||||||
|
switch v := any(value).(type) {
|
||||||
|
case int, int8, int16, int32, int64:
|
||||||
|
return strconv.FormatInt(v.(int64), 10), true
|
||||||
|
case uint, uint8, uint16, uint32, uint64:
|
||||||
|
return strconv.FormatUint(v.(uint64), 10), true
|
||||||
|
case float32:
|
||||||
|
return strconv.FormatFloat(float64(v), 'f', -1, 32), true
|
||||||
|
case float64:
|
||||||
|
return strconv.FormatFloat(v, 'f', -1, 64), true
|
||||||
|
default:
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func partToElems(part any) []Elem {
|
||||||
|
if part == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch part := part.(type) {
|
||||||
|
case string:
|
||||||
|
return []Elem{TextElem(part)}
|
||||||
|
case *Elem:
|
||||||
|
if part == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []Elem{*part}
|
||||||
|
case Elem:
|
||||||
|
return []Elem{part}
|
||||||
|
case []Elem:
|
||||||
|
return part
|
||||||
|
case []*Elem:
|
||||||
|
var rtn []Elem
|
||||||
|
for _, e := range part {
|
||||||
|
if e == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rtn = append(rtn, *e)
|
||||||
|
}
|
||||||
|
return rtn
|
||||||
|
}
|
||||||
|
sval, ok := numToString(part)
|
||||||
|
if ok {
|
||||||
|
return []Elem{TextElem(sval)}
|
||||||
|
}
|
||||||
|
partVal := reflect.ValueOf(part)
|
||||||
|
if partVal.Kind() == reflect.Slice {
|
||||||
|
var rtn []Elem
|
||||||
|
for i := 0; i < partVal.Len(); i++ {
|
||||||
|
subPart := partVal.Index(i).Interface()
|
||||||
|
rtn = append(rtn, partToElems(subPart)...)
|
||||||
|
}
|
||||||
|
return rtn
|
||||||
|
}
|
||||||
|
stringer, ok := part.(fmt.Stringer)
|
||||||
|
if ok {
|
||||||
|
return []Elem{TextElem(stringer.String())}
|
||||||
|
}
|
||||||
|
jsonStr, jsonErr := json.Marshal(part)
|
||||||
|
if jsonErr == nil {
|
||||||
|
return []Elem{TextElem(string(jsonStr))}
|
||||||
|
}
|
||||||
|
typeText := "invalid:" + reflect.TypeOf(part).String()
|
||||||
|
return []Elem{TextElem(typeText)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWaveTag(tag string) bool {
|
||||||
|
return strings.HasPrefix(tag, "wave:") || strings.HasPrefix(tag, "w:")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isBaseTag(tag string) bool {
|
||||||
|
if len(tag) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return tag[0] == '#' || unicode.IsLower(rune(tag[0])) || isWaveTag(tag)
|
||||||
|
}
|
40
pkg/vdom/vdom_comp.go
Normal file
40
pkg/vdom/vdom_comp.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package vdom
|
||||||
|
|
||||||
|
// so components either render to another component (or fragment)
|
||||||
|
// or to a base element (text or vdom). base elements can then render children
|
||||||
|
|
||||||
|
type ChildKey struct {
|
||||||
|
Tag string
|
||||||
|
Idx int
|
||||||
|
Key string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Component struct {
|
||||||
|
Id string
|
||||||
|
Tag string
|
||||||
|
Key string
|
||||||
|
Elem *Elem
|
||||||
|
Mounted bool
|
||||||
|
|
||||||
|
// hooks
|
||||||
|
Hooks []*Hook
|
||||||
|
|
||||||
|
// #text component
|
||||||
|
Text string
|
||||||
|
|
||||||
|
// base component -- vdom, wave elem, or #fragment
|
||||||
|
Children []*Component
|
||||||
|
|
||||||
|
// component -> component
|
||||||
|
Comp *Component
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Component) compMatch(tag string, key string) bool {
|
||||||
|
if c == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return c.Tag == tag && c.Key == key
|
||||||
|
}
|
253
pkg/vdom/vdom_html.go
Normal file
253
pkg/vdom/vdom_html.go
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package vdom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/wavetermdev/htmltoken"
|
||||||
|
)
|
||||||
|
|
||||||
|
// can tokenize and bind HTML to Elems
|
||||||
|
|
||||||
|
func appendChildToStack(stack []*Elem, child *Elem) {
|
||||||
|
if child == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(stack) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parent := stack[len(stack)-1]
|
||||||
|
parent.Children = append(parent.Children, *child)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pushElemStack(stack []*Elem, elem *Elem) []*Elem {
|
||||||
|
if elem == nil {
|
||||||
|
return stack
|
||||||
|
}
|
||||||
|
return append(stack, elem)
|
||||||
|
}
|
||||||
|
|
||||||
|
func popElemStack(stack []*Elem) []*Elem {
|
||||||
|
if len(stack) <= 1 {
|
||||||
|
return stack
|
||||||
|
}
|
||||||
|
curElem := stack[len(stack)-1]
|
||||||
|
appendChildToStack(stack[:len(stack)-1], curElem)
|
||||||
|
return stack[:len(stack)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func curElemTag(stack []*Elem) string {
|
||||||
|
if len(stack) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return stack[len(stack)-1].Tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func finalizeStack(stack []*Elem) *Elem {
|
||||||
|
if len(stack) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for len(stack) > 1 {
|
||||||
|
stack = popElemStack(stack)
|
||||||
|
}
|
||||||
|
rtnElem := stack[0]
|
||||||
|
if len(rtnElem.Children) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(rtnElem.Children) == 1 {
|
||||||
|
return &rtnElem.Children[0]
|
||||||
|
}
|
||||||
|
return rtnElem
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAttr(token htmltoken.Token, key string) string {
|
||||||
|
for _, attr := range token.Attr {
|
||||||
|
if attr.Key == key {
|
||||||
|
return attr.Val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func tokenToElem(token htmltoken.Token, data map[string]any) *Elem {
|
||||||
|
elem := &Elem{Tag: token.Data}
|
||||||
|
if len(token.Attr) > 0 {
|
||||||
|
elem.Props = make(map[string]any)
|
||||||
|
}
|
||||||
|
for _, attr := range token.Attr {
|
||||||
|
if attr.Key == "" || attr.Val == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(attr.Val, "#bind:") {
|
||||||
|
bindKey := attr.Val[6:]
|
||||||
|
bindVal, ok := data[bindKey]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
elem.Props[attr.Key] = bindVal
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
elem.Props[attr.Key] = attr.Val
|
||||||
|
}
|
||||||
|
return elem
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWsChar(char rune) bool {
|
||||||
|
return char == ' ' || char == '\t' || char == '\n' || char == '\r'
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWsByte(char byte) bool {
|
||||||
|
return char == ' ' || char == '\t' || char == '\n' || char == '\r'
|
||||||
|
}
|
||||||
|
|
||||||
|
func isFirstCharLt(s string) bool {
|
||||||
|
for _, char := range s {
|
||||||
|
if isWsChar(char) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return char == '<'
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLastCharGt(s string) bool {
|
||||||
|
for i := len(s) - 1; i >= 0; i-- {
|
||||||
|
char := s[i]
|
||||||
|
if isWsByte(char) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return char == '>'
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAllWhitespace(s string) bool {
|
||||||
|
for _, char := range s {
|
||||||
|
if !isWsChar(char) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimWhitespaceConditionally(s string) string {
|
||||||
|
// Trim leading whitespace if the first non-whitespace character is '<'
|
||||||
|
if isAllWhitespace(s) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if isFirstCharLt(s) {
|
||||||
|
s = strings.TrimLeftFunc(s, func(r rune) bool {
|
||||||
|
return isWsChar(r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Trim trailing whitespace if the last non-whitespace character is '>'
|
||||||
|
if isLastCharGt(s) {
|
||||||
|
s = strings.TrimRightFunc(s, func(r rune) bool {
|
||||||
|
return isWsChar(r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func processWhitespace(htmlStr string) string {
|
||||||
|
lines := strings.Split(htmlStr, "\n")
|
||||||
|
var newLines []string
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmedLine := trimWhitespaceConditionally(line + "\n")
|
||||||
|
if trimmedLine == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newLines = append(newLines, trimmedLine)
|
||||||
|
}
|
||||||
|
return strings.Join(newLines, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func processTextStr(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if isAllWhitespace(s) {
|
||||||
|
return " "
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Bind(htmlStr string, data map[string]any) *Elem {
|
||||||
|
htmlStr = processWhitespace(htmlStr)
|
||||||
|
r := strings.NewReader(htmlStr)
|
||||||
|
iter := htmltoken.NewTokenizer(r)
|
||||||
|
var elemStack []*Elem
|
||||||
|
elemStack = append(elemStack, &Elem{Tag: FragmentTag})
|
||||||
|
var tokenErr error
|
||||||
|
outer:
|
||||||
|
for {
|
||||||
|
tokenType := iter.Next()
|
||||||
|
token := iter.Token()
|
||||||
|
switch tokenType {
|
||||||
|
case htmltoken.StartTagToken:
|
||||||
|
if token.Data == "bind" {
|
||||||
|
tokenErr = errors.New("bind tag must be self closing")
|
||||||
|
break outer
|
||||||
|
}
|
||||||
|
elem := tokenToElem(token, data)
|
||||||
|
elemStack = pushElemStack(elemStack, elem)
|
||||||
|
case htmltoken.EndTagToken:
|
||||||
|
if token.Data == "bind" {
|
||||||
|
tokenErr = errors.New("bind tag must be self closing")
|
||||||
|
break outer
|
||||||
|
}
|
||||||
|
if len(elemStack) <= 1 {
|
||||||
|
tokenErr = fmt.Errorf("end tag %q without start tag", token.Data)
|
||||||
|
break outer
|
||||||
|
}
|
||||||
|
if curElemTag(elemStack) != token.Data {
|
||||||
|
tokenErr = fmt.Errorf("end tag %q does not match start tag %q", token.Data, curElemTag(elemStack))
|
||||||
|
break outer
|
||||||
|
}
|
||||||
|
elemStack = popElemStack(elemStack)
|
||||||
|
case htmltoken.SelfClosingTagToken:
|
||||||
|
if token.Data == "bind" {
|
||||||
|
keyAttr := getAttr(token, "key")
|
||||||
|
dataVal := data[keyAttr]
|
||||||
|
elemList := partToElems(dataVal)
|
||||||
|
for _, elem := range elemList {
|
||||||
|
appendChildToStack(elemStack, &elem)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
elem := tokenToElem(token, data)
|
||||||
|
appendChildToStack(elemStack, elem)
|
||||||
|
case htmltoken.TextToken:
|
||||||
|
if token.Data == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
textStr := processTextStr(token.Data)
|
||||||
|
if textStr == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
elem := TextElem(textStr)
|
||||||
|
appendChildToStack(elemStack, &elem)
|
||||||
|
case htmltoken.CommentToken:
|
||||||
|
continue
|
||||||
|
case htmltoken.DoctypeToken:
|
||||||
|
tokenErr = errors.New("doctype not supported")
|
||||||
|
break outer
|
||||||
|
case htmltoken.ErrorToken:
|
||||||
|
if iter.Err() == io.EOF {
|
||||||
|
break outer
|
||||||
|
}
|
||||||
|
tokenErr = iter.Err()
|
||||||
|
break outer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tokenErr != nil {
|
||||||
|
errTextElem := TextElem(tokenErr.Error())
|
||||||
|
appendChildToStack(elemStack, &errTextElem)
|
||||||
|
}
|
||||||
|
return finalizeStack(elemStack)
|
||||||
|
}
|
328
pkg/vdom/vdom_root.go
Normal file
328
pkg/vdom/vdom_root.go
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package vdom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type vdomContextKeyType struct{}
|
||||||
|
|
||||||
|
var vdomContextKey = vdomContextKeyType{}
|
||||||
|
|
||||||
|
type VDomContextVal struct {
|
||||||
|
Root *RootElem
|
||||||
|
Comp *Component
|
||||||
|
HookIdx int
|
||||||
|
}
|
||||||
|
|
||||||
|
type RootElem struct {
|
||||||
|
OuterCtx context.Context
|
||||||
|
Root *Component
|
||||||
|
CFuncs map[string]CFunc
|
||||||
|
CompMap map[string]*Component // component id -> component
|
||||||
|
EffectWorkQueue []*EffectWorkElem
|
||||||
|
NeedsRenderMap map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
WorkType_Render = "render"
|
||||||
|
WorkType_Effect = "effect"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EffectWorkElem struct {
|
||||||
|
Id string
|
||||||
|
EffectIndex int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RootElem) AddRenderWork(id string) {
|
||||||
|
if r.NeedsRenderMap == nil {
|
||||||
|
r.NeedsRenderMap = make(map[string]bool)
|
||||||
|
}
|
||||||
|
r.NeedsRenderMap[id] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RootElem) AddEffectWork(id string, effectIndex int) {
|
||||||
|
r.EffectWorkQueue = append(r.EffectWorkQueue, &EffectWorkElem{Id: id, EffectIndex: effectIndex})
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeRoot() *RootElem {
|
||||||
|
return &RootElem{
|
||||||
|
Root: nil,
|
||||||
|
CFuncs: make(map[string]CFunc),
|
||||||
|
CompMap: make(map[string]*Component),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RootElem) SetOuterCtx(ctx context.Context) {
|
||||||
|
r.OuterCtx = ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RootElem) RegisterComponent(name string, cfunc CFunc) {
|
||||||
|
r.CFuncs[name] = cfunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RootElem) Render(elem *Elem) {
|
||||||
|
log.Printf("Render %s\n", elem.Tag)
|
||||||
|
r.render(elem, &r.Root)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RootElem) Event(id string, propName string) {
|
||||||
|
comp := r.CompMap[id]
|
||||||
|
if comp == nil || comp.Elem == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fnVal := comp.Elem.Props[propName]
|
||||||
|
if fnVal == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fn, ok := fnVal.(func())
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
// this will be called by the frontend to say the DOM has been mounted
|
||||||
|
// it will eventually send any updated "refs" to the backend as well
|
||||||
|
func (r *RootElem) runWork() {
|
||||||
|
workQueue := r.EffectWorkQueue
|
||||||
|
r.EffectWorkQueue = nil
|
||||||
|
// first, run effect cleanups
|
||||||
|
for _, work := range workQueue {
|
||||||
|
comp := r.CompMap[work.Id]
|
||||||
|
if comp == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hook := comp.Hooks[work.EffectIndex]
|
||||||
|
if hook.UnmountFn != nil {
|
||||||
|
hook.UnmountFn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// now run, new effects
|
||||||
|
for _, work := range workQueue {
|
||||||
|
comp := r.CompMap[work.Id]
|
||||||
|
if comp == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hook := comp.Hooks[work.EffectIndex]
|
||||||
|
if hook.Fn != nil {
|
||||||
|
hook.UnmountFn = hook.Fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// now check if we need a render
|
||||||
|
if len(r.NeedsRenderMap) > 0 {
|
||||||
|
r.NeedsRenderMap = nil
|
||||||
|
r.render(r.Root.Elem, &r.Root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RootElem) render(elem *Elem, comp **Component) {
|
||||||
|
if elem == nil || elem.Tag == "" {
|
||||||
|
r.unmount(comp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
elemKey := elem.Key()
|
||||||
|
if *comp == nil || !(*comp).compMatch(elem.Tag, elemKey) {
|
||||||
|
r.unmount(comp)
|
||||||
|
r.createComp(elem.Tag, elemKey, comp)
|
||||||
|
}
|
||||||
|
(*comp).Elem = elem
|
||||||
|
if elem.Tag == TextTag {
|
||||||
|
r.renderText(elem.Text, comp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if isBaseTag(elem.Tag) {
|
||||||
|
// simple vdom, fragment, wave element
|
||||||
|
r.renderSimple(elem, comp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cfunc := r.CFuncs[elem.Tag]
|
||||||
|
if cfunc == nil {
|
||||||
|
text := fmt.Sprintf("<%s>", elem.Tag)
|
||||||
|
r.renderText(text, comp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.renderComponent(cfunc, elem, comp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RootElem) unmount(comp **Component) {
|
||||||
|
if *comp == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// parent clean up happens first
|
||||||
|
for _, hook := range (*comp).Hooks {
|
||||||
|
if hook.UnmountFn != nil {
|
||||||
|
hook.UnmountFn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// clean up any children
|
||||||
|
if (*comp).Comp != nil {
|
||||||
|
r.unmount(&(*comp).Comp)
|
||||||
|
}
|
||||||
|
if (*comp).Children != nil {
|
||||||
|
for _, child := range (*comp).Children {
|
||||||
|
r.unmount(&child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(r.CompMap, (*comp).Id)
|
||||||
|
*comp = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RootElem) createComp(tag string, key string, comp **Component) {
|
||||||
|
*comp = &Component{Id: uuid.New().String(), Tag: tag, Key: key}
|
||||||
|
r.CompMap[(*comp).Id] = *comp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RootElem) renderText(text string, comp **Component) {
|
||||||
|
if (*comp).Text != text {
|
||||||
|
(*comp).Text = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RootElem) renderChildren(elems []Elem, curChildren []*Component) []*Component {
|
||||||
|
newChildren := make([]*Component, len(elems))
|
||||||
|
curCM := make(map[ChildKey]*Component)
|
||||||
|
usedMap := make(map[*Component]bool)
|
||||||
|
for idx, child := range curChildren {
|
||||||
|
if child.Key != "" {
|
||||||
|
curCM[ChildKey{Tag: child.Tag, Idx: 0, Key: child.Key}] = child
|
||||||
|
} else {
|
||||||
|
curCM[ChildKey{Tag: child.Tag, Idx: idx, Key: ""}] = child
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for idx, elem := range elems {
|
||||||
|
elemKey := elem.Key()
|
||||||
|
var curChild *Component
|
||||||
|
if elemKey != "" {
|
||||||
|
curChild = curCM[ChildKey{Tag: elem.Tag, Idx: 0, Key: elemKey}]
|
||||||
|
} else {
|
||||||
|
curChild = curCM[ChildKey{Tag: elem.Tag, Idx: idx, Key: ""}]
|
||||||
|
}
|
||||||
|
usedMap[curChild] = true
|
||||||
|
newChildren[idx] = curChild
|
||||||
|
r.render(&elem, &newChildren[idx])
|
||||||
|
}
|
||||||
|
for _, child := range curChildren {
|
||||||
|
if !usedMap[child] {
|
||||||
|
r.unmount(&child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newChildren
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RootElem) renderSimple(elem *Elem, comp **Component) {
|
||||||
|
if (*comp).Comp != nil {
|
||||||
|
r.unmount(&(*comp).Comp)
|
||||||
|
}
|
||||||
|
(*comp).Children = r.renderChildren(elem.Children, (*comp).Children)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RootElem) makeRenderContext(comp *Component) context.Context {
|
||||||
|
var ctx context.Context
|
||||||
|
if r.OuterCtx != nil {
|
||||||
|
ctx = r.OuterCtx
|
||||||
|
} else {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
ctx = context.WithValue(ctx, vdomContextKey, &VDomContextVal{Root: r, Comp: comp, HookIdx: 0})
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRenderContext(ctx context.Context) *VDomContextVal {
|
||||||
|
v := ctx.Value(vdomContextKey)
|
||||||
|
if v == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return v.(*VDomContextVal)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RootElem) renderComponent(cfunc CFunc, elem *Elem, comp **Component) {
|
||||||
|
if (*comp).Children != nil {
|
||||||
|
for _, child := range (*comp).Children {
|
||||||
|
r.unmount(&child)
|
||||||
|
}
|
||||||
|
(*comp).Children = nil
|
||||||
|
}
|
||||||
|
props := make(map[string]any)
|
||||||
|
for k, v := range elem.Props {
|
||||||
|
props[k] = v
|
||||||
|
}
|
||||||
|
props[ChildrenPropKey] = elem.Children
|
||||||
|
ctx := r.makeRenderContext(*comp)
|
||||||
|
renderedElem := cfunc(ctx, props)
|
||||||
|
rtnElemArr := partToElems(renderedElem)
|
||||||
|
if len(rtnElemArr) == 0 {
|
||||||
|
r.unmount(&(*comp).Comp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var rtnElem *Elem
|
||||||
|
if len(rtnElemArr) == 1 {
|
||||||
|
rtnElem = &rtnElemArr[0]
|
||||||
|
} else {
|
||||||
|
rtnElem = &Elem{Tag: FragmentTag, Children: rtnElemArr}
|
||||||
|
}
|
||||||
|
r.render(rtnElem, &(*comp).Comp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertPropsToVDom(props map[string]any) map[string]any {
|
||||||
|
if len(props) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
vdomProps := make(map[string]any)
|
||||||
|
for k, v := range props {
|
||||||
|
if v == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val := reflect.ValueOf(v)
|
||||||
|
if val.Kind() == reflect.Func {
|
||||||
|
vdomProps[k] = VDomFuncType{FuncType: "server"}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
vdomProps[k] = v
|
||||||
|
}
|
||||||
|
return vdomProps
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertBaseToVDom(c *Component) *Elem {
|
||||||
|
elem := &Elem{Id: c.Id, Tag: c.Tag}
|
||||||
|
if c.Elem != nil {
|
||||||
|
elem.Props = convertPropsToVDom(c.Elem.Props)
|
||||||
|
}
|
||||||
|
for _, child := range c.Children {
|
||||||
|
childVDom := convertToVDom(child)
|
||||||
|
if childVDom != nil {
|
||||||
|
elem.Children = append(elem.Children, *childVDom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return elem
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertToVDom(c *Component) *Elem {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if c.Tag == TextTag {
|
||||||
|
return &Elem{Tag: TextTag, Text: c.Text}
|
||||||
|
}
|
||||||
|
if isBaseTag(c.Tag) {
|
||||||
|
return convertBaseToVDom(c)
|
||||||
|
} else {
|
||||||
|
return convertToVDom(c.Comp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RootElem) makeVDom(comp *Component) *Elem {
|
||||||
|
vdomElem := convertToVDom(comp)
|
||||||
|
return vdomElem
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RootElem) MakeVDom() *Elem {
|
||||||
|
return r.makeVDom(r.Root)
|
||||||
|
}
|
120
pkg/vdom/vdom_test.go
Normal file
120
pkg/vdom/vdom_test.go
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
package vdom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type renderContextKeyType struct{}
|
||||||
|
|
||||||
|
var renderContextKey = renderContextKeyType{}
|
||||||
|
|
||||||
|
type TestContext struct {
|
||||||
|
ButtonId string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Page(ctx context.Context, props map[string]any) any {
|
||||||
|
clicked, setClicked := UseState(ctx, false)
|
||||||
|
var clickedDiv *Elem
|
||||||
|
if clicked {
|
||||||
|
clickedDiv = Bind(`<div>clicked</div>`, nil)
|
||||||
|
}
|
||||||
|
clickFn := func() {
|
||||||
|
log.Printf("run clickFn\n")
|
||||||
|
setClicked(true)
|
||||||
|
}
|
||||||
|
return Bind(
|
||||||
|
`
|
||||||
|
<div>
|
||||||
|
<h1>hello world</h1>
|
||||||
|
<Button onClick="#bind:clickFn">hello</Button>
|
||||||
|
<bind key="clickedDiv"/>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
map[string]any{"clickFn": clickFn, "clickedDiv": clickedDiv},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Button(ctx context.Context, props map[string]any) any {
|
||||||
|
ref := UseRef(ctx, nil)
|
||||||
|
clName, setClName := UseState(ctx, "button")
|
||||||
|
UseEffect(ctx, func() func() {
|
||||||
|
fmt.Printf("Button useEffect\n")
|
||||||
|
setClName("button mounted")
|
||||||
|
return nil
|
||||||
|
}, nil)
|
||||||
|
compId := UseId(ctx)
|
||||||
|
testContext := getTestContext(ctx)
|
||||||
|
if testContext != nil {
|
||||||
|
testContext.ButtonId = compId
|
||||||
|
}
|
||||||
|
return Bind(`
|
||||||
|
<div className="#bind:clName" ref="#bind:ref" onClick="#bind:onClick">
|
||||||
|
<bind key="children"/>
|
||||||
|
</div>
|
||||||
|
`, map[string]any{"clName": clName, "ref": ref, "onClick": props["onClick"], "children": props["children"]})
|
||||||
|
}
|
||||||
|
|
||||||
|
func printVDom(root *RootElem) {
|
||||||
|
vd := root.MakeVDom()
|
||||||
|
jsonBytes, _ := json.MarshalIndent(vd, "", " ")
|
||||||
|
fmt.Printf("%s\n", string(jsonBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTestContext(ctx context.Context) *TestContext {
|
||||||
|
val := ctx.Value(renderContextKey)
|
||||||
|
if val == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return val.(*TestContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test1(t *testing.T) {
|
||||||
|
log.Printf("hello!\n")
|
||||||
|
testContext := &TestContext{ButtonId: ""}
|
||||||
|
ctx := context.WithValue(context.Background(), renderContextKey, testContext)
|
||||||
|
root := MakeRoot()
|
||||||
|
root.SetOuterCtx(ctx)
|
||||||
|
root.RegisterComponent("Page", Page)
|
||||||
|
root.RegisterComponent("Button", Button)
|
||||||
|
root.Render(E("Page"))
|
||||||
|
if root.Root == nil {
|
||||||
|
t.Fatalf("root.Root is nil")
|
||||||
|
}
|
||||||
|
printVDom(root)
|
||||||
|
root.runWork()
|
||||||
|
printVDom(root)
|
||||||
|
root.Event(testContext.ButtonId, "onClick")
|
||||||
|
root.runWork()
|
||||||
|
printVDom(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBind(t *testing.T) {
|
||||||
|
elem := Bind(`<div>clicked</div>`, nil)
|
||||||
|
jsonBytes, _ := json.MarshalIndent(elem, "", " ")
|
||||||
|
log.Printf("%s\n", string(jsonBytes))
|
||||||
|
|
||||||
|
elem = Bind(`
|
||||||
|
<div>
|
||||||
|
clicked
|
||||||
|
</div>`, nil)
|
||||||
|
jsonBytes, _ = json.MarshalIndent(elem, "", " ")
|
||||||
|
log.Printf("%s\n", string(jsonBytes))
|
||||||
|
|
||||||
|
elem = Bind(`<Button>foo</Button>`, nil)
|
||||||
|
jsonBytes, _ = json.MarshalIndent(elem, "", " ")
|
||||||
|
log.Printf("%s\n", string(jsonBytes))
|
||||||
|
|
||||||
|
elem = Bind(`
|
||||||
|
<div>
|
||||||
|
<h1>hello world</h1>
|
||||||
|
<Button onClick="#bind:clickFn">hello</Button>
|
||||||
|
<bind key="clickedDiv"/>
|
||||||
|
</div>
|
||||||
|
`, nil)
|
||||||
|
jsonBytes, _ = json.MarshalIndent(elem, "", " ")
|
||||||
|
log.Printf("%s\n", string(jsonBytes))
|
||||||
|
}
|
@ -33,6 +33,9 @@ type ORef struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (oref ORef) String() string {
|
func (oref ORef) String() string {
|
||||||
|
if oref.OType == "" || oref.OID == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
return fmt.Sprintf("%s:%s", oref.OType, oref.OID)
|
return fmt.Sprintf("%s:%s", oref.OType, oref.OID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,6 +54,11 @@ func (oref *ORef) UnmarshalJSON(data []byte) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if len(orefStr) == 0 {
|
||||||
|
oref.OType = ""
|
||||||
|
oref.OID = ""
|
||||||
|
return nil
|
||||||
|
}
|
||||||
parsed, err := ParseORef(orefStr)
|
parsed, err := ParseORef(orefStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -36,7 +36,7 @@ func DeleteBlockCommand(w *wshutil.WshRpc, data wshrpc.CommandDeleteBlockData, o
|
|||||||
}
|
}
|
||||||
|
|
||||||
// command "file:append", wshserver.AppendFileCommand
|
// command "file:append", wshserver.AppendFileCommand
|
||||||
func AppendFileCommand(w *wshutil.WshRpc, data wshrpc.CommandAppendFileData, opts *wshrpc.WshRpcCommandOpts) error {
|
func AppendFileCommand(w *wshutil.WshRpc, data wshrpc.CommandFileData, opts *wshrpc.WshRpcCommandOpts) error {
|
||||||
_, err := sendRpcRequestCallHelper[any](w, "file:append", data, opts)
|
_, err := sendRpcRequestCallHelper[any](w, "file:append", data, opts)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -47,6 +47,18 @@ func AppendIJsonCommand(w *wshutil.WshRpc, data wshrpc.CommandAppendIJsonData, o
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// command "file:read", wshserver.ReadFile
|
||||||
|
func ReadFile(w *wshutil.WshRpc, data wshrpc.CommandFileData, opts *wshrpc.WshRpcCommandOpts) (string, error) {
|
||||||
|
resp, err := sendRpcRequestCallHelper[string](w, "file:read", data, opts)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// command "file:write", wshserver.WriteFile
|
||||||
|
func WriteFile(w *wshutil.WshRpc, data wshrpc.CommandFileData, opts *wshrpc.WshRpcCommandOpts) error {
|
||||||
|
_, err := sendRpcRequestCallHelper[any](w, "file:write", data, opts)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// command "getmeta", wshserver.GetMetaCommand
|
// command "getmeta", wshserver.GetMetaCommand
|
||||||
func GetMetaCommand(w *wshutil.WshRpc, data wshrpc.CommandGetMetaData, opts *wshrpc.WshRpcCommandOpts) (map[string]interface {}, error) {
|
func GetMetaCommand(w *wshutil.WshRpc, data wshrpc.CommandGetMetaData, opts *wshrpc.WshRpcCommandOpts) (map[string]interface {}, error) {
|
||||||
resp, err := sendRpcRequestCallHelper[map[string]interface {}](w, "getmeta", data, opts)
|
resp, err := sendRpcRequestCallHelper[map[string]interface {}](w, "getmeta", data, opts)
|
||||||
|
@ -26,6 +26,8 @@ const (
|
|||||||
Command_ResolveIds = "resolveids"
|
Command_ResolveIds = "resolveids"
|
||||||
Command_CreateBlock = "createblock"
|
Command_CreateBlock = "createblock"
|
||||||
Command_DeleteBlock = "deleteblock"
|
Command_DeleteBlock = "deleteblock"
|
||||||
|
Command_WriteFile = "file:write"
|
||||||
|
Command_ReadFile = "file:read"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MetaDataType = map[string]any
|
type MetaDataType = map[string]any
|
||||||
@ -123,10 +125,10 @@ type CommandBlockInputData struct {
|
|||||||
TermSize *shellexec.TermSize `json:"termsize,omitempty"`
|
TermSize *shellexec.TermSize `json:"termsize,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommandAppendFileData struct {
|
type CommandFileData struct {
|
||||||
ZoneId string `json:"zoneid" wshcontext:"BlockId"`
|
ZoneId string `json:"zoneid" wshcontext:"BlockId"`
|
||||||
FileName string `json:"filename"`
|
FileName string `json:"filename"`
|
||||||
Data64 string `json:"data64"`
|
Data64 string `json:"data64,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommandAppendIJsonData struct {
|
type CommandAppendIJsonData struct {
|
||||||
|
@ -45,6 +45,8 @@ var WshServerCommandToDeclMap = map[string]*WshServerMethodDecl{
|
|||||||
wshrpc.Command_AppendFile: GetWshServerMethod(wshrpc.Command_AppendFile, wshutil.RpcType_Call, "AppendFileCommand", WshServerImpl.AppendFileCommand),
|
wshrpc.Command_AppendFile: GetWshServerMethod(wshrpc.Command_AppendFile, wshutil.RpcType_Call, "AppendFileCommand", WshServerImpl.AppendFileCommand),
|
||||||
wshrpc.Command_AppendIJson: GetWshServerMethod(wshrpc.Command_AppendIJson, wshutil.RpcType_Call, "AppendIJsonCommand", WshServerImpl.AppendIJsonCommand),
|
wshrpc.Command_AppendIJson: GetWshServerMethod(wshrpc.Command_AppendIJson, wshutil.RpcType_Call, "AppendIJsonCommand", WshServerImpl.AppendIJsonCommand),
|
||||||
wshrpc.Command_DeleteBlock: GetWshServerMethod(wshrpc.Command_DeleteBlock, wshutil.RpcType_Call, "DeleteBlockCommand", WshServerImpl.DeleteBlockCommand),
|
wshrpc.Command_DeleteBlock: GetWshServerMethod(wshrpc.Command_DeleteBlock, wshutil.RpcType_Call, "DeleteBlockCommand", WshServerImpl.DeleteBlockCommand),
|
||||||
|
wshrpc.Command_WriteFile: GetWshServerMethod(wshrpc.Command_WriteFile, wshutil.RpcType_Call, "WriteFile", WshServerImpl.WriteFile),
|
||||||
|
wshrpc.Command_ReadFile: GetWshServerMethod(wshrpc.Command_ReadFile, wshutil.RpcType_Call, "ReadFile", WshServerImpl.ReadFile),
|
||||||
"streamtest": RespStreamTest_MethodDecl,
|
"streamtest": RespStreamTest_MethodDecl,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,11 +82,11 @@ func (ws *WshServer) GetMetaCommand(ctx context.Context, data wshrpc.CommandGetM
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ws *WshServer) SetMetaCommand(ctx context.Context, data wshrpc.CommandSetMetaData) error {
|
func (ws *WshServer) SetMetaCommand(ctx context.Context, data wshrpc.CommandSetMetaData) error {
|
||||||
|
log.Printf("SETMETA: %s | %v\n", data.ORef, data.Meta)
|
||||||
oref := data.ORef
|
oref := data.ORef
|
||||||
if oref.IsEmpty() {
|
if oref.IsEmpty() {
|
||||||
return fmt.Errorf("no oref")
|
return fmt.Errorf("no oref")
|
||||||
}
|
}
|
||||||
log.Printf("SETMETA: %s | %v\n", oref, data.Meta)
|
|
||||||
obj, err := wstore.DBGetORef(ctx, oref)
|
obj, err := wstore.DBGetORef(ctx, oref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error getting object: %w", err)
|
return fmt.Errorf("error getting object: %w", err)
|
||||||
@ -249,7 +251,36 @@ func (ws *WshServer) BlockInputCommand(ctx context.Context, data wshrpc.CommandB
|
|||||||
return bc.SendInput(inputUnion)
|
return bc.SendInput(inputUnion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ws *WshServer) AppendFileCommand(ctx context.Context, data wshrpc.CommandAppendFileData) error {
|
func (ws *WshServer) WriteFile(ctx context.Context, data wshrpc.CommandFileData) error {
|
||||||
|
dataBuf, err := base64.StdEncoding.DecodeString(data.Data64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error decoding data64: %w", err)
|
||||||
|
}
|
||||||
|
err = filestore.WFS.WriteFile(ctx, data.ZoneId, data.FileName, dataBuf)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error writing to blockfile: %w", err)
|
||||||
|
}
|
||||||
|
eventbus.SendEvent(eventbus.WSEventType{
|
||||||
|
EventType: eventbus.WSEvent_BlockFile,
|
||||||
|
ORef: waveobj.MakeORef(wstore.OType_Block, data.ZoneId).String(),
|
||||||
|
Data: &eventbus.WSFileEventData{
|
||||||
|
ZoneId: data.ZoneId,
|
||||||
|
FileName: data.FileName,
|
||||||
|
FileOp: eventbus.FileOp_Invalidate,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WshServer) ReadFile(ctx context.Context, data wshrpc.CommandFileData) (string, error) {
|
||||||
|
_, dataBuf, err := filestore.WFS.ReadFile(ctx, data.ZoneId, data.FileName)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error reading blockfile: %w", err)
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(dataBuf), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WshServer) AppendFileCommand(ctx context.Context, data wshrpc.CommandFileData) error {
|
||||||
dataBuf, err := base64.StdEncoding.DecodeString(data.Data64)
|
dataBuf, err := base64.StdEncoding.DecodeString(data.Data64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error decoding data64: %w", err)
|
return fmt.Errorf("error decoding data64: %w", err)
|
||||||
|
@ -7,6 +7,15 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
// these should both be 5 characters
|
// these should both be 5 characters
|
||||||
@ -94,3 +103,89 @@ func EncodeWaveOSCMessageEx(oscNum string, msg *RpcMessage) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
return EncodeWaveOSCBytes(oscNum, barr), nil
|
return EncodeWaveOSCBytes(oscNum, barr), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var termModeLock = sync.Mutex{}
|
||||||
|
var termIsRaw bool
|
||||||
|
var origTermState *term.State
|
||||||
|
var shutdownSignalHandlersInstalled bool
|
||||||
|
var shutdownOnce sync.Once
|
||||||
|
var extraShutdownFunc atomic.Pointer[func()]
|
||||||
|
|
||||||
|
func DoShutdown(reason string, exitCode int, quiet bool) {
|
||||||
|
shutdownOnce.Do(func() {
|
||||||
|
defer os.Exit(exitCode)
|
||||||
|
RestoreTermState()
|
||||||
|
extraFn := extraShutdownFunc.Load()
|
||||||
|
if extraFn != nil {
|
||||||
|
(*extraFn)()
|
||||||
|
}
|
||||||
|
if !quiet && reason != "" {
|
||||||
|
log.Printf("shutting down: %s\r\n", reason)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func installShutdownSignalHandlers(quiet bool) {
|
||||||
|
termModeLock.Lock()
|
||||||
|
defer termModeLock.Unlock()
|
||||||
|
if shutdownSignalHandlersInstalled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sigCh := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGINT)
|
||||||
|
go func() {
|
||||||
|
for sig := range sigCh {
|
||||||
|
DoShutdown(fmt.Sprintf("got signal %v", sig), 1, quiet)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetTermRawModeAndInstallShutdownHandlers(quietShutdown bool) {
|
||||||
|
SetTermRawMode()
|
||||||
|
installShutdownSignalHandlers(quietShutdown)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetExtraShutdownFunc(fn func()) {
|
||||||
|
extraShutdownFunc.Store(&fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetTermRawMode() {
|
||||||
|
termModeLock.Lock()
|
||||||
|
defer termModeLock.Unlock()
|
||||||
|
if termIsRaw {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
origState, err := term.MakeRaw(int(os.Stdin.Fd()))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error setting raw mode: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
origTermState = origState
|
||||||
|
termIsRaw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func RestoreTermState() {
|
||||||
|
termModeLock.Lock()
|
||||||
|
defer termModeLock.Unlock()
|
||||||
|
if !termIsRaw || origTermState == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
term.Restore(int(os.Stdin.Fd()), origTermState)
|
||||||
|
termIsRaw = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns (wshRpc, wrappedStdin)
|
||||||
|
func SetupTerminalRpcClient(handlerFn func(*RpcResponseHandler) bool) (*WshRpc, io.Reader) {
|
||||||
|
messageCh := make(chan []byte, 32)
|
||||||
|
outputCh := make(chan []byte, 32)
|
||||||
|
ptyBuf := MakePtyBuffer(WaveServerOSCPrefix, os.Stdin, messageCh)
|
||||||
|
rpcClient := MakeWshRpc(messageCh, outputCh, RpcContext{}, handlerFn)
|
||||||
|
go func() {
|
||||||
|
for msg := range outputCh {
|
||||||
|
barr := EncodeWaveOSCBytes(WaveOSC, msg)
|
||||||
|
os.Stdout.Write(barr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return rpcClient, ptyBuf
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user