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
|
||||
frontend/dist
|
||||
dist/
|
||||
dist-dev/
|
||||
frontend/node_modules
|
||||
node_modules/
|
||||
frontend/bindings
|
||||
@ -11,6 +12,7 @@ bin/
|
||||
*.exe
|
||||
.DS_Store
|
||||
*~
|
||||
out/
|
||||
|
||||
# Yarn Modern
|
||||
.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:
|
||||
APP_NAME: "NextWave"
|
||||
BIN_DIR: "bin"
|
||||
VITE_PORT: "{{.WAILS_VITE_PORT | default 9245}}"
|
||||
VERSION: "0.1.0"
|
||||
|
||||
|
||||
tasks:
|
||||
## -------------------------- Build -------------------------- ##
|
||||
|
||||
build:
|
||||
summary: Builds the application
|
||||
generate:
|
||||
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
|
||||
- go run cmd/generate/main-generate.go
|
||||
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}}"
|
||||
- "cmd/generate/*.go"
|
||||
- "pkg/service/**/*.go"
|
||||
- "pkg/wstore/*.go"
|
||||
|
||||
webpack:
|
||||
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}}'
|
||||
- yarn run webpack --watch --env dev
|
||||
|
||||
## ------> Darwin <------
|
||||
|
||||
package:darwin:
|
||||
summary: Packages a production build of the application into a `.app` bundle
|
||||
platforms: [darwin]
|
||||
deps:
|
||||
- task: build:darwin
|
||||
vars:
|
||||
PRODUCTION: "true"
|
||||
electron:
|
||||
cmds:
|
||||
- task: create:app:bundle
|
||||
|
||||
package:darwin:arm64:
|
||||
summary: Packages a production build of the application into a `.app` bundle
|
||||
platforms: [darwin/arm64]
|
||||
- WAVETERM_DEV=1 yarn run electron dist-dev/emain.js
|
||||
deps:
|
||||
- task: package:darwin
|
||||
vars:
|
||||
ARCH: arm64
|
||||
- build:server
|
||||
|
||||
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
|
||||
build:server:
|
||||
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
|
||||
- go build -o bin/wavesrv cmd/server/main-server.go
|
||||
sources:
|
||||
- "appicon.png"
|
||||
- "cmd/server/*.go"
|
||||
- "pkg/**/*.go"
|
||||
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/*
|
||||
- bin/wavesrv
|
||||
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
|
||||
|
||||
go:mod:tidy:
|
||||
summary: Runs `go mod tidy`
|
||||
@ -394,38 +46,4 @@ tasks:
|
||||
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
|
||||
|
@ -5,22 +5,87 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
"github.com/wavetermdev/thenextwave/pkg/waveobj"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wstore"
|
||||
"github.com/wavetermdev/thenextwave/pkg/service"
|
||||
"github.com/wavetermdev/thenextwave/pkg/tsgen"
|
||||
"github.com/wavetermdev/thenextwave/pkg/util/utilfn"
|
||||
)
|
||||
|
||||
func main() {
|
||||
tsTypesMap := make(map[reflect.Type]string)
|
||||
var waveObj waveobj.WaveObj
|
||||
waveobj.GenerateTSType(reflect.TypeOf(waveobj.ORef{}), tsTypesMap)
|
||||
waveobj.GenerateTSType(reflect.TypeOf(&waveObj).Elem(), tsTypesMap)
|
||||
for _, rtype := range wstore.AllWaveObjTypes() {
|
||||
waveobj.GenerateTSType(rtype, tsTypesMap)
|
||||
func generateTypesFile() error {
|
||||
fd, err := os.Create("frontend/types/gotypes.d.ts")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ts := range tsTypesMap {
|
||||
fmt.Print(ts)
|
||||
fmt.Print("\n")
|
||||
defer fd.Close()
|
||||
fmt.Fprintf(os.Stderr, "generating types file to %s\n", fd.Name())
|
||||
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 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 { HTML5Backend } from "react-dnd-html5-backend";
|
||||
import "../../public/style.less";
|
||||
import "./app.less";
|
||||
import { CenteredDiv } from "./element/quickelems";
|
||||
|
||||
const App = () => {
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React from "react";
|
||||
import "./quickelems.less";
|
||||
|
||||
function CenteredLoadingDiv() {
|
||||
|
@ -1,15 +1,22 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Events } from "@wailsio/runtime";
|
||||
import * as jotai from "jotai";
|
||||
import * as rxjs from "rxjs";
|
||||
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 urlParams = new URLSearchParams(window.location.search);
|
||||
const globalWindowId = urlParams.get("windowid");
|
||||
const globalClientId = urlParams.get("clientid");
|
||||
let globalWindowId: string = null;
|
||||
let globalClientId: string = null;
|
||||
if (typeof window !== "undefined") {
|
||||
// this if statement allows us to use the code in nodejs as well
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
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 clientIdAtom = jotai.atom(null) as jotai.PrimitiveAtom<string>;
|
||||
globalStore.set(windowIdAtom, globalWindowId);
|
||||
@ -18,7 +25,7 @@ const uiContextAtom = jotai.atom((get) => {
|
||||
const windowData = get(windowDataAtom);
|
||||
const uiContext: UIContext = {
|
||||
windowid: get(atoms.windowId),
|
||||
activetabid: windowData.activetabid,
|
||||
activetabid: windowData?.activetabid,
|
||||
};
|
||||
return uiContext;
|
||||
}) as jotai.Atom<UIContext>;
|
||||
@ -34,7 +41,8 @@ const windowDataAtom: jotai.Atom<WaveWindow> = jotai.atom((get) => {
|
||||
if (windowId == 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 windowData = get(windowDataAtom);
|
||||
@ -56,10 +64,10 @@ const atoms = {
|
||||
|
||||
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> {
|
||||
let subject = blockSubjects.get(blockId);
|
||||
function getORefSubject(oref: string): SubjectWithRef<any> {
|
||||
let subject = orefSubjects.get(oref);
|
||||
if (subject == null) {
|
||||
subject = new rxjs.Subject<any>() as any;
|
||||
subject.refCount = 0;
|
||||
@ -67,29 +75,15 @@ function getBlockSubject(blockId: string): SubjectWithRef<any> {
|
||||
subject.refCount--;
|
||||
if (subject.refCount === 0) {
|
||||
subject.complete();
|
||||
blockSubjects.delete(blockId);
|
||||
orefSubjects.delete(oref);
|
||||
}
|
||||
};
|
||||
blockSubjects.set(blockId, subject);
|
||||
orefSubjects.set(oref, subject);
|
||||
}
|
||||
subject.refCount++;
|
||||
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>>();
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import { Call as $Call, Events } from "@wailsio/runtime";
|
||||
// import { Call as $Call, Events } from "@wailsio/runtime";
|
||||
import * as jotai from "jotai";
|
||||
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> = {
|
||||
value: T;
|
||||
@ -54,7 +57,51 @@ function makeORef(otype: string, oid: string): string {
|
||||
}
|
||||
|
||||
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>>();
|
||||
@ -75,6 +122,7 @@ function createWaveValueObject<T extends WaveObj>(oref: string, shouldFetch: boo
|
||||
const localPromise = GetObject<T>(oref);
|
||||
wov.pendingPromise = localPromise;
|
||||
localPromise.then((val) => {
|
||||
console.log("GetObject resolved", oref, val);
|
||||
if (wov.pendingPromise != localPromise) {
|
||||
return;
|
||||
}
|
||||
@ -187,7 +235,7 @@ function useWaveObject<T extends WaveObj>(oref: string): [T, boolean, (val: T) =
|
||||
const [atomVal, setAtomVal] = jotai.useAtom(wov.dataAtom);
|
||||
const simpleSet = (val: T) => {
|
||||
setAtomVal({ value: val, loading: false });
|
||||
UpdateObject(val, false);
|
||||
services.ObjectService.UpdateObject(val, false);
|
||||
};
|
||||
return [atomVal.value, atomVal.loading, simpleSet];
|
||||
}
|
||||
@ -236,48 +284,32 @@ function cleanWaveObjectCache() {
|
||||
}
|
||||
}
|
||||
|
||||
Events.On("waveobj:update", (event: any) => {
|
||||
const data: WaveObjUpdate[] = event?.data;
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray(data)) {
|
||||
console.log("invalid waveobj:update, not an array", data);
|
||||
return;
|
||||
}
|
||||
if (data.length == 0) {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
// Events.On("waveobj:update", (event: any) => {
|
||||
// const data: WaveObjUpdate[] = event?.data;
|
||||
// if (data == null) {
|
||||
// return;
|
||||
// }
|
||||
// if (!Array.isArray(data)) {
|
||||
// console.log("invalid waveobj:update, not an array", data);
|
||||
// return;
|
||||
// }
|
||||
// if (data.length == 0) {
|
||||
// return;
|
||||
// }
|
||||
// updateWaveObjects(data);
|
||||
// });
|
||||
|
||||
// gets the value of a WaveObject from the cache.
|
||||
// should provide getFn if it is available (e.g. inside of a jotai atom)
|
||||
// otherwise it will use the globalStore.get function
|
||||
function getObjectValue<T>(oref: string, getFn?: jotai.Getter): T {
|
||||
const wov = waveObjectValueCache.get(oref);
|
||||
if (wov === undefined) {
|
||||
return null;
|
||||
let wov = waveObjectValueCache.get(oref);
|
||||
if (wov == 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;
|
||||
}
|
||||
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 });
|
||||
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 {
|
||||
callBackendService,
|
||||
cleanWaveObjectCache,
|
||||
clearWaveObjectCache,
|
||||
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
|
||||
|
||||
import { Block, BlockHeader } from "@/app/block/block";
|
||||
import * as services from "@/store/services";
|
||||
import * as WOS from "@/store/wos";
|
||||
|
||||
import { CenteredDiv, CenteredLoadingDiv } from "@/element/quickelems";
|
||||
import { TileLayout } from "@/faraday/index";
|
||||
import { getLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { CenteredDiv, CenteredLoadingDiv } from "../element/quickelems";
|
||||
import "./tab.less";
|
||||
|
||||
const TabContent = ({ tabId }: { tabId: string }) => {
|
||||
@ -34,7 +35,7 @@ const TabContent = ({ tabId }: { tabId: string }) => {
|
||||
|
||||
const onNodeDelete = useCallback((data: TabLayoutData) => {
|
||||
console.log("onNodeDelete", data);
|
||||
return WOS.DeleteBlock(data.blockId);
|
||||
return services.ObjectService.DeleteBlock(data.blockId);
|
||||
}, []);
|
||||
|
||||
if (tabLoading) {
|
||||
|
@ -15,7 +15,7 @@ declare var monaco: Monaco;
|
||||
let monacoLoadedAtom = jotai.atom(false);
|
||||
|
||||
function loadMonaco() {
|
||||
loader.config({ paths: { vs: "./monaco" } });
|
||||
loader.config({ paths: { vs: "./dist-dev/monaco" } });
|
||||
loader
|
||||
.init()
|
||||
.then(() => {
|
||||
|
@ -1,7 +1,6 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { FileInfo } from "@/bindings/fileservice";
|
||||
import { Table, createColumnHelper, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import clsx from "clsx";
|
||||
import * as jotai from "jotai";
|
||||
|
@ -1,9 +1,9 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { FileInfo, FileService, FullFile } from "@/bindings/fileservice";
|
||||
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 util from "@/util/util";
|
||||
import clsx from "clsx";
|
||||
@ -69,7 +69,7 @@ function MarkdownPreview({ contentAtom }: { contentAtom: jotai.Atom<Promise<stri
|
||||
|
||||
function StreamingPreview({ fileInfo }: { fileInfo: FileInfo }) {
|
||||
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") {
|
||||
return (
|
||||
<div className="view-preview view-preview-pdf">
|
||||
@ -114,7 +114,7 @@ function PreviewView({ blockId }: { blockId: string }) {
|
||||
},
|
||||
(get, set, update) => {
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
const statFile = await FileService.StatFile(fileName);
|
||||
// const statFile = await FileService.StatFile(fileName);
|
||||
const statFile = await services.FileService.StatFile(fileName);
|
||||
return statFile;
|
||||
})
|
||||
);
|
||||
@ -134,7 +135,8 @@ function PreviewView({ blockId }: { blockId: string }) {
|
||||
if (fileName == null) {
|
||||
return null;
|
||||
}
|
||||
const file = await FileService.ReadFile(fileName);
|
||||
// const file = await FileService.ReadFile(fileName);
|
||||
const file = await services.FileService.ReadFile(fileName);
|
||||
return file;
|
||||
})
|
||||
);
|
||||
|
@ -1,15 +1,14 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { BlockService } from "@/bindings/blockservice";
|
||||
import { getBlockSubject } from "@/store/global";
|
||||
import { getBackendHostPort, getORefSubject, WOS } from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import { base64ToArray } from "@/util/util";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import type { ITheme } from "@xterm/xterm";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import * as React from "react";
|
||||
|
||||
import useResizeObserver from "@react-hook/resize-observer";
|
||||
import { debounce } from "throttle-debounce";
|
||||
import "./view.less";
|
||||
import "/public/xterm.css";
|
||||
@ -43,12 +42,15 @@ function getThemeFromCSSVars(el: Element): ITheme {
|
||||
}
|
||||
|
||||
function handleResize(fitAddon: FitAddon, blockId: string, term: Terminal) {
|
||||
if (term == null) {
|
||||
return;
|
||||
}
|
||||
const oldRows = term.rows;
|
||||
const oldCols = term.cols;
|
||||
fitAddon.fit();
|
||||
if (oldRows !== term.rows || oldCols !== 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 termRef = React.useRef<Terminal>(null);
|
||||
const initialLoadRef = React.useRef<InitialLoadDataType>({ loaded: false, heldData: [] });
|
||||
|
||||
const [fitAddon, setFitAddon] = React.useState<FitAddon>(null);
|
||||
const [term, setTerm] = React.useState<Terminal>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
console.log("terminal created");
|
||||
const newTerm = new Terminal({
|
||||
@ -80,18 +78,22 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
newTerm.loadAddon(newFitAddon);
|
||||
newTerm.open(connectElemRef.current);
|
||||
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",
|
||||
termsize: { rows: newTerm.rows, cols: newTerm.cols },
|
||||
});
|
||||
newTerm.onData((data) => {
|
||||
const b64data = btoa(data);
|
||||
const inputCmd = { command: "controller:input", blockid: blockId, inputdata64: b64data };
|
||||
BlockService.SendCommand(blockId, inputCmd);
|
||||
services.BlockService.SendCommand(blockId, inputCmd);
|
||||
});
|
||||
|
||||
// block subject
|
||||
const blockSubject = getBlockSubject(blockId);
|
||||
const blockSubject = getORefSubject(WOS.makeORef("block", blockId));
|
||||
blockSubject.subscribe((data) => {
|
||||
// base64 decode
|
||||
const decodedData = base64ToArray(data.ptydata);
|
||||
@ -101,10 +103,6 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
initialLoadRef.current.heldData.push(decodedData);
|
||||
}
|
||||
});
|
||||
|
||||
setTerm(newTerm);
|
||||
setFitAddon(newFitAddon);
|
||||
|
||||
// load data from filestore
|
||||
const startTs = Date.now();
|
||||
let loadedBytes = 0;
|
||||
@ -112,7 +110,7 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
const usp = new URLSearchParams();
|
||||
usp.set("zoneid", blockId);
|
||||
usp.set("name", "main");
|
||||
fetch("/wave/file?" + usp.toString())
|
||||
fetch(getBackendHostPort() + "/wave/file?" + usp.toString())
|
||||
.then((resp) => {
|
||||
if (resp.ok) {
|
||||
return resp.arrayBuffer();
|
||||
@ -133,18 +131,20 @@ const TerminalView = ({ blockId }: { blockId: string }) => {
|
||||
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 () => {
|
||||
newTerm.dispose();
|
||||
blockSubject.release();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleResizeCallback = React.useCallback(() => {
|
||||
debounce(50, () => handleResize(fitAddon, blockId, term));
|
||||
}, [fitAddon, term]);
|
||||
|
||||
useResizeObserver(connectElemRef, handleResizeCallback);
|
||||
|
||||
return (
|
||||
<div className="view-term">
|
||||
<div key="conntectElem" className="term-connectelem" ref={connectElemRef}></div>
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
import { TabContent } from "@/app/tab/tab";
|
||||
import { atoms } from "@/store/global";
|
||||
import * as services from "@/store/services";
|
||||
import * as WOS from "@/store/wos";
|
||||
import { clsx } from "clsx";
|
||||
import * as jotai from "jotai";
|
||||
@ -21,10 +22,10 @@ function Tab({ tabId }: { tabId: string }) {
|
||||
const windowData = jotai.useAtomValue(atoms.waveWindow);
|
||||
const [tabData, tabLoading] = WOS.useWaveObjectValue<Tab>(WOS.makeORef("tab", tabId));
|
||||
function setActiveTab() {
|
||||
WOS.SetActiveTab(tabId);
|
||||
services.ObjectService.SetActiveTab(tabId);
|
||||
}
|
||||
function handleCloseTab() {
|
||||
WOS.CloseTab(tabId);
|
||||
services.ObjectService.CloseTab(tabId);
|
||||
deleteLayoutStateAtomForTab(tabId);
|
||||
}
|
||||
return (
|
||||
@ -45,7 +46,7 @@ function Tab({ tabId }: { tabId: string }) {
|
||||
function TabBar({ workspace }: { workspace: Workspace }) {
|
||||
function handleAddTab() {
|
||||
const newTabName = `Tab-${workspace.tabids.length + 1}`;
|
||||
WOS.AddTabToWorkspace(newTabName, true);
|
||||
services.ObjectService.AddTabToWorkspace(newTabName, true);
|
||||
}
|
||||
const tabIds = workspace?.tabids ?? [];
|
||||
return (
|
||||
@ -83,7 +84,7 @@ function Widgets() {
|
||||
|
||||
async function createBlock(blockDef: BlockDef) {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -122,13 +123,13 @@ function Widgets() {
|
||||
<div className="widget" onClick={() => clickTerminal()}>
|
||||
<i className="fa fa-solid fa-square-terminal fa-fw" />
|
||||
</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" />
|
||||
</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" />
|
||||
</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" />
|
||||
</div>
|
||||
<div className="widget" onClick={() => clickPreview("~")}>
|
||||
|
@ -1,9 +1,9 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { TileLayout } from "./lib/TileLayout.jsx";
|
||||
import { newLayoutTreeStateAtom, useLayoutTreeStateReducerAtom, withLayoutTreeState } from "./lib/layoutAtom.js";
|
||||
import { newLayoutNode } from "./lib/layoutNode.js";
|
||||
import { TileLayout } from "./lib/TileLayout";
|
||||
import { newLayoutTreeStateAtom, useLayoutTreeStateReducerAtom, withLayoutTreeState } from "./lib/layoutAtom";
|
||||
import { newLayoutNode } from "./lib/layoutNode";
|
||||
import type {
|
||||
LayoutNode,
|
||||
LayoutTreeCommitPendingAction,
|
||||
@ -14,8 +14,8 @@ import type {
|
||||
LayoutTreeState,
|
||||
WritableLayoutNodeAtom,
|
||||
WritableLayoutTreeStateAtom,
|
||||
} from "./lib/model.js";
|
||||
import { LayoutTreeActionType } from "./lib/model.js";
|
||||
} from "./lib/model";
|
||||
import { LayoutTreeActionType } from "./lib/model";
|
||||
|
||||
export {
|
||||
LayoutTreeActionType,
|
||||
|
@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
import React, {
|
||||
CSSProperties,
|
||||
ReactNode,
|
||||
RefObject,
|
||||
@ -18,8 +18,8 @@ import { useDrag, useDragLayer, useDrop } from "react-dnd";
|
||||
|
||||
import useResizeObserver from "@react-hook/resize-observer";
|
||||
import { toPng } from "html-to-image";
|
||||
import { useLayoutTreeStateReducerAtom } from "./layoutAtom.js";
|
||||
import { findNode } from "./layoutNode.js";
|
||||
import { useLayoutTreeStateReducerAtom } from "./layoutAtom";
|
||||
import { findNode } from "./layoutNode";
|
||||
import {
|
||||
ContentRenderer,
|
||||
LayoutNode,
|
||||
@ -31,15 +31,9 @@ import {
|
||||
LayoutTreeState,
|
||||
PreviewRenderer,
|
||||
WritableLayoutTreeStateAtom,
|
||||
} from "./model.js";
|
||||
} from "./model";
|
||||
import "./tilelayout.less";
|
||||
import {
|
||||
Dimensions,
|
||||
FlexDirection,
|
||||
setTransform as createTransform,
|
||||
debounce,
|
||||
determineDropDirection,
|
||||
} from "./utils.js";
|
||||
import { Dimensions, FlexDirection, setTransform as createTransform, debounce, determineDropDirection } from "./utils";
|
||||
|
||||
export interface TileLayoutProps<T> {
|
||||
layoutTreeStateAtom: WritableLayoutTreeStateAtom<T>;
|
||||
|
@ -1,10 +1,10 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// 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 { useCallback } from "react";
|
||||
import { layoutTreeStateReducer, newLayoutTreeState } from "./layoutState.js";
|
||||
import { layoutTreeStateReducer, newLayoutTreeState } from "./layoutState";
|
||||
import {
|
||||
LayoutNode,
|
||||
LayoutNodeWaveObj,
|
||||
|
@ -1,8 +1,8 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { LayoutNode } from "./model.js";
|
||||
import { FlexDirection, getCrypto, reverseFlexDirection } from "./utils.js";
|
||||
import { LayoutNode } from "./model";
|
||||
import { FlexDirection, getCrypto, reverseFlexDirection } from "./utils";
|
||||
|
||||
const crypto = getCrypto();
|
||||
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
findNode,
|
||||
findParent,
|
||||
removeChild,
|
||||
} from "./layoutNode.js";
|
||||
} from "./layoutNode";
|
||||
import {
|
||||
LayoutNode,
|
||||
LayoutTreeAction,
|
||||
@ -20,14 +20,14 @@ import {
|
||||
LayoutTreeMoveNodeAction,
|
||||
LayoutTreeState,
|
||||
MoveOperation,
|
||||
} from "./model.js";
|
||||
import { DropDirection, FlexDirection, lazy } from "./utils.js";
|
||||
} from "./model";
|
||||
import { DropDirection, FlexDirection, lazy } from "./utils";
|
||||
|
||||
/**
|
||||
* Initializes a layout tree state.
|
||||
* @param rootNode The root node for the tree.
|
||||
* @returns The state of the tree.
|
||||
*
|
||||
*t
|
||||
* @template T The type of data associated with the nodes of the tree.
|
||||
*/
|
||||
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
|
||||
|
||||
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 = {
|
||||
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;
|
||||
}
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
weight: "700",
|
||||
});
|
||||
@ -42,11 +42,11 @@ function loadLatoFont() {
|
||||
return;
|
||||
}
|
||||
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",
|
||||
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",
|
||||
weight: "700",
|
||||
});
|
||||
@ -61,11 +61,11 @@ function loadFiraCodeFont() {
|
||||
return;
|
||||
}
|
||||
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",
|
||||
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",
|
||||
weight: "700",
|
||||
});
|
||||
@ -80,19 +80,19 @@ function loadHackFont() {
|
||||
return;
|
||||
}
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
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",
|
||||
weight: "700",
|
||||
});
|
||||
@ -111,7 +111,7 @@ function loadBaseFonts() {
|
||||
return;
|
||||
}
|
||||
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",
|
||||
weight: "normal",
|
||||
});
|
||||
|
@ -2,7 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
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 React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
@ -10,13 +11,15 @@ import { App } from "./app/app";
|
||||
import { loadFonts } from "./util/fontutil";
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const windowId = urlParams.get("windowid");
|
||||
const clientId = urlParams.get("clientid");
|
||||
|
||||
loadFonts();
|
||||
let windowId = urlParams.get("windowid");
|
||||
let clientId = urlParams.get("clientid");
|
||||
|
||||
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).globalStore = globalStore;
|
||||
|
||||
@ -30,9 +33,10 @@ matchViewportSize();
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
console.log("DOMContentLoaded");
|
||||
// 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));
|
||||
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 elem = document.getElementById("main");
|
||||
const root = createRoot(elem);
|
||||
|
43
go.mod
43
go.mod
@ -8,58 +8,21 @@ require (
|
||||
github.com/creack/pty v1.1.18
|
||||
github.com/golang-migrate/migrate/v4 v4.17.1
|
||||
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/mattn/go-sqlite3 v1.14.22
|
||||
github.com/mitchellh/mapstructure v1.5.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
|
||||
golang.org/x/sys v0.20.0
|
||||
)
|
||||
|
||||
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/go-multierror v1.1.1 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // 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
|
||||
github.com/stretchr/testify v1.8.4 // 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
|
||||
|
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/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/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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/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/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/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.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
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/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/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/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/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
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/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/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/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/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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/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/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/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/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/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",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"main": "dist/emain.js",
|
||||
"browser": "dist/wave.js",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build --minify false --mode development",
|
||||
@ -14,6 +15,16 @@
|
||||
"test": "vitest"
|
||||
},
|
||||
"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",
|
||||
"@eslint/js": "^9.2.0",
|
||||
"@storybook/addon-essentials": "^8.1.4",
|
||||
@ -24,6 +35,9 @@
|
||||
"@storybook/react": "^8.1.4",
|
||||
"@storybook/react-vite": "^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/react": "^18.3.2",
|
||||
"@types/throttle-debounce": "^5",
|
||||
@ -31,21 +45,32 @@
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"@vitest/coverage-istanbul": "^1.6.0",
|
||||
"@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-config-prettier": "^9.1.0",
|
||||
"less": "^4.2.0",
|
||||
"less-loader": "^12.2.0",
|
||||
"mini-css-extract-plugin": "^2.9.0",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-jsdoc": "^1.3.0",
|
||||
"prettier-plugin-organize-imports": "^3.2.4",
|
||||
"storybook": "^8.1.4",
|
||||
"style-loader": "^4.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths-webpack-plugin": "^4.1.0",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript-eslint": "^7.8.0",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-static-copy": "^1.0.5",
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"vitest": "^1.6.0"
|
||||
"vitest": "^1.6.0",
|
||||
"webpack": "^5.91.0",
|
||||
"webpack-cli": "^5.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@monaco-editor/loader": "^1.4.0",
|
||||
@ -57,6 +82,8 @@
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.11",
|
||||
"electron": "^30.1.0",
|
||||
"html-to-image": "^1.11.11",
|
||||
"immer": "^10.1.1",
|
||||
"jotai": "^2.8.0",
|
||||
|
@ -15,10 +15,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wavetermdev/thenextwave/pkg/eventbus"
|
||||
"github.com/wavetermdev/thenextwave/pkg/filestore"
|
||||
"github.com/wavetermdev/thenextwave/pkg/shellexec"
|
||||
"github.com/wavetermdev/thenextwave/pkg/waveobj"
|
||||
"github.com/wavetermdev/thenextwave/pkg/wstore"
|
||||
)
|
||||
|
||||
@ -97,8 +97,9 @@ func (bc *BlockController) handleShellProcData(data []byte, seqNum int) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("error appending to blockfile: %w", err)
|
||||
}
|
||||
eventbus.SendEvent(application.WailsEvent{
|
||||
Name: "block:ptydata",
|
||||
eventbus.SendEvent(eventbus.WSEventType{
|
||||
EventType: "block:ptydata",
|
||||
ORef: waveobj.MakeORef(wstore.OType_Block, bc.BlockId).String(),
|
||||
Data: map[string]any{
|
||||
"blockid": bc.BlockId,
|
||||
"blockfile": "main",
|
||||
@ -210,9 +211,10 @@ func (bc *BlockController) Run(bdata *wstore.Block) {
|
||||
bc.Status = "done"
|
||||
}
|
||||
})
|
||||
eventbus.SendEvent(application.WailsEvent{
|
||||
Name: "block:done",
|
||||
Data: nil,
|
||||
eventbus.SendEvent(eventbus.WSEventType{
|
||||
EventType: "block:done",
|
||||
ORef: waveobj.MakeORef(wstore.OType_Block, bc.BlockId).String(),
|
||||
Data: nil,
|
||||
})
|
||||
globalLock.Lock()
|
||||
defer globalLock.Unlock()
|
||||
|
@ -4,164 +4,74 @@
|
||||
package eventbus
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wavetermdev/thenextwave/pkg/waveobj"
|
||||
)
|
||||
|
||||
const EventBufferSize = 50
|
||||
|
||||
var EventCh chan application.WailsEvent = make(chan application.WailsEvent, EventBufferSize)
|
||||
var WindowEventCh chan WindowEvent = make(chan WindowEvent, EventBufferSize)
|
||||
var shutdownCh chan struct{} = make(chan struct{})
|
||||
var ErrQueueFull = errors.New("event queue full")
|
||||
|
||||
type WindowEvent struct {
|
||||
WindowId uint
|
||||
Event application.WailsEvent
|
||||
type WSEventType struct {
|
||||
EventType string `json:"eventtype"`
|
||||
ORef string `json:"oref,omitempty"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
type WindowWatchData struct {
|
||||
Window *application.WebviewWindow
|
||||
WaveWindowId string
|
||||
WailsWindowId uint
|
||||
WatchedORefs map[waveobj.ORef]bool
|
||||
WindowWSCh chan any
|
||||
WaveWindowId string
|
||||
WatchedORefs map[waveobj.ORef]bool
|
||||
}
|
||||
|
||||
var globalLock = &sync.Mutex{}
|
||||
var wailsApp *application.App
|
||||
var wailsWindowMap = make(map[uint]*WindowWatchData)
|
||||
var wsMap = make(map[string]*WindowWatchData) // websocketid => WindowWatchData
|
||||
|
||||
func Start() {
|
||||
go processEvents()
|
||||
}
|
||||
|
||||
func Shutdown() {
|
||||
close(shutdownCh)
|
||||
}
|
||||
|
||||
func RegisterWailsApp(app *application.App) {
|
||||
func RegisterWSChannel(connId string, windowId string, ch chan any) {
|
||||
globalLock.Lock()
|
||||
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()
|
||||
defer globalLock.Unlock()
|
||||
if _, found := wailsWindowMap[window.ID()]; found {
|
||||
panic(fmt.Errorf("wails window already registered with eventbus: %d", window.ID()))
|
||||
}
|
||||
wailsWindowMap[window.ID()] = &WindowWatchData{
|
||||
Window: window,
|
||||
WailsWindowId: window.ID(),
|
||||
WaveWindowId: "",
|
||||
WatchedORefs: make(map[waveobj.ORef]bool),
|
||||
}
|
||||
delete(wsMap, connId)
|
||||
}
|
||||
|
||||
func UnregisterWailsWindow(windowId uint) {
|
||||
func getWindowWatchesForWindowId(windowId string) []*WindowWatchData {
|
||||
globalLock.Lock()
|
||||
defer globalLock.Unlock()
|
||||
delete(wailsWindowMap, windowId)
|
||||
}
|
||||
|
||||
func emitEventToWindow(event WindowEvent) {
|
||||
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)
|
||||
var watches []*WindowWatchData
|
||||
for _, wdata := range wsMap {
|
||||
if wdata.WaveWindowId == windowId {
|
||||
watches = append(watches, wdata)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
return watches
|
||||
}
|
||||
|
||||
func SendORefEvent(oref waveobj.ORef, event application.WailsEvent) {
|
||||
wins := findWindowIdsByORef(oref)
|
||||
for _, windowId := range wins {
|
||||
SendWindowEvent(windowId, event)
|
||||
func getAllWatches() []*WindowWatchData {
|
||||
globalLock.Lock()
|
||||
defer globalLock.Unlock()
|
||||
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 {
|
||||
select {
|
||||
case EventCh <- event:
|
||||
return nil
|
||||
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
|
||||
}
|
||||
func SendEvent(event WSEventType) {
|
||||
wwdArr := getAllWatches()
|
||||
for _, wdata := range wwdArr {
|
||||
wdata.WindowWSCh <- event
|
||||
}
|
||||
}
|
||||
|
@ -9,12 +9,20 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/wavetermdev/thenextwave/pkg/blockcontroller"
|
||||
"github.com/wavetermdev/thenextwave/pkg/service/servicemeta"
|
||||
)
|
||||
|
||||
type BlockService struct{}
|
||||
|
||||
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 {
|
||||
cmd, err := blockcontroller.ParseCmdMap(cmdMap)
|
||||
if err != nil {
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/wavetermdev/thenextwave/pkg/blockcontroller"
|
||||
"github.com/wavetermdev/thenextwave/pkg/service/servicemeta"
|
||||
"github.com/wavetermdev/thenextwave/pkg/waveobj"
|
||||
"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
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -39,11 +47,17 @@ func (svc *ObjectService) GetObject(orefStr string) (any, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting object: %w", err)
|
||||
}
|
||||
rtn, err := waveobj.ToJsonMap(obj)
|
||||
return rtn, err
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
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)
|
||||
defer cancelFn()
|
||||
|
||||
@ -78,30 +92,41 @@ func updatesRtn(ctx context.Context, rtnVal map[string]any) (any, error) {
|
||||
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)
|
||||
defer cancelFn()
|
||||
ctx = wstore.ContextWithUpdates(ctx)
|
||||
windowData, err := wstore.DBMustGet[*wstore.Window](ctx, uiContext.WindowId)
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating tab: %w", err)
|
||||
return "", nil, fmt.Errorf("error creating tab: %w", err)
|
||||
}
|
||||
if activateTab {
|
||||
err = wstore.SetActiveTab(ctx, uiContext.WindowId, tab.OID)
|
||||
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)
|
||||
rtn["tabid"] = waveobj.GetOID(tab)
|
||||
return updatesRtn(ctx, rtn)
|
||||
return tab.OID, wstore.ContextGetUpdatesRtn(ctx), nil
|
||||
}
|
||||
|
||||
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)
|
||||
defer cancelFn()
|
||||
ctx = wstore.ContextWithUpdates(ctx)
|
||||
@ -122,32 +147,51 @@ func (svc *ObjectService) SetActiveTab(uiContext wstore.UIContext, tabId string)
|
||||
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 == "" {
|
||||
return nil, fmt.Errorf("no active tab")
|
||||
return "", nil, fmt.Errorf("no active tab")
|
||||
}
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
|
||||
defer cancelFn()
|
||||
ctx = wstore.ContextWithUpdates(ctx)
|
||||
blockData, err := wstore.CreateBlock(ctx, uiContext.ActiveTabId, blockDef, rtOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating block: %w", err)
|
||||
return "", nil, fmt.Errorf("error creating block: %w", err)
|
||||
}
|
||||
if blockData.Controller != "" {
|
||||
err = blockcontroller.StartBlockController(ctx, blockData.OID)
|
||||
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)
|
||||
rtn["blockId"] = blockData.OID
|
||||
return updatesRtn(ctx, rtn)
|
||||
return blockData.OID, wstore.ContextGetUpdatesRtn(ctx), nil
|
||||
}
|
||||
|
||||
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)
|
||||
defer cancelFn()
|
||||
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)
|
||||
}
|
||||
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)
|
||||
defer cancelFn()
|
||||
ctx = wstore.ContextWithUpdates(ctx)
|
||||
@ -191,10 +241,16 @@ func (svc *ObjectService) CloseTab(uiContext wstore.UIContext, tabId string) (an
|
||||
}
|
||||
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)
|
||||
defer cancelFn()
|
||||
ctx = wstore.ContextWithUpdates(ctx)
|
||||
@ -206,18 +262,23 @@ func (svc *ObjectService) UpdateObjectMeta(uiContext wstore.UIContext, orefStr s
|
||||
if err != nil {
|
||||
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)
|
||||
defer cancelFn()
|
||||
ctx = wstore.ContextWithUpdates(ctx)
|
||||
|
||||
oref, err := waveobj.ORefFromMap(objData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("objData is not a valid object, requires otype and oid: %w", err)
|
||||
if waveObj == nil {
|
||||
return nil, fmt.Errorf("update wavobj is nil")
|
||||
}
|
||||
oref := waveobj.ORefFromWaveObj(waveObj)
|
||||
found, err := wstore.DBExistsORef(ctx, *oref)
|
||||
if err != nil {
|
||||
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 {
|
||||
return nil, fmt.Errorf("object not found: %s", oref)
|
||||
}
|
||||
newObj, err := waveobj.FromJsonMap(objData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error converting data to valid wave object: %w", err)
|
||||
}
|
||||
err = wstore.DBUpdate(ctx, newObj)
|
||||
err = wstore.DBUpdate(ctx, waveObj)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error updating object: %w", err)
|
||||
}
|
||||
if returnUpdates {
|
||||
return updatesRtn(ctx, nil)
|
||||
return wstore.ContextGetUpdatesRtn(ctx), 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()
|
||||
ecmd := exec.Command(shellPath, "-i", "-l")
|
||||
ecmd.Env = os.Environ()
|
||||
ecmd.Dir = wavebase.GetHomeDir()
|
||||
envToAdd := shellutil.WaveshellEnvVars(shellutil.DefaultTermType)
|
||||
if os.Getenv("LANG") == "" {
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func MakeORef(otype string, oid string) ORef {
|
||||
return ORef{
|
||||
OType: otype,
|
||||
OID: oid,
|
||||
}
|
||||
}
|
||||
|
||||
type WaveObj interface {
|
||||
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))
|
||||
}
|
||||
|
||||
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) {
|
||||
if w == nil {
|
||||
return nil, nil
|
||||
@ -227,7 +246,13 @@ func ORefFromMap(m map[string]any) (*ORef, error) {
|
||||
return nil, err
|
||||
}
|
||||
return &oref, nil
|
||||
}
|
||||
|
||||
func ORefFromWaveObj(w WaveObj) *ORef {
|
||||
return &ORef{
|
||||
OType: w.GetOType(),
|
||||
OID: GetOID(w),
|
||||
}
|
||||
}
|
||||
|
||||
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{}{}
|
||||
|
||||
type UpdatesRtnType = []WaveObjUpdate
|
||||
|
||||
func init() {
|
||||
for _, rtype := range AllWaveObjTypes() {
|
||||
waveobj.RegisterType(rtype)
|
||||
@ -67,6 +69,18 @@ func ContextGetUpdates(ctx context.Context) map[waveobj.ORef]WaveObjUpdate {
|
||||
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 {
|
||||
updatesVal := ctx.Value(waveObjUpdateKey)
|
||||
if updatesVal == nil {
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/wavetermdev/thenextwave/pkg/waveobj"
|
||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/dbutil"
|
||||
)
|
||||
|
||||
var ErrNotFound = fmt.Errorf("not found")
|
||||
@ -121,7 +122,7 @@ func dbSelectOIDs(ctx context.Context, otype string, oids []string) ([]waveobj.W
|
||||
table := tableNameFromOType(otype)
|
||||
query := fmt.Sprintf("SELECT oid, version, data FROM %s WHERE oid IN (SELECT value FROM json_each(?))", table)
|
||||
var rows []idDataType
|
||||
tx.Select(&rows, query, oids)
|
||||
tx.Select(&rows, query, dbutil.QuickJson(oids))
|
||||
rtn := make([]waveobj.WaveObj, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
waveObj, err := waveobj.FromJson(row.Data)
|
||||
|
@ -5,6 +5,7 @@ package wstore
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/wavetermdev/thenextwave/pkg/shellexec"
|
||||
@ -52,6 +53,65 @@ func (update WaveObjUpdate) MarshalJSON() ([]byte, error) {
|
||||
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 {
|
||||
OID string `json:"oid"`
|
||||
Version int `json:"version"`
|
||||
@ -106,6 +166,14 @@ func (*Tab) GetOType() string {
|
||||
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 {
|
||||
OID string `json:"oid"`
|
||||
Version int `json:"version"`
|
||||
|
@ -1,8 +1,9 @@
|
||||
/** @type {import("prettier").Config} */
|
||||
export default {
|
||||
module.exports = {
|
||||
plugins: ["prettier-plugin-jsdoc", "prettier-plugin-organize-imports"],
|
||||
printWidth: 120,
|
||||
trailingComma: "es5",
|
||||
useTabs: false,
|
||||
jsdocVerticalAlignment: true,
|
||||
jsdocSeparateReturnsFromParam: true,
|
||||
jsdocSeparateTagGroups: true,
|
||||
|
@ -5,12 +5,13 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Wails App</title>
|
||||
<link rel="stylesheet" href="/fontawesome/css/fontawesome.min.css" />
|
||||
<link rel="stylesheet" href="/fontawesome/css/brands.min.css" />
|
||||
<link rel="stylesheet" href="/fontawesome/css/solid.min.css" />
|
||||
<link rel="stylesheet" href="/fontawesome/css/sharp-solid.min.css" />
|
||||
<link rel="stylesheet" href="/fontawesome/css/sharp-regular.min.css" />
|
||||
<script type="module" src="/frontend/wave.ts"></script>
|
||||
<link rel="stylesheet" href="public/fontawesome/css/fontawesome.min.css" />
|
||||
<link rel="stylesheet" href="public/fontawesome/css/brands.min.css" />
|
||||
<link rel="stylesheet" href="public/fontawesome/css/solid.min.css" />
|
||||
<link rel="stylesheet" href="public/fontawesome/css/sharp-solid.min.css" />
|
||||
<link rel="stylesheet" href="public/fontawesome/css/sharp-regular.min.css" />
|
||||
<script type="module" src="./dist-dev/wave.js"></script>
|
||||
<link rel="stylesheet" href="./dist-dev/wave.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="main"></div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"include": ["frontend/**/*"],
|
||||
"include": ["frontend/**/*", "emain/**/*"],
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"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