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
|
||||
|
||||
type WaveAppStyle struct {
|
||||
BackgroundColor string `json:"backgroundColor,omitempty"`
|
||||
Color string `json:"color,omitempty"`
|
||||
Border string `json:"border,omitempty"`
|
||||
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"`
|
||||
}
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
type WaveAppMouseEvent struct {
|
||||
TargetId string `json:"targetid"`
|
||||
}
|
||||
"github.com/wavetermdev/thenextwave/pkg/vdom"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wshutil"
|
||||
)
|
||||
|
||||
type WaveAppChangeEvent struct {
|
||||
TargetId string `json:"targetid"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
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
|
||||
func Page(ctx context.Context, props map[string]any) any {
|
||||
clicked, setClicked := vdom.UseState(ctx, false)
|
||||
var clickedDiv *vdom.Elem
|
||||
if clicked {
|
||||
clickedDiv = vdom.Bind(`<div>clicked</div>`, nil)
|
||||
}
|
||||
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() {
|
||||
|
||||
wshutil.SetTermRawModeAndInstallShutdownHandlers(true)
|
||||
defer wshutil.RestoreTermState()
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wshutil"
|
||||
)
|
||||
|
||||
var deleteBlockCmd = &cobra.Command{
|
||||
@ -32,7 +33,7 @@ func deleteBlockRun(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("%v\n", err)
|
||||
return
|
||||
}
|
||||
setTermRawMode()
|
||||
wshutil.SetTermRawModeAndInstallShutdownHandlers(true)
|
||||
fullORef, err := resolveSimpleId(oref)
|
||||
if err != nil {
|
||||
fmt.Printf("error resolving oref: %v\r\n", err)
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wshrpc/wshclient"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wshutil"
|
||||
)
|
||||
|
||||
var getMetaCmd = &cobra.Command{
|
||||
@ -37,7 +38,7 @@ func getMetaRun(cmd *cobra.Command, args []string) {
|
||||
return
|
||||
}
|
||||
|
||||
setTermRawMode()
|
||||
wshutil.SetTermRawModeAndInstallShutdownHandlers(true)
|
||||
fullORef, err := resolveSimpleId(oref)
|
||||
if err != nil {
|
||||
fmt.Printf("error resolving oref: %v\r\n", err)
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wshutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -20,20 +21,20 @@ var htmlCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
func htmlRun(cmd *cobra.Command, args []string) {
|
||||
defer doShutdown("normal exit", 0)
|
||||
defer wshutil.DoShutdown("normal exit", 0, true)
|
||||
setTermHtmlMode()
|
||||
for {
|
||||
var buf [1]byte
|
||||
_, err := WrappedStdin.Read(buf[:])
|
||||
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 {
|
||||
doShutdown("read Ctrl-C from stdin", 1)
|
||||
wshutil.DoShutdown("read Ctrl-C from stdin", 1, true)
|
||||
break
|
||||
}
|
||||
if buf[0] == 'x' {
|
||||
doShutdown("read 'x' from stdin", 0)
|
||||
wshutil.DoShutdown("read 'x' from stdin", 0, true)
|
||||
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"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@ -21,7 +18,6 @@ import (
|
||||
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wshrpc/wshclient"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wshutil"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -32,85 +28,37 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
var shutdownOnce sync.Once
|
||||
var origTermState *term.State
|
||||
var madeRaw bool
|
||||
var usingHtmlMode bool
|
||||
var shutdownSignalHandlersInstalled bool
|
||||
var WrappedStdin io.Reader
|
||||
var RpcClient *wshutil.WshRpc
|
||||
|
||||
func doShutdown(reason string, exitCode int) {
|
||||
shutdownOnce.Do(func() {
|
||||
defer os.Exit(exitCode)
|
||||
if reason != "" {
|
||||
log.Printf("shutting down: %s\r\n", reason)
|
||||
func extraShutdownFn() {
|
||||
if usingHtmlMode {
|
||||
cmd := &wshrpc.CommandSetMetaData{
|
||||
Meta: map[string]any{"term:mode": nil},
|
||||
}
|
||||
if usingHtmlMode {
|
||||
cmd := &wshrpc.CommandSetMetaData{
|
||||
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)
|
||||
}
|
||||
})
|
||||
RpcClient.SendCommand(wshrpc.Command_SetMeta, cmd)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// returns the wrapped stdin and a new rpc client (that wraps the stdin input and stdout output)
|
||||
func setupRpcClient(handlerFn wshutil.CommandHandlerFnType) {
|
||||
log.Printf("setup rpc client\r\n")
|
||||
messageCh := make(chan []byte, 32)
|
||||
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
|
||||
RpcClient, WrappedStdin = wshutil.SetupTerminalRpcClient(handlerFn)
|
||||
}
|
||||
|
||||
func setTermHtmlMode() {
|
||||
installShutdownSignalHandlers()
|
||||
setTermRawMode()
|
||||
wshutil.SetExtraShutdownFunc(extraShutdownFn)
|
||||
wshutil.SetTermRawModeAndInstallShutdownHandlers(true)
|
||||
cmd := &wshrpc.CommandSetMetaData{
|
||||
Meta: map[string]any{"term:mode": "html"},
|
||||
}
|
||||
RpcClient.SendCommand(wshrpc.Command_SetMeta, cmd)
|
||||
usingHtmlMode = true
|
||||
}
|
||||
|
||||
func installShutdownSignalHandlers() {
|
||||
if shutdownSignalHandlersInstalled {
|
||||
return
|
||||
err := RpcClient.SendCommand(wshrpc.Command_SetMeta, cmd)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error setting html mode: %v\r\n", err)
|
||||
}
|
||||
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)
|
||||
break
|
||||
}
|
||||
}()
|
||||
usingHtmlMode = true
|
||||
}
|
||||
|
||||
var oidRe = regexp.MustCompile(`^[0-9a-f]{8}$`)
|
||||
@ -162,7 +110,7 @@ func resolveSimpleId(id string) (*waveobj.ORef, error) {
|
||||
|
||||
// Execute executes the root command.
|
||||
func Execute() error {
|
||||
defer doShutdown("", 0)
|
||||
defer wshutil.DoShutdown("", 0, false)
|
||||
setupRpcClient(nil)
|
||||
return rootCmd.Execute()
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wshutil"
|
||||
)
|
||||
|
||||
var setMetaCmd = &cobra.Command{
|
||||
@ -74,7 +75,7 @@ func setMetaRun(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("%v\n", err)
|
||||
return
|
||||
}
|
||||
setTermRawMode()
|
||||
wshutil.SetTermRawModeAndInstallShutdownHandlers(true)
|
||||
fullORef, err := resolveSimpleId(oref)
|
||||
if err != nil {
|
||||
fmt.Printf("error resolving oref: %v\n", err)
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wshrpc"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wshutil"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wstore"
|
||||
)
|
||||
|
||||
@ -43,7 +44,7 @@ func viewRun(cmd *cobra.Command, args []string) {
|
||||
if err != nil {
|
||||
log.Printf("error getting file info: %v\n", err)
|
||||
}
|
||||
setTermRawMode()
|
||||
wshutil.SetTermRawModeAndInstallShutdownHandlers(true)
|
||||
viewWshCmd := &wshrpc.CommandCreateBlockData{
|
||||
BlockDef: &wstore.BlockDef{
|
||||
View: "preview",
|
||||
|
@ -28,7 +28,7 @@ class WshServerType {
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@ -37,6 +37,16 @@ class WshServerType {
|
||||
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]
|
||||
GetMetaCommand(data: CommandGetMetaData, opts?: WshRpcCommandOpts): Promise<MetaType> {
|
||||
return WOS.wshServerRpcHelper_call("getmeta", data, opts);
|
||||
|
@ -9,11 +9,11 @@ import clsx from "clsx";
|
||||
import { produce } from "immer";
|
||||
import * as jotai from "jotai";
|
||||
import * as React from "react";
|
||||
import { IJsonView } from "./ijson";
|
||||
import { TermStickers } from "./termsticker";
|
||||
import { TermWrap } from "./termwrap";
|
||||
|
||||
import { WshServer } from "@/app/store/wshserver";
|
||||
import { VDomView } from "@/app/view/term/vdom";
|
||||
import "public/xterm.css";
|
||||
import "./term.less";
|
||||
|
||||
@ -100,16 +100,24 @@ type InitialLoadDataType = {
|
||||
heldData: Uint8Array[];
|
||||
};
|
||||
|
||||
const IJSONConst = {
|
||||
function vdomText(text: string): VDomElem {
|
||||
return {
|
||||
tag: "#text",
|
||||
text: text,
|
||||
};
|
||||
}
|
||||
|
||||
const testVDom: VDomElem = {
|
||||
id: "testid1",
|
||||
tag: "div",
|
||||
children: [
|
||||
{
|
||||
tag: "h1",
|
||||
children: ["Hello World"],
|
||||
children: [vdomText("Hello World")],
|
||||
},
|
||||
{
|
||||
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 key="htmlElemContent" className="term-htmlelem-content">
|
||||
<IJsonView rootNode={IJSONConst} />
|
||||
<VDomView rootNode={testVDom} />
|
||||
</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;
|
||||
};
|
||||
|
||||
// wshrpc.CommandAppendFileData
|
||||
type CommandAppendFileData = {
|
||||
zoneid: string;
|
||||
filename: string;
|
||||
data64: string;
|
||||
};
|
||||
|
||||
// wshrpc.CommandAppendIJsonData
|
||||
type CommandAppendIJsonData = {
|
||||
zoneid: string;
|
||||
@ -100,6 +93,13 @@ declare global {
|
||||
blockid: string;
|
||||
};
|
||||
|
||||
// wshrpc.CommandFileData
|
||||
type CommandFileData = {
|
||||
zoneid: string;
|
||||
filename: string;
|
||||
data64?: string;
|
||||
};
|
||||
|
||||
// wshrpc.CommandGetMetaData
|
||||
type CommandGetMetaData = {
|
||||
oref: ORef;
|
||||
@ -297,6 +297,29 @@ declare global {
|
||||
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 = {
|
||||
wscommand: string;
|
||||
} & ( SetBlockTermSizeWSCommand | BlockInputWSCommand | WSRpcCommand );
|
||||
|
6
go.mod
6
go.mod
@ -1,8 +1,6 @@
|
||||
module github.com/wavetermdev/thenextwave
|
||||
|
||||
go 1.22
|
||||
|
||||
toolchain go1.22.1
|
||||
go 1.22.4
|
||||
|
||||
require (
|
||||
github.com/alexflint/go-filemutex v1.3.0
|
||||
@ -18,6 +16,7 @@ require (
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/sawka/txwrap v0.2.0
|
||||
github.com/wavetermdev/htmltoken v0.1.0
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/wavetermdev/waveterm/wavesrv v0.0.0-20240508181017-d07068c09d94
|
||||
golang.org/x/crypto v0.25.0
|
||||
@ -32,6 +31,7 @@ require (
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/testify v1.8.4 // 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
|
||||
)
|
||||
|
||||
|
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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
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/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||
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=
|
||||
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/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.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
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
|
||||
}
|
||||
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 {
|
||||
cmdOpts = shellexec.CommandOptsType{Interactive: true, Login: true}
|
||||
cmdOpts.Interactive = true
|
||||
cmdOpts.Login = true
|
||||
} else if bc.ControllerType == BlockController_Cmd {
|
||||
if _, ok := blockMeta["cmd"].(string); ok {
|
||||
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 {
|
||||
cmdEnv := blockMeta["cmd:env"].(map[string]any)
|
||||
cmdOpts.Env = make(map[string]string)
|
||||
for k, v := range cmdEnv {
|
||||
if v == nil {
|
||||
continue
|
||||
|
@ -33,8 +33,9 @@ type WSEventType struct {
|
||||
}
|
||||
|
||||
const (
|
||||
FileOp_Append = "append"
|
||||
FileOp_Truncate = "truncate"
|
||||
FileOp_Append = "append"
|
||||
FileOp_Truncate = "truncate"
|
||||
FileOp_Invalidate = "invalidate"
|
||||
)
|
||||
|
||||
type WSFileEventData struct {
|
||||
|
@ -176,6 +176,11 @@ func StartRemoteShellProc(termSize TermSize, cmdStr string, cmdOpts CommandOptsT
|
||||
session.Stdin = cmdTty
|
||||
session.Stdout = 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)
|
||||
|
||||
sessionWrap := SessionWrap{session, cmdCombined, cmdTty}
|
||||
@ -216,6 +221,7 @@ func StartShellProc(termSize TermSize, cmdStr string, cmdOpts CommandOptsType) (
|
||||
envToAdd["LANG"] = wavebase.DetermineLang()
|
||||
}
|
||||
shellutil.UpdateCmdEnv(ecmd, envToAdd)
|
||||
shellutil.UpdateCmdEnv(ecmd, cmdOpts.Env)
|
||||
cmdPty, cmdTty, err := pty.Open()
|
||||
if err != nil {
|
||||
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/tsgen/tsgenmeta"
|
||||
"github.com/wavetermdev/thenextwave/pkg/userinput"
|
||||
"github.com/wavetermdev/thenextwave/pkg/vdom"
|
||||
"github.com/wavetermdev/thenextwave/pkg/waveobj"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wconfig"
|
||||
"github.com/wavetermdev/thenextwave/pkg/web/webcmd"
|
||||
@ -41,6 +42,9 @@ var ExtraTypes = []any{
|
||||
wshutil.RpcMessage{},
|
||||
wshrpc.WshServerCommandMeta{},
|
||||
userinput.UserInputRequest{},
|
||||
vdom.Elem{},
|
||||
vdom.VDomFuncType{},
|
||||
vdom.VDomRefType{},
|
||||
}
|
||||
|
||||
// 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{
|
||||
"Window": "WaveWindow",
|
||||
"Elem": "VDomElem",
|
||||
}
|
||||
|
||||
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 {
|
||||
if oref.OType == "" || oref.OID == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s:%s", oref.OType, oref.OID)
|
||||
}
|
||||
|
||||
@ -51,6 +54,11 @@ func (oref *ORef) UnmarshalJSON(data []byte) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(orefStr) == 0 {
|
||||
oref.OType = ""
|
||||
oref.OID = ""
|
||||
return nil
|
||||
}
|
||||
parsed, err := ParseORef(orefStr)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -36,7 +36,7 @@ func DeleteBlockCommand(w *wshutil.WshRpc, data wshrpc.CommandDeleteBlockData, o
|
||||
}
|
||||
|
||||
// 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)
|
||||
return err
|
||||
}
|
||||
@ -47,6 +47,18 @@ func AppendIJsonCommand(w *wshutil.WshRpc, data wshrpc.CommandAppendIJsonData, o
|
||||
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
|
||||
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)
|
||||
|
@ -26,6 +26,8 @@ const (
|
||||
Command_ResolveIds = "resolveids"
|
||||
Command_CreateBlock = "createblock"
|
||||
Command_DeleteBlock = "deleteblock"
|
||||
Command_WriteFile = "file:write"
|
||||
Command_ReadFile = "file:read"
|
||||
)
|
||||
|
||||
type MetaDataType = map[string]any
|
||||
@ -123,10 +125,10 @@ type CommandBlockInputData struct {
|
||||
TermSize *shellexec.TermSize `json:"termsize,omitempty"`
|
||||
}
|
||||
|
||||
type CommandAppendFileData struct {
|
||||
type CommandFileData struct {
|
||||
ZoneId string `json:"zoneid" wshcontext:"BlockId"`
|
||||
FileName string `json:"filename"`
|
||||
Data64 string `json:"data64"`
|
||||
Data64 string `json:"data64,omitempty"`
|
||||
}
|
||||
|
||||
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_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_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,
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
log.Printf("SETMETA: %s | %v\n", data.ORef, data.Meta)
|
||||
oref := data.ORef
|
||||
if oref.IsEmpty() {
|
||||
return fmt.Errorf("no oref")
|
||||
}
|
||||
log.Printf("SETMETA: %s | %v\n", oref, data.Meta)
|
||||
obj, err := wstore.DBGetORef(ctx, oref)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding data64: %w", err)
|
||||
|
@ -7,6 +7,15 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// these should both be 5 characters
|
||||
@ -94,3 +103,89 @@ func EncodeWaveOSCMessageEx(oscNum string, msg *RpcMessage) ([]byte, error) {
|
||||
}
|
||||
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