mirror of
https://github.com/wavetermdev/backup.git
synced 2026-08-05 13:57:07 -07:00
working on vdom implementation, other fixes (#136)
This commit is contained in:
+43
-37
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
+15
-67
@@ -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>
|
||||
|
||||
@@ -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 };
|
||||
Vendored
+30
-7
@@ -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 );
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user