mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
port to electron (#33)
This commit is contained in:
parent
9f32a53485
commit
1874d9a252
2
.gitignore
vendored
2
.gitignore
vendored
@ -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
431
Taskfile.old.yml
Normal 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
|
420
Taskfile.yml
420
Taskfile.yml
@ -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
|
|
||||||
|
@ -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 {
|
||||||
tsTypesMap := make(map[reflect.Type]string)
|
fd, err := os.Create("frontend/types/gotypes.d.ts")
|
||||||
var waveObj waveobj.WaveObj
|
if err != nil {
|
||||||
waveobj.GenerateTSType(reflect.TypeOf(waveobj.ORef{}), tsTypesMap)
|
return err
|
||||||
waveobj.GenerateTSType(reflect.TypeOf(&waveObj).Elem(), tsTypesMap)
|
|
||||||
for _, rtype := range wstore.AllWaveObjTypes() {
|
|
||||||
waveobj.GenerateTSType(rtype, tsTypesMap)
|
|
||||||
}
|
}
|
||||||
for _, ts := range tsTypesMap {
|
defer fd.Close()
|
||||||
fmt.Print(ts)
|
fmt.Fprintf(os.Stderr, "generating types file to %s\n", fd.Name())
|
||||||
fmt.Print("\n")
|
tsTypesMap := make(map[reflect.Type]string)
|
||||||
|
tsgen.GenerateWaveObjTypes(tsTypesMap)
|
||||||
|
err = tsgen.GenerateServiceTypes(tsTypesMap)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error generating service types: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
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, "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
95
cmd/server/main-server.go
Normal 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
243
emain/emain.ts
Normal 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
6
emain/preload.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// Copyright 2024, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
let { contextBridge, ipcRenderer } = require("electron");
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld("api", {});
|
@ -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
62
frontend/app/app.less
Normal 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);
|
||||||
|
}
|
@ -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 = () => {
|
||||||
|
@ -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() {
|
||||||
|
@ -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();
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
let globalWindowId: string = null;
|
||||||
const globalWindowId = urlParams.get("windowid");
|
let globalClientId: string = null;
|
||||||
const globalClientId = urlParams.get("clientid");
|
if (typeof window !== "undefined") {
|
||||||
|
// this if statement allows us to use the code in nodejs as well
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
globalWindowId = urlParams.get("windowid") || "74eba2d0-22fc-4221-82ad-d028dd496342";
|
||||||
|
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 };
|
||||||
|
100
frontend/app/store/services.ts
Normal file
100
frontend/app/store/services.ts
Normal 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()
|
||||||
|
|
@ -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
254
frontend/app/store/ws.ts
Normal 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 };
|
@ -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) {
|
||||||
|
@ -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(() => {
|
||||||
|
@ -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";
|
||||||
|
@ -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;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
@ -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("~")}>
|
||||||
|
@ -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,
|
||||||
|
@ -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>;
|
||||||
|
@ -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,
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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> {
|
||||||
|
104
frontend/types/custom.d.ts
vendored
104
frontend/types/custom.d.ts
vendored
@ -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
174
frontend/types/gotypes.d.ts
vendored
Normal 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 {}
|
@ -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",
|
||||||
});
|
});
|
||||||
|
@ -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
43
go.mod
@ -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
178
go.sum
@ -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
305
main.go
@ -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)
|
|
||||||
}
|
|
31
package.json
31
package.json
@ -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",
|
||||||
|
@ -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,9 +211,10 @@ 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",
|
||||||
Data: nil,
|
ORef: waveobj.MakeORef(wstore.OType_Block, bc.BlockId).String(),
|
||||||
|
Data: nil,
|
||||||
})
|
})
|
||||||
globalLock.Lock()
|
globalLock.Lock()
|
||||||
defer globalLock.Unlock()
|
defer globalLock.Unlock()
|
||||||
|
@ -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,
|
||||||
|
WatchedORefs: make(map[waveobj.ORef]bool),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func RegisterWailsWindow(window *application.WebviewWindow, windowId string) {
|
func UnregisterWSChannel(connId string) {
|
||||||
globalLock.Lock()
|
globalLock.Lock()
|
||||||
defer globalLock.Unlock()
|
defer globalLock.Unlock()
|
||||||
if _, found := wailsWindowMap[window.ID()]; found {
|
delete(wsMap, connId)
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func UnregisterWailsWindow(windowId uint) {
|
func getWindowWatchesForWindowId(windowId string) []*WindowWatchData {
|
||||||
globalLock.Lock()
|
globalLock.Lock()
|
||||||
defer globalLock.Unlock()
|
defer globalLock.Unlock()
|
||||||
delete(wailsWindowMap, windowId)
|
var watches []*WindowWatchData
|
||||||
}
|
for _, wdata := range wsMap {
|
||||||
|
if wdata.WaveWindowId == windowId {
|
||||||
func emitEventToWindow(event WindowEvent) {
|
watches = append(watches, wdata)
|
||||||
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()
|
|
||||||
defer globalLock.Unlock()
|
|
||||||
var ids []uint
|
|
||||||
for _, wdata := range wailsWindowMap {
|
|
||||||
if wdata.WatchedORefs[oref] {
|
|
||||||
ids = append(ids, wdata.WailsWindowId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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
429
pkg/service/service.go
Normal 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
|
||||||
|
}
|
10
pkg/service/servicemeta/servicemeta.go
Normal file
10
pkg/service/servicemeta/servicemeta.go
Normal 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
|
||||||
|
}
|
@ -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
354
pkg/tsgen/tsgen.go
Normal 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
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
217
pkg/web/web.go
Normal 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
215
pkg/web/ws.go
Normal 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
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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"`
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"include": ["frontend/**/*"],
|
"include": ["frontend/**/*", "emain/**/*"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es6",
|
"target": "es6",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
|
5
version.js
Normal file
5
version.js
Normal 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
30
webpack.config.js
Normal 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
114
webpack/webpack.electron.js
Normal 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
154
webpack/webpack.web.js
Normal 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 };
|
Loading…
Reference in New Issue
Block a user