working on vdom implementation, other fixes (#136)

This commit is contained in:
Mike Sawka
2024-07-23 13:16:53 -07:00
committed by GitHub
parent e6f60ff210
commit 6c2ef6cb99
28 changed files with 1503 additions and 138 deletions
+43 -37
View File
@@ -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()
}
+2 -1
View File
@@ -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)
+2 -1
View File
@@ -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)
+5 -4
View File
@@ -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
View 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))
}
+15 -67
View File
@@ -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()
}
+2 -1
View File
@@ -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)
+2 -1
View File
@@ -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",
+11 -1
View File
@@ -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);
+13 -5
View File
@@ -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
View 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 };
+30 -7
View File
@@ -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 );
+3 -3
View File
@@ -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
View File
@@ -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=
+7 -3
View File
@@ -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
+3 -2
View File
@@ -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 {
+6
View File
@@ -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)
+5
View File
@@ -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
View 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
View 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
}

Some files were not shown because too many files have changed in this diff Show More