port to electron (#33)

This commit is contained in:
Mike Sawka 2024-06-11 17:42:10 -07:00 committed by GitHub
parent 9f32a53485
commit 1874d9a252
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
62 changed files with 6498 additions and 1615 deletions

2
.gitignore vendored
View File

@ -1,6 +1,7 @@
.task .task
frontend/dist frontend/dist
dist/ dist/
dist-dev/
frontend/node_modules frontend/node_modules
node_modules/ node_modules/
frontend/bindings frontend/bindings
@ -11,6 +12,7 @@ bin/
*.exe *.exe
.DS_Store .DS_Store
*~ *~
out/
# Yarn Modern # Yarn Modern
.pnp.* .pnp.*

431
Taskfile.old.yml Normal file
View File

@ -0,0 +1,431 @@
version: "3"
vars:
APP_NAME: "NextWave"
BIN_DIR: "bin"
VITE_PORT: "{{.WAILS_VITE_PORT | default 9245}}"
tasks:
## -------------------------- Build -------------------------- ##
build:
summary: Builds the application
cmds:
# Build for current OS
- task: build:{{OS}}
# Uncomment to build for specific OSes
# - task: build:linux
# - task: build:windows
# - task: build:darwin
## ------> Windows <-------
build:windows:
summary: Builds the application for Windows
deps:
- task: go:mod:tidy
- task: build:frontend
- task: generate:icons
- task: generate:syso
vars:
ARCH: "{{.ARCH}}"
cmds:
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}.exe
vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -ldflags="-w -s -H windowsgui"{{else}}-gcflags=all="-l"{{end}}'
env:
GOOS: windows
CGO_ENABLED: 0
GOARCH: "{{.ARCH | default ARCH}}"
PRODUCTION: '{{.PRODUCTION | default "false"}}'
build:windows:prod:arm64:
summary: Creates a production build of the application
cmds:
- task: build:windows
vars:
ARCH: arm64
PRODUCTION: "true"
build:windows:prod:amd64:
summary: Creates a production build of the application
cmds:
- task: build:windows
vars:
ARCH: amd64
PRODUCTION: "true"
build:windows:debug:arm64:
summary: Creates a debug build of the application
cmds:
- task: build:windows
vars:
ARCH: arm64
build:windows:debug:amd64:
summary: Creates a debug build of the application
cmds:
- task: build:windows
vars:
ARCH: amd64
## ------> Darwin <-------
build:darwin:
summary: Creates a production build of the application
deps:
- task: go:mod:tidy
- task: build:frontend
- task: generate:icons
cmds:
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}
vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -ldflags="-w -s"{{else}}-gcflags=all="-l"{{end}}'
env:
GOOS: darwin
CGO_ENABLED: 1
GOARCH: "{{.ARCH | default ARCH}}"
CGO_CFLAGS: "-mmacosx-version-min=10.15"
CGO_LDFLAGS: "-mmacosx-version-min=10.15"
MACOSX_DEPLOYMENT_TARGET: "10.15"
PRODUCTION: '{{.PRODUCTION | default "false"}}'
build:darwin:prod:arm64:
summary: Creates a production build of the application
cmds:
- task: build:darwin
vars:
ARCH: arm64
PRODUCTION: "true"
build:darwin:prod:amd64:
summary: Creates a production build of the application
cmds:
- task: build:darwin
vars:
ARCH: amd64
PRODUCTION: "true"
build:darwin:debug:arm64:
summary: Creates a debug build of the application
cmds:
- task: build:darwin
vars:
ARCH: arm64
build:darwin:debug:amd64:
summary: Creates a debug build of the application
cmds:
- task: build:darwin
vars:
ARCH: amd64
## ------> Linux <-------
build:linux:
summary: Builds the application for Linux
deps:
- task: go:mod:tidy
- task: build:frontend
- task: generate:icons
vars:
ARCH: "{{.ARCH}}"
cmds:
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/w2
vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -ldflags="-w -s"{{else}}-gcflags=all="-l"{{end}}'
env:
GOOS: linux
CGO_ENABLED: 1
GOARCH: "{{.ARCH | default ARCH}}"
PRODUCTION: '{{.PRODUCTION | default "false"}}'
build:linux:prod:arm64:
summary: Creates a production build of the application
cmds:
- task: build:linux
vars:
ARCH: arm64
PRODUCTION: "true"
build:linux:prod:amd64:
summary: Creates a production build of the application
cmds:
- task: build:linux
vars:
ARCH: amd64
PRODUCTION: "true"
build:linux:debug:arm64:
summary: Creates a debug build of the application
cmds:
- task: build:linux
vars:
ARCH: arm64
build:linux:debug:amd64:
summary: Creates a debug build of the application
cmds:
- task: build:linux
vars:
ARCH: amd64
## -------------------------- Package -------------------------- ##
package:
summary: Packages a production build of the application into a bundle
cmds:
# Package for current OS
- task: package:{{OS}}
# Package for specific os/arch
# - task: package:darwin:arm64
# - task: package:darwin:amd64
# - task: package:windows:arm64
# - task: package:windows:amd64
## ------> Windows <------
package:windows:
summary: Packages a production build of the application into a `.exe` bundle
cmds:
- task: create:nsis:installer
vars:
ARCH: "{{.ARCH}}"
vars:
ARCH: "{{.ARCH | default ARCH}}"
package:windows:arm64:
summary: Packages a production build of the application into a `.exe` bundle
cmds:
- task: package:windows
vars:
ARCH: arm64
package:windows:amd64:
summary: Packages a production build of the application into a `.exe` bundle
cmds:
- task: package:windows
vars:
ARCH: amd64
generate:syso:
summary: Generates Windows `.syso` file
dir: build
cmds:
- wails3 generate syso -arch {{.ARCH}} -icon icon.ico -manifest wails.exe.manifest -info info.json -out ../wails.syso
vars:
ARCH: "{{.ARCH | default ARCH}}"
create:nsis:installer:
summary: Creates an NSIS installer
label: "NSIS Installer ({{.ARCH}})"
dir: build/nsis
sources:
- "{{.ROOT_DIR}}\\bin\\{{.APP_NAME}}.exe"
generates:
- "{{.ROOT_DIR}}\\bin\\{{.APP_NAME}}-{{.ARCH}}-installer.exe"
deps:
- task: build:windows
vars:
PRODUCTION: "true"
ARCH: "{{.ARCH}}"
cmds:
- makensis -DARG_WAILS_'{{.ARG_FLAG}}'_BINARY="{{.ROOT_DIR}}\{{.BIN_DIR}}\{{.APP_NAME}}.exe" project.nsi
vars:
ARCH: "{{.ARCH | default ARCH}}"
ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}'
## ------> Darwin <------
package:darwin:
summary: Packages a production build of the application into a `.app` bundle
platforms: [darwin]
deps:
- task: build:darwin
vars:
PRODUCTION: "true"
cmds:
- task: create:app:bundle
package:darwin:arm64:
summary: Packages a production build of the application into a `.app` bundle
platforms: [darwin/arm64]
deps:
- task: package:darwin
vars:
ARCH: arm64
package:darwin:amd64:
summary: Packages a production build of the application into a `.app` bundle
platforms: [darwin/amd64]
deps:
- task: package:darwin
vars:
ARCH: amd64
create:app:bundle:
summary: Creates an `.app` bundle
cmds:
- mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/{MacOS,Resources}
- cp build/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources
- cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS
- cp build/Info.plist {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents
## ------> Linux <------
package:linux:
summary: Packages a production build of the application for Linux
platforms: [linux]
deps:
- task: build:linux
vars:
PRODUCTION: "true"
cmds:
- task: create:appimage
create:appimage:
summary: Creates an AppImage
dir: build/appimage
platforms: [linux]
deps:
- task: build:linux
vars:
PRODUCTION: "true"
- task: generate:linux:dotdesktop
cmds:
# Copy binary + icon to appimage dir
- cp {{.APP_BINARY}} {{.APP_NAME}}
- cp ../appicon.png appicon.png
# Generate AppImage
- wails3 generate appimage -binary {{.APP_NAME}} -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/appimage
vars:
APP_NAME: "{{.APP_NAME}}"
APP_BINARY: "../../bin/{{.APP_NAME}}"
ICON: "../appicon.png"
DESKTOP_FILE: "{{.APP_NAME}}.desktop"
OUTPUT_DIR: "../../bin"
generate:linux:dotdesktop:
summary: Generates a `.desktop` file
dir: build
sources:
- "appicon.png"
generates:
- "{{.ROOT_DIR}}/build/appimage/{{.APP_NAME}}.desktop"
cmds:
- mkdir -p {{.ROOT_DIR}}/build/appimage
# Run `wails3 generate .desktop -help` for all the options
- wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile {{.ROOT_DIR}}/build/appimage/{{.APP_NAME}}.desktop -categories "{{.CATEGORIES}}"
# -comment "A comment"
# -terminal "true"
# -version "1.0"
# -genericname "Generic Name"
# -keywords "keyword1;keyword2;"
# -startupnotify "true"
# -mimetype "application/x-extension1;application/x-extension2;"
vars:
APP_NAME: "{{.APP_NAME}}"
EXEC: "{{.APP_NAME}}"
ICON: "appicon"
CATEGORIES: "Development;"
OUTPUTFILE: "{{.ROOT_DIR}}/build/appimage/{{.APP_NAME}}.desktop"
## -------------------------- Misc -------------------------- ##
generate:icons:
summary: Generates Windows `.ico` and Mac `.icns` files from an image
dir: build
sources:
- "appicon.png"
generates:
- "icons.icns"
- "icons.ico"
method: timestamp
cmds:
# Generates both .ico and .icns files
# commented out for now
# - wails3 generate icons -input appicon.png
install:frontend:deps:
summary: Install frontend dependencies
sources:
- package.json
- yarn.lock
generates:
- node_modules/*
preconditions:
- sh: yarn --version
msg: "Looks like yarn isn't installed."
cmds:
- yarn
build:frontend:
summary: Build the frontend project
sources:
- "**/*"
generates:
- dist/*
deps:
- install:frontend:deps
- generate:bindings
cmds:
- yarn build
generate:bindings:
summary: Generates bindings for the frontend
sources:
- "**/*.go"
generates:
- "frontend/bindings/**/*"
cmds:
- wails3 generate bindings -silent -ts
# - wails3 generate bindings -silent
go:mod:tidy:
summary: Runs `go mod tidy`
internal: true
generates:
- go.sum
sources:
- go.mod
cmds:
- go mod tidy
# ----------------------- dev ----------------------- #
run:
summary: Runs the application
cmds:
- task: run:{{OS}}
run:windows:
cmds:
- '{{.BIN_DIR}}\\{{.APP_NAME}}.exe'
run:linux:
cmds:
- "{{.BIN_DIR}}/{{.APP_NAME}}"
run:darwin:
cmds:
- "{{.BIN_DIR}}/{{.APP_NAME}}"
dev:frontend:
summary: Runs the frontend in development mode
deps:
- task: install:frontend:deps
cmds:
- yarn dev --port {{.VITE_PORT}} --strictPort
dev:
summary: Runs the application in development mode
cmds:
- wails3 dev -config ./build/devmode.config.yaml -port {{.VITE_PORT}}
dev:reload:
summary: Reloads the application
cmds:
- task: run

View File

@ -3,386 +3,38 @@ version: "3"
vars: vars:
APP_NAME: "NextWave" APP_NAME: "NextWave"
BIN_DIR: "bin" BIN_DIR: "bin"
VITE_PORT: "{{.WAILS_VITE_PORT | default 9245}}" VERSION: "0.1.0"
tasks: tasks:
## -------------------------- Build -------------------------- ## generate:
build:
summary: Builds the application
cmds: cmds:
# Build for current OS - go run cmd/generate/main-generate.go
- task: build:{{OS}}
# Uncomment to build for specific OSes
# - task: build:linux
# - task: build:windows
# - task: build:darwin
## ------> Windows <-------
build:windows:
summary: Builds the application for Windows
deps:
- task: go:mod:tidy
- task: build:frontend
- task: generate:icons
- task: generate:syso
vars:
ARCH: "{{.ARCH}}"
cmds:
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}.exe
vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -ldflags="-w -s -H windowsgui"{{else}}-gcflags=all="-l"{{end}}'
env:
GOOS: windows
CGO_ENABLED: 0
GOARCH: "{{.ARCH | default ARCH}}"
PRODUCTION: '{{.PRODUCTION | default "false"}}'
build:windows:prod:arm64:
summary: Creates a production build of the application
cmds:
- task: build:windows
vars:
ARCH: arm64
PRODUCTION: "true"
build:windows:prod:amd64:
summary: Creates a production build of the application
cmds:
- task: build:windows
vars:
ARCH: amd64
PRODUCTION: "true"
build:windows:debug:arm64:
summary: Creates a debug build of the application
cmds:
- task: build:windows
vars:
ARCH: arm64
build:windows:debug:amd64:
summary: Creates a debug build of the application
cmds:
- task: build:windows
vars:
ARCH: amd64
## ------> Darwin <-------
build:darwin:
summary: Creates a production build of the application
deps:
- task: go:mod:tidy
- task: build:frontend
- task: generate:icons
cmds:
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}
vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -ldflags="-w -s"{{else}}-gcflags=all="-l"{{end}}'
env:
GOOS: darwin
CGO_ENABLED: 1
GOARCH: "{{.ARCH | default ARCH}}"
CGO_CFLAGS: "-mmacosx-version-min=10.15"
CGO_LDFLAGS: "-mmacosx-version-min=10.15"
MACOSX_DEPLOYMENT_TARGET: "10.15"
PRODUCTION: '{{.PRODUCTION | default "false"}}'
build:darwin:prod:arm64:
summary: Creates a production build of the application
cmds:
- task: build:darwin
vars:
ARCH: arm64
PRODUCTION: "true"
build:darwin:prod:amd64:
summary: Creates a production build of the application
cmds:
- task: build:darwin
vars:
ARCH: amd64
PRODUCTION: "true"
build:darwin:debug:arm64:
summary: Creates a debug build of the application
cmds:
- task: build:darwin
vars:
ARCH: arm64
build:darwin:debug:amd64:
summary: Creates a debug build of the application
cmds:
- task: build:darwin
vars:
ARCH: amd64
## ------> Linux <-------
build:linux:
summary: Builds the application for Linux
deps:
- task: go:mod:tidy
- task: build:frontend
- task: generate:icons
vars:
ARCH: "{{.ARCH}}"
cmds:
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/w2
vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -ldflags="-w -s"{{else}}-gcflags=all="-l"{{end}}'
env:
GOOS: linux
CGO_ENABLED: 1
GOARCH: "{{.ARCH | default ARCH}}"
PRODUCTION: '{{.PRODUCTION | default "false"}}'
build:linux:prod:arm64:
summary: Creates a production build of the application
cmds:
- task: build:linux
vars:
ARCH: arm64
PRODUCTION: "true"
build:linux:prod:amd64:
summary: Creates a production build of the application
cmds:
- task: build:linux
vars:
ARCH: amd64
PRODUCTION: "true"
build:linux:debug:arm64:
summary: Creates a debug build of the application
cmds:
- task: build:linux
vars:
ARCH: arm64
build:linux:debug:amd64:
summary: Creates a debug build of the application
cmds:
- task: build:linux
vars:
ARCH: amd64
## -------------------------- Package -------------------------- ##
package:
summary: Packages a production build of the application into a bundle
cmds:
# Package for current OS
- task: package:{{OS}}
# Package for specific os/arch
# - task: package:darwin:arm64
# - task: package:darwin:amd64
# - task: package:windows:arm64
# - task: package:windows:amd64
## ------> Windows <------
package:windows:
summary: Packages a production build of the application into a `.exe` bundle
cmds:
- task: create:nsis:installer
vars:
ARCH: "{{.ARCH}}"
vars:
ARCH: "{{.ARCH | default ARCH}}"
package:windows:arm64:
summary: Packages a production build of the application into a `.exe` bundle
cmds:
- task: package:windows
vars:
ARCH: arm64
package:windows:amd64:
summary: Packages a production build of the application into a `.exe` bundle
cmds:
- task: package:windows
vars:
ARCH: amd64
generate:syso:
summary: Generates Windows `.syso` file
dir: build
cmds:
- wails3 generate syso -arch {{.ARCH}} -icon icon.ico -manifest wails.exe.manifest -info info.json -out ../wails.syso
vars:
ARCH: "{{.ARCH | default ARCH}}"
create:nsis:installer:
summary: Creates an NSIS installer
label: "NSIS Installer ({{.ARCH}})"
dir: build/nsis
sources: sources:
- "{{.ROOT_DIR}}\\bin\\{{.APP_NAME}}.exe" - "cmd/generate/*.go"
generates: - "pkg/service/**/*.go"
- "{{.ROOT_DIR}}\\bin\\{{.APP_NAME}}-{{.ARCH}}-installer.exe" - "pkg/wstore/*.go"
deps:
- task: build:windows webpack:
vars:
PRODUCTION: "true"
ARCH: "{{.ARCH}}"
cmds: cmds:
- makensis -DARG_WAILS_'{{.ARG_FLAG}}'_BINARY="{{.ROOT_DIR}}\{{.BIN_DIR}}\{{.APP_NAME}}.exe" project.nsi - yarn run webpack --watch --env dev
vars:
ARCH: "{{.ARCH | default ARCH}}"
ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}'
## ------> Darwin <------ electron:
package:darwin:
summary: Packages a production build of the application into a `.app` bundle
platforms: [darwin]
deps:
- task: build:darwin
vars:
PRODUCTION: "true"
cmds: cmds:
- task: create:app:bundle - WAVETERM_DEV=1 yarn run electron dist-dev/emain.js
package:darwin:arm64:
summary: Packages a production build of the application into a `.app` bundle
platforms: [darwin/arm64]
deps: deps:
- task: package:darwin - build:server
vars:
ARCH: arm64
package:darwin:amd64: build:server:
summary: Packages a production build of the application into a `.app` bundle
platforms: [darwin/amd64]
deps:
- task: package:darwin
vars:
ARCH: amd64
create:app:bundle:
summary: Creates an `.app` bundle
cmds: cmds:
- mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/{MacOS,Resources} - go build -o bin/wavesrv cmd/server/main-server.go
- cp build/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources
- cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS
- cp build/Info.plist {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents
## ------> Linux <------
package:linux:
summary: Packages a production build of the application for Linux
platforms: [linux]
deps:
- task: build:linux
vars:
PRODUCTION: "true"
cmds:
- task: create:appimage
create:appimage:
summary: Creates an AppImage
dir: build/appimage
platforms: [linux]
deps:
- task: build:linux
vars:
PRODUCTION: "true"
- task: generate:linux:dotdesktop
cmds:
# Copy binary + icon to appimage dir
- cp {{.APP_BINARY}} {{.APP_NAME}}
- cp ../appicon.png appicon.png
# Generate AppImage
- wails3 generate appimage -binary {{.APP_NAME}} -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/appimage
vars:
APP_NAME: "{{.APP_NAME}}"
APP_BINARY: "../../bin/{{.APP_NAME}}"
ICON: "../appicon.png"
DESKTOP_FILE: "{{.APP_NAME}}.desktop"
OUTPUT_DIR: "../../bin"
generate:linux:dotdesktop:
summary: Generates a `.desktop` file
dir: build
sources: sources:
- "appicon.png" - "cmd/server/*.go"
- "pkg/**/*.go"
generates: generates:
- "{{.ROOT_DIR}}/build/appimage/{{.APP_NAME}}.desktop" - bin/wavesrv
cmds:
- mkdir -p {{.ROOT_DIR}}/build/appimage
# Run `wails3 generate .desktop -help` for all the options
- wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile {{.ROOT_DIR}}/build/appimage/{{.APP_NAME}}.desktop -categories "{{.CATEGORIES}}"
# -comment "A comment"
# -terminal "true"
# -version "1.0"
# -genericname "Generic Name"
# -keywords "keyword1;keyword2;"
# -startupnotify "true"
# -mimetype "application/x-extension1;application/x-extension2;"
vars:
APP_NAME: "{{.APP_NAME}}"
EXEC: "{{.APP_NAME}}"
ICON: "appicon"
CATEGORIES: "Development;"
OUTPUTFILE: "{{.ROOT_DIR}}/build/appimage/{{.APP_NAME}}.desktop"
## -------------------------- Misc -------------------------- ##
generate:icons:
summary: Generates Windows `.ico` and Mac `.icns` files from an image
dir: build
sources:
- "appicon.png"
generates:
- "icons.icns"
- "icons.ico"
method: timestamp
cmds:
# Generates both .ico and .icns files
# commented out for now
# - wails3 generate icons -input appicon.png
install:frontend:deps:
summary: Install frontend dependencies
sources:
- package.json
- yarn.lock
generates:
- node_modules/*
preconditions:
- sh: yarn --version
msg: "Looks like yarn isn't installed."
cmds:
- yarn
build:frontend:
summary: Build the frontend project
sources:
- "**/*"
generates:
- dist/*
deps: deps:
- install:frontend:deps - go:mod:tidy
- generate:bindings
cmds:
- yarn build
generate:bindings:
summary: Generates bindings for the frontend
sources:
- "**/*.go"
generates:
- "frontend/bindings/**/*"
cmds:
- wails3 generate bindings -silent -ts
# - wails3 generate bindings -silent
go:mod:tidy: go:mod:tidy:
summary: Runs `go mod tidy` summary: Runs `go mod tidy`
@ -394,38 +46,4 @@ tasks:
cmds: cmds:
- go mod tidy - go mod tidy
# ----------------------- dev ----------------------- #
run:
summary: Runs the application
cmds:
- task: run:{{OS}}
run:windows:
cmds:
- '{{.BIN_DIR}}\\{{.APP_NAME}}.exe'
run:linux:
cmds:
- "{{.BIN_DIR}}/{{.APP_NAME}}"
run:darwin:
cmds:
- "{{.BIN_DIR}}/{{.APP_NAME}}"
dev:frontend:
summary: Runs the frontend in development mode
deps:
- task: install:frontend:deps
cmds:
- yarn dev --port {{.VITE_PORT}} --strictPort
dev:
summary: Runs the application in development mode
cmds:
- wails3 dev -config ./build/devmode.config.yaml -port {{.VITE_PORT}}
dev:reload:
summary: Reloads the application
cmds:
- task: run

View File

@ -5,22 +5,87 @@ package main
import ( import (
"fmt" "fmt"
"os"
"reflect" "reflect"
"sort"
"github.com/wavetermdev/thenextwave/pkg/waveobj" "github.com/wavetermdev/thenextwave/pkg/service"
"github.com/wavetermdev/thenextwave/pkg/wstore" "github.com/wavetermdev/thenextwave/pkg/tsgen"
"github.com/wavetermdev/thenextwave/pkg/util/utilfn"
) )
func main() { func generateTypesFile() error {
fd, err := os.Create("frontend/types/gotypes.d.ts")
if err != nil {
return err
}
defer fd.Close()
fmt.Fprintf(os.Stderr, "generating types file to %s\n", fd.Name())
tsTypesMap := make(map[reflect.Type]string) tsTypesMap := make(map[reflect.Type]string)
var waveObj waveobj.WaveObj tsgen.GenerateWaveObjTypes(tsTypesMap)
waveobj.GenerateTSType(reflect.TypeOf(waveobj.ORef{}), tsTypesMap) err = tsgen.GenerateServiceTypes(tsTypesMap)
waveobj.GenerateTSType(reflect.TypeOf(&waveObj).Elem(), tsTypesMap) if err != nil {
for _, rtype := range wstore.AllWaveObjTypes() { fmt.Fprintf(os.Stderr, "Error generating service types: %v\n", err)
waveobj.GenerateTSType(rtype, tsTypesMap) os.Exit(1)
} }
for _, ts := range tsTypesMap { fmt.Fprintf(fd, "// Copyright 2024, Command Line Inc.\n")
fmt.Print(ts) fmt.Fprintf(fd, "// SPDX-License-Identifier: Apache-2.0\n\n")
fmt.Print("\n") fmt.Fprintf(fd, "// generated by cmd/generate/main-generate.go\n\n")
fmt.Fprintf(fd, "declare global {\n\n")
var keys []reflect.Type
for key := range tsTypesMap {
keys = append(keys, key)
}
sort.Slice(keys, func(i, j int) bool {
iname, _ := tsgen.TypeToTSType(keys[i])
jname, _ := tsgen.TypeToTSType(keys[j])
return iname < jname
})
for _, key := range keys {
tsCode := tsTypesMap[key]
istr := utilfn.IndentString(" ", tsCode)
fmt.Fprint(fd, istr)
}
fmt.Fprintf(fd, "}\n\n")
fmt.Fprintf(fd, "export {}\n")
return nil
}
func generateServicesFile() error {
fd, err := os.Create("frontend/app/store/services.ts")
if err != nil {
return err
}
defer fd.Close()
fmt.Fprintf(os.Stderr, "generating services file to %s\n", fd.Name())
fmt.Fprintf(fd, "// Copyright 2024, Command Line Inc.\n")
fmt.Fprintf(fd, "// SPDX-License-Identifier: Apache-2.0\n\n")
fmt.Fprintf(fd, "// generated by cmd/generate/main-generate.go\n\n")
fmt.Fprintf(fd, "import * as WOS from \"./wos\";\n\n")
orderedKeys := utilfn.GetOrderedMapKeys(service.ServiceMap)
for _, serviceName := range orderedKeys {
serviceObj := service.ServiceMap[serviceName]
svcStr := tsgen.GenerateServiceClass(serviceName, serviceObj)
fmt.Fprint(fd, svcStr)
fmt.Fprint(fd, "\n")
}
return nil
}
func main() {
err := service.ValidateServiceMap()
if err != nil {
fmt.Fprintf(os.Stderr, "Error validating service map: %v\n", err)
os.Exit(1)
}
err = generateTypesFile()
if err != nil {
fmt.Fprintf(os.Stderr, "Error generating types file: %v\n", err)
os.Exit(1)
}
err = generateServicesFile()
if err != nil {
fmt.Fprintf(os.Stderr, "Error generating services file: %v\n", err)
os.Exit(1)
} }
} }

95
cmd/server/main-server.go Normal file
View File

@ -0,0 +1,95 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"runtime"
"strconv"
"syscall"
"time"
"github.com/wavetermdev/thenextwave/pkg/filestore"
"github.com/wavetermdev/thenextwave/pkg/service"
"github.com/wavetermdev/thenextwave/pkg/wavebase"
"github.com/wavetermdev/thenextwave/pkg/web"
"github.com/wavetermdev/thenextwave/pkg/wstore"
)
const ReadySignalPidVarName = "WAVETERM_READY_SIGNAL_PID"
func doShutdown(reason string) {
log.Printf("shutting down: %s\n", reason)
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn()
// TODO deal with flush in progress
filestore.WFS.FlushCache(ctx)
time.Sleep(200 * time.Millisecond)
os.Exit(0)
}
func installShutdownSignalHandlers() {
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))
break
}
}()
}
func main() {
err := service.ValidateServiceMap()
if err != nil {
log.Printf("error validating service map: %v\n", err)
return
}
err = wavebase.EnsureWaveHomeDir()
if err != nil {
log.Printf("error ensuring wave home dir: %v\n", err)
return
}
waveLock, err := wavebase.AcquireWaveLock()
if err != nil {
log.Printf("error acquiring wave lock (another instance of Wave is likely running): %v\n", err)
return
}
log.Printf("wave home dir: %s\n", wavebase.GetWaveHomeDir())
err = filestore.InitFilestore()
if err != nil {
log.Printf("error initializing filestore: %v\n", err)
return
}
err = wstore.InitWStore()
if err != nil {
log.Printf("error initializing wstore: %v\n", err)
return
}
err = wstore.EnsureInitialData()
if err != nil {
log.Printf("error ensuring initial data: %v\n", err)
return
}
installShutdownSignalHandlers()
go web.RunWebSocketServer()
go func() {
time.Sleep(30 * time.Millisecond)
pidStr := os.Getenv(ReadySignalPidVarName)
if pidStr != "" {
pid, err := strconv.Atoi(pidStr)
if err == nil {
syscall.Kill(pid, syscall.SIGUSR1)
}
}
}()
web.RunWebServer() // blocking
runtime.KeepAlive(waveLock)
}

243
emain/emain.ts Normal file
View File

@ -0,0 +1,243 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as electron from "electron";
import * as child_process from "node:child_process";
import * as path from "path";
import { debounce } from "throttle-debounce";
import * as services from "../frontend/app/store/services";
const electronApp = electron.app;
const isDev = true;
const WaveAppPathVarName = "WAVETERM_APP_PATH";
const WaveDevVarName = "WAVETERM_DEV";
const WaveSrvReadySignalPidVarName = "WAVETERM_READY_SIGNAL_PID";
const AuthKeyFile = "waveterm.authkey";
const DevServerEndpoint = "http://127.0.0.1:8190";
const ProdServerEndpoint = "http://127.0.0.1:1719";
const DistDir = "dist-dev";
let waveSrvReadyResolve = (value: boolean) => {};
let waveSrvReady: Promise<boolean> = new Promise((resolve, _) => {
waveSrvReadyResolve = resolve;
});
let waveSrvProc: child_process.ChildProcessWithoutNullStreams | null = null;
electronApp.setName(isDev ? "NextWave (Dev)" : "NextWave");
const unamePlatform = process.platform;
let unameArch: string = process.arch;
if (unameArch == "x64") {
unameArch = "amd64";
}
function getBaseHostPort(): string {
if (isDev) {
return DevServerEndpoint;
}
return ProdServerEndpoint;
}
// must match golang
function getWaveHomeDir() {
return path.join(process.env.HOME, ".w2");
}
function getElectronAppBasePath(): string {
return path.dirname(__dirname);
}
function getGoAppBasePath(): string {
const appDir = getElectronAppBasePath();
if (appDir.endsWith(".asar")) {
return `${appDir}.unpacked`;
} else {
return appDir;
}
}
function getWaveSrvPath(): string {
return path.join(getGoAppBasePath(), "bin", "wavesrv");
}
function getWaveSrvCmd(): string {
const waveSrvPath = getWaveSrvPath();
const waveHome = getWaveHomeDir();
const logFile = path.join(waveHome, "wavesrv.log");
return `"${waveSrvPath}" >> "${logFile}" 2>&1`;
}
function getWaveSrvCwd(): string {
return getWaveHomeDir();
}
function runWaveSrv(): Promise<boolean> {
let pResolve: (value: boolean) => void;
let pReject: (reason?: any) => void;
const rtnPromise = new Promise<boolean>((argResolve, argReject) => {
pResolve = argResolve;
pReject = argReject;
});
const envCopy = { ...process.env };
envCopy[WaveAppPathVarName] = getGoAppBasePath();
if (isDev) {
envCopy[WaveDevVarName] = "1";
}
envCopy[WaveSrvReadySignalPidVarName] = process.pid.toString();
const waveSrvCmd = getWaveSrvCmd();
console.log("trying to run local server", waveSrvCmd);
const proc = child_process.execFile("bash", ["-c", waveSrvCmd], {
cwd: getWaveSrvCwd(),
env: envCopy,
});
proc.on("exit", (e) => {
console.log("wavesrv exited, shutting down");
electronApp.quit();
});
proc.on("spawn", (e) => {
console.log("spawnned wavesrv");
waveSrvProc = proc;
pResolve(true);
});
proc.on("error", (e) => {
console.log("error running wavesrv", e);
pReject(e);
});
proc.stdout.on("data", (_) => {
return;
});
proc.stderr.on("data", (_) => {
return;
});
return rtnPromise;
}
function mainResizeHandler(_: any, win: Electron.BrowserWindow) {
if (win == null || win.isDestroyed() || win.fullScreen) {
return;
}
const bounds = win.getBounds();
const winSize = { width: bounds.width, height: bounds.height, top: bounds.y, left: bounds.x };
const url = new URL(getBaseHostPort() + "/api/set-winsize");
// TODO
}
function shNavHandler(event: Electron.Event<Electron.WebContentsWillNavigateEventParams>, url: string) {
event.preventDefault();
if (url.startsWith("https://") || url.startsWith("http://") || url.startsWith("file://")) {
console.log("open external, shNav", url);
electron.shell.openExternal(url);
} else {
console.log("navigation canceled", url);
}
}
function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWillFrameNavigateEventParams>) {
if (!event.frame?.parent) {
// only use this handler to process iframe events (non-iframe events go to shNavHandler)
return;
}
const url = event.url;
console.log(`frame-navigation url=${url} frame=${event.frame.name}`);
if (event.frame.name == "webview") {
// "webview" links always open in new window
// this will *not* effect the initial load because srcdoc does not count as an electron navigation
console.log("open external, frameNav", url);
event.preventDefault();
electron.shell.openExternal(url);
return;
}
if (event.frame.name == "pdfview" && url.startsWith("blob:file:///")) {
// allowed
return;
}
event.preventDefault();
console.log("frame navigation canceled");
}
function createWindow(client: Client, waveWindow: WaveWindow): Electron.BrowserWindow {
const win = new electron.BrowserWindow({
x: 200,
y: 200,
titleBarStyle: "hiddenInset",
width: waveWindow.winsize.width,
height: waveWindow.winsize.height,
minWidth: 500,
minHeight: 300,
icon:
unamePlatform == "linux"
? path.join(getElectronAppBasePath(), "public/logos/wave-logo-dark.png")
: undefined,
webPreferences: {
preload: path.join(getElectronAppBasePath(), DistDir, "preload.js"),
},
show: false,
autoHideMenuBar: true,
backgroundColor: "#000000",
});
win.once("ready-to-show", () => {
win.show();
});
// const indexHtml = isDev ? "index-dev.html" : "index.html";
let usp = new URLSearchParams();
usp.set("clientid", client.oid);
usp.set("windowid", waveWindow.oid);
const indexHtml = "index.html";
win.loadFile(path.join(getElectronAppBasePath(), "public", indexHtml), { search: usp.toString() });
win.webContents.on("will-navigate", shNavHandler);
win.webContents.on("will-frame-navigate", shFrameNavHandler);
win.on(
"resize",
debounce(400, (e) => mainResizeHandler(e, win))
);
win.on(
"move",
debounce(400, (e) => mainResizeHandler(e, win))
);
win.webContents.on("zoom-changed", (e) => {
win.webContents.send("zoom-changed");
});
win.webContents.setWindowOpenHandler(({ url, frameName }) => {
if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file://")) {
console.log("openExternal fallback", url);
electron.shell.openExternal(url);
}
console.log("window-open denied", url);
return { action: "deny" };
});
return win;
}
process.on("SIGUSR1", function () {
waveSrvReadyResolve(true);
});
(async () => {
const startTs = Date.now();
const instanceLock = electronApp.requestSingleInstanceLock();
if (!instanceLock) {
console.log("waveterm-app could not get single-instance-lock, shutting down");
electronApp.quit();
return;
}
try {
await runWaveSrv();
} catch (e) {
console.log(e.toString());
}
console.log("waiting for wavesrv ready signal (SIGUSR1)");
const ready = await waveSrvReady;
console.log("wavesrv ready signal received", ready, Date.now() - startTs, "ms");
let clientData = await services.ClientService.GetClientData();
let windowData: WaveWindow = (await services.ObjectService.GetObject(
"window:" + clientData.mainwindowid
)) as WaveWindow;
await electronApp.whenReady();
await createWindow(clientData, windowData);
electronApp.on("activate", () => {
if (electron.BrowserWindow.getAllWindows().length === 0) {
createWindow(clientData, windowData);
}
});
})();

6
emain/preload.js Normal file
View File

@ -0,0 +1,6 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
let { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("api", {});

View File

@ -4,4 +4,18 @@ import eslint from "@eslint/js";
import eslintConfigPrettier from "eslint-config-prettier"; import eslintConfigPrettier from "eslint-config-prettier";
import tseslint from "typescript-eslint"; import tseslint from "typescript-eslint";
export default tseslint.config(eslint.configs.recommended, ...tseslint.configs.recommended, eslintConfigPrettier); const baseConfig = tseslint.config(eslint.configs.recommended, ...tseslint.configs.recommended, eslintConfigPrettier);
const customConfig = {
...baseConfig,
overrides: [
{
files: ["emain/emain.ts", "vite.config.ts", "electron.vite.config.ts"],
env: {
node: true,
},
},
],
};
export default customConfig;

62
frontend/app/app.less Normal file
View File

@ -0,0 +1,62 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
@import "./reset.less";
@import "./theme.less";
body {
display: flex;
flex-direction: row;
width: 100vw;
height: 100vh;
background-color: var(--main-bg-color);
color: var(--main-text-color);
font: var(--base-font);
overflow: hidden;
}
*::-webkit-scrollbar {
width: 4px;
height: 4px;
}
*::-webkit-scrollbar-track {
background-color: var(--scrollbar-background-color) !important;
}
*::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-thumb-color) !important;
border-radius: 4px;
margin: 0 1px 0 1px;
}
*::-webkit-scrollbar-thumb:hover {
background-color: var(--scrollbar-thumb-hover-color) !important;
}
.flex-spacer {
flex-grow: 1;
}
.text-fixed {
font: var(--fixed-font);
}
#main,
.mainapp {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.titlebar {
height: 35px;
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
-webkit-app-region: drag;
}
.error-boundary {
color: var(--error-color);
}

View File

@ -8,7 +8,7 @@ import { Provider } from "jotai";
import { DndProvider } from "react-dnd"; import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend"; import { HTML5Backend } from "react-dnd-html5-backend";
import "../../public/style.less"; import "./app.less";
import { CenteredDiv } from "./element/quickelems"; import { CenteredDiv } from "./element/quickelems";
const App = () => { const App = () => {

View File

@ -1,6 +1,7 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import React from "react";
import "./quickelems.less"; import "./quickelems.less";
function CenteredLoadingDiv() { function CenteredLoadingDiv() {

View File

@ -1,15 +1,22 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { Events } from "@wailsio/runtime";
import * as jotai from "jotai"; import * as jotai from "jotai";
import * as rxjs from "rxjs"; import * as rxjs from "rxjs";
import * as WOS from "./wos"; import * as WOS from "./wos";
import { WSControl } from "./ws";
// TODO remove the window dependency completely
// we should have the initialization be more orderly -- proceed directly from wave.ts instead of on its own.
const globalStore = jotai.createStore(); const globalStore = jotai.createStore();
let globalWindowId: string = null;
let globalClientId: string = null;
if (typeof window !== "undefined") {
// this if statement allows us to use the code in nodejs as well
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const globalWindowId = urlParams.get("windowid"); globalWindowId = urlParams.get("windowid") || "74eba2d0-22fc-4221-82ad-d028dd496342";
const globalClientId = urlParams.get("clientid"); globalClientId = urlParams.get("clientid") || "f4bc1713-a364-41b3-a5c4-b000ba10d622";
}
const windowIdAtom = jotai.atom(null) as jotai.PrimitiveAtom<string>; const windowIdAtom = jotai.atom(null) as jotai.PrimitiveAtom<string>;
const clientIdAtom = jotai.atom(null) as jotai.PrimitiveAtom<string>; const clientIdAtom = jotai.atom(null) as jotai.PrimitiveAtom<string>;
globalStore.set(windowIdAtom, globalWindowId); globalStore.set(windowIdAtom, globalWindowId);
@ -18,7 +25,7 @@ const uiContextAtom = jotai.atom((get) => {
const windowData = get(windowDataAtom); const windowData = get(windowDataAtom);
const uiContext: UIContext = { const uiContext: UIContext = {
windowid: get(atoms.windowId), windowid: get(atoms.windowId),
activetabid: windowData.activetabid, activetabid: windowData?.activetabid,
}; };
return uiContext; return uiContext;
}) as jotai.Atom<UIContext>; }) as jotai.Atom<UIContext>;
@ -34,7 +41,8 @@ const windowDataAtom: jotai.Atom<WaveWindow> = jotai.atom((get) => {
if (windowId == null) { if (windowId == null) {
return null; return null;
} }
return WOS.getObjectValue(WOS.makeORef("window", windowId), get); const rtn = WOS.getObjectValue<WaveWindow>(WOS.makeORef("window", windowId), get);
return rtn;
}); });
const workspaceAtom: jotai.Atom<Workspace> = jotai.atom((get) => { const workspaceAtom: jotai.Atom<Workspace> = jotai.atom((get) => {
const windowData = get(windowDataAtom); const windowData = get(windowDataAtom);
@ -56,10 +64,10 @@ const atoms = {
type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void }; type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void };
const blockSubjects = new Map<string, SubjectWithRef<any>>(); const orefSubjects = new Map<string, SubjectWithRef<any>>();
function getBlockSubject(blockId: string): SubjectWithRef<any> { function getORefSubject(oref: string): SubjectWithRef<any> {
let subject = blockSubjects.get(blockId); let subject = orefSubjects.get(oref);
if (subject == null) { if (subject == null) {
subject = new rxjs.Subject<any>() as any; subject = new rxjs.Subject<any>() as any;
subject.refCount = 0; subject.refCount = 0;
@ -67,29 +75,15 @@ function getBlockSubject(blockId: string): SubjectWithRef<any> {
subject.refCount--; subject.refCount--;
if (subject.refCount === 0) { if (subject.refCount === 0) {
subject.complete(); subject.complete();
blockSubjects.delete(blockId); orefSubjects.delete(oref);
} }
}; };
blockSubjects.set(blockId, subject); orefSubjects.set(oref, subject);
} }
subject.refCount++; subject.refCount++;
return subject; return subject;
} }
Events.On("block:ptydata", (event: any) => {
const data = event?.data;
if (data?.blockid == null) {
console.log("block:ptydata with null blockid");
return;
}
// we don't use getBlockSubject here because we don't want to create a new subject
const subject = blockSubjects.get(data.blockid);
if (subject == null) {
return;
}
subject.next(data);
});
const blockCache = new Map<string, Map<string, any>>(); const blockCache = new Map<string, Map<string, any>>();
function useBlockCache<T>(blockId: string, name: string, makeFn: () => T): T { function useBlockCache<T>(blockId: string, name: string, makeFn: () => T): T {
@ -123,4 +117,44 @@ function useBlockAtom<T>(blockId: string, name: string, makeFn: () => jotai.Atom
return atom as jotai.Atom<T>; return atom as jotai.Atom<T>;
} }
export { WOS, atoms, getBlockSubject, globalStore, useBlockAtom, useBlockCache }; function getBackendHostPort(): string {
// TODO deal with dev/production
return "http://localhost:8190";
}
function getBackendWSHostPort(): string {
return "ws://localhost:8191";
}
let globalWS: WSControl = null;
function handleWSEventMessage(msg: WSEventType) {
if (msg.oref == null) {
console.log("unsupported event", msg);
return;
}
// we don't use getORefSubject here because we don't want to create a new subject
const subject = orefSubjects.get(msg.oref);
if (subject == null) {
return;
}
subject.next(msg.data);
}
function handleWSMessage(msg: any) {
if (msg == null) {
return;
}
if (msg.eventtype != null) {
handleWSEventMessage(msg);
}
}
function initWS() {
globalWS = new WSControl(getBackendWSHostPort(), globalStore, globalWindowId, "", (msg) => {
handleWSMessage(msg);
});
globalWS.connectNow("initWS");
}
export { WOS, atoms, getBackendHostPort, getORefSubject, globalStore, globalWS, initWS, useBlockAtom, useBlockCache };

View File

@ -0,0 +1,100 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
// generated by cmd/generate/main-generate.go
import * as WOS from "./wos";
// blockservice.BlockService (block)
class BlockServiceType {
// send command to block
SendCommand(blockid: string, command: MetaType): Promise<void> {
return WOS.callBackendService("block", "SendCommand", Array.from(arguments))
}
}
export const BlockService = new BlockServiceType()
// clientservice.ClientService (client)
class ClientServiceType {
GetClientData(): Promise<Client> {
return WOS.callBackendService("client", "GetClientData", Array.from(arguments))
}
GetTab(arg1: string): Promise<Tab> {
return WOS.callBackendService("client", "GetTab", Array.from(arguments))
}
GetWindow(arg1: string): Promise<Window> {
return WOS.callBackendService("client", "GetWindow", Array.from(arguments))
}
GetWorkspace(arg1: string): Promise<Workspace> {
return WOS.callBackendService("client", "GetWorkspace", Array.from(arguments))
}
}
export const ClientService = new ClientServiceType()
// fileservice.FileService (file)
class FileServiceType {
GetWaveFile(arg1: string, arg2: string): Promise<any> {
return WOS.callBackendService("file", "GetWaveFile", Array.from(arguments))
}
ReadFile(arg1: string): Promise<FullFile> {
return WOS.callBackendService("file", "ReadFile", Array.from(arguments))
}
StatFile(arg1: string): Promise<FileInfo> {
return WOS.callBackendService("file", "StatFile", Array.from(arguments))
}
}
export const FileService = new FileServiceType()
// objectservice.ObjectService (object)
class ObjectServiceType {
// @returns tabId (and object updates)
AddTabToWorkspace(tabName: string, activateTab: boolean): Promise<string> {
return WOS.callBackendService("object", "AddTabToWorkspace", Array.from(arguments))
}
// @returns object updates
CloseTab(tabId: string): Promise<void> {
return WOS.callBackendService("object", "CloseTab", Array.from(arguments))
}
// @returns blockId (and object updates)
CreateBlock(blockDef: BlockDef, rtOpts: RuntimeOpts): Promise<string> {
return WOS.callBackendService("object", "CreateBlock", Array.from(arguments))
}
// @returns object updates
DeleteBlock(blockId: string): Promise<void> {
return WOS.callBackendService("object", "DeleteBlock", Array.from(arguments))
}
// get wave object by oref
GetObject(oref: string): Promise<WaveObj> {
return WOS.callBackendService("object", "GetObject", Array.from(arguments))
}
// @returns objects
GetObjects(orefs: string[]): Promise<WaveObj[]> {
return WOS.callBackendService("object", "GetObjects", Array.from(arguments))
}
// @returns object updates
SetActiveTab(tabId: string): Promise<void> {
return WOS.callBackendService("object", "SetActiveTab", Array.from(arguments))
}
// @returns object updates
UpdateObject(waveObj: WaveObj, returnUpdates: boolean): Promise<void> {
return WOS.callBackendService("object", "UpdateObject", Array.from(arguments))
}
// @returns object updates
UpdateObjectMeta(oref: string, meta: MetaType): Promise<void> {
return WOS.callBackendService("object", "UpdateObjectMeta", Array.from(arguments))
}
}
export const ObjectService = new ObjectServiceType()

View File

@ -3,10 +3,13 @@
// WaveObjectStore // WaveObjectStore
import { Call as $Call, Events } from "@wailsio/runtime"; // import { Call as $Call, Events } from "@wailsio/runtime";
import * as jotai from "jotai"; import * as jotai from "jotai";
import * as React from "react"; import * as React from "react";
import { atoms, globalStore } from "./global"; import { atoms, getBackendHostPort, globalStore } from "./global";
import * as services from "./services";
const IsElectron = true;
type WaveObjectDataItemType<T extends WaveObj> = { type WaveObjectDataItemType<T extends WaveObj> = {
value: T; value: T;
@ -54,7 +57,51 @@ function makeORef(otype: string, oid: string): string {
} }
function GetObject<T>(oref: string): Promise<T> { function GetObject<T>(oref: string): Promise<T> {
return $Call.ByName("github.com/wavetermdev/thenextwave/pkg/service/objectservice.ObjectService.GetObject", oref); return callBackendService("object", "GetObject", [oref], true);
}
function callBackendService(service: string, method: string, args: any[], noUIContext?: boolean): Promise<any> {
const startTs = Date.now();
let uiContext: UIContext = null;
if (!noUIContext) {
uiContext = globalStore.get(atoms.uiContext);
}
let waveCall: WebCallType = {
service: service,
method: method,
args: args,
uicontext: uiContext,
};
// usp is just for debugging (easier to filter URLs)
let methodName = service + "." + method;
let usp = new URLSearchParams();
usp.set("service", service);
usp.set("method", method);
let fetchPromise = fetch(getBackendHostPort() + "/wave/service?" + usp.toString(), {
method: "POST",
body: JSON.stringify(waveCall),
});
let prtn = fetchPromise
.then((resp) => {
if (!resp.ok) {
throw new Error(`call ${methodName} failed: ${resp.status} ${resp.statusText}`);
}
return resp.json();
})
.then((respData: WebReturnType) => {
if (respData == null) {
return null;
}
if (respData.updates != null) {
updateWaveObjects(respData.updates);
}
if (respData.error != null) {
throw new Error(`call ${methodName} error: ${respData.error}`);
}
console.log("Call", methodName, Date.now() - startTs + "ms");
return respData.data;
});
return prtn;
} }
const waveObjectValueCache = new Map<string, WaveObjectValue<any>>(); const waveObjectValueCache = new Map<string, WaveObjectValue<any>>();
@ -75,6 +122,7 @@ function createWaveValueObject<T extends WaveObj>(oref: string, shouldFetch: boo
const localPromise = GetObject<T>(oref); const localPromise = GetObject<T>(oref);
wov.pendingPromise = localPromise; wov.pendingPromise = localPromise;
localPromise.then((val) => { localPromise.then((val) => {
console.log("GetObject resolved", oref, val);
if (wov.pendingPromise != localPromise) { if (wov.pendingPromise != localPromise) {
return; return;
} }
@ -187,7 +235,7 @@ function useWaveObject<T extends WaveObj>(oref: string): [T, boolean, (val: T) =
const [atomVal, setAtomVal] = jotai.useAtom(wov.dataAtom); const [atomVal, setAtomVal] = jotai.useAtom(wov.dataAtom);
const simpleSet = (val: T) => { const simpleSet = (val: T) => {
setAtomVal({ value: val, loading: false }); setAtomVal({ value: val, loading: false });
UpdateObject(val, false); services.ObjectService.UpdateObject(val, false);
}; };
return [atomVal.value, atomVal.loading, simpleSet]; return [atomVal.value, atomVal.loading, simpleSet];
} }
@ -236,48 +284,32 @@ function cleanWaveObjectCache() {
} }
} }
Events.On("waveobj:update", (event: any) => { // Events.On("waveobj:update", (event: any) => {
const data: WaveObjUpdate[] = event?.data; // const data: WaveObjUpdate[] = event?.data;
if (data == null) { // if (data == null) {
return; // return;
} // }
if (!Array.isArray(data)) { // if (!Array.isArray(data)) {
console.log("invalid waveobj:update, not an array", data); // console.log("invalid waveobj:update, not an array", data);
return; // return;
} // }
if (data.length == 0) { // if (data.length == 0) {
return; // return;
} // }
updateWaveObjects(data); // updateWaveObjects(data);
}); // });
function wrapObjectServiceCall<T>(fnName: string, ...args: any[]): Promise<T> {
const uiContext = globalStore.get(atoms.uiContext);
const startTs = Date.now();
let prtn = $Call.ByName(
"github.com/wavetermdev/thenextwave/pkg/service/objectservice.ObjectService." + fnName,
uiContext,
...args
);
prtn = prtn.then((val) => {
console.log("Call", fnName, Date.now() - startTs + "ms");
if (val.updates) {
updateWaveObjects(val.updates);
}
return val;
});
return prtn;
}
// gets the value of a WaveObject from the cache. // gets the value of a WaveObject from the cache.
// should provide getFn if it is available (e.g. inside of a jotai atom) // should provide getFn if it is available (e.g. inside of a jotai atom)
// otherwise it will use the globalStore.get function // otherwise it will use the globalStore.get function
function getObjectValue<T>(oref: string, getFn?: jotai.Getter): T { function getObjectValue<T>(oref: string, getFn?: jotai.Getter): T {
const wov = waveObjectValueCache.get(oref); let wov = waveObjectValueCache.get(oref);
if (wov === undefined) { if (wov == null) {
return null; console.log("wov is null, creating new wov", oref);
wov = createWaveValueObject(oref, true);
waveObjectValueCache.set(oref, wov);
} }
if (getFn === undefined) { if (getFn == null) {
getFn = globalStore.get; getFn = globalStore.get;
} }
const atomVal = getFn(wov.dataAtom); const atomVal = getFn(wov.dataAtom);
@ -298,38 +330,12 @@ function setObjectValue<T extends WaveObj>(value: T, setFn?: jotai.Setter, pushT
} }
setFn(wov.dataAtom, { value: value, loading: false }); setFn(wov.dataAtom, { value: value, loading: false });
if (pushToServer) { if (pushToServer) {
UpdateObject(value, false); services.ObjectService.UpdateObject(value, false);
} }
} }
export function AddTabToWorkspace(tabName: string, activateTab: boolean): Promise<{ tabId: string }> {
return wrapObjectServiceCall("AddTabToWorkspace", tabName, activateTab);
}
export function SetActiveTab(tabId: string): Promise<void> {
return wrapObjectServiceCall("SetActiveTab", tabId);
}
export function CreateBlock(blockDef: BlockDef, rtOpts: RuntimeOpts): Promise<{ blockId: string }> {
return wrapObjectServiceCall("CreateBlock", blockDef, rtOpts);
}
export function DeleteBlock(blockId: string): Promise<void> {
return wrapObjectServiceCall("DeleteBlock", blockId);
}
export function CloseTab(tabId: string): Promise<void> {
return wrapObjectServiceCall("CloseTab", tabId);
}
export function UpdateObjectMeta(blockId: string, meta: MetadataType): Promise<void> {
return wrapObjectServiceCall("UpdateObjectMeta", blockId, meta);
}
export function UpdateObject(waveObj: WaveObj, returnUpdates: boolean): Promise<WaveObjUpdate[]> {
return wrapObjectServiceCall("UpdateObject", waveObj, returnUpdates);
}
export { export {
callBackendService,
cleanWaveObjectCache, cleanWaveObjectCache,
clearWaveObjectCache, clearWaveObjectCache,
getObjectValue, getObjectValue,

254
frontend/app/store/ws.ts Normal file
View File

@ -0,0 +1,254 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import * as jotai from "jotai";
import { sprintf } from "sprintf-js";
import { v4 as uuidv4 } from "uuid";
const MaxWebSocketSendSize = 1024 * 1024; // 1MB
type RpcEntry = {
reqId: string;
startTs: number;
method: string;
resolve: (any) => void;
reject: (any) => void;
promise: Promise<any>;
};
type JotaiStore = {
get: <Value>(atom: jotai.Atom<Value>) => Value;
set: <Value>(atom: jotai.WritableAtom<Value, [Value], void>, value: Value) => void;
};
class WSControl {
wsConn: any;
open: jotai.WritableAtom<boolean, [boolean], void>;
opening: boolean = false;
reconnectTimes: number = 0;
msgQueue: any[] = [];
windowId: string;
messageCallback: (any) => void = null;
watchSessionId: string = null;
watchScreenId: string = null;
wsLog: string[] = [];
authKey: string;
baseHostPort: string;
lastReconnectTime: number = 0;
rpcMap: Map<string, RpcEntry> = new Map(); // reqId -> RpcEntry
jotaiStore: JotaiStore;
constructor(
baseHostPort: string,
jotaiStore: JotaiStore,
windowId: string,
authKey: string,
messageCallback: (any) => void
) {
this.baseHostPort = baseHostPort;
this.messageCallback = messageCallback;
this.windowId = windowId;
this.authKey = authKey;
this.open = jotai.atom(false);
this.jotaiStore = jotaiStore;
setInterval(this.sendPing.bind(this), 5000);
}
log(str: string) {
let ts = Date.now();
this.wsLog.push("[" + ts + "] " + str);
if (this.wsLog.length > 50) {
this.wsLog.splice(0, this.wsLog.length - 50);
}
}
setOpen(val: boolean) {
this.jotaiStore.set(this.open, val);
}
isOpen() {
return this.jotaiStore.get(this.open);
}
connectNow(desc: string) {
if (this.isOpen()) {
return;
}
this.lastReconnectTime = Date.now();
this.log(sprintf("try reconnect (%s)", desc));
this.opening = true;
this.wsConn = new WebSocket(this.baseHostPort + "/ws?windowid=" + this.windowId);
this.wsConn.onopen = this.onopen.bind(this);
this.wsConn.onmessage = this.onmessage.bind(this);
this.wsConn.onclose = this.onclose.bind(this);
// turns out onerror is not necessary (onclose always follows onerror)
// this.wsConn.onerror = this.onerror;
}
reconnect(forceClose?: boolean) {
if (this.isOpen()) {
if (forceClose) {
this.wsConn.close(); // this will force a reconnect
}
return;
}
this.reconnectTimes++;
if (this.reconnectTimes > 20) {
this.log("cannot connect, giving up");
return;
}
let timeoutArr = [0, 0, 2, 5, 10, 10, 30, 60];
let timeout = 60;
if (this.reconnectTimes < timeoutArr.length) {
timeout = timeoutArr[this.reconnectTimes];
}
if (Date.now() - this.lastReconnectTime < 500) {
timeout = 1;
}
if (timeout > 0) {
this.log(sprintf("sleeping %ds", timeout));
}
setTimeout(() => {
this.connectNow(String(this.reconnectTimes));
}, timeout * 1000);
}
onclose(event: any) {
// console.log("close", event);
if (event.wasClean) {
this.log("connection closed");
} else {
this.log("connection error/disconnected");
}
if (this.isOpen() || this.opening) {
this.setOpen(false);
this.opening = false;
this.reconnect();
}
}
onopen() {
this.log("connection open");
this.setOpen(true);
this.opening = false;
this.runMsgQueue();
// reconnectTimes is reset in onmessage:hello
}
runMsgQueue() {
if (!this.isOpen()) {
return;
}
if (this.msgQueue.length == 0) {
return;
}
let msg = this.msgQueue.shift();
this.sendMessage(msg);
setTimeout(() => {
this.runMsgQueue();
}, 100);
}
onmessage(event: any) {
let eventData = null;
if (event.data != null) {
eventData = JSON.parse(event.data);
}
if (eventData == null) {
return;
}
if (eventData.type == "ping") {
this.wsConn.send(JSON.stringify({ type: "pong", stime: Date.now() }));
return;
}
if (eventData.type == "pong") {
// nothing
return;
}
if (eventData.type == "hello") {
this.reconnectTimes = 0;
return;
}
if (eventData.type == "rpcresp") {
this.handleRpcResp(eventData);
return;
}
if (this.messageCallback) {
try {
this.messageCallback(eventData);
} catch (e) {
console.log("[error] messageCallback", e);
}
}
}
sendPing() {
if (!this.isOpen()) {
return;
}
this.wsConn.send(JSON.stringify({ type: "ping", stime: Date.now() }));
}
handleRpcResp(data: any) {
let reqId = data.reqid;
let rpcEntry = this.rpcMap.get(reqId);
if (rpcEntry == null) {
console.log("rpcresp for unknown reqid", reqId);
return;
}
this.rpcMap.delete(reqId);
console.log("rpcresp", rpcEntry.method, Math.round(performance.now() - rpcEntry.startTs) + "ms");
if (data.error != null) {
rpcEntry.reject(data.error);
} else {
rpcEntry.resolve(data.data);
}
}
doRpc(method: string, params: any[]): Promise<any> {
if (!this.isOpen()) {
return Promise.reject("not connected");
}
let reqId = uuidv4();
let req = { type: "rpc", method: method, params: params, reqid: reqId };
let rpcEntry: RpcEntry = {
method: method,
startTs: performance.now(),
reqId: reqId,
resolve: null,
reject: null,
promise: null,
};
let rpcPromise = new Promise((resolve, reject) => {
rpcEntry.resolve = resolve;
rpcEntry.reject = reject;
});
rpcEntry.promise = rpcPromise;
this.rpcMap.set(reqId, rpcEntry);
this.wsConn.send(JSON.stringify(req));
return rpcPromise;
}
sendMessage(data: any) {
if (!this.isOpen()) {
return;
}
let msg = JSON.stringify(data);
const byteSize = new Blob([msg]).size;
if (byteSize > MaxWebSocketSendSize) {
console.log("ws message too large", byteSize, data.type, msg.substring(0, 100));
return;
}
this.wsConn.send(msg);
}
pushMessage(data: any) {
if (!this.isOpen()) {
this.msgQueue.push(data);
return;
}
this.sendMessage(data);
}
}
export { WSControl };

View File

@ -2,13 +2,14 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { Block, BlockHeader } from "@/app/block/block"; import { Block, BlockHeader } from "@/app/block/block";
import * as services from "@/store/services";
import * as WOS from "@/store/wos"; import * as WOS from "@/store/wos";
import { CenteredDiv, CenteredLoadingDiv } from "@/element/quickelems";
import { TileLayout } from "@/faraday/index"; import { TileLayout } from "@/faraday/index";
import { getLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom"; import { getLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { CenteredDiv, CenteredLoadingDiv } from "../element/quickelems";
import "./tab.less"; import "./tab.less";
const TabContent = ({ tabId }: { tabId: string }) => { const TabContent = ({ tabId }: { tabId: string }) => {
@ -34,7 +35,7 @@ const TabContent = ({ tabId }: { tabId: string }) => {
const onNodeDelete = useCallback((data: TabLayoutData) => { const onNodeDelete = useCallback((data: TabLayoutData) => {
console.log("onNodeDelete", data); console.log("onNodeDelete", data);
return WOS.DeleteBlock(data.blockId); return services.ObjectService.DeleteBlock(data.blockId);
}, []); }, []);
if (tabLoading) { if (tabLoading) {

View File

@ -15,7 +15,7 @@ declare var monaco: Monaco;
let monacoLoadedAtom = jotai.atom(false); let monacoLoadedAtom = jotai.atom(false);
function loadMonaco() { function loadMonaco() {
loader.config({ paths: { vs: "./monaco" } }); loader.config({ paths: { vs: "./dist-dev/monaco" } });
loader loader
.init() .init()
.then(() => { .then(() => {

View File

@ -1,7 +1,6 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { FileInfo } from "@/bindings/fileservice";
import { Table, createColumnHelper, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"; import { Table, createColumnHelper, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import clsx from "clsx"; import clsx from "clsx";
import * as jotai from "jotai"; import * as jotai from "jotai";

View File

@ -1,9 +1,9 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { FileInfo, FileService, FullFile } from "@/bindings/fileservice";
import { Markdown } from "@/element/markdown"; import { Markdown } from "@/element/markdown";
import { useBlockAtom, useBlockCache } from "@/store/global"; import { getBackendHostPort, useBlockAtom, useBlockCache } from "@/store/global";
import * as services from "@/store/services";
import * as WOS from "@/store/wos"; import * as WOS from "@/store/wos";
import * as util from "@/util/util"; import * as util from "@/util/util";
import clsx from "clsx"; import clsx from "clsx";
@ -69,7 +69,7 @@ function MarkdownPreview({ contentAtom }: { contentAtom: jotai.Atom<Promise<stri
function StreamingPreview({ fileInfo }: { fileInfo: FileInfo }) { function StreamingPreview({ fileInfo }: { fileInfo: FileInfo }) {
const filePath = fileInfo.path; const filePath = fileInfo.path;
const streamingUrl = "/wave/stream-file?path=" + encodeURIComponent(filePath); const streamingUrl = getBackendHostPort() + "/wave/stream-file?path=" + encodeURIComponent(filePath);
if (fileInfo.mimetype == "application/pdf") { if (fileInfo.mimetype == "application/pdf") {
return ( return (
<div className="view-preview view-preview-pdf"> <div className="view-preview view-preview-pdf">
@ -114,7 +114,7 @@ function PreviewView({ blockId }: { blockId: string }) {
}, },
(get, set, update) => { (get, set, update) => {
const blockId = get(blockAtom)?.oid; const blockId = get(blockAtom)?.oid;
WOS.UpdateObjectMeta(`block:${blockId}`, { file: update }); services.ObjectService.UpdateObjectMeta(`block:${blockId}`, { file: update });
} }
) )
); );
@ -124,7 +124,8 @@ function PreviewView({ blockId }: { blockId: string }) {
if (fileName == null) { if (fileName == null) {
return null; return null;
} }
const statFile = await FileService.StatFile(fileName); // const statFile = await FileService.StatFile(fileName);
const statFile = await services.FileService.StatFile(fileName);
return statFile; return statFile;
}) })
); );
@ -134,7 +135,8 @@ function PreviewView({ blockId }: { blockId: string }) {
if (fileName == null) { if (fileName == null) {
return null; return null;
} }
const file = await FileService.ReadFile(fileName); // const file = await FileService.ReadFile(fileName);
const file = await services.FileService.ReadFile(fileName);
return file; return file;
}) })
); );

View File

@ -1,15 +1,14 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { BlockService } from "@/bindings/blockservice"; import { getBackendHostPort, getORefSubject, WOS } from "@/store/global";
import { getBlockSubject } from "@/store/global"; import * as services from "@/store/services";
import { base64ToArray } from "@/util/util"; import { base64ToArray } from "@/util/util";
import { FitAddon } from "@xterm/addon-fit"; import { FitAddon } from "@xterm/addon-fit";
import type { ITheme } from "@xterm/xterm"; import type { ITheme } from "@xterm/xterm";
import { Terminal } from "@xterm/xterm"; import { Terminal } from "@xterm/xterm";
import * as React from "react"; import * as React from "react";
import useResizeObserver from "@react-hook/resize-observer";
import { debounce } from "throttle-debounce"; import { debounce } from "throttle-debounce";
import "./view.less"; import "./view.less";
import "/public/xterm.css"; import "/public/xterm.css";
@ -43,12 +42,15 @@ function getThemeFromCSSVars(el: Element): ITheme {
} }
function handleResize(fitAddon: FitAddon, blockId: string, term: Terminal) { function handleResize(fitAddon: FitAddon, blockId: string, term: Terminal) {
if (term == null) {
return;
}
const oldRows = term.rows; const oldRows = term.rows;
const oldCols = term.cols; const oldCols = term.cols;
fitAddon.fit(); fitAddon.fit();
if (oldRows !== term.rows || oldCols !== term.cols) { if (oldRows !== term.rows || oldCols !== term.cols) {
const resizeCommand = { command: "controller:input", termsize: { rows: term.rows, cols: term.cols } }; const resizeCommand = { command: "controller:input", termsize: { rows: term.rows, cols: term.cols } };
BlockService.SendCommand(blockId, resizeCommand); services.BlockService.SendCommand(blockId, resizeCommand);
} }
} }
@ -61,10 +63,6 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
const connectElemRef = React.useRef<HTMLDivElement>(null); const connectElemRef = React.useRef<HTMLDivElement>(null);
const termRef = React.useRef<Terminal>(null); const termRef = React.useRef<Terminal>(null);
const initialLoadRef = React.useRef<InitialLoadDataType>({ loaded: false, heldData: [] }); const initialLoadRef = React.useRef<InitialLoadDataType>({ loaded: false, heldData: [] });
const [fitAddon, setFitAddon] = React.useState<FitAddon>(null);
const [term, setTerm] = React.useState<Terminal>(null);
React.useEffect(() => { React.useEffect(() => {
console.log("terminal created"); console.log("terminal created");
const newTerm = new Terminal({ const newTerm = new Terminal({
@ -80,18 +78,22 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
newTerm.loadAddon(newFitAddon); newTerm.loadAddon(newFitAddon);
newTerm.open(connectElemRef.current); newTerm.open(connectElemRef.current);
newFitAddon.fit(); newFitAddon.fit();
BlockService.SendCommand(blockId, { // BlockService.SendCommand(blockId, {
// command: "controller:input",
// termsize: { rows: newTerm.rows, cols: newTerm.cols },
// });
services.BlockService.SendCommand(blockId, {
command: "controller:input", command: "controller:input",
termsize: { rows: newTerm.rows, cols: newTerm.cols }, termsize: { rows: newTerm.rows, cols: newTerm.cols },
}); });
newTerm.onData((data) => { newTerm.onData((data) => {
const b64data = btoa(data); const b64data = btoa(data);
const inputCmd = { command: "controller:input", blockid: blockId, inputdata64: b64data }; const inputCmd = { command: "controller:input", blockid: blockId, inputdata64: b64data };
BlockService.SendCommand(blockId, inputCmd); services.BlockService.SendCommand(blockId, inputCmd);
}); });
// block subject // block subject
const blockSubject = getBlockSubject(blockId); const blockSubject = getORefSubject(WOS.makeORef("block", blockId));
blockSubject.subscribe((data) => { blockSubject.subscribe((data) => {
// base64 decode // base64 decode
const decodedData = base64ToArray(data.ptydata); const decodedData = base64ToArray(data.ptydata);
@ -101,10 +103,6 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
initialLoadRef.current.heldData.push(decodedData); initialLoadRef.current.heldData.push(decodedData);
} }
}); });
setTerm(newTerm);
setFitAddon(newFitAddon);
// load data from filestore // load data from filestore
const startTs = Date.now(); const startTs = Date.now();
let loadedBytes = 0; let loadedBytes = 0;
@ -112,7 +110,7 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
const usp = new URLSearchParams(); const usp = new URLSearchParams();
usp.set("zoneid", blockId); usp.set("zoneid", blockId);
usp.set("name", "main"); usp.set("name", "main");
fetch("/wave/file?" + usp.toString()) fetch(getBackendHostPort() + "/wave/file?" + usp.toString())
.then((resp) => { .then((resp) => {
if (resp.ok) { if (resp.ok) {
return resp.arrayBuffer(); return resp.arrayBuffer();
@ -133,18 +131,20 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
console.log(`terminal loaded file ${loadedBytes} bytes, ${Date.now() - startTs}ms`); console.log(`terminal loaded file ${loadedBytes} bytes, ${Date.now() - startTs}ms`);
}); });
const resize_debounced = debounce(50, () => {
handleResize(newFitAddon, blockId, newTerm);
});
const rszObs = new ResizeObserver(() => {
resize_debounced();
});
rszObs.observe(connectElemRef.current);
return () => { return () => {
newTerm.dispose(); newTerm.dispose();
blockSubject.release(); blockSubject.release();
}; };
}, []); }, []);
const handleResizeCallback = React.useCallback(() => {
debounce(50, () => handleResize(fitAddon, blockId, term));
}, [fitAddon, term]);
useResizeObserver(connectElemRef, handleResizeCallback);
return ( return (
<div className="view-term"> <div className="view-term">
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div> <div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>

View File

@ -3,6 +3,7 @@
import { TabContent } from "@/app/tab/tab"; import { TabContent } from "@/app/tab/tab";
import { atoms } from "@/store/global"; import { atoms } from "@/store/global";
import * as services from "@/store/services";
import * as WOS from "@/store/wos"; import * as WOS from "@/store/wos";
import { clsx } from "clsx"; import { clsx } from "clsx";
import * as jotai from "jotai"; import * as jotai from "jotai";
@ -21,10 +22,10 @@ function Tab({ tabId }: { tabId: string }) {
const windowData = jotai.useAtomValue(atoms.waveWindow); const windowData = jotai.useAtomValue(atoms.waveWindow);
const [tabData, tabLoading] = WOS.useWaveObjectValue<Tab>(WOS.makeORef("tab", tabId)); const [tabData, tabLoading] = WOS.useWaveObjectValue<Tab>(WOS.makeORef("tab", tabId));
function setActiveTab() { function setActiveTab() {
WOS.SetActiveTab(tabId); services.ObjectService.SetActiveTab(tabId);
} }
function handleCloseTab() { function handleCloseTab() {
WOS.CloseTab(tabId); services.ObjectService.CloseTab(tabId);
deleteLayoutStateAtomForTab(tabId); deleteLayoutStateAtomForTab(tabId);
} }
return ( return (
@ -45,7 +46,7 @@ function Tab({ tabId }: { tabId: string }) {
function TabBar({ workspace }: { workspace: Workspace }) { function TabBar({ workspace }: { workspace: Workspace }) {
function handleAddTab() { function handleAddTab() {
const newTabName = `Tab-${workspace.tabids.length + 1}`; const newTabName = `Tab-${workspace.tabids.length + 1}`;
WOS.AddTabToWorkspace(newTabName, true); services.ObjectService.AddTabToWorkspace(newTabName, true);
} }
const tabIds = workspace?.tabids ?? []; const tabIds = workspace?.tabids ?? [];
return ( return (
@ -83,7 +84,7 @@ function Widgets() {
async function createBlock(blockDef: BlockDef) { async function createBlock(blockDef: BlockDef) {
const rtOpts: RuntimeOpts = { termsize: { rows: 25, cols: 80 } }; const rtOpts: RuntimeOpts = { termsize: { rows: 25, cols: 80 } };
const { blockId } = await WOS.CreateBlock(blockDef, rtOpts); const blockId = await services.ObjectService.CreateBlock(blockDef, rtOpts);
addBlockToTab(blockId); addBlockToTab(blockId);
} }
@ -122,13 +123,13 @@ function Widgets() {
<div className="widget" onClick={() => clickTerminal()}> <div className="widget" onClick={() => clickTerminal()}>
<i className="fa fa-solid fa-square-terminal fa-fw" /> <i className="fa fa-solid fa-square-terminal fa-fw" />
</div> </div>
<div className="widget" onClick={() => clickPreview("README.md")}> <div className="widget" onClick={() => clickPreview("~/work/wails/thenextwave/README.md")}>
<i className="fa fa-solid fa-files fa-fw" /> <i className="fa fa-solid fa-files fa-fw" />
</div> </div>
<div className="widget" onClick={() => clickPreview("go.mod")}> <div className="widget" onClick={() => clickPreview("~/work/wails/thenextwave/go.mod")}>
<i className="fa fa-solid fa-files fa-fw" /> <i className="fa fa-solid fa-files fa-fw" />
</div> </div>
<div className="widget" onClick={() => clickPreview("build/appicon.png")}> <div className="widget" onClick={() => clickPreview("~/work/wails/thenextwave/build/appicon.png")}>
<i className="fa fa-solid fa-files fa-fw" /> <i className="fa fa-solid fa-files fa-fw" />
</div> </div>
<div className="widget" onClick={() => clickPreview("~")}> <div className="widget" onClick={() => clickPreview("~")}>

View File

@ -1,9 +1,9 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { TileLayout } from "./lib/TileLayout.jsx"; import { TileLayout } from "./lib/TileLayout";
import { newLayoutTreeStateAtom, useLayoutTreeStateReducerAtom, withLayoutTreeState } from "./lib/layoutAtom.js"; import { newLayoutTreeStateAtom, useLayoutTreeStateReducerAtom, withLayoutTreeState } from "./lib/layoutAtom";
import { newLayoutNode } from "./lib/layoutNode.js"; import { newLayoutNode } from "./lib/layoutNode";
import type { import type {
LayoutNode, LayoutNode,
LayoutTreeCommitPendingAction, LayoutTreeCommitPendingAction,
@ -14,8 +14,8 @@ import type {
LayoutTreeState, LayoutTreeState,
WritableLayoutNodeAtom, WritableLayoutNodeAtom,
WritableLayoutTreeStateAtom, WritableLayoutTreeStateAtom,
} from "./lib/model.js"; } from "./lib/model";
import { LayoutTreeActionType } from "./lib/model.js"; import { LayoutTreeActionType } from "./lib/model";
export { export {
LayoutTreeActionType, LayoutTreeActionType,

View File

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import clsx from "clsx"; import clsx from "clsx";
import { import React, {
CSSProperties, CSSProperties,
ReactNode, ReactNode,
RefObject, RefObject,
@ -18,8 +18,8 @@ import { useDrag, useDragLayer, useDrop } from "react-dnd";
import useResizeObserver from "@react-hook/resize-observer"; import useResizeObserver from "@react-hook/resize-observer";
import { toPng } from "html-to-image"; import { toPng } from "html-to-image";
import { useLayoutTreeStateReducerAtom } from "./layoutAtom.js"; import { useLayoutTreeStateReducerAtom } from "./layoutAtom";
import { findNode } from "./layoutNode.js"; import { findNode } from "./layoutNode";
import { import {
ContentRenderer, ContentRenderer,
LayoutNode, LayoutNode,
@ -31,15 +31,9 @@ import {
LayoutTreeState, LayoutTreeState,
PreviewRenderer, PreviewRenderer,
WritableLayoutTreeStateAtom, WritableLayoutTreeStateAtom,
} from "./model.js"; } from "./model";
import "./tilelayout.less"; import "./tilelayout.less";
import { import { Dimensions, FlexDirection, setTransform as createTransform, debounce, determineDropDirection } from "./utils";
Dimensions,
FlexDirection,
setTransform as createTransform,
debounce,
determineDropDirection,
} from "./utils.js";
export interface TileLayoutProps<T> { export interface TileLayoutProps<T> {
layoutTreeStateAtom: WritableLayoutTreeStateAtom<T>; layoutTreeStateAtom: WritableLayoutTreeStateAtom<T>;

View File

@ -1,10 +1,10 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { WOS } from "@/app/store/global.js"; import { WOS } from "@/app/store/global";
import { Atom, Getter, PrimitiveAtom, WritableAtom, atom, useAtom } from "jotai"; import { Atom, Getter, PrimitiveAtom, WritableAtom, atom, useAtom } from "jotai";
import { useCallback } from "react"; import { useCallback } from "react";
import { layoutTreeStateReducer, newLayoutTreeState } from "./layoutState.js"; import { layoutTreeStateReducer, newLayoutTreeState } from "./layoutState";
import { import {
LayoutNode, LayoutNode,
LayoutNodeWaveObj, LayoutNodeWaveObj,

View File

@ -1,8 +1,8 @@
// Copyright 2024, Command Line Inc. // Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { LayoutNode } from "./model.js"; import { LayoutNode } from "./model";
import { FlexDirection, getCrypto, reverseFlexDirection } from "./utils.js"; import { FlexDirection, getCrypto, reverseFlexDirection } from "./utils";
const crypto = getCrypto(); const crypto = getCrypto();

View File

@ -9,7 +9,7 @@ import {
findNode, findNode,
findParent, findParent,
removeChild, removeChild,
} from "./layoutNode.js"; } from "./layoutNode";
import { import {
LayoutNode, LayoutNode,
LayoutTreeAction, LayoutTreeAction,
@ -20,14 +20,14 @@ import {
LayoutTreeMoveNodeAction, LayoutTreeMoveNodeAction,
LayoutTreeState, LayoutTreeState,
MoveOperation, MoveOperation,
} from "./model.js"; } from "./model";
import { DropDirection, FlexDirection, lazy } from "./utils.js"; import { DropDirection, FlexDirection, lazy } from "./utils";
/** /**
* Initializes a layout tree state. * Initializes a layout tree state.
* @param rootNode The root node for the tree. * @param rootNode The root node for the tree.
* @returns The state of the tree. * @returns The state of the tree.
* *t
* @template T The type of data associated with the nodes of the tree. * @template T The type of data associated with the nodes of the tree.
*/ */
export function newLayoutTreeState<T>(rootNode: LayoutNode<T>): LayoutTreeState<T> { export function newLayoutTreeState<T>(rootNode: LayoutNode<T>): LayoutTreeState<T> {

View File

@ -2,110 +2,6 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
declare global { declare global {
type UIContext = {
windowid: string;
activetabid: string;
};
type MetadataType = { [key: string]: any };
type ORef = {
otype: string;
oid: string;
};
interface WaveObj {
otype: string;
oid: string;
version: number;
}
type WaveObjUpdate = {
updatetype: "update" | "delete";
otype: string;
oid: string;
obj?: WaveObj;
};
type Block = WaveObj & {
blockdef: BlockDef;
controller: string;
view: string;
meta?: { [key: string]: any };
runtimeopts?: RuntimeOpts;
};
type BlockDef = {
controller?: string;
view?: string;
files?: { [key: string]: FileDef };
meta?: { [key: string]: any };
};
type FileDef = {
filetype?: string;
path?: string;
url?: string;
content?: string;
meta?: { [key: string]: any };
};
type TermSize = {
rows: number;
cols: number;
};
type Client = {
otype: string;
oid: string;
version: number;
mainwindowid: string;
};
type Tab = {
otype: string;
oid: string;
version: number;
name: string;
blockids: string[];
layoutNode: string;
};
type Point = {
x: number;
y: number;
};
type WinSize = {
width: number;
height: number;
};
type Workspace = {
otype: string;
oid: string;
version: number;
name: string;
tabids: string[];
};
type RuntimeOpts = {
termsize?: TermSize;
winsize?: WinSize;
};
type WaveWindow = {
otype: string;
oid: string;
version: number;
workspaceid: string;
activetabid: string;
activeblockmap: { [key: string]: string };
pos: Point;
winsize: WinSize;
lastfocusts: number;
};
type TabLayoutData = { type TabLayoutData = {
blockId: string; blockId: string;
}; };

174
frontend/types/gotypes.d.ts vendored Normal file
View File

@ -0,0 +1,174 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
// generated by cmd/generate/main-generate.go
declare global {
// wstore.Block
type Block = WaveObj & {
blockdef: BlockDef;
controller: string;
view: string;
runtimeopts?: RuntimeOpts;
meta: MetaType;
};
// wstore.BlockDef
type BlockDef = {
controller?: string;
view?: string;
files?: {[key: string]: FileDef};
meta?: MetaType;
};
// wstore.Client
type Client = WaveObj & {
mainwindowid: string;
meta: MetaType;
};
// wstore.FileDef
type FileDef = {
filetype?: string;
path?: string;
url?: string;
content?: string;
meta?: MetaType;
};
// fileservice.FileInfo
type FileInfo = {
path: string;
notfound?: boolean;
size: number;
mode: number;
modtime: number;
isdir?: boolean;
mimetype?: string;
};
// fileservice.FullFile
type FullFile = {
info: FileInfo;
data64: string;
};
// wstore.LayoutNode
type LayoutNode = WaveObj & {
node?: any;
meta?: MetaType;
};
type MetaType = {[key: string]: any}
// servicemeta.MethodMeta
type MethodMeta = {
Desc: string;
ArgNames: string[];
ReturnDesc: string;
};
// waveobj.ORef
type ORef = {
otype: string;
oid: string;
};
// wstore.Point
type Point = {
x: number;
y: number;
};
// wstore.RuntimeOpts
type RuntimeOpts = {
termsize?: TermSize;
winsize?: WinSize;
};
// wstore.Tab
type Tab = WaveObj & {
name: string;
layoutNode: string;
blockids: string[];
meta: MetaType;
};
// shellexec.TermSize
type TermSize = {
rows: number;
cols: number;
};
// wstore.UIContext
type UIContext = {
windowid: string;
activetabid: string;
};
// eventbus.WSEventType
type WSEventType = {
eventtype: string;
oref?: string;
data: any;
};
// waveobj.WaveObj
type WaveObj = {
otype: string;
oid: string;
version: number;
};
// wstore.WaveObjUpdate
type WaveObjUpdate = {
updatetype: string;
otype: string;
oid: string;
obj?: WaveObj;
};
// service.WebCallType
type WebCallType = {
service: string;
method: string;
uicontext?: UIContext;
args: any[];
};
// service.WebReturnType
type WebReturnType = {
success?: boolean;
error?: string;
data?: any;
updates?: WaveObjUpdate[];
};
// wstore.WinSize
type WinSize = {
width: number;
height: number;
};
// wstore.Window
type WaveWindow = WaveObj & {
workspaceid: string;
activetabid: string;
activeblockmap: {[key: string]: string};
pos: Point;
winsize: WinSize;
lastfocusts: number;
meta: MetaType;
};
// wstore.Workspace
type Workspace = WaveObj & {
name: string;
tabids: string[];
meta: MetaType;
};
}
export {}

View File

@ -17,15 +17,15 @@ function loadJetBrainsMonoFont() {
return; return;
} }
isJetBrainsMonoLoaded = true; isJetBrainsMonoLoaded = true;
const jbmFontNormal = new FontFace("JetBrains Mono", "url('/fonts/jetbrains-mono-v13-latin-regular.woff2')", { const jbmFontNormal = new FontFace("JetBrains Mono", "url('public/fonts/jetbrains-mono-v13-latin-regular.woff2')", {
style: "normal", style: "normal",
weight: "400", weight: "400",
}); });
const jbmFont200 = new FontFace("JetBrains Mono", "url('/fonts/jetbrains-mono-v13-latin-200.woff2')", { const jbmFont200 = new FontFace("JetBrains Mono", "url('public/fonts/jetbrains-mono-v13-latin-200.woff2')", {
style: "normal", style: "normal",
weight: "200", weight: "200",
}); });
const jbmFont700 = new FontFace("JetBrains Mono", "url('/fonts/jetbrains-mono-v13-latin-700.woff2')", { const jbmFont700 = new FontFace("JetBrains Mono", "url('public/fonts/jetbrains-mono-v13-latin-700.woff2')", {
style: "normal", style: "normal",
weight: "700", weight: "700",
}); });
@ -42,11 +42,11 @@ function loadLatoFont() {
return; return;
} }
isLatoFontLoaded = true; isLatoFontLoaded = true;
const latoFont = new FontFace("Lato", "url('/fonts/lato-regular.woff')", { const latoFont = new FontFace("Lato", "url('public/fonts/lato-regular.woff')", {
style: "normal", style: "normal",
weight: "400", weight: "400",
}); });
const latoFontBold = new FontFace("Lato", "url('/fonts/lato-bold.woff')", { const latoFontBold = new FontFace("Lato", "url('public/fonts/lato-bold.woff')", {
style: "normal", style: "normal",
weight: "700", weight: "700",
}); });
@ -61,11 +61,11 @@ function loadFiraCodeFont() {
return; return;
} }
isFiraCodeLoaded = true; isFiraCodeLoaded = true;
const firaCodeRegular = new FontFace("Fira Code", "url('/fonts/firacode-regular.woff2')", { const firaCodeRegular = new FontFace("Fira Code", "url('public/fonts/firacode-regular.woff2')", {
style: "normal", style: "normal",
weight: "400", weight: "400",
}); });
const firaCodeBold = new FontFace("Fira Code", "url('/fonts/firacode-bold.woff2')", { const firaCodeBold = new FontFace("Fira Code", "url('public/fonts/firacode-bold.woff2')", {
style: "normal", style: "normal",
weight: "700", weight: "700",
}); });
@ -80,19 +80,19 @@ function loadHackFont() {
return; return;
} }
isHackFontLoaded = true; isHackFontLoaded = true;
const hackRegular = new FontFace("Hack", "url('/fonts/hack-regular.woff2')", { const hackRegular = new FontFace("Hack", "url('public/fonts/hack-regular.woff2')", {
style: "normal", style: "normal",
weight: "400", weight: "400",
}); });
const hackBold = new FontFace("Hack", "url('/fonts/hack-bold.woff2')", { const hackBold = new FontFace("Hack", "url('public/fonts/hack-bold.woff2')", {
style: "normal", style: "normal",
weight: "700", weight: "700",
}); });
const hackItalic = new FontFace("Hack", "url('/fonts/hack-italic.woff2')", { const hackItalic = new FontFace("Hack", "url('public/fonts/hack-italic.woff2')", {
style: "italic", style: "italic",
weight: "400", weight: "400",
}); });
const hackBoldItalic = new FontFace("Hack", "url('/fonts/hack-bolditalic.woff2')", { const hackBoldItalic = new FontFace("Hack", "url('public/fonts/hack-bolditalic.woff2')", {
style: "italic", style: "italic",
weight: "700", weight: "700",
}); });
@ -111,7 +111,7 @@ function loadBaseFonts() {
return; return;
} }
isBaseFontsLoaded = true; isBaseFontsLoaded = true;
const mmFont = new FontFace("Martian Mono", "url(/fonts/MartianMono-VariableFont_wdth,wght.ttf)", { const mmFont = new FontFace("Martian Mono", "url('public/fonts/MartianMono-VariableFont_wdth,wght.ttf')", {
style: "normal", style: "normal",
weight: "normal", weight: "normal",
}); });

View File

@ -2,7 +2,8 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { Client } from "@/gopkg/wstore"; import { Client } from "@/gopkg/wstore";
import { globalStore } from "@/store/global"; import { globalStore, globalWS, initWS } from "@/store/global";
import * as services from "@/store/services";
import * as WOS from "@/store/wos"; import * as WOS from "@/store/wos";
import * as React from "react"; import * as React from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
@ -10,13 +11,15 @@ import { App } from "./app/app";
import { loadFonts } from "./util/fontutil"; import { loadFonts } from "./util/fontutil";
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const windowId = urlParams.get("windowid"); let windowId = urlParams.get("windowid");
const clientId = urlParams.get("clientid"); let clientId = urlParams.get("clientid");
loadFonts();
console.log("Wave Starting"); console.log("Wave Starting");
console.log("clientid", clientId, "windowid", windowId);
loadFonts();
initWS();
(window as any).globalWS = globalWS;
(window as any).WOS = WOS; (window as any).WOS = WOS;
(window as any).globalStore = globalStore; (window as any).globalStore = globalStore;
@ -30,9 +33,10 @@ matchViewportSize();
document.addEventListener("DOMContentLoaded", async () => { document.addEventListener("DOMContentLoaded", async () => {
console.log("DOMContentLoaded"); console.log("DOMContentLoaded");
// ensures client/window/workspace are loaded into the cache before rendering // ensures client/window/workspace are loaded into the cache before rendering
await WOS.loadAndPinWaveObject<Client>(WOS.makeORef("client", clientId)); const client = await WOS.loadAndPinWaveObject<Client>(WOS.makeORef("client", clientId));
const waveWindow = await WOS.loadAndPinWaveObject<WaveWindow>(WOS.makeORef("window", windowId)); const waveWindow = await WOS.loadAndPinWaveObject<WaveWindow>(WOS.makeORef("window", windowId));
await WOS.loadAndPinWaveObject<Workspace>(WOS.makeORef("workspace", waveWindow.workspaceid)); await WOS.loadAndPinWaveObject<Workspace>(WOS.makeORef("workspace", waveWindow.workspaceid));
services.ObjectService.SetActiveTab(waveWindow.activetabid); // no need to wait
const reactElem = React.createElement(App, null, null); const reactElem = React.createElement(App, null, null);
const elem = document.getElementById("main"); const elem = document.getElementById("main");
const root = createRoot(elem); const root = createRoot(elem);

43
go.mod
View File

@ -8,58 +8,21 @@ require (
github.com/creack/pty v1.1.18 github.com/creack/pty v1.1.18
github.com/golang-migrate/migrate/v4 v4.17.1 github.com/golang-migrate/migrate/v4 v4.17.1
github.com/google/uuid v1.4.0 github.com/google/uuid v1.4.0
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.5.0
github.com/jmoiron/sqlx v1.4.0 github.com/jmoiron/sqlx v1.4.0
github.com/mattn/go-sqlite3 v1.14.22 github.com/mattn/go-sqlite3 v1.14.22
github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/mapstructure v1.5.0
github.com/sawka/txwrap v0.2.0 github.com/sawka/txwrap v0.2.0
github.com/wailsapp/wails/v3 v3.0.0-alpha.0
github.com/wavetermdev/waveterm/wavesrv v0.0.0-20240508181017-d07068c09d94 github.com/wavetermdev/waveterm/wavesrv v0.0.0-20240508181017-d07068c09d94
golang.org/x/sys v0.20.0 golang.org/x/sys v0.20.0
) )
require ( require (
dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/ebitengine/purego v0.4.0-alpha.4 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.5.0 // indirect
github.com/go-git/go-git/v5 v5.11.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/stretchr/testify v1.8.4 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
github.com/leaanthony/u v1.1.0 // indirect
github.com/lmittmann/tint v1.0.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/samber/lo v1.38.1 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/skeema/knownhosts v1.2.1 // indirect
github.com/wailsapp/go-webview2 v1.0.9 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
go.uber.org/atomic v1.7.0 // indirect go.uber.org/atomic v1.7.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/tools v0.21.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
) )
replace github.com/wailsapp/wails/v3 => ../wails/v3 replace github.com/wailsapp/wails/v3 => ../wails/v3

178
go.sum
View File

@ -1,216 +1,46 @@
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg=
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/purego v0.4.0-alpha.4 h1:Y7yIV06Yo5M2BAdD7EVPhfp6LZ0tEcQo5770OhYUVes=
github.com/ebitengine/purego v0.4.0-alpha.4/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4=
github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4= github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4=
github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM= github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI=
github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lmittmann/tint v1.0.4 h1:LeYihpJ9hyGvE0w+K2okPTGUdVLfng1+nDNVR4vWISc=
github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/sawka/txwrap v0.2.0 h1:V3LfvKVLULxcYSxdMguLwFyQFMEU9nFDJopg0ZkL+94= github.com/sawka/txwrap v0.2.0 h1:V3LfvKVLULxcYSxdMguLwFyQFMEU9nFDJopg0ZkL+94=
github.com/sawka/txwrap v0.2.0/go.mod h1:wwQ2SQiN4U+6DU/iVPhbvr7OzXAtgZlQCIGuvOswEfA= github.com/sawka/txwrap v0.2.0/go.mod h1:wwQ2SQiN4U+6DU/iVPhbvr7OzXAtgZlQCIGuvOswEfA=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ=
github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/wailsapp/go-webview2 v1.0.9 h1:lrU+q0cf1wgLdR69rN+ZnRtMJNaJRrcQ4ELxoO7/xjs=
github.com/wailsapp/go-webview2 v1.0.9/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wavetermdev/waveterm/wavesrv v0.0.0-20240508181017-d07068c09d94 h1:/SPCxd4KHlS4eRTreYEXWFRr8WfRFBcChlV5cgkaO58= github.com/wavetermdev/waveterm/wavesrv v0.0.0-20240508181017-d07068c09d94 h1:/SPCxd4KHlS4eRTreYEXWFRr8WfRFBcChlV5cgkaO58=
github.com/wavetermdev/waveterm/wavesrv v0.0.0-20240508181017-d07068c09d94/go.mod h1:ywoo7DXdYueQ0tTPhVoB+wzRTgERSE19EA3mR6KGRaI= github.com/wavetermdev/waveterm/wavesrv v0.0.0-20240508181017-d07068c09d94/go.mod h1:ywoo7DXdYueQ0tTPhVoB+wzRTgERSE19EA3mR6KGRaI=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

305
main.go
View File

@ -1,305 +0,0 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package main
// Note, main.go needs to be in the root of the project for the go:embed directive to work.
import (
"context"
"embed"
"encoding/base64"
"encoding/json"
"fmt"
"io/fs"
"log"
"net/http"
"os"
"os/signal"
"runtime"
"strings"
"syscall"
"time"
"github.com/google/uuid"
"github.com/wavetermdev/thenextwave/pkg/eventbus"
"github.com/wavetermdev/thenextwave/pkg/filestore"
"github.com/wavetermdev/thenextwave/pkg/service/blockservice"
"github.com/wavetermdev/thenextwave/pkg/service/clientservice"
"github.com/wavetermdev/thenextwave/pkg/service/fileservice"
"github.com/wavetermdev/thenextwave/pkg/service/objectservice"
"github.com/wavetermdev/thenextwave/pkg/wavebase"
"github.com/wavetermdev/thenextwave/pkg/wstore"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/events"
)
//go:embed dist
var assets embed.FS
//go:embed build/icons.icns
var appIcon []byte
func createAppMenu(app *application.App) *application.Menu {
menu := application.NewMenu()
menu.AddRole(application.AppMenu)
fileMenu := menu.AddSubmenu("File")
// newWindow := fileMenu.Add("New Window")
// newWindow.OnClick(func(appContext *application.Context) {
// createWindow(app)
// })
closeWindow := fileMenu.Add("Close Window")
closeWindow.OnClick(func(appContext *application.Context) {
app.CurrentWindow().Close()
})
menu.AddRole(application.EditMenu)
menu.AddRole(application.ViewMenu)
menu.AddRole(application.WindowMenu)
menu.AddRole(application.HelpMenu)
return menu
}
func storeWindowSizeAndPos(windowId string, window *application.WebviewWindow) {
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
defer cancelFn()
windowData, err := wstore.DBGet[*wstore.Window](ctx, windowId)
if err != nil {
log.Printf("error getting window data: %v\n", err)
return
}
winWidth, winHeight := window.Size()
windowData.WinSize.Width = winWidth
windowData.WinSize.Height = winHeight
x, y := window.AbsolutePosition()
windowData.Pos.X = x
windowData.Pos.Y = y
err = wstore.DBUpdate(ctx, windowData)
if err != nil {
log.Printf("error updating window size: %v\n", err)
}
}
func createWindow(windowData *wstore.Window, app *application.App) {
client, err := wstore.DBGetSingleton[*wstore.Client](context.Background())
if err != nil {
panic(fmt.Errorf("error getting client data: %w", err))
}
// TODO: x/y pos is not getting restored correctly. window seems to ignore the x/y values on startup
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "Wave Terminal",
Mac: application.MacWindow{
InvisibleTitleBarHeight: 50,
Backdrop: application.MacBackdropTranslucent,
TitleBar: application.MacTitleBarHiddenInset,
},
BackgroundColour: application.NewRGBA(0, 0, 0, 255),
URL: "/public/index.html?windowid=" + windowData.OID + "&clientid=" + client.OID,
X: windowData.Pos.X,
Y: windowData.Pos.Y,
Width: windowData.WinSize.Width,
Height: windowData.WinSize.Height,
ZoomControlEnabled: true,
})
eventbus.RegisterWailsWindow(window, windowData.OID)
window.On(events.Common.WindowClosing, func(event *application.WindowEvent) {
eventbus.UnregisterWailsWindow(window.ID())
})
window.On(events.Mac.WindowDidResize, func(event *application.WindowEvent) {
storeWindowSizeAndPos(windowData.OID, window)
})
window.On(events.Mac.WindowDidMove, func(event *application.WindowEvent) {
storeWindowSizeAndPos(windowData.OID, window)
})
window.Show()
go func() {
time.Sleep(100 * time.Millisecond)
objectService := &objectservice.ObjectService{}
uiContext := wstore.UIContext{
WindowId: windowData.OID,
ActiveTabId: windowData.ActiveTabId,
}
_, err := objectService.SetActiveTab(uiContext, windowData.ActiveTabId)
if err != nil {
log.Printf("error setting active tab for new window: %v\n", err)
}
}()
}
type waveAssetHandler struct {
AssetHandler http.Handler
}
func serveWaveFile(w http.ResponseWriter, r *http.Request) {
zoneId := r.URL.Query().Get("zoneid")
name := r.URL.Query().Get("name")
if _, err := uuid.Parse(zoneId); err != nil {
http.Error(w, fmt.Sprintf("invalid zoneid: %v", err), http.StatusBadRequest)
return
}
if name == "" {
http.Error(w, "name is required", http.StatusBadRequest)
return
}
file, err := filestore.WFS.Stat(r.Context(), zoneId, name)
if err == fs.ErrNotExist {
http.NotFound(w, r)
return
}
if err != nil {
http.Error(w, fmt.Sprintf("error getting file info: %v", err), http.StatusInternalServerError)
return
}
jsonFileBArr, err := json.Marshal(file)
if err != nil {
http.Error(w, fmt.Sprintf("error serializing file info: %v", err), http.StatusInternalServerError)
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Length", fmt.Sprintf("%d", file.Size))
w.Header().Set("X-ZoneFileInfo", base64.StdEncoding.EncodeToString(jsonFileBArr))
w.Header().Set("Last-Modified", time.UnixMilli(file.ModTs).UTC().Format(http.TimeFormat))
if file.Size == 0 {
w.WriteHeader(http.StatusOK)
return
}
for offset := file.DataStartIdx(); offset < file.Size; offset += filestore.DefaultPartDataSize {
_, data, err := filestore.WFS.ReadAt(r.Context(), zoneId, name, offset, filestore.DefaultPartDataSize)
if err != nil {
if offset == 0 {
http.Error(w, fmt.Sprintf("error reading file: %v", err), http.StatusInternalServerError)
} else {
// nothing to do, the headers have already been sent
log.Printf("error reading file %s/%s @ %d: %v\n", zoneId, name, offset, err)
}
return
}
w.Write(data)
}
}
func serveWaveUrls(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache")
if r.URL.Path == "/wave/stream-file" {
fileName := r.URL.Query().Get("path")
fileName = wavebase.ExpandHomeDir(fileName)
http.ServeFile(w, r, fileName)
return
}
if r.URL.Path == "/wave/file" {
serveWaveFile(w, r)
return
}
http.NotFound(w, r)
}
func (wah waveAssetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" && strings.HasPrefix(r.URL.Path, "/wave/") {
serveWaveUrls(w, r)
return
}
wah.AssetHandler.ServeHTTP(w, r)
}
func doShutdown(reason string) {
log.Printf("shutting down: %s\n", reason)
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn()
// TODO deal with flush in progress
filestore.WFS.FlushCache(ctx)
time.Sleep(200 * time.Millisecond)
os.Exit(0)
}
func installShutdownSignalHandlers() {
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))
break
}
}()
}
func main() {
err := wavebase.EnsureWaveHomeDir()
if err != nil {
log.Printf("error ensuring wave home dir: %v\n", err)
return
}
waveLock, err := wavebase.AcquireWaveLock()
if err != nil {
log.Printf("error acquiring wave lock (another instance of Wave is likely running): %v\n", err)
return
}
log.Printf("wave home dir: %s\n", wavebase.GetWaveHomeDir())
err = filestore.InitFilestore()
if err != nil {
log.Printf("error initializing filestore: %v\n", err)
return
}
err = wstore.InitWStore()
if err != nil {
log.Printf("error initializing wstore: %v\n", err)
return
}
err = wstore.EnsureInitialData()
if err != nil {
log.Printf("error ensuring initial data: %v\n", err)
return
}
installShutdownSignalHandlers()
app := application.New(application.Options{
Name: "NextWave",
Description: "The Next Wave Terminal",
Services: []application.Service{
application.NewService(&fileservice.FileService{}),
application.NewService(&blockservice.BlockService{}),
application.NewService(&clientservice.ClientService{}),
application.NewService(&objectservice.ObjectService{}),
},
Icon: appIcon,
Assets: application.AssetOptions{
Handler: waveAssetHandler{AssetHandler: application.AssetFileServerFS(assets)},
},
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
})
menu := createAppMenu(app)
app.SetMenu(menu)
eventbus.RegisterWailsApp(app)
setupCtx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
defer cancelFn()
client, err := wstore.DBGetSingleton[*wstore.Client](setupCtx)
if err != nil {
log.Printf("error getting client data: %v\n", err)
return
}
mainWindow, err := wstore.DBGet[*wstore.Window](setupCtx, client.MainWindowId)
if err != nil {
log.Printf("error getting main window: %v\n", err)
return
}
if mainWindow == nil {
log.Printf("no main window data\n")
return
}
createWindow(mainWindow, app)
eventbus.Start()
defer eventbus.Shutdown()
// blocking
err = app.Run()
// If an error occurred while running the application, log it and exit.
if err != nil {
log.Printf("run error: %v\n", err)
}
runtime.KeepAlive(waveLock)
}

View File

@ -2,7 +2,8 @@
"name": "thenextwave", "name": "thenextwave",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "main": "dist/emain.js",
"browser": "dist/wave.js",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build --minify false --mode development", "build": "vite build --minify false --mode development",
@ -14,6 +15,16 @@
"test": "vitest" "test": "vitest"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.24.7",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-decorators": "^7.24.7",
"@babel/plugin-proposal-private-methods": "^7.18.6",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/plugin-transform-react-jsx": "^7.24.7",
"@babel/plugin-transform-runtime": "^7.24.7",
"@babel/preset-env": "^7.24.7",
"@babel/preset-react": "^7.24.7",
"@babel/preset-typescript": "^7.24.7",
"@chromatic-com/storybook": "^1.5.0", "@chromatic-com/storybook": "^1.5.0",
"@eslint/js": "^9.2.0", "@eslint/js": "^9.2.0",
"@storybook/addon-essentials": "^8.1.4", "@storybook/addon-essentials": "^8.1.4",
@ -24,6 +35,9 @@
"@storybook/react": "^8.1.4", "@storybook/react": "^8.1.4",
"@storybook/react-vite": "^8.1.4", "@storybook/react-vite": "^8.1.4",
"@storybook/test": "^8.1.4", "@storybook/test": "^8.1.4",
"@types/babel__core": "^7",
"@types/babel__plugin-transform-runtime": "^7",
"@types/babel__preset-env": "^7",
"@types/node": "^20.12.12", "@types/node": "^20.12.12",
"@types/react": "^18.3.2", "@types/react": "^18.3.2",
"@types/throttle-debounce": "^5", "@types/throttle-debounce": "^5",
@ -31,21 +45,32 @@
"@vitejs/plugin-react": "^4.3.0", "@vitejs/plugin-react": "^4.3.0",
"@vitest/coverage-istanbul": "^1.6.0", "@vitest/coverage-istanbul": "^1.6.0",
"@wailsio/runtime": "^3.0.0-alpha.24", "@wailsio/runtime": "^3.0.0-alpha.24",
"babel-loader": "^9.1.3",
"babel-plugin-jsx-control-statements": "^4.1.2",
"copy-webpack-plugin": "^12.0.2",
"css-loader": "^7.1.2",
"electron-vite": "^2.2.0",
"eslint": "^9.2.0", "eslint": "^9.2.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"less": "^4.2.0", "less": "^4.2.0",
"less-loader": "^12.2.0",
"mini-css-extract-plugin": "^2.9.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"prettier-plugin-jsdoc": "^1.3.0", "prettier-plugin-jsdoc": "^1.3.0",
"prettier-plugin-organize-imports": "^3.2.4", "prettier-plugin-organize-imports": "^3.2.4",
"storybook": "^8.1.4", "storybook": "^8.1.4",
"style-loader": "^4.0.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsconfig-paths-webpack-plugin": "^4.1.0",
"tslib": "^2.6.2", "tslib": "^2.6.2",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"typescript-eslint": "^7.8.0", "typescript-eslint": "^7.8.0",
"vite": "^5.0.0", "vite": "^5.0.0",
"vite-plugin-static-copy": "^1.0.5", "vite-plugin-static-copy": "^1.0.5",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0" "vitest": "^1.6.0",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4"
}, },
"dependencies": { "dependencies": {
"@monaco-editor/loader": "^1.4.0", "@monaco-editor/loader": "^1.4.0",
@ -57,6 +82,8 @@
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dayjs": "^1.11.11",
"electron": "^30.1.0",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"immer": "^10.1.1", "immer": "^10.1.1",
"jotai": "^2.8.0", "jotai": "^2.8.0",

View File

@ -15,10 +15,10 @@ import (
"time" "time"
"github.com/creack/pty" "github.com/creack/pty"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wavetermdev/thenextwave/pkg/eventbus" "github.com/wavetermdev/thenextwave/pkg/eventbus"
"github.com/wavetermdev/thenextwave/pkg/filestore" "github.com/wavetermdev/thenextwave/pkg/filestore"
"github.com/wavetermdev/thenextwave/pkg/shellexec" "github.com/wavetermdev/thenextwave/pkg/shellexec"
"github.com/wavetermdev/thenextwave/pkg/waveobj"
"github.com/wavetermdev/thenextwave/pkg/wstore" "github.com/wavetermdev/thenextwave/pkg/wstore"
) )
@ -97,8 +97,9 @@ func (bc *BlockController) handleShellProcData(data []byte, seqNum int) error {
if err != nil { if err != nil {
return fmt.Errorf("error appending to blockfile: %w", err) return fmt.Errorf("error appending to blockfile: %w", err)
} }
eventbus.SendEvent(application.WailsEvent{ eventbus.SendEvent(eventbus.WSEventType{
Name: "block:ptydata", EventType: "block:ptydata",
ORef: waveobj.MakeORef(wstore.OType_Block, bc.BlockId).String(),
Data: map[string]any{ Data: map[string]any{
"blockid": bc.BlockId, "blockid": bc.BlockId,
"blockfile": "main", "blockfile": "main",
@ -210,8 +211,9 @@ func (bc *BlockController) Run(bdata *wstore.Block) {
bc.Status = "done" bc.Status = "done"
} }
}) })
eventbus.SendEvent(application.WailsEvent{ eventbus.SendEvent(eventbus.WSEventType{
Name: "block:done", EventType: "block:done",
ORef: waveobj.MakeORef(wstore.OType_Block, bc.BlockId).String(),
Data: nil, Data: nil,
}) })
globalLock.Lock() globalLock.Lock()

View File

@ -4,164 +4,74 @@
package eventbus package eventbus
import ( import (
"errors"
"fmt"
"log"
"runtime/debug"
"sync" "sync"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wavetermdev/thenextwave/pkg/waveobj" "github.com/wavetermdev/thenextwave/pkg/waveobj"
) )
const EventBufferSize = 50 type WSEventType struct {
EventType string `json:"eventtype"`
var EventCh chan application.WailsEvent = make(chan application.WailsEvent, EventBufferSize) ORef string `json:"oref,omitempty"`
var WindowEventCh chan WindowEvent = make(chan WindowEvent, EventBufferSize) Data any `json:"data"`
var shutdownCh chan struct{} = make(chan struct{})
var ErrQueueFull = errors.New("event queue full")
type WindowEvent struct {
WindowId uint
Event application.WailsEvent
} }
type WindowWatchData struct { type WindowWatchData struct {
Window *application.WebviewWindow WindowWSCh chan any
WaveWindowId string WaveWindowId string
WailsWindowId uint
WatchedORefs map[waveobj.ORef]bool WatchedORefs map[waveobj.ORef]bool
} }
var globalLock = &sync.Mutex{} var globalLock = &sync.Mutex{}
var wailsApp *application.App var wsMap = make(map[string]*WindowWatchData) // websocketid => WindowWatchData
var wailsWindowMap = make(map[uint]*WindowWatchData)
func Start() { func RegisterWSChannel(connId string, windowId string, ch chan any) {
go processEvents()
}
func Shutdown() {
close(shutdownCh)
}
func RegisterWailsApp(app *application.App) {
globalLock.Lock() globalLock.Lock()
defer globalLock.Unlock() defer globalLock.Unlock()
wailsApp = app wsMap[connId] = &WindowWatchData{
} WindowWSCh: ch,
WaveWindowId: windowId,
func RegisterWailsWindow(window *application.WebviewWindow, windowId string) {
globalLock.Lock()
defer globalLock.Unlock()
if _, found := wailsWindowMap[window.ID()]; found {
panic(fmt.Errorf("wails window already registered with eventbus: %d", window.ID()))
}
wailsWindowMap[window.ID()] = &WindowWatchData{
Window: window,
WailsWindowId: window.ID(),
WaveWindowId: "",
WatchedORefs: make(map[waveobj.ORef]bool), WatchedORefs: make(map[waveobj.ORef]bool),
} }
} }
func UnregisterWailsWindow(windowId uint) { func UnregisterWSChannel(connId string) {
globalLock.Lock() globalLock.Lock()
defer globalLock.Unlock() defer globalLock.Unlock()
delete(wailsWindowMap, windowId) delete(wsMap, connId)
} }
func emitEventToWindow(event WindowEvent) { func getWindowWatchesForWindowId(windowId string) []*WindowWatchData {
globalLock.Lock()
wdata := wailsWindowMap[event.WindowId]
globalLock.Unlock()
if wdata != nil {
wdata.Window.DispatchWailsEvent(&event.Event)
}
}
func emitEventToAllWindows(event *application.WailsEvent) {
globalLock.Lock()
wins := make([]*application.WebviewWindow, 0, len(wailsWindowMap))
for _, wdata := range wailsWindowMap {
wins = append(wins, wdata.Window)
}
globalLock.Unlock()
for _, window := range wins {
window.DispatchWailsEvent(event)
}
}
func SendEvent(event application.WailsEvent) {
EventCh <- event
}
func findWindowIdsByORef(oref waveobj.ORef) []uint {
globalLock.Lock() globalLock.Lock()
defer globalLock.Unlock() defer globalLock.Unlock()
var ids []uint var watches []*WindowWatchData
for _, wdata := range wailsWindowMap { for _, wdata := range wsMap {
if wdata.WatchedORefs[oref] { if wdata.WaveWindowId == windowId {
ids = append(ids, wdata.WailsWindowId) watches = append(watches, wdata)
} }
} }
return ids return watches
} }
func SendORefEvent(oref waveobj.ORef, event application.WailsEvent) { func getAllWatches() []*WindowWatchData {
wins := findWindowIdsByORef(oref) globalLock.Lock()
for _, windowId := range wins { defer globalLock.Unlock()
SendWindowEvent(windowId, event) watches := make([]*WindowWatchData, 0, len(wsMap))
for _, wdata := range wsMap {
watches = append(watches, wdata)
}
return watches
}
func SendEventToWindow(windowId string, event WSEventType) {
wwdArr := getWindowWatchesForWindowId(windowId)
for _, wdata := range wwdArr {
wdata.WindowWSCh <- event
} }
} }
func SendEventNonBlocking(event application.WailsEvent) error { func SendEvent(event WSEventType) {
select { wwdArr := getAllWatches()
case EventCh <- event: for _, wdata := range wwdArr {
return nil wdata.WindowWSCh <- event
default:
return ErrQueueFull
}
}
func SendWindowEvent(windowId uint, event application.WailsEvent) {
WindowEventCh <- WindowEvent{
WindowId: windowId,
Event: event,
}
}
func SendWindowEventNonBlocking(windowId uint, event application.WailsEvent) error {
select {
case WindowEventCh <- WindowEvent{
WindowId: windowId,
Event: event,
}:
return nil
default:
return ErrQueueFull
}
}
func processEvents() {
defer func() {
if r := recover(); r != nil {
log.Printf("eventbus panic: %v\n", r)
debug.PrintStack()
}
}()
log.Printf("eventbus starting\n")
for {
select {
case event := <-EventCh:
emitEventToAllWindows(&event)
case windowEvent := <-WindowEventCh:
emitEventToWindow(windowEvent)
case <-shutdownCh:
log.Printf("eventbus shutting down\n")
return
}
} }
} }

View File

@ -9,12 +9,20 @@ import (
"time" "time"
"github.com/wavetermdev/thenextwave/pkg/blockcontroller" "github.com/wavetermdev/thenextwave/pkg/blockcontroller"
"github.com/wavetermdev/thenextwave/pkg/service/servicemeta"
) )
type BlockService struct{} type BlockService struct{}
const DefaultTimeout = 2 * time.Second const DefaultTimeout = 2 * time.Second
func (bs *BlockService) SendCommand_Meta() servicemeta.MethodMeta {
return servicemeta.MethodMeta{
Desc: "send command to block",
ArgNames: []string{"blockid", "command"},
}
}
func (bs *BlockService) SendCommand(blockId string, cmdMap map[string]any) error { func (bs *BlockService) SendCommand(blockId string, cmdMap map[string]any) error {
cmd, err := blockcontroller.ParseCmdMap(cmdMap) cmd, err := blockcontroller.ParseCmdMap(cmdMap)
if err != nil { if err != nil {

View File

@ -12,6 +12,7 @@ import (
"time" "time"
"github.com/wavetermdev/thenextwave/pkg/blockcontroller" "github.com/wavetermdev/thenextwave/pkg/blockcontroller"
"github.com/wavetermdev/thenextwave/pkg/service/servicemeta"
"github.com/wavetermdev/thenextwave/pkg/waveobj" "github.com/wavetermdev/thenextwave/pkg/waveobj"
"github.com/wavetermdev/thenextwave/pkg/wstore" "github.com/wavetermdev/thenextwave/pkg/wstore"
) )
@ -28,7 +29,14 @@ func parseORef(oref string) (*waveobj.ORef, error) {
return &waveobj.ORef{OType: fields[0], OID: fields[1]}, nil return &waveobj.ORef{OType: fields[0], OID: fields[1]}, nil
} }
func (svc *ObjectService) GetObject(orefStr string) (any, error) { func (svc *ObjectService) GetObject_Meta() servicemeta.MethodMeta {
return servicemeta.MethodMeta{
Desc: "get wave object by oref",
ArgNames: []string{"oref"},
}
}
func (svc *ObjectService) GetObject(orefStr string) (waveobj.WaveObj, error) {
oref, err := parseORef(orefStr) oref, err := parseORef(orefStr)
if err != nil { if err != nil {
return nil, err return nil, err
@ -39,11 +47,17 @@ func (svc *ObjectService) GetObject(orefStr string) (any, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting object: %w", err) return nil, fmt.Errorf("error getting object: %w", err)
} }
rtn, err := waveobj.ToJsonMap(obj) return obj, nil
return rtn, err
} }
func (svc *ObjectService) GetObjects(orefStrArr []string) (any, error) { func (svc *ObjectService) GetObjects_Meta() servicemeta.MethodMeta {
return servicemeta.MethodMeta{
ArgNames: []string{"orefs"},
ReturnDesc: "objects",
}
}
func (svc *ObjectService) GetObjects(orefStrArr []string) ([]waveobj.WaveObj, error) {
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancelFn() defer cancelFn()
@ -78,30 +92,41 @@ func updatesRtn(ctx context.Context, rtnVal map[string]any) (any, error) {
return rtnVal, nil return rtnVal, nil
} }
func (svc *ObjectService) AddTabToWorkspace(uiContext wstore.UIContext, tabName string, activateTab bool) (any, error) { func (svc *ObjectService) AddTabToWorkspace_Meta() servicemeta.MethodMeta {
return servicemeta.MethodMeta{
ArgNames: []string{"uiContext", "tabName", "activateTab"},
ReturnDesc: "tabId",
}
}
func (svc *ObjectService) AddTabToWorkspace(uiContext wstore.UIContext, tabName string, activateTab bool) (string, wstore.UpdatesRtnType, error) {
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancelFn() defer cancelFn()
ctx = wstore.ContextWithUpdates(ctx) ctx = wstore.ContextWithUpdates(ctx)
windowData, err := wstore.DBMustGet[*wstore.Window](ctx, uiContext.WindowId) windowData, err := wstore.DBMustGet[*wstore.Window](ctx, uiContext.WindowId)
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting window: %w", err) return "", nil, fmt.Errorf("error getting window: %w", err)
} }
tab, err := wstore.CreateTab(ctx, windowData.WorkspaceId, tabName) tab, err := wstore.CreateTab(ctx, windowData.WorkspaceId, tabName)
if err != nil { if err != nil {
return nil, fmt.Errorf("error creating tab: %w", err) return "", nil, fmt.Errorf("error creating tab: %w", err)
} }
if activateTab { if activateTab {
err = wstore.SetActiveTab(ctx, uiContext.WindowId, tab.OID) err = wstore.SetActiveTab(ctx, uiContext.WindowId, tab.OID)
if err != nil { if err != nil {
return nil, fmt.Errorf("error setting active tab: %w", err) return "", nil, fmt.Errorf("error setting active tab: %w", err)
} }
} }
rtn := make(map[string]any) return tab.OID, wstore.ContextGetUpdatesRtn(ctx), nil
rtn["tabid"] = waveobj.GetOID(tab)
return updatesRtn(ctx, rtn)
} }
func (svc *ObjectService) SetActiveTab(uiContext wstore.UIContext, tabId string) (any, error) { func (svc *ObjectService) SetActiveTab_Meta() servicemeta.MethodMeta {
return servicemeta.MethodMeta{
ArgNames: []string{"uiContext", "tabId"},
}
}
func (svc *ObjectService) SetActiveTab(uiContext wstore.UIContext, tabId string) (wstore.UpdatesRtnType, error) {
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancelFn() defer cancelFn()
ctx = wstore.ContextWithUpdates(ctx) ctx = wstore.ContextWithUpdates(ctx)
@ -122,32 +147,51 @@ func (svc *ObjectService) SetActiveTab(uiContext wstore.UIContext, tabId string)
continue continue
} }
} }
return updatesRtn(ctx, nil) blockORefs := tab.GetBlockORefs()
blocks, err := wstore.DBSelectORefs(ctx, blockORefs)
if err != nil {
return nil, fmt.Errorf("error getting tab blocks: %w", err)
}
updates := wstore.ContextGetUpdatesRtn(ctx)
updates = append(updates, wstore.MakeUpdate(tab))
updates = append(updates, wstore.MakeUpdates(blocks)...)
return updates, nil
} }
func (svc *ObjectService) CreateBlock(uiContext wstore.UIContext, blockDef *wstore.BlockDef, rtOpts *wstore.RuntimeOpts) (any, error) { func (svc *ObjectService) CreateBlock_Meta() servicemeta.MethodMeta {
return servicemeta.MethodMeta{
ArgNames: []string{"uiContext", "blockDef", "rtOpts"},
ReturnDesc: "blockId",
}
}
func (svc *ObjectService) CreateBlock(uiContext wstore.UIContext, blockDef *wstore.BlockDef, rtOpts *wstore.RuntimeOpts) (string, wstore.UpdatesRtnType, error) {
if uiContext.ActiveTabId == "" { if uiContext.ActiveTabId == "" {
return nil, fmt.Errorf("no active tab") return "", nil, fmt.Errorf("no active tab")
} }
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancelFn() defer cancelFn()
ctx = wstore.ContextWithUpdates(ctx) ctx = wstore.ContextWithUpdates(ctx)
blockData, err := wstore.CreateBlock(ctx, uiContext.ActiveTabId, blockDef, rtOpts) blockData, err := wstore.CreateBlock(ctx, uiContext.ActiveTabId, blockDef, rtOpts)
if err != nil { if err != nil {
return nil, fmt.Errorf("error creating block: %w", err) return "", nil, fmt.Errorf("error creating block: %w", err)
} }
if blockData.Controller != "" { if blockData.Controller != "" {
err = blockcontroller.StartBlockController(ctx, blockData.OID) err = blockcontroller.StartBlockController(ctx, blockData.OID)
if err != nil { if err != nil {
return nil, fmt.Errorf("error starting block controller: %w", err) return "", nil, fmt.Errorf("error starting block controller: %w", err)
} }
} }
rtn := make(map[string]any) return blockData.OID, wstore.ContextGetUpdatesRtn(ctx), nil
rtn["blockId"] = blockData.OID
return updatesRtn(ctx, rtn)
} }
func (svc *ObjectService) DeleteBlock(uiContext wstore.UIContext, blockId string) (any, error) { func (svc *ObjectService) DeleteBlock_Meta() servicemeta.MethodMeta {
return servicemeta.MethodMeta{
ArgNames: []string{"uiContext", "blockId"},
}
}
func (svc *ObjectService) DeleteBlock(uiContext wstore.UIContext, blockId string) (wstore.UpdatesRtnType, error) {
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancelFn() defer cancelFn()
ctx = wstore.ContextWithUpdates(ctx) ctx = wstore.ContextWithUpdates(ctx)
@ -156,10 +200,16 @@ func (svc *ObjectService) DeleteBlock(uiContext wstore.UIContext, blockId string
return nil, fmt.Errorf("error deleting block: %w", err) return nil, fmt.Errorf("error deleting block: %w", err)
} }
blockcontroller.StopBlockController(blockId) blockcontroller.StopBlockController(blockId)
return updatesRtn(ctx, nil) return wstore.ContextGetUpdatesRtn(ctx), nil
} }
func (svc *ObjectService) CloseTab(uiContext wstore.UIContext, tabId string) (any, error) { func (svc *ObjectService) CloseTab_Meta() servicemeta.MethodMeta {
return servicemeta.MethodMeta{
ArgNames: []string{"uiContext", "tabId"},
}
}
func (svc *ObjectService) CloseTab(uiContext wstore.UIContext, tabId string) (wstore.UpdatesRtnType, error) {
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancelFn() defer cancelFn()
ctx = wstore.ContextWithUpdates(ctx) ctx = wstore.ContextWithUpdates(ctx)
@ -191,10 +241,16 @@ func (svc *ObjectService) CloseTab(uiContext wstore.UIContext, tabId string) (an
} }
wstore.SetActiveTab(ctx, uiContext.WindowId, newActiveTabId) wstore.SetActiveTab(ctx, uiContext.WindowId, newActiveTabId)
} }
return updatesRtn(ctx, nil) return wstore.ContextGetUpdatesRtn(ctx), nil
} }
func (svc *ObjectService) UpdateObjectMeta(uiContext wstore.UIContext, orefStr string, meta map[string]any) (any, error) { func (svc *ObjectService) UpdateObjectMeta_Meta() servicemeta.MethodMeta {
return servicemeta.MethodMeta{
ArgNames: []string{"uiContext", "oref", "meta"},
}
}
func (svc *ObjectService) UpdateObjectMeta(uiContext wstore.UIContext, orefStr string, meta map[string]any) (wstore.UpdatesRtnType, error) {
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancelFn() defer cancelFn()
ctx = wstore.ContextWithUpdates(ctx) ctx = wstore.ContextWithUpdates(ctx)
@ -206,18 +262,23 @@ func (svc *ObjectService) UpdateObjectMeta(uiContext wstore.UIContext, orefStr s
if err != nil { if err != nil {
return nil, fmt.Errorf("error updateing %q meta: %w", orefStr, err) return nil, fmt.Errorf("error updateing %q meta: %w", orefStr, err)
} }
return updatesRtn(ctx, nil) return wstore.ContextGetUpdatesRtn(ctx), nil
} }
func (svc *ObjectService) UpdateObject(uiContext wstore.UIContext, objData map[string]any, returnUpdates bool) (any, error) { func (svc *ObjectService) UpdateObject_Meta() servicemeta.MethodMeta {
return servicemeta.MethodMeta{
ArgNames: []string{"uiContext", "waveObj", "returnUpdates"},
}
}
func (svc *ObjectService) UpdateObject(uiContext wstore.UIContext, waveObj waveobj.WaveObj, returnUpdates bool) (wstore.UpdatesRtnType, error) {
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout) ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancelFn() defer cancelFn()
ctx = wstore.ContextWithUpdates(ctx) ctx = wstore.ContextWithUpdates(ctx)
if waveObj == nil {
oref, err := waveobj.ORefFromMap(objData) return nil, fmt.Errorf("update wavobj is nil")
if err != nil {
return nil, fmt.Errorf("objData is not a valid object, requires otype and oid: %w", err)
} }
oref := waveobj.ORefFromWaveObj(waveObj)
found, err := wstore.DBExistsORef(ctx, *oref) found, err := wstore.DBExistsORef(ctx, *oref)
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting object: %w", err) return nil, fmt.Errorf("error getting object: %w", err)
@ -225,16 +286,12 @@ func (svc *ObjectService) UpdateObject(uiContext wstore.UIContext, objData map[s
if !found { if !found {
return nil, fmt.Errorf("object not found: %s", oref) return nil, fmt.Errorf("object not found: %s", oref)
} }
newObj, err := waveobj.FromJsonMap(objData) err = wstore.DBUpdate(ctx, waveObj)
if err != nil {
return nil, fmt.Errorf("error converting data to valid wave object: %w", err)
}
err = wstore.DBUpdate(ctx, newObj)
if err != nil { if err != nil {
return nil, fmt.Errorf("error updating object: %w", err) return nil, fmt.Errorf("error updating object: %w", err)
} }
if returnUpdates { if returnUpdates {
return updatesRtn(ctx, nil) return wstore.ContextGetUpdatesRtn(ctx), nil
} }
return nil, nil return nil, nil
} }

429
pkg/service/service.go Normal file
View File

@ -0,0 +1,429 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package service
import (
"context"
"fmt"
"reflect"
"strings"
"github.com/wavetermdev/thenextwave/pkg/service/blockservice"
"github.com/wavetermdev/thenextwave/pkg/service/clientservice"
"github.com/wavetermdev/thenextwave/pkg/service/fileservice"
"github.com/wavetermdev/thenextwave/pkg/service/objectservice"
"github.com/wavetermdev/thenextwave/pkg/service/servicemeta"
"github.com/wavetermdev/thenextwave/pkg/waveobj"
"github.com/wavetermdev/thenextwave/pkg/wstore"
)
var ServiceMap = map[string]any{
"block": &blockservice.BlockService{},
"object": &objectservice.ObjectService{},
"file": &fileservice.FileService{},
"client": &clientservice.ClientService{},
}
var contextRType = reflect.TypeOf((*context.Context)(nil)).Elem()
var errorRType = reflect.TypeOf((*error)(nil)).Elem()
var updatesRType = reflect.TypeOf(([]wstore.WaveObjUpdate{}))
var waveObjRType = reflect.TypeOf((*waveobj.WaveObj)(nil)).Elem()
var waveObjSliceRType = reflect.TypeOf([]waveobj.WaveObj{})
var waveObjMapRType = reflect.TypeOf(map[string]waveobj.WaveObj{})
var methodMetaRType = reflect.TypeOf(servicemeta.MethodMeta{})
var waveObjUpdateRType = reflect.TypeOf(wstore.WaveObjUpdate{})
var uiContextRType = reflect.TypeOf((*wstore.UIContext)(nil)).Elem()
type WebCallType struct {
Service string `json:"service"`
Method string `json:"method"`
UIContext *wstore.UIContext `json:"uicontext,omitempty"`
Args []any `json:"args"`
}
type WebReturnType struct {
Success bool `json:"success,omitempty"`
Error string `json:"error,omitempty"`
Data any `json:"data,omitempty"`
Updates []wstore.WaveObjUpdate `json:"updates,omitempty"`
}
func convertNumber(argType reflect.Type, jsonArg float64) (any, error) {
switch argType.Kind() {
case reflect.Int:
return int(jsonArg), nil
case reflect.Int8:
return int8(jsonArg), nil
case reflect.Int16:
return int16(jsonArg), nil
case reflect.Int32:
return int32(jsonArg), nil
case reflect.Int64:
return int64(jsonArg), nil
case reflect.Uint:
return uint(jsonArg), nil
case reflect.Uint8:
return uint8(jsonArg), nil
case reflect.Uint16:
return uint16(jsonArg), nil
case reflect.Uint32:
return uint32(jsonArg), nil
case reflect.Uint64:
return uint64(jsonArg), nil
case reflect.Float32:
return float32(jsonArg), nil
case reflect.Float64:
return jsonArg, nil
}
return nil, fmt.Errorf("invalid number type %s", argType)
}
func convertComplex(argType reflect.Type, jsonArg any) (any, error) {
nativeArgVal := reflect.New(argType)
err := waveobj.DoMapStucture(nativeArgVal.Interface(), jsonArg)
if err != nil {
return nil, err
}
return nativeArgVal.Elem().Interface(), nil
}
func isSpecialWaveArgType(argType reflect.Type) bool {
return argType == waveObjRType || argType == waveObjSliceRType || argType == waveObjMapRType
}
func convertSpecial(argType reflect.Type, jsonArg any) (any, error) {
jsonType := reflect.TypeOf(jsonArg)
if argType == waveObjRType {
if jsonType.Kind() != reflect.Map {
return nil, fmt.Errorf("cannot convert %T to %s", jsonArg, argType)
}
return waveobj.FromJsonMap(jsonArg.(map[string]any))
} else if argType == waveObjSliceRType {
if jsonType.Kind() != reflect.Slice {
return nil, fmt.Errorf("cannot convert %T to %s", jsonArg, argType)
}
sliceArg := jsonArg.([]any)
nativeSlice := make([]waveobj.WaveObj, len(sliceArg))
for idx, elem := range sliceArg {
elemMap, ok := elem.(map[string]any)
if !ok {
return nil, fmt.Errorf("cannot convert %T to %s (idx %d is not a map, is %T)", jsonArg, waveObjSliceRType, idx, elem)
}
nativeObj, err := waveobj.FromJsonMap(elemMap)
if err != nil {
return nil, fmt.Errorf("cannot convert %T to %s (idx %d) error: %v", jsonArg, waveObjSliceRType, idx, err)
}
nativeSlice[idx] = nativeObj
}
return nativeSlice, nil
} else if argType == waveObjMapRType {
if jsonType.Kind() != reflect.Map {
return nil, fmt.Errorf("cannot convert %T to %s", jsonArg, argType)
}
mapArg := jsonArg.(map[string]any)
nativeMap := make(map[string]waveobj.WaveObj)
for key, elem := range mapArg {
elemMap, ok := elem.(map[string]any)
if !ok {
return nil, fmt.Errorf("cannot convert %T to %s (key %s is not a map, is %T)", jsonArg, waveObjMapRType, key, elem)
}
nativeObj, err := waveobj.FromJsonMap(elemMap)
if err != nil {
return nil, fmt.Errorf("cannot convert %T to %s (key %s) error: %v", jsonArg, waveObjMapRType, key, err)
}
nativeMap[key] = nativeObj
}
return nativeMap, nil
} else {
return nil, fmt.Errorf("invalid special wave argument type %s", argType)
}
}
func convertSpecialForReturn(argType reflect.Type, nativeArg any) (any, error) {
if argType == waveObjRType {
return waveobj.ToJsonMap(nativeArg.(waveobj.WaveObj))
} else if argType == waveObjSliceRType {
nativeSlice := nativeArg.([]waveobj.WaveObj)
jsonSlice := make([]map[string]any, len(nativeSlice))
for idx, elem := range nativeSlice {
elemMap, err := waveobj.ToJsonMap(elem)
if err != nil {
return nil, err
}
jsonSlice[idx] = elemMap
}
return jsonSlice, nil
} else if argType == waveObjMapRType {
nativeMap := nativeArg.(map[string]waveobj.WaveObj)
jsonMap := make(map[string]map[string]any)
for key, elem := range nativeMap {
elemMap, err := waveobj.ToJsonMap(elem)
if err != nil {
return nil, err
}
jsonMap[key] = elemMap
}
return jsonMap, nil
} else {
return nil, fmt.Errorf("invalid special wave argument type %s", argType)
}
}
func convertArgument(argType reflect.Type, jsonArg any) (any, error) {
if jsonArg == nil {
return nil, nil
}
if isSpecialWaveArgType(argType) {
return convertSpecial(argType, jsonArg)
}
jsonType := reflect.TypeOf(jsonArg)
switch argType.Kind() {
case reflect.String:
if jsonType.Kind() == reflect.String {
return jsonArg, nil
}
return nil, fmt.Errorf("cannot convert %T to %s", jsonArg, argType)
case reflect.Bool:
if jsonType.Kind() == reflect.Bool {
return jsonArg, nil
}
return nil, fmt.Errorf("cannot convert %T to %s", jsonArg, argType)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Float32, reflect.Float64:
if jsonType.Kind() == reflect.Float64 {
return convertNumber(argType, jsonArg.(float64))
}
return nil, fmt.Errorf("cannot convert %T to %s", jsonArg, argType)
case reflect.Map:
if argType.Key().Kind() != reflect.String {
return nil, fmt.Errorf("invalid map key type %s", argType.Key())
}
if jsonType.Kind() != reflect.Map {
return nil, fmt.Errorf("cannot convert %T to %s", jsonArg, argType)
}
return convertComplex(argType, jsonArg)
case reflect.Slice:
if jsonType.Kind() != reflect.Slice {
return nil, fmt.Errorf("cannot convert %T to %s", jsonArg, argType)
}
return convertComplex(argType, jsonArg)
case reflect.Struct:
if jsonType.Kind() != reflect.Map {
return nil, fmt.Errorf("cannot convert %T to %s", jsonArg, argType)
}
return convertComplex(argType, jsonArg)
case reflect.Ptr:
if argType.Elem().Kind() != reflect.Struct {
return nil, fmt.Errorf("invalid pointer type %s", argType)
}
if jsonType.Kind() != reflect.Map {
return nil, fmt.Errorf("cannot convert %T to %s", jsonArg, argType)
}
return convertComplex(argType, jsonArg)
default:
return nil, fmt.Errorf("invalid argument type %s", argType)
}
}
func isNilable(val reflect.Value) bool {
switch val.Kind() {
case reflect.Ptr, reflect.Slice, reflect.Map, reflect.Interface, reflect.Chan, reflect.Func:
return true
}
return false
}
func convertReturnValues(rtnVals []reflect.Value) *WebReturnType {
rtn := &WebReturnType{}
if len(rtnVals) == 0 {
return rtn
}
for _, val := range rtnVals {
if isNilable(val) && val.IsNil() {
continue
}
valType := val.Type()
if valType == errorRType {
rtn.Error = val.Interface().(error).Error()
continue
}
if valType == updatesRType {
// has a special MarshalJSON method
rtn.Updates = val.Interface().([]wstore.WaveObjUpdate)
continue
}
if isSpecialWaveArgType(valType) {
jsonVal, err := convertSpecialForReturn(valType, val.Interface())
if err != nil {
rtn.Error = fmt.Errorf("cannot convert special return value: %v", err).Error()
continue
}
rtn.Data = jsonVal
continue
}
rtn.Data = val.Interface()
}
if rtn.Error == "" {
rtn.Success = true
}
return rtn
}
func webErrorRtn(err error) *WebReturnType {
return &WebReturnType{
Error: err.Error(),
}
}
func CallService(ctx context.Context, webCall WebCallType) *WebReturnType {
svcObj := ServiceMap[webCall.Service]
if svcObj == nil {
return webErrorRtn(fmt.Errorf("invalid service: %q", webCall.Service))
}
method := reflect.ValueOf(svcObj).MethodByName(webCall.Method)
if !method.IsValid() {
return webErrorRtn(fmt.Errorf("invalid method: %s.%s", webCall.Service, webCall.Method))
}
var valueArgs []reflect.Value
argIdx := 0
for idx := 0; idx < method.Type().NumIn(); idx++ {
argType := method.Type().In(idx)
if idx == 0 && argType == contextRType {
valueArgs = append(valueArgs, reflect.ValueOf(ctx))
continue
}
if argType == uiContextRType {
if webCall.UIContext == nil {
return webErrorRtn(fmt.Errorf("missing UIContext for %s.%s", webCall.Service, webCall.Method))
}
valueArgs = append(valueArgs, reflect.ValueOf(*webCall.UIContext))
continue
}
if argIdx >= len(webCall.Args) {
return webErrorRtn(fmt.Errorf("not enough arguments passed %s.%s idx:%d (type %T)", webCall.Service, webCall.Method, idx, argType))
}
nativeArg, err := convertArgument(argType, webCall.Args[argIdx])
if err != nil {
return webErrorRtn(fmt.Errorf("cannot convert argument %s.%s type:%T idx:%d error:%v", webCall.Service, webCall.Method, argType, idx, err))
}
valueArgs = append(valueArgs, reflect.ValueOf(nativeArg))
argIdx++
}
retValArr := method.Call(valueArgs)
return convertReturnValues(retValArr)
}
// ValidateServiceArg validates the argument type for a service method
// does not allow interfaces (and the obvious invalid types)
// arguments + return values have special handling for wave objects
func baseValidateServiceArg(argType reflect.Type) error {
if argType == waveObjUpdateRType {
// has special MarshalJSON method, so it is safe
return nil
}
switch argType.Kind() {
case reflect.Ptr, reflect.Slice, reflect.Array:
return baseValidateServiceArg(argType.Elem())
case reflect.Map:
if argType.Key().Kind() != reflect.String {
return fmt.Errorf("invalid map key type %s", argType.Key())
}
return baseValidateServiceArg(argType.Elem())
case reflect.Struct:
for idx := 0; idx < argType.NumField(); idx++ {
if err := baseValidateServiceArg(argType.Field(idx).Type); err != nil {
return err
}
}
case reflect.Interface:
return fmt.Errorf("invalid argument type %s: contains interface", argType)
case reflect.Chan, reflect.Func, reflect.Complex128, reflect.Complex64, reflect.Invalid, reflect.Uintptr, reflect.UnsafePointer:
return fmt.Errorf("invalid argument type %s", argType)
}
return nil
}
func validateMethodReturnArg(retType reflect.Type) error {
// specifically allow waveobj.WaveObj, []waveobj.WaveObj, map[string]waveobj.WaveObj, and error
if isSpecialWaveArgType(retType) || retType == errorRType {
return nil
}
return baseValidateServiceArg(retType)
}
func validateMethodArg(argType reflect.Type) error {
// specifically allow waveobj.WaveObj, []waveobj.WaveObj, map[string]waveobj.WaveObj, and context.Context
if isSpecialWaveArgType(argType) || argType == contextRType {
return nil
}
return baseValidateServiceArg(argType)
}
func validateServiceMethod(service string, method reflect.Method) error {
for idx := 0; idx < method.Type.NumOut(); idx++ {
if err := validateMethodReturnArg(method.Type.Out(idx)); err != nil {
return fmt.Errorf("invalid return type %s.%s %s: %v", service, method.Name, method.Type.Out(idx), err)
}
}
for idx := 1; idx < method.Type.NumIn(); idx++ {
// skip the first argument which is the receiver
if err := validateMethodArg(method.Type.In(idx)); err != nil {
return fmt.Errorf("invalid argument type %s.%s %s: %v", service, method.Name, method.Type.In(idx), err)
}
}
return nil
}
func validateServiceMetaMethod(service string, method reflect.Method) error {
if method.Type.NumIn() != 1 {
return fmt.Errorf("invalid number of arguments %s.%s: got:%d, expected just the receiver", service, method.Name, method.Type.NumIn())
}
if method.Type.NumOut() != 1 && method.Type.Out(0) != methodMetaRType {
return fmt.Errorf("invalid return type %s.%s: got:%s, expected servicemeta.MethodMeta", service, method.Name, method.Type.Out(0))
}
return nil
}
func ValidateService(serviceName string, svcObj any) error {
svcType := reflect.TypeOf(svcObj)
if svcType.Kind() != reflect.Ptr {
return fmt.Errorf("service object %q must be a pointer", serviceName)
}
svcType = svcType.Elem()
if svcType.Kind() != reflect.Struct {
return fmt.Errorf("service object %q must be a ptr to struct", serviceName)
}
for idx := 0; idx < svcType.NumMethod(); idx++ {
method := svcType.Method(idx)
if strings.HasSuffix(method.Name, "_Meta") {
err := validateServiceMetaMethod(serviceName, method)
if err != nil {
return err
}
}
if err := validateServiceMethod(serviceName, method); err != nil {
return err
}
}
return nil
}
func ValidateServiceMap() error {
for svcName, svcObj := range ServiceMap {
if err := ValidateService(svcName, svcObj); err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,10 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package servicemeta
type MethodMeta struct {
Desc string
ArgNames []string
ReturnDesc string
}

View File

@ -38,6 +38,7 @@ func StartShellProc(termSize TermSize) (*ShellProc, error) {
shellPath := shellutil.DetectLocalShellPath() shellPath := shellutil.DetectLocalShellPath()
ecmd := exec.Command(shellPath, "-i", "-l") ecmd := exec.Command(shellPath, "-i", "-l")
ecmd.Env = os.Environ() ecmd.Env = os.Environ()
ecmd.Dir = wavebase.GetHomeDir()
envToAdd := shellutil.WaveshellEnvVars(shellutil.DefaultTermType) envToAdd := shellutil.WaveshellEnvVars(shellutil.DefaultTermType)
if os.Getenv("LANG") == "" { if os.Getenv("LANG") == "" {
envToAdd["LANG"] = wavebase.DetermineLang() envToAdd["LANG"] = wavebase.DetermineLang()

354
pkg/tsgen/tsgen.go Normal file
View File

@ -0,0 +1,354 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package tsgen
import (
"bytes"
"context"
"fmt"
"reflect"
"strings"
"github.com/wavetermdev/thenextwave/pkg/eventbus"
"github.com/wavetermdev/thenextwave/pkg/service"
"github.com/wavetermdev/thenextwave/pkg/service/servicemeta"
"github.com/wavetermdev/thenextwave/pkg/waveobj"
"github.com/wavetermdev/thenextwave/pkg/wstore"
)
var contextRType = reflect.TypeOf((*context.Context)(nil)).Elem()
var errorRType = reflect.TypeOf((*error)(nil)).Elem()
var anyRType = reflect.TypeOf((*interface{})(nil)).Elem()
var metaRType = reflect.TypeOf((*map[string]any)(nil)).Elem()
var uiContextRType = reflect.TypeOf((*wstore.UIContext)(nil)).Elem()
var waveObjRType = reflect.TypeOf((*waveobj.WaveObj)(nil)).Elem()
var updatesRtnRType = reflect.TypeOf(wstore.UpdatesRtnType{})
func generateTSMethodTypes(method reflect.Method, tsTypesMap map[reflect.Type]string) error {
for idx := 1; idx < method.Type.NumIn(); idx++ {
// skip receiver
inType := method.Type.In(idx)
GenerateTSType(inType, tsTypesMap)
}
for idx := 0; idx < method.Type.NumOut(); idx++ {
outType := method.Type.Out(idx)
GenerateTSType(outType, tsTypesMap)
}
return nil
}
func getTSFieldName(field reflect.StructField) string {
jsonTag := field.Tag.Get("json")
if jsonTag != "" {
parts := strings.Split(jsonTag, ",")
namePart := parts[0]
if namePart != "" {
if namePart == "-" {
return ""
}
return namePart
}
// if namePart is empty, still uses default
}
return field.Name
}
func isFieldOmitEmpty(field reflect.StructField) bool {
jsonTag := field.Tag.Get("json")
if jsonTag != "" {
parts := strings.Split(jsonTag, ",")
if len(parts) > 1 {
for _, part := range parts[1:] {
if part == "omitempty" {
return true
}
}
}
}
return false
}
func TypeToTSType(t reflect.Type) (string, []reflect.Type) {
switch t.Kind() {
case reflect.String:
return "string", nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Float32, reflect.Float64:
return "number", nil
case reflect.Bool:
return "boolean", nil
case reflect.Slice, reflect.Array:
elemType, subTypes := TypeToTSType(t.Elem())
if elemType == "" {
return "", nil
}
return fmt.Sprintf("%s[]", elemType), subTypes
case reflect.Map:
if t.Key().Kind() != reflect.String {
return "", nil
}
if t == metaRType {
return "MetaType", nil
}
elemType, subTypes := TypeToTSType(t.Elem())
if elemType == "" {
return "", nil
}
return fmt.Sprintf("{[key: string]: %s}", elemType), subTypes
case reflect.Struct:
return t.Name(), []reflect.Type{t}
case reflect.Ptr:
return TypeToTSType(t.Elem())
case reflect.Interface:
if t == waveObjRType {
return "WaveObj", nil
}
return "any", nil
default:
return "", nil
}
}
var tsRenameMap = map[string]string{
"Window": "WaveWindow",
}
func generateTSTypeInternal(rtype reflect.Type) (string, []reflect.Type) {
var buf bytes.Buffer
waveObjType := reflect.TypeOf((*waveobj.WaveObj)(nil)).Elem()
tsTypeName := rtype.Name()
if tsRename, ok := tsRenameMap[tsTypeName]; ok {
tsTypeName = tsRename
}
var isWaveObj bool
buf.WriteString(fmt.Sprintf("// %s\n", rtype.String()))
if rtype.Implements(waveObjType) || reflect.PointerTo(rtype).Implements(waveObjType) {
isWaveObj = true
buf.WriteString(fmt.Sprintf("type %s = WaveObj & {\n", tsTypeName))
} else {
buf.WriteString(fmt.Sprintf("type %s = {\n", tsTypeName))
}
var subTypes []reflect.Type
for i := 0; i < rtype.NumField(); i++ {
field := rtype.Field(i)
if field.PkgPath != "" {
continue
}
fieldName := getTSFieldName(field)
if fieldName == "" {
continue
}
if isWaveObj && (fieldName == waveobj.OTypeKeyName || fieldName == waveobj.OIDKeyName || fieldName == waveobj.VersionKeyName) {
continue
}
optMarker := ""
if isFieldOmitEmpty(field) {
optMarker = "?"
}
tsTypeTag := field.Tag.Get("tstype")
if tsTypeTag != "" {
buf.WriteString(fmt.Sprintf(" %s%s: %s;\n", fieldName, optMarker, tsTypeTag))
continue
}
tsType, fieldSubTypes := TypeToTSType(field.Type)
if tsType == "" {
continue
}
subTypes = append(subTypes, fieldSubTypes...)
if tsType == "UIContext" {
optMarker = "?"
}
buf.WriteString(fmt.Sprintf(" %s%s: %s;\n", fieldName, optMarker, tsType))
}
buf.WriteString("};\n")
return buf.String(), subTypes
}
func GenerateWaveObjTSType() string {
var buf bytes.Buffer
buf.WriteString("// waveobj.WaveObj\n")
buf.WriteString("type WaveObj = {\n")
buf.WriteString(" otype: string;\n")
buf.WriteString(" oid: string;\n")
buf.WriteString(" version: number;\n")
buf.WriteString("};\n")
return buf.String()
}
func GenerateMetaType() string {
return "type MetaType = {[key: string]: any}\n"
}
func GenerateTSType(rtype reflect.Type, tsTypesMap map[reflect.Type]string) {
if rtype == nil {
return
}
if rtype == metaRType {
tsTypesMap[metaRType] = GenerateMetaType()
return
}
if rtype == contextRType || rtype == errorRType || rtype == anyRType {
return
}
if rtype.Kind() == reflect.Slice {
rtype = rtype.Elem()
}
if rtype.Kind() == reflect.Map {
rtype = rtype.Elem()
}
if rtype.Kind() == reflect.Ptr {
rtype = rtype.Elem()
}
if _, ok := tsTypesMap[rtype]; ok {
return
}
if rtype == waveObjRType {
tsTypesMap[rtype] = GenerateWaveObjTSType()
return
}
if rtype.Kind() != reflect.Struct {
return
}
tsType, subTypes := generateTSTypeInternal(rtype)
tsTypesMap[rtype] = tsType
for _, subType := range subTypes {
GenerateTSType(subType, tsTypesMap)
}
}
func hasUpdatesReturn(method reflect.Method) bool {
for idx := 0; idx < method.Type.NumOut(); idx++ {
outType := method.Type.Out(idx)
if outType == updatesRtnRType {
return true
}
}
return false
}
func GenerateMethodSignature(serviceName string, method reflect.Method, meta servicemeta.MethodMeta, isFirst bool) string {
var sb strings.Builder
mayReturnUpdates := hasUpdatesReturn(method)
if (meta.Desc != "" || meta.ReturnDesc != "" || mayReturnUpdates) && !isFirst {
sb.WriteString("\n")
}
if meta.Desc != "" {
sb.WriteString(fmt.Sprintf(" // %s\n", meta.Desc))
}
if mayReturnUpdates || meta.ReturnDesc != "" {
if mayReturnUpdates && meta.ReturnDesc != "" {
sb.WriteString(fmt.Sprintf(" // @returns %s (and object updates)\n", meta.ReturnDesc))
} else if mayReturnUpdates {
sb.WriteString(" // @returns object updates\n")
} else {
sb.WriteString(fmt.Sprintf(" // @returns %s\n", meta.ReturnDesc))
}
}
sb.WriteString(" ")
sb.WriteString(method.Name)
sb.WriteString("(")
wroteArg := false
// skip first arg, which is the receiver
for idx := 1; idx < method.Type.NumIn(); idx++ {
if wroteArg {
sb.WriteString(", ")
}
inType := method.Type.In(idx)
if inType == contextRType || inType == uiContextRType {
continue
}
tsTypeName, _ := TypeToTSType(inType)
var argName string
if idx-1 < len(meta.ArgNames) {
argName = meta.ArgNames[idx-1] // subtract 1 for receiver
} else {
argName = fmt.Sprintf("arg%d", idx)
}
sb.WriteString(fmt.Sprintf("%s: %s", argName, tsTypeName))
wroteArg = true
}
sb.WriteString("): ")
wroteRtn := false
for idx := 0; idx < method.Type.NumOut(); idx++ {
outType := method.Type.Out(idx)
if outType == errorRType {
continue
}
if outType == updatesRtnRType {
continue
}
tsTypeName, _ := TypeToTSType(outType)
sb.WriteString(fmt.Sprintf("Promise<%s>", tsTypeName))
wroteRtn = true
}
if !wroteRtn {
sb.WriteString("Promise<void>")
}
sb.WriteString(" {\n")
return sb.String()
}
func GenerateMethodBody(serviceName string, method reflect.Method, meta servicemeta.MethodMeta) string {
return fmt.Sprintf(" return WOS.callBackendService(%q, %q, Array.from(arguments))\n", serviceName, method.Name)
}
func GenerateServiceClass(serviceName string, serviceObj any) string {
serviceType := reflect.TypeOf(serviceObj)
var sb strings.Builder
tsServiceName := serviceType.Elem().Name()
sb.WriteString(fmt.Sprintf("// %s (%s)\n", serviceType.Elem().String(), serviceName))
sb.WriteString("class ")
sb.WriteString(tsServiceName + "Type")
sb.WriteString(" {\n")
isFirst := true
for midx := 0; midx < serviceType.NumMethod(); midx++ {
method := serviceType.Method(midx)
if strings.HasSuffix(method.Name, "_Meta") {
continue
}
var meta servicemeta.MethodMeta
metaMethod, found := serviceType.MethodByName(method.Name + "_Meta")
if found {
serviceObjVal := reflect.ValueOf(serviceObj)
metaVal := metaMethod.Func.Call([]reflect.Value{serviceObjVal})
meta = metaVal[0].Interface().(servicemeta.MethodMeta)
}
sb.WriteString(GenerateMethodSignature(serviceName, method, meta, isFirst))
sb.WriteString(GenerateMethodBody(serviceName, method, meta))
sb.WriteString(" }\n")
isFirst = false
}
sb.WriteString("}\n\n")
sb.WriteString(fmt.Sprintf("export const %s = new %sType()\n", tsServiceName, tsServiceName))
return sb.String()
}
func GenerateWaveObjTypes(tsTypesMap map[reflect.Type]string) {
GenerateTSType(reflect.TypeOf(waveobj.ORef{}), tsTypesMap)
GenerateTSType(reflect.TypeOf((*waveobj.WaveObj)(nil)).Elem(), tsTypesMap)
GenerateTSType(reflect.TypeOf(map[string]any{}), tsTypesMap)
GenerateTSType(reflect.TypeOf(service.WebCallType{}), tsTypesMap)
GenerateTSType(reflect.TypeOf(service.WebReturnType{}), tsTypesMap)
GenerateTSType(reflect.TypeOf(wstore.UIContext{}), tsTypesMap)
GenerateTSType(reflect.TypeOf(eventbus.WSEventType{}), tsTypesMap)
for _, rtype := range wstore.AllWaveObjTypes() {
GenerateTSType(rtype, tsTypesMap)
}
}
func GenerateServiceTypes(tsTypesMap map[reflect.Type]string) error {
for _, serviceObj := range service.ServiceMap {
serviceType := reflect.TypeOf(serviceObj)
for midx := 0; midx < serviceType.NumMethod(); midx++ {
method := serviceType.Method(midx)
err := generateTSMethodTypes(method, tsTypesMap)
if err != nil {
return fmt.Errorf("error generating TS method types for %s.%s: %v", serviceType, method.Name, err)
}
}
}
return nil
}

View File

@ -710,3 +710,29 @@ func StructToJsonMap(v interface{}) (map[string]any, error) {
} }
return m, nil return m, nil
} }
func IndentString(indent string, str string) string {
splitArr := strings.Split(str, "\n")
var rtn strings.Builder
for _, line := range splitArr {
if line == "" {
rtn.WriteByte('\n')
continue
}
rtn.WriteString(indent)
rtn.WriteString(line)
rtn.WriteByte('\n')
}
return rtn.String()
}
func PrintIndentedStr(indent string, str string) {
splitArr := strings.Split(str, "\n")
for _, line := range splitArr {
if line == "" {
fmt.Printf("\n")
continue
}
fmt.Printf("%s%s\n", indent, line)
}
}

View File

@ -32,6 +32,13 @@ func (oref ORef) String() string {
return fmt.Sprintf("%s:%s", oref.OType, oref.OID) return fmt.Sprintf("%s:%s", oref.OType, oref.OID)
} }
func MakeORef(otype string, oid string) ORef {
return ORef{
OType: otype,
OID: oid,
}
}
type WaveObj interface { type WaveObj interface {
GetOType() string // should not depend on object state (should work with nil value) GetOType() string // should not depend on object state (should work with nil value)
} }
@ -155,6 +162,18 @@ func SetMeta(waveObj WaveObj, meta map[string]any) {
reflect.ValueOf(waveObj).Elem().FieldByIndex(desc.MetaField.Index).Set(reflect.ValueOf(meta)) reflect.ValueOf(waveObj).Elem().FieldByIndex(desc.MetaField.Index).Set(reflect.ValueOf(meta))
} }
func DoMapStucture(out any, input any) error {
dconfig := &mapstructure.DecoderConfig{
Result: out,
TagName: "json",
}
decoder, err := mapstructure.NewDecoder(dconfig)
if err != nil {
return err
}
return decoder.Decode(input)
}
func ToJsonMap(w WaveObj) (map[string]any, error) { func ToJsonMap(w WaveObj) (map[string]any, error) {
if w == nil { if w == nil {
return nil, nil return nil, nil
@ -227,7 +246,13 @@ func ORefFromMap(m map[string]any) (*ORef, error) {
return nil, err return nil, err
} }
return &oref, nil return &oref, nil
}
func ORefFromWaveObj(w WaveObj) *ORef {
return &ORef{
OType: w.GetOType(),
OID: GetOID(w),
}
} }
func FromJsonGen[T WaveObj](data []byte) (T, error) { func FromJsonGen[T WaveObj](data []byte) (T, error) {

View File

@ -1,160 +0,0 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package waveobj
import (
"bytes"
"fmt"
"reflect"
"strings"
)
func getTSFieldName(field reflect.StructField) string {
jsonTag := field.Tag.Get("json")
if jsonTag != "" {
parts := strings.Split(jsonTag, ",")
namePart := parts[0]
if namePart != "" {
if namePart == "-" {
return ""
}
return namePart
}
// if namePart is empty, still uses default
}
return field.Name
}
func isFieldOmitEmpty(field reflect.StructField) bool {
jsonTag := field.Tag.Get("json")
if jsonTag != "" {
parts := strings.Split(jsonTag, ",")
if len(parts) > 1 {
for _, part := range parts[1:] {
if part == "omitempty" {
return true
}
}
}
}
return false
}
func typeToTSType(t reflect.Type) (string, []reflect.Type) {
switch t.Kind() {
case reflect.String:
return "string", nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Float32, reflect.Float64:
return "number", nil
case reflect.Bool:
return "boolean", nil
case reflect.Slice, reflect.Array:
elemType, subTypes := typeToTSType(t.Elem())
if elemType == "" {
return "", nil
}
return fmt.Sprintf("%s[]", elemType), subTypes
case reflect.Map:
if t.Key().Kind() != reflect.String {
return "", nil
}
elemType, subTypes := typeToTSType(t.Elem())
if elemType == "" {
return "", nil
}
return fmt.Sprintf("{[key: string]: %s}", elemType), subTypes
case reflect.Struct:
return t.Name(), []reflect.Type{t}
case reflect.Ptr:
return typeToTSType(t.Elem())
case reflect.Interface:
return "any", nil
default:
return "", nil
}
}
var tsRenameMap = map[string]string{
"Window": "WaveWindow",
}
func generateTSTypeInternal(rtype reflect.Type) (string, []reflect.Type) {
var buf bytes.Buffer
waveObjType := reflect.TypeOf((*WaveObj)(nil)).Elem()
tsTypeName := rtype.Name()
if tsRename, ok := tsRenameMap[tsTypeName]; ok {
tsTypeName = tsRename
}
var isWaveObj bool
if rtype.Implements(waveObjType) || reflect.PointerTo(rtype).Implements(waveObjType) {
isWaveObj = true
buf.WriteString(fmt.Sprintf("type %s = WaveObj & {\n", tsTypeName))
} else {
buf.WriteString(fmt.Sprintf("type %s = {\n", tsTypeName))
}
var subTypes []reflect.Type
for i := 0; i < rtype.NumField(); i++ {
field := rtype.Field(i)
if field.PkgPath != "" {
continue
}
fieldName := getTSFieldName(field)
if fieldName == "" {
continue
}
if isWaveObj && (fieldName == OTypeKeyName || fieldName == OIDKeyName || fieldName == VersionKeyName) {
continue
}
optMarker := ""
if isFieldOmitEmpty(field) {
optMarker = "?"
}
tsTypeTag := field.Tag.Get("tstype")
if tsTypeTag != "" {
buf.WriteString(fmt.Sprintf(" %s%s: %s;\n", fieldName, optMarker, tsTypeTag))
continue
}
tsType, fieldSubTypes := typeToTSType(field.Type)
if tsType == "" {
continue
}
subTypes = append(subTypes, fieldSubTypes...)
buf.WriteString(fmt.Sprintf(" %s%s: %s;\n", fieldName, optMarker, tsType))
}
buf.WriteString("};\n")
return buf.String(), subTypes
}
func GenerateWaveObjTSType() string {
var buf bytes.Buffer
buf.WriteString("type WaveObj = {\n")
buf.WriteString(" otype: string;\n")
buf.WriteString(" oid: string;\n")
buf.WriteString(" version: number;\n")
buf.WriteString("};\n")
return buf.String()
}
func GenerateTSType(rtype reflect.Type, tsTypesMap map[reflect.Type]string) {
if rtype == nil {
return
}
if rtype.Kind() == reflect.Ptr {
rtype = rtype.Elem()
}
if _, ok := tsTypesMap[rtype]; ok {
return
}
if rtype == waveObjRType {
tsTypesMap[rtype] = GenerateWaveObjTSType()
return
}
tsType, subTypes := generateTSTypeInternal(rtype)
tsTypesMap[rtype] = tsType
for _, subType := range subTypes {
GenerateTSType(subType, tsTypesMap)
}
}

View File

@ -1,30 +0,0 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package waveobj
import (
"log"
"reflect"
"testing"
)
type TestBlock struct {
BlockId string `json:"blockid" waveobj:"oid"`
Name string `json:"name"`
}
func (TestBlock) GetOType() string {
return "block"
}
func TestGenerate(t *testing.T) {
log.Printf("Testing Generate\n")
tsMap := make(map[reflect.Type]string)
var waveObj WaveObj
GenerateTSType(reflect.TypeOf(&waveObj).Elem(), tsMap)
GenerateTSType(reflect.TypeOf(TestBlock{}), tsMap)
for k, v := range tsMap {
log.Printf("Type: %v, TS:\n%s\n", k, v)
}
}

217
pkg/web/web.go Normal file
View File

@ -0,0 +1,217 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package web
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/fs"
"log"
"net/http"
"runtime/debug"
"time"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/wavetermdev/thenextwave/pkg/filestore"
"github.com/wavetermdev/thenextwave/pkg/service"
"github.com/wavetermdev/thenextwave/pkg/wavebase"
)
type WebFnType = func(http.ResponseWriter, *http.Request)
// Header constants
const (
CacheControlHeaderKey = "Cache-Control"
CacheControlHeaderNoCache = "no-cache"
ContentTypeHeaderKey = "Content-Type"
ContentTypeJson = "application/json"
ContentTypeBinary = "application/octet-stream"
ContentLengthHeaderKey = "Content-Length"
LastModifiedHeaderKey = "Last-Modified"
WaveZoneFileInfoHeaderKey = "X-ZoneFileInfo"
)
const HttpReadTimeout = 5 * time.Second
const HttpWriteTimeout = 21 * time.Second
const HttpMaxHeaderBytes = 60000
const HttpTimeoutDuration = 21 * time.Second
const MainServerAddr = "127.0.0.1:1719" // wavesrv, P=16+1, S=19, PS=1719
const WebSocketServerAddr = "127.0.0.1:1723" // wavesrv:websocket, P=16+1, W=23, PW=1723
const MainServerDevAddr = "127.0.0.1:8190"
const WebSocketServerDevAddr = "127.0.0.1:8191"
const WSStateReconnectTime = 30 * time.Second
const WSStatePacketChSize = 20
type WebFnOpts struct {
AllowCaching bool
JsonErrors bool
}
func handleService(w http.ResponseWriter, r *http.Request) {
bodyData, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Unable to read request body", http.StatusBadRequest)
return
}
defer r.Body.Close()
if r.Method != http.MethodPost {
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
return
}
var webCall service.WebCallType
err = json.Unmarshal(bodyData, &webCall)
if err != nil {
http.Error(w, fmt.Sprintf("invalid request body: %v", err), http.StatusBadRequest)
}
rtn := service.CallService(r.Context(), webCall)
jsonRtn, err := json.Marshal(rtn)
if err != nil {
http.Error(w, fmt.Sprintf("error serializing response: %v", err), http.StatusInternalServerError)
}
w.Header().Set(ContentTypeHeaderKey, ContentTypeJson)
w.Header().Set(ContentLengthHeaderKey, fmt.Sprintf("%d", len(jsonRtn)))
w.WriteHeader(http.StatusOK)
w.Write(jsonRtn)
}
func marshalReturnValue(data any, err error) []byte {
var mapRtn = make(map[string]any)
if err != nil {
mapRtn["error"] = err.Error()
} else {
mapRtn["success"] = true
mapRtn["data"] = data
}
rtn, err := json.Marshal(mapRtn)
if err != nil {
return marshalReturnValue(nil, fmt.Errorf("error serializing response: %v", err))
}
return rtn
}
func handleWaveFile(w http.ResponseWriter, r *http.Request) {
zoneId := r.URL.Query().Get("zoneid")
name := r.URL.Query().Get("name")
if _, err := uuid.Parse(zoneId); err != nil {
http.Error(w, fmt.Sprintf("invalid zoneid: %v", err), http.StatusBadRequest)
return
}
if name == "" {
http.Error(w, "name is required", http.StatusBadRequest)
return
}
file, err := filestore.WFS.Stat(r.Context(), zoneId, name)
if err == fs.ErrNotExist {
http.NotFound(w, r)
return
}
if err != nil {
http.Error(w, fmt.Sprintf("error getting file info: %v", err), http.StatusInternalServerError)
return
}
jsonFileBArr, err := json.Marshal(file)
if err != nil {
http.Error(w, fmt.Sprintf("error serializing file info: %v", err), http.StatusInternalServerError)
}
// can make more efficient by checking modtime + If-Modified-Since headers to allow caching
w.Header().Set(ContentTypeHeaderKey, ContentTypeBinary)
w.Header().Set(ContentLengthHeaderKey, fmt.Sprintf("%d", file.Size))
w.Header().Set(WaveZoneFileInfoHeaderKey, base64.StdEncoding.EncodeToString(jsonFileBArr))
w.Header().Set(LastModifiedHeaderKey, time.UnixMilli(file.ModTs).UTC().Format(http.TimeFormat))
if file.Size == 0 {
w.WriteHeader(http.StatusOK)
return
}
for offset := file.DataStartIdx(); offset < file.Size; offset += filestore.DefaultPartDataSize {
_, data, err := filestore.WFS.ReadAt(r.Context(), zoneId, name, offset, filestore.DefaultPartDataSize)
if err != nil {
if offset == 0 {
http.Error(w, fmt.Sprintf("error reading file: %v", err), http.StatusInternalServerError)
} else {
// nothing to do, the headers have already been sent
log.Printf("error reading file %s/%s @ %d: %v\n", zoneId, name, offset, err)
}
return
}
w.Write(data)
}
}
func handleStreamFile(w http.ResponseWriter, r *http.Request) {
fileName := r.URL.Query().Get("path")
fileName = wavebase.ExpandHomeDir(fileName)
http.ServeFile(w, r, fileName)
}
func WebFnWrap(opts WebFnOpts, fn WebFnType) WebFnType {
return func(w http.ResponseWriter, r *http.Request) {
defer func() {
recErr := recover()
if recErr == nil {
return
}
panicStr := fmt.Sprintf("panic: %v", recErr)
log.Printf("panic: %v\n", recErr)
debug.PrintStack()
if opts.JsonErrors {
jsonRtn := marshalReturnValue(nil, fmt.Errorf(panicStr))
w.Header().Set(ContentTypeHeaderKey, ContentTypeJson)
w.Header().Set(ContentLengthHeaderKey, fmt.Sprintf("%d", len(jsonRtn)))
w.WriteHeader(http.StatusOK)
w.Write(jsonRtn)
} else {
http.Error(w, panicStr, http.StatusInternalServerError)
}
}()
if !opts.AllowCaching {
w.Header().Set(CacheControlHeaderKey, CacheControlHeaderNoCache)
}
// reqAuthKey := r.Header.Get("X-AuthKey")
// if reqAuthKey == "" {
// w.WriteHeader(http.StatusInternalServerError)
// w.Write([]byte("no x-authkey header"))
// return
// }
// if reqAuthKey != scbase.WaveAuthKey {
// w.WriteHeader(http.StatusInternalServerError)
// w.Write([]byte("x-authkey header is invalid"))
// return
// }
fn(w, r)
}
}
// blocking
// TODO: create listener separately and use http.Serve, so we can signal SIGUSR1 in a better way
func RunWebServer() {
gr := mux.NewRouter()
gr.HandleFunc("/wave/stream-file", WebFnWrap(WebFnOpts{AllowCaching: true}, handleStreamFile))
gr.HandleFunc("/wave/file", WebFnWrap(WebFnOpts{AllowCaching: false}, handleWaveFile))
gr.HandleFunc("/wave/service", WebFnWrap(WebFnOpts{JsonErrors: true}, handleService))
serverAddr := MainServerAddr
if wavebase.IsDevMode() {
serverAddr = MainServerDevAddr
}
server := &http.Server{
Addr: serverAddr,
ReadTimeout: HttpReadTimeout,
WriteTimeout: HttpWriteTimeout,
MaxHeaderBytes: HttpMaxHeaderBytes,
Handler: http.TimeoutHandler(gr, HttpTimeoutDuration, "Timeout"),
}
log.Printf("Running main server on %s\n", serverAddr)
err := server.ListenAndServe()
if err != nil {
log.Printf("ERROR: %v\n", err)
}
}

215
pkg/web/ws.go Normal file
View File

@ -0,0 +1,215 @@
// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package web
import (
"encoding/json"
"fmt"
"log"
"net/http"
"runtime/debug"
"sync"
"time"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"github.com/wavetermdev/thenextwave/pkg/eventbus"
)
const wsReadWaitTimeout = 15 * time.Second
const wsWriteWaitTimeout = 10 * time.Second
const wsPingPeriodTickTime = 10 * time.Second
const wsInitialPingTime = 1 * time.Second
func RunWebSocketServer() {
gr := mux.NewRouter()
gr.HandleFunc("/ws", HandleWs)
serverAddr := WebSocketServerDevAddr
server := &http.Server{
Addr: serverAddr,
ReadTimeout: HttpReadTimeout,
WriteTimeout: HttpWriteTimeout,
MaxHeaderBytes: HttpMaxHeaderBytes,
Handler: gr,
}
server.SetKeepAlivesEnabled(false)
log.Printf("Running websocket server on %s\n", serverAddr)
err := server.ListenAndServe()
if err != nil {
log.Printf("[error] trying to run websocket server: %v\n", err)
}
}
var WebSocketUpgrader = websocket.Upgrader{
ReadBufferSize: 4 * 1024,
WriteBufferSize: 32 * 1024,
HandshakeTimeout: 1 * time.Second,
CheckOrigin: func(r *http.Request) bool { return true },
}
func HandleWs(w http.ResponseWriter, r *http.Request) {
err := HandleWsInternal(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func getMessageType(jmsg map[string]any) string {
if str, ok := jmsg["type"].(string); ok {
return str
}
return ""
}
func getStringFromMap(jmsg map[string]any, key string) string {
if str, ok := jmsg[key].(string); ok {
return str
}
return ""
}
func processMessage(jmsg map[string]any, outputCh chan any) {
msgType := getMessageType(jmsg)
if msgType != "rpc" {
return
}
reqId := getStringFromMap(jmsg, "reqid")
var rtnErr error
defer func() {
r := recover()
if r != nil {
rtnErr = fmt.Errorf("panic: %v", r)
log.Printf("panic in processMessage: %v\n", r)
debug.PrintStack()
}
if rtnErr == nil {
return
}
rtn := map[string]any{"type": "rpcresp", "reqid": reqId, "error": rtnErr.Error()}
outputCh <- rtn
}()
method := getStringFromMap(jmsg, "method")
rtnErr = fmt.Errorf("unknown method %q", method)
}
func ReadLoop(conn *websocket.Conn, outputCh chan any, closeCh chan any) {
readWait := wsReadWaitTimeout
conn.SetReadLimit(64 * 1024)
conn.SetReadDeadline(time.Now().Add(readWait))
defer close(closeCh)
for {
_, message, err := conn.ReadMessage()
if err != nil {
log.Printf("ReadPump error: %v\n", err)
break
}
jmsg := map[string]any{}
err = json.Unmarshal(message, &jmsg)
if err != nil {
log.Printf("Error unmarshalling json: %v\n", err)
break
}
conn.SetReadDeadline(time.Now().Add(readWait))
msgType := getMessageType(jmsg)
if msgType == "pong" {
// nothing
continue
}
if msgType == "ping" {
now := time.Now()
pongMessage := map[string]interface{}{"type": "pong", "stime": now.UnixMilli()}
outputCh <- pongMessage
continue
}
go processMessage(jmsg, outputCh)
}
}
func WritePing(conn *websocket.Conn) error {
now := time.Now()
pingMessage := map[string]interface{}{"type": "ping", "stime": now.UnixMilli()}
jsonVal, _ := json.Marshal(pingMessage)
_ = conn.SetWriteDeadline(time.Now().Add(wsWriteWaitTimeout)) // no error
err := conn.WriteMessage(websocket.TextMessage, jsonVal)
if err != nil {
return err
}
return nil
}
func WriteLoop(conn *websocket.Conn, outputCh chan any, closeCh chan any) {
ticker := time.NewTicker(wsInitialPingTime)
defer ticker.Stop()
initialPing := true
for {
select {
case msg := <-outputCh:
var barr []byte
var err error
if _, ok := msg.([]byte); ok {
barr = msg.([]byte)
} else {
barr, err = json.Marshal(msg)
if err != nil {
log.Printf("cannot marshal websocket message: %v\n", err)
// just loop again
break
}
}
err = conn.WriteMessage(websocket.TextMessage, barr)
if err != nil {
conn.Close()
log.Printf("WritePump error: %v\n", err)
return
}
case <-ticker.C:
err := WritePing(conn)
if err != nil {
log.Printf("WritePump error: %v\n", err)
return
}
if initialPing {
initialPing = false
ticker.Reset(wsPingPeriodTickTime)
}
case <-closeCh:
return
}
}
}
func HandleWsInternal(w http.ResponseWriter, r *http.Request) error {
windowId := r.URL.Query().Get("windowid")
if windowId == "" {
return fmt.Errorf("windowid is required")
}
conn, err := WebSocketUpgrader.Upgrade(w, r, nil)
if err != nil {
return fmt.Errorf("WebSocket Upgrade Failed: %v", err)
}
defer conn.Close()
wsConnId := uuid.New().String()
log.Printf("New websocket connection: windowid:%s connid:%s\n", windowId, wsConnId)
outputCh := make(chan any, 100)
closeCh := make(chan any)
eventbus.RegisterWSChannel(wsConnId, windowId, outputCh)
defer eventbus.UnregisterWSChannel(wsConnId)
wg := &sync.WaitGroup{}
wg.Add(2)
go func() {
// read loop
defer wg.Done()
ReadLoop(conn, outputCh, closeCh)
}()
go func() {
// write loop
defer wg.Done()
WriteLoop(conn, outputCh, closeCh)
}()
wg.Wait()
return nil
}

View File

@ -16,6 +16,8 @@ import (
var waveObjUpdateKey = struct{}{} var waveObjUpdateKey = struct{}{}
type UpdatesRtnType = []WaveObjUpdate
func init() { func init() {
for _, rtype := range AllWaveObjTypes() { for _, rtype := range AllWaveObjTypes() {
waveobj.RegisterType(rtype) waveobj.RegisterType(rtype)
@ -67,6 +69,18 @@ func ContextGetUpdates(ctx context.Context) map[waveobj.ORef]WaveObjUpdate {
return rtn return rtn
} }
func ContextGetUpdatesRtn(ctx context.Context) UpdatesRtnType {
updatesMap := ContextGetUpdates(ctx)
if updatesMap == nil {
return nil
}
rtn := make(UpdatesRtnType, 0, len(updatesMap))
for _, v := range updatesMap {
rtn = append(rtn, v)
}
return rtn
}
func ContextGetUpdate(ctx context.Context, oref waveobj.ORef) *WaveObjUpdate { func ContextGetUpdate(ctx context.Context, oref waveobj.ORef) *WaveObjUpdate {
updatesVal := ctx.Value(waveObjUpdateKey) updatesVal := ctx.Value(waveObjUpdateKey)
if updatesVal == nil { if updatesVal == nil {

View File

@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"github.com/wavetermdev/thenextwave/pkg/waveobj" "github.com/wavetermdev/thenextwave/pkg/waveobj"
"github.com/wavetermdev/waveterm/wavesrv/pkg/dbutil"
) )
var ErrNotFound = fmt.Errorf("not found") var ErrNotFound = fmt.Errorf("not found")
@ -121,7 +122,7 @@ func dbSelectOIDs(ctx context.Context, otype string, oids []string) ([]waveobj.W
table := tableNameFromOType(otype) table := tableNameFromOType(otype)
query := fmt.Sprintf("SELECT oid, version, data FROM %s WHERE oid IN (SELECT value FROM json_each(?))", table) query := fmt.Sprintf("SELECT oid, version, data FROM %s WHERE oid IN (SELECT value FROM json_each(?))", table)
var rows []idDataType var rows []idDataType
tx.Select(&rows, query, oids) tx.Select(&rows, query, dbutil.QuickJson(oids))
rtn := make([]waveobj.WaveObj, 0, len(rows)) rtn := make([]waveobj.WaveObj, 0, len(rows))
for _, row := range rows { for _, row := range rows {
waveObj, err := waveobj.FromJson(row.Data) waveObj, err := waveobj.FromJson(row.Data)

View File

@ -5,6 +5,7 @@ package wstore
import ( import (
"encoding/json" "encoding/json"
"fmt"
"reflect" "reflect"
"github.com/wavetermdev/thenextwave/pkg/shellexec" "github.com/wavetermdev/thenextwave/pkg/shellexec"
@ -52,6 +53,65 @@ func (update WaveObjUpdate) MarshalJSON() ([]byte, error) {
return json.Marshal(rtn) return json.Marshal(rtn)
} }
func MakeUpdate(obj waveobj.WaveObj) WaveObjUpdate {
return WaveObjUpdate{
UpdateType: UpdateType_Update,
OType: obj.GetOType(),
OID: waveobj.GetOID(obj),
Obj: obj,
}
}
func MakeUpdates(objs []waveobj.WaveObj) []WaveObjUpdate {
rtn := make([]WaveObjUpdate, 0, len(objs))
for _, obj := range objs {
rtn = append(rtn, MakeUpdate(obj))
}
return rtn
}
func (update *WaveObjUpdate) UnmarshalJSON(data []byte) error {
var objMap map[string]any
err := json.Unmarshal(data, &objMap)
if err != nil {
return err
}
var ok1, ok2, ok3 bool
if _, found := objMap["updatetype"]; !found {
return fmt.Errorf("missing updatetype (in WaveObjUpdate)")
}
update.UpdateType, ok1 = objMap["updatetype"].(string)
if !ok1 {
return fmt.Errorf("in WaveObjUpdate bad updatetype type %T", objMap["updatetype"])
}
if _, found := objMap["otype"]; !found {
return fmt.Errorf("missing otype (in WaveObjUpdate)")
}
update.OType, ok2 = objMap["otype"].(string)
if !ok2 {
return fmt.Errorf("in WaveObjUpdate bad otype type %T", objMap["otype"])
}
if _, found := objMap["oid"]; !found {
return fmt.Errorf("missing oid (in WaveObjUpdate)")
}
update.OID, ok3 = objMap["oid"].(string)
if !ok3 {
return fmt.Errorf("in WaveObjUpdate bad oid type %T", objMap["oid"])
}
if _, found := objMap["obj"]; found {
objMap, ok := objMap["obj"].(map[string]any)
if !ok {
return fmt.Errorf("in WaveObjUpdate bad obj type %T", objMap["obj"])
}
waveObj, err := waveobj.FromJsonMap(objMap)
if err != nil {
return fmt.Errorf("in WaveObjUpdate error decoding obj: %w", err)
}
update.Obj = waveObj
}
return nil
}
type Client struct { type Client struct {
OID string `json:"oid"` OID string `json:"oid"`
Version int `json:"version"` Version int `json:"version"`
@ -106,6 +166,14 @@ func (*Tab) GetOType() string {
return OType_Tab return OType_Tab
} }
func (t *Tab) GetBlockORefs() []waveobj.ORef {
rtn := make([]waveobj.ORef, 0, len(t.BlockIds))
for _, blockId := range t.BlockIds {
rtn = append(rtn, waveobj.ORef{OType: OType_Block, OID: blockId})
}
return rtn
}
type LayoutNode struct { type LayoutNode struct {
OID string `json:"oid"` OID string `json:"oid"`
Version int `json:"version"` Version int `json:"version"`

View File

@ -1,8 +1,9 @@
/** @type {import("prettier").Config} */ /** @type {import("prettier").Config} */
export default { module.exports = {
plugins: ["prettier-plugin-jsdoc", "prettier-plugin-organize-imports"], plugins: ["prettier-plugin-jsdoc", "prettier-plugin-organize-imports"],
printWidth: 120, printWidth: 120,
trailingComma: "es5", trailingComma: "es5",
useTabs: false,
jsdocVerticalAlignment: true, jsdocVerticalAlignment: true,
jsdocSeparateReturnsFromParam: true, jsdocSeparateReturnsFromParam: true,
jsdocSeparateTagGroups: true, jsdocSeparateTagGroups: true,

View File

@ -5,12 +5,13 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Wails App</title> <title>Wails App</title>
<link rel="stylesheet" href="/fontawesome/css/fontawesome.min.css" /> <link rel="stylesheet" href="public/fontawesome/css/fontawesome.min.css" />
<link rel="stylesheet" href="/fontawesome/css/brands.min.css" /> <link rel="stylesheet" href="public/fontawesome/css/brands.min.css" />
<link rel="stylesheet" href="/fontawesome/css/solid.min.css" /> <link rel="stylesheet" href="public/fontawesome/css/solid.min.css" />
<link rel="stylesheet" href="/fontawesome/css/sharp-solid.min.css" /> <link rel="stylesheet" href="public/fontawesome/css/sharp-solid.min.css" />
<link rel="stylesheet" href="/fontawesome/css/sharp-regular.min.css" /> <link rel="stylesheet" href="public/fontawesome/css/sharp-regular.min.css" />
<script type="module" src="/frontend/wave.ts"></script> <script type="module" src="./dist-dev/wave.js"></script>
<link rel="stylesheet" href="./dist-dev/wave.css" />
</head> </head>
<body> <body>
<div id="main"></div> <div id="main"></div>

View File

@ -1,5 +1,5 @@
{ {
"include": ["frontend/**/*"], "include": ["frontend/**/*", "emain/**/*"],
"compilerOptions": { "compilerOptions": {
"target": "es6", "target": "es6",
"module": "commonjs", "module": "commonjs",

5
version.js Normal file
View File

@ -0,0 +1,5 @@
const path = require("path");
const packageJson = require(path.resolve(__dirname, "package.json"));
const VERSION = `${packageJson.version}`;
module.exports = VERSION;

30
webpack.config.js Normal file
View File

@ -0,0 +1,30 @@
const { webDev, webProd } = require("./webpack/webpack.web.js");
const { electronDev, electronProd } = require("./webpack/webpack.electron.js");
module.exports = (env) => {
if (env.prod) {
console.log("using PROD (web+electron) webpack environment");
return [webProd, electronProd];
}
if (env["prod:web"]) {
console.log("using PROD (web) webpack environment");
return webProd;
}
if (env["prod:electron"]) {
console.log("using PROD (electron) webpack environment");
return electronProd;
}
if (env.dev) {
console.log("using DEV (web+electron) webpack environment");
return [webDev, electronDev];
}
if (env["dev:web"]) {
console.log("using DEV (web) webpack environment");
return webDev;
}
if (env["dev:electron"]) {
console.log("using DEV (electron) webpack environment");
return electronDev;
}
console.log("must specify a webpack environment using --env [dev|prod]");
};

114
webpack/webpack.electron.js Normal file
View File

@ -0,0 +1,114 @@
const webpack = require("webpack");
const webpackMerge = require("webpack-merge");
const path = require("path");
const moment = require("dayjs");
const VERSION = require("../version.js");
const CopyPlugin = require("copy-webpack-plugin");
function makeBuildStr() {
let buildStr = moment().format("YYYYMMDD-HHmmss");
// console.log("waveterm:electron " + VERSION + " build " + buildStr);
return buildStr;
}
const BUILD = makeBuildStr();
var electronCommon = {
entry: {
emain: ["./emain/emain.ts"],
},
target: "electron-main",
externals: {
fs: "require('fs')",
"fs-ext": "require('fs-ext')",
},
devtool: "source-map",
module: {
rules: [
{
test: /\.tsx?$/,
// exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: [
[
"@babel/preset-env",
{
targets:
"defaults and not ie > 0 and not op_mini all and not op_mob > 0 and not kaios > 0 and not and_qq > 0 and not and_uc > 0 and not baidu > 0",
},
],
"@babel/preset-react",
"@babel/preset-typescript",
],
plugins: [
["@babel/transform-runtime", { regenerator: true }],
"@babel/plugin-transform-react-jsx",
["@babel/plugin-proposal-decorators", { legacy: true }],
["@babel/plugin-proposal-class-properties", { loose: true }],
["@babel/plugin-proposal-private-methods", { loose: true }],
["@babel/plugin-proposal-private-property-in-object", { loose: true }],
"babel-plugin-jsx-control-statements",
],
},
},
},
],
},
resolve: {
extensions: [".ts", ".tsx", ".js"],
alias: {
"@/app": path.resolve(__dirname, "../src/app/"),
"@/util": path.resolve(__dirname, "../src/util/"),
"@/models": path.resolve(__dirname, "../src/models/"),
"@/common": path.resolve(__dirname, "../src/app/common/"),
"@/elements": path.resolve(__dirname, "../src/app/common/elements/"),
"@/modals": path.resolve(__dirname, "../src/app/common/modals/"),
"@/assets": path.resolve(__dirname, "../src/app/assets/"),
"@/plugins": path.resolve(__dirname, "../src/plugins/"),
"@/autocomplete": path.resolve(__dirname, "../src/autocomplete/"),
},
},
};
var electronDev = webpackMerge.merge(electronCommon, {
mode: "development",
output: {
path: path.resolve(__dirname, "../dist-dev"),
filename: "[name].js",
},
plugins: [
new CopyPlugin({
patterns: [{ from: "emain/preload.js", to: "preload.js" }],
}),
new webpack.DefinePlugin({
__WAVETERM_DEV__: "true",
__WAVETERM_VERSION__: JSON.stringify(VERSION),
__WAVETERM_BUILD__: JSON.stringify("devbuild"),
}),
],
});
var electronProd = webpackMerge.merge(electronCommon, {
mode: "production",
output: {
path: path.resolve(__dirname, "../dist"),
filename: "[name].js",
},
plugins: [
new CopyPlugin({
patterns: [{ from: "src/electron/preload.js", to: "preload.js" }],
}),
new webpack.DefinePlugin({
__WAVETERM_DEV__: "false",
__WAVETERM_VERSION__: JSON.stringify(VERSION),
__WAVETERM_BUILD__: JSON.stringify(BUILD),
}),
],
optimization: {
minimize: true,
},
});
module.exports = { electronDev: electronDev, electronProd: electronProd };

154
webpack/webpack.web.js Normal file
View File

@ -0,0 +1,154 @@
const webpack = require("webpack");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CopyPlugin = require("copy-webpack-plugin");
const webpackMerge = require("webpack-merge");
const path = require("path");
const moment = require("dayjs");
const VERSION = require("../version.js");
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
function makeBuildStr() {
let buildStr = moment().format("YYYYMMDD-HHmmss");
// console.log("waveterm:web " + VERSION + " build " + buildStr);
return buildStr;
}
const BUILD = makeBuildStr();
let BundleAnalyzerPlugin = null;
if (process.env.WEBPACK_ANALYZE) {
BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
}
var webCommon = {
entry: {
wave: ["./frontend/wave.ts"],
},
module: {
rules: [
{
test: /\.tsx?$/,
// exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: [
[
"@babel/preset-env",
{
targets:
"defaults and not ie > 0 and not op_mini all and not op_mob > 0 and not kaios > 0 and not and_qq > 0 and not and_uc > 0 and not baidu > 0",
},
],
"@babel/preset-react",
"@babel/preset-typescript",
],
plugins: [
["@babel/transform-runtime", { regenerator: true }],
"@babel/plugin-transform-react-jsx",
["@babel/plugin-proposal-decorators", { legacy: true }],
["@babel/plugin-proposal-class-properties", { loose: true }],
["@babel/plugin-proposal-private-methods", { loose: true }],
["@babel/plugin-proposal-private-property-in-object", { loose: true }],
"babel-plugin-jsx-control-statements",
],
},
},
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
{
test: /\.less$/,
use: [{ loader: MiniCssExtractPlugin.loader }, "css-loader", "less-loader"],
},
{
test: /\.svg$/,
use: [{ loader: "@svgr/webpack", options: { icon: true, svgo: false } }, "file-loader"],
},
{
test: /\.md$/,
type: "asset/source",
},
{
test: /\.(png|jpe?g|gif)$/i,
type: "asset/resource",
},
],
},
resolve: {
extensions: [".ts", ".tsx", ".js", ".mjs", ".cjs", ".wasm", ".json", ".less", ".css"],
plugins: [
new TsconfigPathsPlugin({
configFile: path.resolve(__dirname, "../tsconfig.json"),
}),
],
},
};
var webDev = webpackMerge.merge(webCommon, {
mode: "development",
output: {
path: path.resolve(__dirname, "../dist-dev"),
filename: "[name].js",
},
devtool: "source-map",
devServer: {
static: {
directory: path.join(__dirname, "../public"),
},
port: 9000,
headers: {
"Cache-Control": "no-store",
},
},
plugins: [
new MiniCssExtractPlugin({ filename: "[name].css", ignoreOrder: true }),
new CopyPlugin({
patterns: [
{
from: "min/vs",
to: "monaco",
context: "node_modules/monaco-editor/",
},
],
}),
new webpack.ProvidePlugin({
React: 'react'
}),
new webpack.DefinePlugin({
__WAVETERM_DEV__: "true",
__WAVETERM_VERSION__: JSON.stringify(VERSION),
__WAVETERM_BUILD__: JSON.stringify("devbuild"),
}),
],
watchOptions: {
aggregateTimeout: 200,
},
});
var webProd = webpackMerge.merge(webCommon, {
mode: "production",
output: {
path: path.resolve(__dirname, "../dist"),
filename: "[name].js",
},
devtool: "source-map",
plugins: [
new MiniCssExtractPlugin({ filename: "[name].css", ignoreOrder: true }),
new webpack.DefinePlugin({
__WAVETERM_DEV__: "false",
__WAVETERM_VERSION__: JSON.stringify(VERSION),
__WAVETERM_BUILD__: JSON.stringify(BUILD),
}),
],
optimization: {
minimize: true,
},
});
if (BundleAnalyzerPlugin != null) {
webProd.plugins.push(new BundleAnalyzerPlugin());
}
module.exports = { webDev: webDev, webProd: webProd };

2970
yarn.lock

File diff suppressed because it is too large Load Diff