mirror of
https://github.com/wavetermdev/backup.git
synced 2026-08-05 13:57:07 -07:00
port to electron (#33)
This commit is contained in:
@@ -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.*
|
||||
|
||||
@@ -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
|
||||
+19
-401
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright 2024, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
let { contextBridge, ipcRenderer } = require("electron");
|
||||
|
||||
contextBridge.exposeInMainWorld("api", {});
|
||||
+15
-1
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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()
|
||||
|
||||
+74
-68
@@ -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,
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user