Skip to content

Commit eabda0d

Browse files
committed
update AppOpts, simplify, working on build and tw.css...
1 parent e9d0258 commit eabda0d

9 files changed

Lines changed: 192 additions & 85 deletions

File tree

tsunami/app/tsunamiapp.go

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727

2828
const TsunamiListenAddrEnvVar = "TSUNAMI_LISTENADDR"
2929
const DefaultListenAddr = "localhost:0"
30+
const DefaultComponentName = "App"
3031

3132
type SSEvent struct {
3233
Event string
@@ -37,8 +38,6 @@ type AppOpts struct {
3738
Title string // window title
3839
CloseOnCtrlC bool
3940
GlobalKeyboardEvents bool
40-
GlobalStyles []byte
41-
RootComponentName string // defaults to "App"
4241
}
4342

4443
type Client struct {
@@ -47,11 +46,11 @@ type Client struct {
4746
Root *comp.RootElem
4847
RootElem *vdom.VDomElem
4948
CurrentClientId string
49+
ServerId string
5050
IsDone bool
5151
DoneReason string
5252
DoneCh chan struct{}
5353
SSEventCh chan SSEvent
54-
Opts rpctypes.VDomBackendOpts
5554
GlobalEventHandler func(client *Client, event vdom.VDomEvent)
5655
GlobalStylesOption *FileHandlerOption
5756
UrlHandlerMux *mux.Router
@@ -65,6 +64,8 @@ func MakeClient(appOpts AppOpts) *Client {
6564
DoneCh: make(chan struct{}),
6665
SSEventCh: make(chan SSEvent, 100),
6766
UrlHandlerMux: mux.NewRouter(),
67+
ServerId: uuid.New().String(),
68+
RootElem: vdom.E(DefaultComponentName),
6869
}
6970
client.SetAppOpts(appOpts)
7071
return client
@@ -114,27 +115,27 @@ func (c *Client) SetAppOpts(appOpts AppOpts) {
114115
c.Lock.Lock()
115116
defer c.Lock.Unlock()
116117

117-
if appOpts.RootComponentName == "" {
118-
appOpts.RootComponentName = "App"
119-
}
120-
121118
c.AppOpts = appOpts
119+
}
120+
121+
func getFaviconPath() string {
122+
if staticFS != nil {
123+
faviconNames := []string{"favicon.ico", "favicon.png", "favicon.svg", "favicon.gif", "favicon.jpg"}
124+
for _, name := range faviconNames {
125+
if _, err := staticFS.Open(name); err == nil {
126+
return "/static/" + name
127+
}
128+
}
129+
}
130+
return "/wave-logo-256.png"
131+
}
122132

123-
// Update the VDomBackendOpts
124-
c.Opts.CloseOnCtrlC = appOpts.CloseOnCtrlC
125-
c.Opts.GlobalKeyboardEvents = appOpts.GlobalKeyboardEvents
126-
c.Opts.Title = appOpts.Title
127-
128-
// Update RootElem if component name changed
129-
c.RootElem = vdom.E(appOpts.RootComponentName)
130-
131-
// Update global styles
132-
if len(appOpts.GlobalStyles) > 0 {
133-
c.Opts.GlobalStyles = true
134-
c.GlobalStylesOption = &FileHandlerOption{Data: appOpts.GlobalStyles, MimeType: "text/css"}
135-
} else {
136-
c.Opts.GlobalStyles = false
137-
c.GlobalStylesOption = nil
133+
func (c *Client) makeBackendOpts() *rpctypes.VDomBackendOpts {
134+
return &rpctypes.VDomBackendOpts{
135+
Title: c.AppOpts.Title,
136+
CloseOnCtrlC: c.AppOpts.CloseOnCtrlC,
137+
GlobalKeyboardEvents: c.AppOpts.GlobalKeyboardEvents,
138+
FaviconPath: getFaviconPath(),
138139
}
139140
}
140141

@@ -275,10 +276,11 @@ func (c *Client) fullRender() (*rpctypes.VDomBackendUpdate, error) {
275276
renderedVDom = makeNullVDom()
276277
}
277278
return &rpctypes.VDomBackendUpdate{
278-
Type: "backendupdate",
279-
Ts: time.Now().UnixMilli(),
280-
HasWork: len(c.Root.EffectWorkQueue) > 0,
281-
Opts: &c.Opts,
279+
Type: "backendupdate",
280+
Ts: time.Now().UnixMilli(),
281+
ServerId: c.ServerId,
282+
HasWork: len(c.Root.EffectWorkQueue) > 0,
283+
Opts: c.makeBackendOpts(),
282284
RenderUpdates: []rpctypes.VDomRenderUpdate{
283285
{UpdateType: "root", VDom: renderedVDom},
284286
},
@@ -295,8 +297,9 @@ func (c *Client) incrementalRender() (*rpctypes.VDomBackendUpdate, error) {
295297
renderedVDom = makeNullVDom()
296298
}
297299
return &rpctypes.VDomBackendUpdate{
298-
Type: "backendupdate",
299-
Ts: time.Now().UnixMilli(),
300+
Type: "backendupdate",
301+
Ts: time.Now().UnixMilli(),
302+
ServerId: c.ServerId,
300303
RenderUpdates: []rpctypes.VDomRenderUpdate{
301304
{UpdateType: "root", VDom: renderedVDom},
302305
},

tsunami/build/build.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import (
44
"fmt"
55
"log"
66
"os"
7+
"os/exec"
78
"path/filepath"
9+
"regexp"
10+
"strconv"
811
"strings"
912
)
1013

@@ -13,6 +16,81 @@ type BuildOpts struct {
1316
Verbose bool
1417
}
1518

19+
func verifyEnvironment(verbose bool) error {
20+
// Check if go is in PATH
21+
goPath, err := exec.LookPath("go")
22+
if err != nil {
23+
return fmt.Errorf("go command not found in PATH: %w", err)
24+
}
25+
26+
// Run go version command
27+
cmd := exec.Command(goPath, "version")
28+
output, err := cmd.Output()
29+
if err != nil {
30+
return fmt.Errorf("failed to run 'go version': %w", err)
31+
}
32+
33+
// Parse go version output and check for 1.21+
34+
versionStr := strings.TrimSpace(string(output))
35+
if verbose {
36+
log.Printf("Found %s", versionStr)
37+
}
38+
39+
// Extract version like "go1.21.0" from output
40+
versionRegex := regexp.MustCompile(`go1\.(\d+)`)
41+
matches := versionRegex.FindStringSubmatch(versionStr)
42+
if len(matches) < 2 {
43+
return fmt.Errorf("unable to parse go version from: %s", versionStr)
44+
}
45+
46+
minor, err := strconv.Atoi(matches[1])
47+
if err != nil || minor < 21 {
48+
return fmt.Errorf("go version 1.21 or higher required, found: %s", versionStr)
49+
}
50+
51+
// Check if npx is in PATH
52+
_, err = exec.LookPath("npx")
53+
if err != nil {
54+
return fmt.Errorf("npx command not found in PATH: %w", err)
55+
}
56+
57+
if verbose {
58+
log.Printf("Found npx in PATH")
59+
}
60+
61+
// Check Tailwind CSS version
62+
tailwindCmd := exec.Command("npx", "@tailwindcss/cli")
63+
tailwindOutput, err := tailwindCmd.CombinedOutput()
64+
if err != nil {
65+
return fmt.Errorf("failed to run 'npx @tailwindcss/cli': %w", err)
66+
}
67+
68+
tailwindStr := strings.TrimSpace(string(tailwindOutput))
69+
lines := strings.Split(tailwindStr, "\n")
70+
if len(lines) == 0 {
71+
return fmt.Errorf("no output from tailwindcss command")
72+
}
73+
74+
firstLine := lines[0]
75+
if verbose {
76+
log.Printf("Found %s", firstLine)
77+
}
78+
79+
// Check for v4 (format: "≈ tailwindcss v4.1.12")
80+
tailwindRegex := regexp.MustCompile(`tailwindcss v(\d+)`)
81+
matches = tailwindRegex.FindStringSubmatch(firstLine)
82+
if len(matches) < 2 {
83+
return fmt.Errorf("unable to parse tailwindcss version from: %s", firstLine)
84+
}
85+
86+
majorVersion, err := strconv.Atoi(matches[1])
87+
if err != nil || majorVersion != 4 {
88+
return fmt.Errorf("tailwindcss v4 required, found: %s", firstLine)
89+
}
90+
91+
return nil
92+
}
93+
1694
func verifyTsunamiDir(dir string) error {
1795
if dir == "" {
1896
return fmt.Errorf("directory path cannot be empty")
@@ -53,6 +131,10 @@ func verifyTsunamiDir(dir string) error {
53131
}
54132

55133
func TsunamiBuild(opts BuildOpts) error {
134+
if err := verifyEnvironment(opts.Verbose); err != nil {
135+
return err
136+
}
137+
56138
if err := verifyTsunamiDir(opts.Dir); err != nil {
57139
return err
58140
}
Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,10 @@ import (
99
"github.com/wavetermdev/waveterm/tsunami/vdom"
1010
)
1111

12-
//go:embed tw.css
13-
var styleCSS []byte
14-
1512
func init() {
1613
// Set up the default client with embedded Tailwind styles and ctrl-c handling
1714
app.SetAppOpts(app.AppOpts{
1815
CloseOnCtrlC: true,
19-
GlobalStyles: styleCSS,
2016
Title: "Todo App (Tsunami Demo)",
2117
})
2218
}

tsunami/frontend/index.html

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
5-
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
65
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
76
<title>Tsunami App</title>
87
</head>
9-
<body>
8+
<body className="bg-background text-primary">
109
<div id="root"></div>
1110
<script type="module" src="/src/main.tsx"></script>
1211
</body>

tsunami/frontend/src/app.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,34 @@
11
// Copyright 2025, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
import { useState, useEffect } from "react";
45
import { TsunamiModel } from "@/model/tsunami-model";
56
import { VDomView } from "./vdom";
67

7-
const globalModel = new TsunamiModel();
8-
98
function App() {
9+
const [remountKey, setRemountKey] = useState(0);
10+
const [model, setModel] = useState(() => {
11+
const newModel = new TsunamiModel();
12+
newModel.remountCallback = () => {
13+
setRemountKey(prev => prev + 1);
14+
};
15+
return newModel;
16+
});
17+
18+
useEffect(() => {
19+
// Create a new model when remount key changes
20+
if (remountKey > 0) {
21+
const newModel = new TsunamiModel();
22+
newModel.remountCallback = () => {
23+
setRemountKey(prev => prev + 1);
24+
};
25+
setModel(newModel);
26+
}
27+
}, [remountKey]);
28+
1029
return (
1130
<div className="min-h-screen bg-background text-foreground">
12-
<VDomView model={globalModel} />
31+
<VDomView key={remountKey} model={model} />
1332
</div>
1433
);
1534
}

tsunami/frontend/src/model/tsunami-model.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@ function annotateEvent(event: VDomEvent, propName: string, reactEvent: React.Syn
8383

8484
export class TsunamiModel {
8585
clientId: string;
86+
serverId: string;
8687
viewRef: React.RefObject<HTMLDivElement> = { current: null };
88+
remountCallback: (() => void) | null = null;
8789
vdomRoot: jotai.PrimitiveAtom<VDomElem> = jotai.atom();
8890
atoms: Map<string, AtomContainer> = new Map(); // key is atomname
8991
refs: Map<string, RefContainer> = new Map(); // key is refid
@@ -108,6 +110,7 @@ export class TsunamiModel {
108110
globalVersion: jotai.PrimitiveAtom<number> = jotai.atom(0);
109111
hasBackendWork: boolean = false;
110112
noPadding: jotai.PrimitiveAtom<boolean>;
113+
cachedFaviconPath: string | null = null;
111114

112115
constructor() {
113116
this.clientId = getOrCreateClientId();
@@ -531,10 +534,46 @@ export class TsunamiModel {
531534
}
532535
}
533536

537+
updateFavicon(faviconPath: string | null) {
538+
if (faviconPath === this.cachedFaviconPath) {
539+
return;
540+
}
541+
542+
this.cachedFaviconPath = faviconPath;
543+
544+
let existingFavicon = document.querySelector('link[rel="icon"]') as HTMLLinkElement;
545+
546+
if (faviconPath) {
547+
if (existingFavicon) {
548+
existingFavicon.href = faviconPath;
549+
} else {
550+
const link = document.createElement('link');
551+
link.rel = 'icon';
552+
link.href = faviconPath;
553+
document.head.appendChild(link);
554+
}
555+
} else {
556+
if (existingFavicon) {
557+
existingFavicon.remove();
558+
}
559+
}
560+
}
561+
534562
handleBackendUpdate(update: VDomBackendUpdate) {
535563
if (update == null) {
536564
return;
537565
}
566+
567+
// Check if serverId is changing and trigger remount if needed
568+
if (this.serverId != null && this.serverId !== update.serverid) {
569+
// Server ID changed - need to remount the entire app
570+
if (this.remountCallback) {
571+
this.remountCallback();
572+
}
573+
return;
574+
}
575+
576+
this.serverId = update.serverid;
538577
getDefaultStore().set(this.contextActive, true);
539578
const idMap = new Map<string, VDomElem>();
540579
const vdomRoot = getDefaultStore().get(this.vdomRoot);
@@ -543,6 +582,9 @@ export class TsunamiModel {
543582
if (update.opts.title && update.opts.title.trim() !== "") {
544583
document.title = update.opts.title;
545584
}
585+
if (update.opts.faviconpath !== undefined) {
586+
this.updateFavicon(update.opts.faviconpath);
587+
}
546588
}
547589
makeVDomIdMap(vdomRoot, idMap);
548590
this.handleRenderUpdates(update, idMap);

tsunami/frontend/src/types/vdom.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@ type VDomAsyncInitiationRequest = {
1212
type VDomBackendOpts = {
1313
closeonctrlc?: boolean;
1414
globalkeyboardevents?: boolean;
15-
globalstyles?: boolean;
1615
title?: string;
16+
faviconpath?: string;
1717
};
1818

1919
// vdom.VDomBackendUpdate
2020
type VDomBackendUpdate = {
2121
type: "backendupdate";
2222
ts: number;
23+
serverid: string;
2324
opts?: VDomBackendOpts;
2425
haswork?: boolean;
2526
renderupdates?: VDomRenderUpdate[];

0 commit comments

Comments
 (0)