diff --git a/_client_cgo.go b/_client_cgo.go
new file mode 100644
index 0000000..dfa085e
--- /dev/null
+++ b/_client_cgo.go
@@ -0,0 +1,336 @@
+//go:build !cgo
+
+package client
+
+import (
+ _ "embed"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+ "time"
+
+ "apigo.cc/gojs"
+ "apigo.cc/gojs/goja"
+ "github.com/ssgo/tool/watcher"
+ webview "github.com/webview/webview_go"
+)
+
+//go:embed client.ts
+var clientTS string
+
+//go:embed README.md
+var clientMD string
+
+//go:embed app.js
+var appCode string
+
+var binds = map[string]any{}
+var bindsLock = sync.RWMutex{}
+
+var windowsId uint64
+var windows = map[uint64]*Webview{}
+var windowsLock = sync.RWMutex{}
+var waitChan chan bool
+
+func Bind(name string, fn any) {
+ bindsLock.Lock()
+ binds[name] = fn
+ bindsLock.Unlock()
+}
+
+func Unbind(name string) {
+ bindsLock.Lock()
+ delete(binds, name)
+ bindsLock.Unlock()
+}
+
+type Webview struct {
+ id uint64
+ w webview.WebView
+ isRunning bool
+ // isAsync bool
+ isDebug bool
+ webRoot string
+ watcher *watcher.Watcher
+ html string
+ url string
+ closeChan chan bool
+}
+
+func Wait() {
+ // 如果有窗口在运行,等待窗口关闭
+ windowsLock.RLock()
+ if len(windows) > 0 {
+ waitChan = make(chan bool, 1)
+ }
+ windowsLock.RUnlock()
+
+ // fmt.Println("====== wait start")
+ if waitChan != nil {
+ <-waitChan
+ waitChan = nil
+ }
+ // fmt.Println("====== wait end")
+}
+
+func CloseAll() {
+ windows1 := map[uint64]*Webview{}
+ windowsLock.Lock()
+ for id, w := range windows {
+ windows1[id] = w
+ }
+ windowsLock.Unlock()
+ for _, w := range windows1 {
+ w.close()
+ }
+}
+
+func (w *Webview) close() {
+ // fmt.Println("====== w.Close()")
+ if w.isRunning {
+ w.isRunning = false
+ // w.closeChan = make(chan bool, 1)
+ // fmt.Println("====== w.w.Terminate()")
+ w.w.Terminate()
+ // fmt.Println("====== w.w.Terminate() ok")
+ // <-w.closeChan
+ // w.closeChan = nil
+ }
+}
+
+func (w *Webview) doClose() {
+ // fmt.Println("====== w.doClose()")
+ if w.watcher != nil {
+ w.watcher.Stop()
+ w.watcher = nil
+ }
+ windowsLock.Lock()
+ delete(windows, w.id)
+ n := len(windows)
+ windowsLock.Unlock()
+
+ // fmt.Println("====== w.w.Destroy()")
+ w.w.Destroy()
+ // fmt.Println("====== w.w.Destroy() ok")
+
+ // 解除 w.Close 的等待
+ if w.closeChan != nil {
+ w.closeChan <- true
+ }
+
+ // 解除 gojs.WaitAll 的等待
+ if waitChan != nil && n == 0 {
+ waitChan <- true
+ }
+}
+
+func (w *Webview) loadHtml(html string) {
+ w.w.SetHtml(html)
+}
+
+func (w *Webview) loadURL(url string) {
+ if url != "" {
+ if strings.Contains(url, "://") {
+ w.w.Navigate(url)
+ } else {
+ if !filepath.IsAbs(url) {
+ curPath, _ := os.Getwd()
+ url = filepath.Join(curPath, url)
+ }
+ w.webRoot = filepath.Dir(url)
+ url = "file://" + url
+ w.w.Navigate(url)
+ }
+ }
+ w.w.Navigate(url)
+}
+
+func (w *Webview) reload() {
+ if w.html != "" {
+ w.loadHtml(w.html)
+ } else {
+ w.loadURL(w.url)
+ }
+}
+
+func (w *Webview) setTitle(title string) {
+ w.w.SetTitle(title)
+}
+
+func (w *Webview) setSize(width int, height int, sizeMode string) {
+ w.w.SetSize(width, height, getSizeMode(sizeMode))
+}
+
+func (w *Webview) eval(code string) {
+ w.w.Eval(code)
+}
+
+func (w *Webview) Close(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
+ w.close()
+ return nil
+}
+
+func (w *Webview) LoadHtml(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
+ args := gojs.MakeArgs(&argsIn, vm).Check(1)
+ w.loadHtml(args.Str(0))
+ return nil
+}
+
+func (w *Webview) LoadURL(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
+ args := gojs.MakeArgs(&argsIn, vm).Check(1)
+ w.loadURL(args.Str(0))
+ return nil
+}
+
+func (w *Webview) Dispatch(f func()) {
+ w.w.Dispatch(f)
+}
+
+func (w *Webview) SetTitle(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
+ args := gojs.MakeArgs(&argsIn, vm).Check(1)
+ w.setTitle(args.Str(0))
+ return nil
+}
+
+func (w *Webview) SetSize(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
+ args := gojs.MakeArgs(&argsIn, vm).Check(2)
+ w.setSize(args.Int(0), args.Int(1), args.Str(2))
+ return nil
+}
+
+func (w *Webview) Eval(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
+ args := gojs.MakeArgs(&argsIn, vm).Check(1)
+ w.eval(args.Str(0))
+ return nil
+}
+
+func (w *Webview) binds() {
+ tmpBinds := map[string]any{}
+ bindsLock.RLock()
+ for k, v := range binds {
+ tmpBinds[k] = v
+ }
+ bindsLock.RUnlock()
+ for k, v := range tmpBinds {
+ _ = w.w.Bind(k, v)
+ }
+ _ = w.w.Bind("__closeWindow", w.close)
+ _ = w.w.Bind("__setTitle", w.setTitle)
+ _ = w.w.Bind("__setSize", w.setSize)
+ _ = w.w.Bind("__eval", w.eval)
+ w.w.Init(appCode)
+}
+
+func getSizeMode(sizeMode string) webview.Hint {
+ switch sizeMode {
+ case "none":
+ return webview.HintNone
+ case "fixed":
+ return webview.HintFixed
+ case "min":
+ return webview.HintMin
+ case "max":
+ return webview.HintMax
+ default:
+ return webview.HintNone
+ }
+}
+
+func init() {
+ obj := map[string]any{
+ "open": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
+ args := gojs.MakeArgs(&argsIn, vm)
+ title := ""
+ width := 800
+ height := 600
+ sizeMode := "none"
+ isDebug := false
+ html := ""
+ url := ""
+ if opt := args.Obj(0); opt != nil {
+ if titleV := opt.Str("title"); titleV != "" {
+ title = titleV
+ }
+ if widthV := opt.Int("width"); widthV != 0 {
+ width = widthV
+ }
+ if heightV := opt.Int("height"); heightV != 0 {
+ height = heightV
+ }
+ if sizeModeV := opt.Str("sizeMode"); sizeModeV != "" {
+ sizeMode = sizeModeV
+ }
+ if isDebugV := opt.Bool("isDebug"); isDebugV {
+ isDebug = isDebugV
+ }
+ if htmlV := opt.Str("html"); htmlV != "" {
+ html = htmlV
+ }
+ if urlV := opt.Str("url"); urlV != "" {
+ url = urlV
+ if strings.HasPrefix(url, "file://") {
+ url = url[7:]
+ }
+ }
+ }
+
+ w := &Webview{isDebug: isDebug, html: html, url: url}
+ // startChan := make(chan bool, 1)
+ gojs.RunInMain(func() {
+ windowsLock.Lock()
+ w.id = windowsId
+ windowsId++
+ windows[w.id] = w
+ windowsLock.Unlock()
+ w.w = webview.New(isDebug)
+ w.w.SetTitle(title)
+ w.w.SetSize(width, height, getSizeMode(sizeMode))
+ w.binds()
+
+ if w.isDebug {
+ var isWaitingRun = false
+ if w.webRoot == "" && w.html != "" {
+ w.webRoot = "."
+ }
+ if w.webRoot != "" {
+ w.watcher, _ = watcher.Start([]string{w.webRoot}, []string{"html", "js", "css"}, func(filename string, event string) {
+ if !isWaitingRun {
+ isWaitingRun = true
+ go func() {
+ time.Sleep(time.Millisecond * 10)
+ isWaitingRun = false
+ w.w.Eval("location.reload()")
+ }()
+ }
+ })
+ }
+ }
+
+ w.isRunning = true
+
+ w.reload()
+ // startChan <- true
+ w.w.Run()
+ // fmt.Println("====== w.w.Run() end")
+ w.doClose()
+ })
+
+ // <-startChan
+ return vm.ToValue(gojs.MakeMap(w))
+ },
+ "closeAll": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
+ CloseAll()
+ return nil
+ },
+ }
+
+ gojs.Register("apigo.cc/gojs/client", gojs.Module{
+ Object: obj,
+ Desc: "web client framework by github.com/webview/webview",
+ TsCode: clientTS,
+ Example: clientMD,
+ WaitForStop: Wait,
+ OnKill: CloseAll,
+ })
+}
diff --git a/aaa/main.go b/aaa/main.go
new file mode 100644
index 0000000..f0c88ac
--- /dev/null
+++ b/aaa/main.go
@@ -0,0 +1,17 @@
+package main
+
+import (
+ "runtime"
+
+ webview "github.com/webview/webview_go"
+)
+
+func main() {
+ runtime.LockOSThread() // 锁定当前线程为主线程
+ w := webview.New(false)
+ defer w.Destroy()
+ w.SetTitle("WebView Demo")
+ w.SetSize(800, 600, webview.HintNone)
+ w.Navigate("https://example.com") // 或本地文件:"file:///path/to/index.html"
+ w.Run()
+}
diff --git a/cgo.yml b/cgo.yml
index 853fe84..abe3003 100644
--- a/cgo.yml
+++ b/cgo.yml
@@ -1,3 +1,3 @@
-CGO_ENABLED: true
+# CGO_ENABLED: true
CGO_CPPFLAGS:
- -I%PROJECT%/include
diff --git a/client.go b/client.go
index 9b4b6a8..3571fab 100644
--- a/client.go
+++ b/client.go
@@ -2,8 +2,6 @@ package client
import (
_ "embed"
- "os"
- "path/filepath"
"strings"
"sync"
"time"
@@ -11,9 +9,11 @@ import (
"apigo.cc/gojs"
"apigo.cc/gojs/goja"
"github.com/ssgo/tool/watcher"
- webview "github.com/webview/webview_go"
)
+// TODO 桌面应用使用 https://github.com/neutralinojs/neutralinojs
+// TODO App使用 https://pkg.go.dev/golang.org/x/mobile/cmd/gomobile
+
//go:embed client.ts
var clientTS string
@@ -43,19 +43,6 @@ func Unbind(name string) {
bindsLock.Unlock()
}
-type Webview struct {
- id uint64
- w webview.WebView
- isRunning bool
- // isAsync bool
- isDebug bool
- webRoot string
- watcher *watcher.Watcher
- html string
- url string
- closeChan chan bool
-}
-
func Wait() {
// 如果有窗口在运行,等待窗口关闭
windowsLock.RLock()
@@ -84,107 +71,6 @@ func CloseAll() {
}
}
-func (w *Webview) close() {
- // fmt.Println("====== w.Close()")
- if w.isRunning {
- w.isRunning = false
- // w.closeChan = make(chan bool, 1)
- // fmt.Println("====== w.w.Terminate()")
- w.w.Terminate()
- // fmt.Println("====== w.w.Terminate() ok")
- // <-w.closeChan
- // w.closeChan = nil
- }
-}
-
-func (w *Webview) doClose() {
- // fmt.Println("====== w.doClose()")
- if w.watcher != nil {
- w.watcher.Stop()
- w.watcher = nil
- }
- windowsLock.Lock()
- delete(windows, w.id)
- n := len(windows)
- windowsLock.Unlock()
-
- // fmt.Println("====== w.w.Destroy()")
- w.w.Destroy()
- // fmt.Println("====== w.w.Destroy() ok")
-
- // 解除 w.Close 的等待
- if w.closeChan != nil {
- w.closeChan <- true
- }
-
- // 解除 gojs.WaitAll 的等待
- if waitChan != nil && n == 0 {
- waitChan <- true
- }
-}
-
-func (w *Webview) loadHtml(html string) {
- w.w.SetHtml(html)
-}
-
-func (w *Webview) loadURL(url string) {
- if url != "" {
- if strings.Contains(url, "://") {
- w.w.Navigate(url)
- } else {
- if !filepath.IsAbs(url) {
- curPath, _ := os.Getwd()
- url = filepath.Join(curPath, url)
- }
- w.webRoot = filepath.Dir(url)
- url = "file://" + url
- w.w.Navigate(url)
- }
- }
- w.w.Navigate(url)
-}
-
-func (w *Webview) reload() {
- if w.html != "" {
- w.loadHtml(w.html)
- } else {
- w.loadURL(w.url)
- }
-}
-
-func (w *Webview) setTitle(title string) {
- w.w.SetTitle(title)
-}
-
-func (w *Webview) setSize(width int, height int, sizeMode string) {
- w.w.SetSize(width, height, getSizeMode(sizeMode))
-}
-
-func (w *Webview) eval(code string) {
- w.w.Eval(code)
-}
-
-func (w *Webview) Close(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
- w.close()
- return nil
-}
-
-func (w *Webview) LoadHtml(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
- args := gojs.MakeArgs(&argsIn, vm).Check(1)
- w.loadHtml(args.Str(0))
- return nil
-}
-
-func (w *Webview) LoadURL(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
- args := gojs.MakeArgs(&argsIn, vm).Check(1)
- w.loadURL(args.Str(0))
- return nil
-}
-
-func (w *Webview) Dispatch(f func()) {
- w.w.Dispatch(f)
-}
-
func (w *Webview) SetTitle(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(1)
w.setTitle(args.Str(0))
@@ -203,36 +89,21 @@ func (w *Webview) Eval(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
return nil
}
-func (w *Webview) binds() {
- tmpBinds := map[string]any{}
- bindsLock.RLock()
- for k, v := range binds {
- tmpBinds[k] = v
- }
- bindsLock.RUnlock()
- for k, v := range tmpBinds {
- _ = w.w.Bind(k, v)
- }
- _ = w.w.Bind("__closeWindow", w.close)
- _ = w.w.Bind("__setTitle", w.setTitle)
- _ = w.w.Bind("__setSize", w.setSize)
- _ = w.w.Bind("__eval", w.eval)
- w.w.Init(appCode)
+func (w *Webview) Close(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
+ w.close()
+ return nil
}
-func getSizeMode(sizeMode string) webview.Hint {
- switch sizeMode {
- case "none":
- return webview.HintNone
- case "fixed":
- return webview.HintFixed
- case "min":
- return webview.HintMin
- case "max":
- return webview.HintMax
- default:
- return webview.HintNone
- }
+func (w *Webview) LoadHtml(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
+ args := gojs.MakeArgs(&argsIn, vm).Check(1)
+ w.loadHtml(args.Str(0))
+ return nil
+}
+
+func (w *Webview) LoadURL(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
+ args := gojs.MakeArgs(&argsIn, vm).Check(1)
+ w.loadURL(args.Str(0))
+ return nil
}
func init() {
@@ -275,30 +146,37 @@ func init() {
w := &Webview{isDebug: isDebug, html: html, url: url}
// startChan := make(chan bool, 1)
+ // fmt.Println("====== start")
gojs.RunInMain(func() {
+ // fmt.Println("====== gojs.RunInMain")
windowsLock.Lock()
w.id = windowsId
windowsId++
windows[w.id] = w
windowsLock.Unlock()
- w.w = webview.New(isDebug)
- w.w.SetTitle(title)
- w.w.SetSize(width, height, getSizeMode(sizeMode))
+ // fmt.Println("====== w.New()")
+ w.New(isDebug)
+ // fmt.Println("====== 1")
+ w.setTitle(title)
+ // fmt.Println("====== 2")
+ w.setSize(width, height, sizeMode)
+ // fmt.Println("====== 3")
w.binds()
+ // fmt.Println("====== w.Run()")
if w.isDebug {
var isWaitingRun = false
if w.webRoot == "" && w.html != "" {
w.webRoot = "."
}
if w.webRoot != "" {
- w.watcher, _ = watcher.Start([]string{w.webRoot}, []string{"html", "js", "css"}, func(filename string, event string) {
+ w.watcher, _ = watcher.Start([]string{w.webRoot}, []string{"html", "js", "css"}, nil, func(filename string, event string) {
if !isWaitingRun {
isWaitingRun = true
go func() {
time.Sleep(time.Millisecond * 10)
isWaitingRun = false
- w.w.Eval("location.reload()")
+ w.eval("location.reload()")
}()
}
})
@@ -307,10 +185,12 @@ func init() {
w.isRunning = true
+ // fmt.Println("====== w.reload()")
w.reload()
// startChan <- true
- w.w.Run()
- // fmt.Println("====== w.w.Run() end")
+ // fmt.Println("====== w.Run()")
+ w.Run()
+ // fmt.Println("====== w.Run() end")
w.doClose()
})
@@ -325,7 +205,7 @@ func init() {
gojs.Register("apigo.cc/gojs/client", gojs.Module{
Object: obj,
- Desc: "web client framework by github.com/webview/webview",
+ Desc: "web client framework by github.com/chromedp/chromedp or github.com/webview/webview",
TsCode: clientTS,
Example: clientMD,
WaitForStop: Wait,
diff --git a/client_chrome.go b/client_chrome.go
new file mode 100644
index 0000000..7d0d458
--- /dev/null
+++ b/client_chrome.go
@@ -0,0 +1,352 @@
+//go:build !cgo
+// +build !cgo
+
+package client
+
+import (
+ "bufio"
+ "context"
+ _ "embed"
+ "fmt"
+ "net/url"
+ "os"
+ "path/filepath"
+ "reflect"
+ "runtime"
+ "strconv"
+ "strings"
+
+ chromeRuntime "github.com/chromedp/cdproto/runtime"
+ "github.com/chromedp/chromedp"
+ "github.com/ssgo/tool/watcher"
+ "github.com/ssgo/u"
+)
+
+type Webview struct {
+ id uint64
+ ctx context.Context
+ cancel context.CancelFunc
+ isRunning bool
+ // isAsync bool
+ isDebug bool
+ webRoot string
+ watcher *watcher.Watcher
+ html string
+ url string
+ closeChan chan bool
+ width int
+ height int
+ title string
+ evals []string
+ bindFuncs map[string]any
+}
+
+var chromeVersion = -1
+
+func (w *Webview) New(isDebug bool) {
+ if chromeVersion == -1 {
+ chromeVersion = getChromeVersion()
+ }
+ w.isDebug = isDebug
+}
+
+func (w *Webview) close() {
+ // fmt.Println("====== w.Close()")
+ if w.isRunning {
+ w.isRunning = false
+ // w.closeChan = make(chan bool, 1)
+ // fmt.Println("====== w.w.Terminate()")
+ w.cancel()
+ // fmt.Println("====== w.w.Terminate() ok")
+ // <-w.closeChan
+ // w.closeChan = nil
+ }
+}
+
+func (w *Webview) doClose() {
+ // fmt.Println("====== w.doClose()")
+ if w.watcher != nil {
+ w.watcher.Stop()
+ w.watcher = nil
+ }
+ windowsLock.Lock()
+ delete(windows, w.id)
+ n := len(windows)
+ windowsLock.Unlock()
+
+ // fmt.Println("====== w.w.Destroy()")
+ if w.cancel != nil {
+ w.cancel()
+ }
+ // fmt.Println("====== w.w.Destroy() ok")
+
+ // 解除 w.Close 的等待
+ if w.closeChan != nil {
+ w.closeChan <- true
+ }
+
+ // 解除 gojs.WaitAll 的等待
+ if waitChan != nil && n == 0 {
+ waitChan <- true
+ }
+}
+
+func (w *Webview) loadHtml(html string) {
+ w.url = "data:text/html;charset=utf-8," + url.PathEscape(html)
+ if w.ctx != nil {
+ chromedp.Run(w.ctx, chromedp.Navigate(w.url))
+ }
+}
+
+func (w *Webview) loadURL(url string) {
+ if url != "" {
+ if strings.Contains(url, "://") {
+ // chromedp.Run(w.ctx, chromedp.Navigate(url))
+ w.url = url
+ } else {
+ if !filepath.IsAbs(url) {
+ curPath, _ := os.Getwd()
+ url = filepath.Join(curPath, url)
+ }
+ w.webRoot = filepath.Dir(url)
+ w.url = "file://" + url
+ // chromedp.Run(w.ctx, chromedp.Navigate(url))
+ }
+ } else {
+ // chromedp.Run(w.ctx, chromedp.Navigate("about:blank"))
+ w.url = "about:blank"
+ }
+
+ if w.ctx != nil {
+ chromedp.Run(w.ctx, chromedp.Navigate(w.url))
+ }
+}
+
+func (w *Webview) reload() {
+ if w.html != "" {
+ w.loadHtml(w.html)
+ } else {
+ w.loadURL(w.url)
+ }
+}
+
+func (w *Webview) setTitle(title string) {
+ w.title = title
+ if w.ctx != nil {
+ chromedp.Run(w.ctx, chromedp.Evaluate(`document.title = "`+w.title+`"`, nil))
+ }
+}
+
+func (w *Webview) setSize(width int, height int, sizeMode string) {
+ w.width = width
+ w.height = height
+ if w.ctx != nil {
+ // chromedp.Run(w.ctx, chromedp.EmulateViewport(int64(w.width), int64(w.height)))
+ }
+}
+
+func (w *Webview) eval(code string) any {
+ if w.ctx == nil {
+ w.evals = append(w.evals, code)
+ return nil
+ }
+
+ var result any
+ chromedp.Run(w.ctx, chromedp.Evaluate(code, &result))
+ return result
+}
+
+func (w *Webview) Dispatch(f func()) {
+ // w.w.Dispatch(f)
+ f()
+}
+
+func (w *Webview) Run() {
+ // chromeVersion = 0
+ if chromeVersion > 0 {
+ // 根据版本调整配置
+ opts := append(chromedp.DefaultExecAllocatorOptions[:],
+ chromedp.Flag("headless", false), // 显示窗口
+ chromedp.Flag("window-size", fmt.Sprintf("%d,%d", w.width, w.height)), // 窗口尺寸
+ chromedp.Flag("app", w.url), // 直接启动到目标页
+ chromedp.Flag("disable-notifications", true), // 禁用通知
+ chromedp.Flag("no-first-run", true), // 跳过首次运行
+ chromedp.Flag("no-default-browser-check", true), // 禁用默认浏览器检查
+ chromedp.Flag("disable-component-update", true), // 禁用组件更新
+ chromedp.Flag("disable-background-networking", true), // 禁用后台网络
+ chromedp.Flag("disable-client-side-phishing-detection", true), // 禁用钓鱼检测
+ )
+
+ if chromeVersion >= 115 {
+ // 新版本
+ opts = append(opts,
+ chromedp.Flag("enable-automation", false),
+ // chromedp.Flag("excludeSwitches", []string{"enable-automation"}),
+ chromedp.Flag("useAutomationExtension", false),
+ )
+ } else if chromeVersion >= 90 {
+ // Chrome 90+ 参数
+ opts = append(opts,
+ chromedp.Flag("disable-blink-features", "AutomationControlled"),
+ )
+ } else {
+ // Chrome 89 及以下参数
+ opts = append(opts,
+ chromedp.Flag("disable-infobars", true),
+ chromedp.Flag("disable-extensions", true),
+ )
+ }
+
+ allocCtx, cancel1 := chromedp.NewExecAllocator(context.Background(), opts...)
+ ctx, cancel2 := chromedp.NewContext(allocCtx)
+ w.ctx = ctx
+ w.cancel = func() {
+ cancel2()
+ cancel1()
+ }
+
+ codes := []string{getDetectionScript(chromeVersion)}
+ if w.evals != nil {
+ codes = append(codes, strings.Join(w.evals, "\n"))
+ }
+
+ chromedp.ListenTarget(w.ctx, func(ev any) {
+ if ev, ok := ev.(*chromeRuntime.EventBindingCalled); ok {
+ if fn, ok := w.bindFuncs[ev.Name]; ok {
+ fnV := reflect.ValueOf(fn)
+ if fnV.Kind() == reflect.Func {
+ var args []reflect.Value
+ if ev.Payload != "" {
+ argsV := u.FinalValue(reflect.ValueOf(u.UnJson(ev.Payload, nil)))
+ if argsV.Kind() == reflect.Slice {
+ for i := 0; i < fnV.Type().NumIn(); i++ {
+ argType := fnV.Type().In(i)
+ argValueP := reflect.New(argType)
+ argValue := argValueP.Elem()
+ if i < argsV.Len() {
+ u.Convert(argsV.Index(i), argValueP)
+ }
+ args = append(args, argValue)
+ }
+ }
+ go func() {
+ _ = fnV.Call(args)
+ }()
+ }
+ }
+ }
+ }
+ })
+
+ actions := []chromedp.Action{}
+
+ for k, _ := range w.bindFuncs {
+ actions = append(actions, chromeRuntime.AddBinding(k))
+ codes = append(codes, fmt.Sprintf("window.__%s = %s; window.%s = function () { __%s(JSON.stringify(Array.from(arguments))) }", k, k, k, k))
+ }
+ codes = append(codes, appCode)
+ actions = append(actions, chromedp.Evaluate(strings.Join(codes, "\n"), nil))
+
+ exitChan := make(chan bool, 1)
+ chromedp.Run(w.ctx, actions...)
+
+ go func() {
+ <-w.ctx.Done()
+ exitChan <- true
+ }()
+
+ <-exitChan
+ } else {
+ if runtime.GOOS == "windows" {
+ u.RunCommand("start", w.url)
+ } else if runtime.GOOS == "darwin" {
+ u.RunCommand("open", w.url)
+ } else {
+ u.RunCommand("xdg-open", w.url)
+ }
+
+ fmt.Println("URL: ", w.url)
+ fmt.Println("Press Enter to exit...")
+ reader := bufio.NewReader(os.Stdin)
+ _, _ = reader.ReadString('\n')
+ }
+}
+
+func (w *Webview) binds() {
+ w.bindFuncs = map[string]any{}
+ bindsLock.RLock()
+ for k, v := range binds {
+ w.bindFuncs[k] = v
+ }
+ bindsLock.RUnlock()
+
+ w.bindFuncs["__closeWindow"] = w.close
+ w.bindFuncs["__setTitle"] = w.setTitle
+ w.bindFuncs["__setSize"] = w.setSize
+ w.bindFuncs["__eval"] = w.eval
+}
+
+// 获取浏览器版本(精确到主版本号)
+func getChromeVersion() int {
+ var ua string
+ ctx, cancel := chromedp.NewContext(context.Background())
+ defer cancel()
+ if err := chromedp.Run(ctx,
+ chromedp.Evaluate("navigator.userAgent", &ua),
+ ); err != nil {
+ return 0
+ }
+
+ // 解析示例:Mozilla/5.0... Chrome/124.0.0.0...
+ start := strings.Index(ua, "Chrome/")
+ if start == -1 {
+ return 0
+ }
+
+ versionPart := ua[start+7:]
+ end := strings.Index(versionPart, ".")
+ if end == -1 {
+ return 0
+ }
+
+ version, _ := strconv.Atoi(versionPart[:end])
+ return version
+}
+
+// 生成版本自适应的检测脚本
+func getDetectionScript(version int) string {
+ baseJS := `
+ (() => {
+ // 核心属性修改
+ Object.defineProperty(navigator, 'webdriver', {
+ get: () => false,
+ configurable: true
+ });
+
+ // 通用内存清理
+ const legacyProps = ['$cdc', '__driver_evaluate'];
+ const modernProps = ['_cdc', '__driver', '__automation'];
+ [...legacyProps, ...modernProps].forEach(p => {
+ try { delete window[p] } catch(e){}
+ try { delete document[p] } catch(e){}
+ });`
+
+ if version >= 120 {
+ // 修复点:将 JS 模板字符串改为普通字符串拼接
+ baseJS += `
+ // 伪造 Chrome 运行时
+ window.chrome = {
+ runtime: {
+ id: 'mock_extension',
+ getManifest: () => ({ version: '1.0' }),
+ getURL: function(path) {
+ return 'chrome-extension://mock_extension/' + path;
+ },
+ onMessage: { addListener: function(){} }
+ }
+ };`
+ }
+
+ baseJS += "\n})();"
+ return baseJS + "\n"
+}
diff --git a/client_webview.go b/client_webview.go
new file mode 100644
index 0000000..ab16fa9
--- /dev/null
+++ b/client_webview.go
@@ -0,0 +1,153 @@
+//go:build cgo
+// +build cgo
+
+package client
+
+import (
+ _ "embed"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/ssgo/tool/watcher"
+ webview "github.com/webview/webview_go"
+)
+
+type Webview struct {
+ id uint64
+ w webview.WebView
+ isRunning bool
+ // isAsync bool
+ isDebug bool
+ webRoot string
+ watcher *watcher.Watcher
+ html string
+ url string
+ closeChan chan bool
+}
+
+func (w *Webview) New(isDebug bool) {
+ w.w = webview.New(isDebug)
+}
+
+func (w *Webview) close() {
+ // fmt.Println("====== w.Close()")
+ if w.isRunning {
+ w.isRunning = false
+ // w.closeChan = make(chan bool, 1)
+ // fmt.Println("====== w.w.Terminate()")
+ w.w.Terminate()
+ // fmt.Println("====== w.w.Terminate() ok")
+ // <-w.closeChan
+ // w.closeChan = nil
+ }
+}
+
+func (w *Webview) doClose() {
+ // fmt.Println("====== w.doClose()")
+ if w.watcher != nil {
+ w.watcher.Stop()
+ w.watcher = nil
+ }
+ windowsLock.Lock()
+ delete(windows, w.id)
+ n := len(windows)
+ windowsLock.Unlock()
+
+ // fmt.Println("====== w.w.Destroy()")
+ w.w.Destroy()
+ // fmt.Println("====== w.w.Destroy() ok")
+
+ // 解除 w.Close 的等待
+ if w.closeChan != nil {
+ w.closeChan <- true
+ }
+
+ // 解除 gojs.WaitAll 的等待
+ if waitChan != nil && n == 0 {
+ waitChan <- true
+ }
+}
+
+func (w *Webview) loadHtml(html string) {
+ w.w.SetHtml(html)
+}
+
+func (w *Webview) loadURL(url string) {
+ if url != "" {
+ if strings.Contains(url, "://") {
+ w.w.Navigate(url)
+ } else {
+ if !filepath.IsAbs(url) {
+ curPath, _ := os.Getwd()
+ url = filepath.Join(curPath, url)
+ }
+ w.webRoot = filepath.Dir(url)
+ url = "file://" + url
+ w.w.Navigate(url)
+ }
+ } else {
+ w.w.Navigate("about:blank")
+ }
+}
+
+func (w *Webview) reload() {
+ if w.html != "" {
+ w.loadHtml(w.html)
+ } else {
+ w.loadURL(w.url)
+ }
+}
+
+func (w *Webview) setTitle(title string) {
+ w.w.SetTitle(title)
+}
+
+func (w *Webview) setSize(width int, height int, sizeMode string) {
+ w.w.SetSize(width, height, getSizeMode(sizeMode))
+}
+
+func (w *Webview) eval(code string) any {
+ w.w.Eval(code)
+ return nil
+}
+
+func (w *Webview) Dispatch(f func()) {
+ w.w.Dispatch(f)
+}
+
+func (w *Webview) Run() {
+ w.w.Run()
+}
+
+func (w *Webview) binds() {
+ tmpBinds := map[string]any{}
+ bindsLock.RLock()
+ for k, v := range binds {
+ tmpBinds[k] = v
+ }
+ bindsLock.RUnlock()
+ for k, v := range tmpBinds {
+ _ = w.w.Bind(k, v)
+ }
+ _ = w.w.Bind("__closeWindow", w.close)
+ _ = w.w.Bind("__setTitle", w.setTitle)
+ _ = w.w.Bind("__setSize", w.setSize)
+ _ = w.w.Bind("__eval", w.eval)
+ w.w.Init(appCode)
+}
+
+func getSizeMode(sizeMode string) webview.Hint {
+ switch sizeMode {
+ case "none":
+ return webview.HintNone
+ case "fixed":
+ return webview.HintFixed
+ case "min":
+ return webview.HintMin
+ case "max":
+ return webview.HintMax
+ default:
+ return webview.HintNone
+ }
+}
diff --git a/demo/app/.gitignore b/demo/app/.gitignore
new file mode 100644
index 0000000..4a3dc9d
--- /dev/null
+++ b/demo/app/.gitignore
@@ -0,0 +1,13 @@
+# Developer tools' files
+.lite_workspace.lua
+
+# Neutralinojs binaries and builds
+/bin
+/dist
+
+# Neutralinojs client (minified)
+neutralino.js
+
+# Neutralinojs related files
+.storage
+*.log
diff --git a/demo/app/LICENSE b/demo/app/LICENSE
new file mode 100644
index 0000000..2f31491
--- /dev/null
+++ b/demo/app/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Neutralinojs and contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/demo/app/README.md b/demo/app/README.md
new file mode 100644
index 0000000..fe348b7
--- /dev/null
+++ b/demo/app/README.md
@@ -0,0 +1,15 @@
+# neutralinojs-minimal
+
+The default template for a Neutralinojs app. It's possible to use your favorite frontend framework by using [these steps](https://neutralino.js.org/docs/getting-started/using-frontend-libraries).
+
+## Contributors
+
+[](https://github.com/neutralinojs/neutralinojs-minimal/graphs/contributors)
+
+## License
+
+[MIT](LICENSE)
+
+## Icon credits
+
+- `trayIcon.png` - Made by [Freepik](https://www.freepik.com) and downloaded from [Flaticon](https://www.flaticon.com)
diff --git a/demo/app/neutralino.config.json b/demo/app/neutralino.config.json
new file mode 100644
index 0000000..8d47bc6
--- /dev/null
+++ b/demo/app/neutralino.config.json
@@ -0,0 +1,83 @@
+{
+ "$schema": "https://raw.githubusercontent.com/neutralinojs/neutralinojs/main/schemas/neutralino.config.schema.json",
+ "applicationId": "js.neutralino.sample",
+ "version": "1.0.0",
+ "defaultMode": "window",
+ "port": 0,
+ "documentRoot": "/resources/",
+ "url": "/",
+ "enableServer": true,
+ "enableNativeAPI": true,
+ "tokenSecurity": "one-time",
+ "logging": {
+ "enabled": true,
+ "writeToLogFile": true
+ },
+ "nativeAllowList": [
+ "app.*",
+ "os.*",
+ "debug.log"
+ ],
+ "globalVariables": {
+ "TEST1": "Hello",
+ "TEST2": [
+ 2,
+ 4,
+ 5
+ ],
+ "TEST3": {
+ "value1": 10,
+ "value2": {}
+ }
+ },
+ "modes": {
+ "window": {
+ "title": "app",
+ "width": 800,
+ "height": 500,
+ "minWidth": 400,
+ "minHeight": 200,
+ "center": true,
+ "fullScreen": false,
+ "alwaysOnTop": false,
+ "icon": "/resources/icons/appIcon.png",
+ "enableInspector": true,
+ "borderless": false,
+ "maximize": false,
+ "hidden": false,
+ "resizable": true,
+ "exitProcessOnClose": false
+ },
+ "browser": {
+ "globalVariables": {
+ "TEST": "Test value browser"
+ },
+ "nativeBlockList": [
+ "filesystem.*"
+ ]
+ },
+ "cloud": {
+ "url": "/resources/#cloud",
+ "nativeAllowList": [
+ "app.*"
+ ]
+ },
+ "chrome": {
+ "width": 800,
+ "height": 500,
+ "args": "--user-agent=\"Neutralinojs chrome mode\"",
+ "nativeBlockList": [
+ "filesystem.*",
+ "os.*"
+ ]
+ }
+ },
+ "cli": {
+ "binaryName": "app",
+ "resourcesPath": "/resources/",
+ "extensionsPath": "/extensions/",
+ "clientLibrary": "/resources/js/neutralino.js",
+ "binaryVersion": "6.1.0",
+ "clientVersion": "6.1.0"
+ }
+}
\ No newline at end of file
diff --git a/demo/app/resources/icons/appIcon.png b/demo/app/resources/icons/appIcon.png
new file mode 100644
index 0000000..d708bc3
Binary files /dev/null and b/demo/app/resources/icons/appIcon.png differ
diff --git a/demo/app/resources/icons/logo.gif b/demo/app/resources/icons/logo.gif
new file mode 100644
index 0000000..058c9bf
Binary files /dev/null and b/demo/app/resources/icons/logo.gif differ
diff --git a/demo/app/resources/icons/trayIcon.png b/demo/app/resources/icons/trayIcon.png
new file mode 100644
index 0000000..bcfeb18
Binary files /dev/null and b/demo/app/resources/icons/trayIcon.png differ
diff --git a/demo/app/resources/index.html b/demo/app/resources/index.html
new file mode 100644
index 0000000..edcee61
--- /dev/null
+++ b/demo/app/resources/index.html
@@ -0,0 +1,27 @@
+
+
+
+
+ NeutralinoJs sample app
+
+
+
+
+
NeutralinoJs
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demo/app/resources/js/main.js b/demo/app/resources/js/main.js
new file mode 100644
index 0000000..09c297f
--- /dev/null
+++ b/demo/app/resources/js/main.js
@@ -0,0 +1,99 @@
+// This is just a sample app. You can structure your Neutralinojs app code as you wish.
+// This example app is written with vanilla JavaScript and HTML.
+// Feel free to use any frontend framework you like :)
+// See more details: https://neutralino.js.org/docs/how-to/use-a-frontend-library
+
+/*
+ Function to display information about the Neutralino app.
+ This function updates the content of the 'info' element in the HTML
+ with details regarding the running Neutralino application, including
+ its ID, port, operating system, and version information.
+*/
+function showInfo() {
+ document.getElementById('info').innerHTML = `
+ ${NL_APPID} is running on port ${NL_PORT} inside ${NL_OS}
+
+ server: v${NL_VERSION} . client: v${NL_CVERSION}
+ `;
+}
+
+/*
+ Function to open the official Neutralino documentation in the default web browser.
+*/
+function openDocs() {
+ Neutralino.os.open("https://neutralino.js.org/docs");
+}
+
+/*
+ Function to open a tutorial video on Neutralino's official YouTube channel in the default web browser.
+*/
+function openTutorial() {
+ Neutralino.os.open("https://www.youtube.com/c/CodeZri");
+}
+
+/*
+ Function to set up a system tray menu with options specific to the window mode.
+ This function checks if the application is running in window mode, and if so,
+ it defines the tray menu items and sets up the tray accordingly.
+*/
+function setTray() {
+ // Tray menu is only available in window mode
+ if(NL_MODE != "window") {
+ console.log("INFO: Tray menu is only available in the window mode.");
+ return;
+ }
+
+ // Define tray menu items
+ let tray = {
+ icon: "/resources/icons/trayIcon.png",
+ menuItems: [
+ {id: "VERSION", text: "Get version"},
+ {id: "SEP", text: "-"},
+ {id: "QUIT", text: "Quit"}
+ ]
+ };
+
+ // Set the tray menu
+ Neutralino.os.setTray(tray);
+}
+
+/*
+ Function to handle click events on the tray menu items.
+ This function performs different actions based on the clicked item's ID,
+ such as displaying version information or exiting the application.
+*/
+function onTrayMenuItemClicked(event) {
+ switch(event.detail.id) {
+ case "VERSION":
+ // Display version information
+ Neutralino.os.showMessageBox("Version information",
+ `Neutralinojs server: v${NL_VERSION} | Neutralinojs client: v${NL_CVERSION}`);
+ break;
+ case "QUIT":
+ // Exit the application
+ Neutralino.app.exit();
+ break;
+ }
+}
+
+/*
+ Function to handle the window close event by gracefully exiting the Neutralino application.
+*/
+function onWindowClose() {
+ Neutralino.app.exit();
+}
+
+// Initialize Neutralino
+Neutralino.init();
+
+// Register event listeners
+Neutralino.events.on("trayMenuItemClicked", onTrayMenuItemClicked);
+Neutralino.events.on("windowClose", onWindowClose);
+
+// Conditional initialization: Set up system tray if not running on macOS
+if(NL_OS != "Darwin") { // TODO: Fix https://github.com/neutralinojs/neutralinojs/issues/615
+ setTray();
+}
+
+// Display app information
+showInfo();
diff --git a/demo/app/resources/js/neutralino.d.ts b/demo/app/resources/js/neutralino.d.ts
new file mode 100644
index 0000000..97dc7e6
--- /dev/null
+++ b/demo/app/resources/js/neutralino.d.ts
@@ -0,0 +1,897 @@
+// Type definitions for Neutralino 6.1.0
+// Project: https://github.com/neutralinojs
+// Definitions project: https://github.com/neutralinojs/neutralino.js
+
+declare namespace Neutralino {
+
+namespace filesystem {
+ interface DirectoryEntry {
+ entry: string;
+ path: string;
+ type: string;
+ }
+ interface FileReaderOptions {
+ pos: number;
+ size: number;
+ }
+ interface DirectoryReaderOptions {
+ recursive: boolean;
+ }
+ interface OpenedFile {
+ id: number;
+ eof: boolean;
+ pos: number;
+ lastRead: number;
+ }
+ interface Stats {
+ size: number;
+ isFile: boolean;
+ isDirectory: boolean;
+ createdAt: number;
+ modifiedAt: number;
+ }
+ interface Watcher {
+ id: number;
+ path: string;
+ }
+ interface CopyOptions {
+ recursive: boolean;
+ overwrite: boolean;
+ skip: boolean;
+ }
+ interface PathParts {
+ rootName: string;
+ rootDirectory: string;
+ rootPath: string;
+ relativePath: string;
+ parentPath: string;
+ filename: string;
+ stem: string;
+ extension: string;
+ }
+ interface Permissions {
+ all: boolean;
+ ownerAll: boolean;
+ ownerRead: boolean;
+ ownerWrite: boolean;
+ ownerExec: boolean;
+ groupAll: boolean;
+ groupRead: boolean;
+ groupWrite: boolean;
+ groupExec: boolean;
+ othersAll: boolean;
+ othersRead: boolean;
+ othersWrite: boolean;
+ othersExec: boolean;
+ }
+ type PermissionsMode = "ADD" | "REPLACE" | "REMOVE";
+ function createDirectory(path: string): Promise;
+ function remove(path: string): Promise;
+ function writeFile(path: string, data: string): Promise;
+ function appendFile(path: string, data: string): Promise;
+ function writeBinaryFile(path: string, data: ArrayBuffer): Promise;
+ function appendBinaryFile(path: string, data: ArrayBuffer): Promise;
+ function readFile(path: string, options?: FileReaderOptions): Promise;
+ function readBinaryFile(path: string, options?: FileReaderOptions): Promise;
+ function openFile(path: string): Promise;
+ function createWatcher(path: string): Promise;
+ function removeWatcher(id: number): Promise;
+ function getWatchers(): Promise;
+ function updateOpenedFile(id: number, event: string, data?: any): Promise;
+ function getOpenedFileInfo(id: number): Promise;
+ function readDirectory(path: string, options?: DirectoryReaderOptions): Promise;
+ function copy(source: string, destination: string, options?: CopyOptions): Promise;
+ function move(source: string, destination: string): Promise;
+ function getStats(path: string): Promise;
+ function getAbsolutePath(path: string): Promise;
+ function getRelativePath(path: string, base?: string): Promise;
+ function getPathParts(path: string): Promise;
+ function getPermissions(path: string): Promise;
+ function setPermissions(path: string, permissions: Permissions, mode: PermissionsMode): Promise;
+}
+namespace os {
+ // debug
+ enum LoggerType {
+ WARNING = "WARNING",
+ ERROR = "ERROR",
+ INFO = "INFO"
+ }
+ // os
+ enum Icon {
+ WARNING = "WARNING",
+ ERROR = "ERROR",
+ INFO = "INFO",
+ QUESTION = "QUESTION"
+ }
+ enum MessageBoxChoice {
+ OK = "OK",
+ OK_CANCEL = "OK_CANCEL",
+ YES_NO = "YES_NO",
+ YES_NO_CANCEL = "YES_NO_CANCEL",
+ RETRY_CANCEL = "RETRY_CANCEL",
+ ABORT_RETRY_IGNORE = "ABORT_RETRY_IGNORE"
+ }
+ //clipboard
+ enum ClipboardFormat {
+ unknown = "unknown",
+ text = "text",
+ image = "image"
+ }
+ // NL_GLOBALS
+ enum Mode {
+ window = "window",
+ browser = "browser",
+ cloud = "cloud",
+ chrome = "chrome"
+ }
+ enum OperatingSystem {
+ Linux = "Linux",
+ Windows = "Windows",
+ Darwin = "Darwin",
+ FreeBSD = "FreeBSD",
+ Unknown = "Unknown"
+ }
+ enum Architecture {
+ x64 = "x64",
+ arm = "arm",
+ itanium = "itanium",
+ ia32 = "ia32",
+ unknown = "unknown"
+ }
+ interface ExecCommandOptions {
+ stdIn?: string;
+ background?: boolean;
+ cwd?: string;
+ }
+ interface ExecCommandResult {
+ pid: number;
+ stdOut: string;
+ stdErr: string;
+ exitCode: number;
+ }
+ interface SpawnedProcess {
+ id: number;
+ pid: number;
+ }
+ interface SpawnedProcessOptions {
+ cwd?: string;
+ envs?: Record;
+ }
+ interface Envs {
+ [key: string]: string;
+ }
+ interface OpenDialogOptions {
+ multiSelections?: boolean;
+ filters?: Filter[];
+ defaultPath?: string;
+ }
+ interface FolderDialogOptions {
+ defaultPath?: string;
+ }
+ interface SaveDialogOptions {
+ forceOverwrite?: boolean;
+ filters?: Filter[];
+ defaultPath?: string;
+ }
+ interface Filter {
+ name: string;
+ extensions: string[];
+ }
+ interface TrayOptions {
+ icon: string;
+ menuItems: TrayMenuItem[];
+ }
+ interface TrayMenuItem {
+ id?: string;
+ text: string;
+ isDisabled?: boolean;
+ isChecked?: boolean;
+ }
+ type KnownPath = "config" | "data" | "cache" | "documents" | "pictures" | "music" | "video" | "downloads" | "savedGames1" | "savedGames2" | "temp";
+ function execCommand(command: string, options?: ExecCommandOptions): Promise;
+ function spawnProcess(command: string, options?: SpawnedProcessOptions): Promise;
+ function updateSpawnedProcess(id: number, event: string, data?: any): Promise;
+ function getSpawnedProcesses(): Promise;
+ function getEnv(key: string): Promise;
+ function getEnvs(): Promise;
+ function showOpenDialog(title?: string, options?: OpenDialogOptions): Promise;
+ function showFolderDialog(title?: string, options?: FolderDialogOptions): Promise;
+ function showSaveDialog(title?: string, options?: SaveDialogOptions): Promise;
+ function showNotification(title: string, content: string, icon?: Icon): Promise;
+ function showMessageBox(title: string, content: string, choice?: MessageBoxChoice, icon?: Icon): Promise;
+ function setTray(options: TrayOptions): Promise;
+ function open(url: string): Promise;
+ function getPath(name: KnownPath): Promise;
+}
+namespace computer {
+ interface MemoryInfo {
+ physical: {
+ total: number;
+ available: number;
+ };
+ virtual: {
+ total: number;
+ available: number;
+ };
+ }
+ interface KernelInfo {
+ variant: string;
+ version: string;
+ }
+ interface OSInfo {
+ name: string;
+ description: string;
+ version: string;
+ }
+ interface CPUInfo {
+ vendor: string;
+ model: string;
+ frequency: number;
+ architecture: string;
+ logicalThreads: number;
+ physicalCores: number;
+ physicalUnits: number;
+ }
+ interface Display {
+ id: number;
+ resolution: Resolution;
+ dpi: number;
+ bpp: number;
+ refreshRate: number;
+ }
+ interface Resolution {
+ width: number;
+ height: number;
+ }
+ interface MousePosition {
+ x: number;
+ y: number;
+ }
+ function getMemoryInfo(): Promise;
+ function getArch(): Promise;
+ function getKernelInfo(): Promise;
+ function getOSInfo(): Promise;
+ function getCPUInfo(): Promise;
+ function getDisplays(): Promise;
+ function getMousePosition(): Promise;
+}
+namespace storage {
+ function setData(key: string, data: string): Promise;
+ function getData(key: string): Promise;
+ function getKeys(): Promise;
+}
+namespace debug {
+ // debug
+ enum LoggerType {
+ WARNING = "WARNING",
+ ERROR = "ERROR",
+ INFO = "INFO"
+ }
+ // os
+ enum Icon {
+ WARNING = "WARNING",
+ ERROR = "ERROR",
+ INFO = "INFO",
+ QUESTION = "QUESTION"
+ }
+ enum MessageBoxChoice {
+ OK = "OK",
+ OK_CANCEL = "OK_CANCEL",
+ YES_NO = "YES_NO",
+ YES_NO_CANCEL = "YES_NO_CANCEL",
+ RETRY_CANCEL = "RETRY_CANCEL",
+ ABORT_RETRY_IGNORE = "ABORT_RETRY_IGNORE"
+ }
+ //clipboard
+ enum ClipboardFormat {
+ unknown = "unknown",
+ text = "text",
+ image = "image"
+ }
+ // NL_GLOBALS
+ enum Mode {
+ window = "window",
+ browser = "browser",
+ cloud = "cloud",
+ chrome = "chrome"
+ }
+ enum OperatingSystem {
+ Linux = "Linux",
+ Windows = "Windows",
+ Darwin = "Darwin",
+ FreeBSD = "FreeBSD",
+ Unknown = "Unknown"
+ }
+ enum Architecture {
+ x64 = "x64",
+ arm = "arm",
+ itanium = "itanium",
+ ia32 = "ia32",
+ unknown = "unknown"
+ }
+ function log(message: string, type?: LoggerType): Promise;
+}
+namespace app {
+ interface OpenActionOptions {
+ url: string;
+ }
+ interface RestartOptions {
+ args: string;
+ }
+ function exit(code?: number): Promise;
+ function killProcess(): Promise;
+ function restartProcess(options?: RestartOptions): Promise;
+ function getConfig(): Promise;
+ function broadcast(event: string, data?: any): Promise;
+ function readProcessInput(readAll?: boolean): Promise;
+ function writeProcessOutput(data: string): Promise;
+ function writeProcessError(data: string): Promise;
+}
+namespace window {
+ interface WindowOptions extends WindowSizeOptions, WindowPosOptions {
+ title?: string;
+ icon?: string;
+ fullScreen?: boolean;
+ alwaysOnTop?: boolean;
+ enableInspector?: boolean;
+ borderless?: boolean;
+ maximize?: boolean;
+ hidden?: boolean;
+ maximizable?: boolean;
+ useSavedState?: boolean;
+ exitProcessOnClose?: boolean;
+ extendUserAgentWith?: string;
+ injectGlobals?: boolean;
+ injectClientLibrary?: boolean;
+ injectScript?: string;
+ processArgs?: string;
+ }
+ interface WindowSizeOptions {
+ width?: number;
+ height?: number;
+ minWidth?: number;
+ minHeight?: number;
+ maxWidth?: number;
+ maxHeight?: number;
+ resizable?: boolean;
+ }
+ interface WindowPosOptions {
+ x: number;
+ y: number;
+ }
+ interface WindowMenu extends Array {
+ }
+ interface WindowMenuItem {
+ id?: string;
+ text: string;
+ isDisabled?: boolean;
+ isChecked?: boolean;
+ menuItems?: WindowMenuItem[];
+ }
+ function setTitle(title: string): Promise;
+ function getTitle(): Promise;
+ function maximize(): Promise;
+ function unmaximize(): Promise;
+ function isMaximized(): Promise;
+ function minimize(): Promise;
+ function unminimize(): Promise;
+ function isMinimized(): Promise;
+ function setFullScreen(): Promise;
+ function exitFullScreen(): Promise;
+ function isFullScreen(): Promise;
+ function show(): Promise;
+ function hide(): Promise;
+ function isVisible(): Promise;
+ function focus(): Promise;
+ function setIcon(icon: string): Promise;
+ function move(x: number, y: number): Promise;
+ function center(): Promise;
+ type DraggableRegionOptions = {
+ /**
+ * If set to `true`, the region will always capture the pointer,
+ * ensuring dragging doesn't break on fast pointer movement.
+ * Note that it prevents child elements from receiving any pointer events.
+ * Defaults to `false`.
+ */
+ alwaysCapture?: boolean;
+ /**
+ * Minimum distance between cursor's starting and current position
+ * after which dragging is started. This helps prevent accidental dragging
+ * while interacting with child elements.
+ * Defaults to `10`. (In pixels.)
+ */
+ dragMinDistance?: number;
+ };
+ function setDraggableRegion(domElementOrId: string | HTMLElement, options?: DraggableRegionOptions): Promise<{
+ success: true;
+ message: string;
+ }>;
+ function unsetDraggableRegion(domElementOrId: string | HTMLElement): Promise<{
+ success: true;
+ message: string;
+ }>;
+ function setSize(options: WindowSizeOptions): Promise;
+ function getSize(): Promise;
+ function getPosition(): Promise;
+ function setAlwaysOnTop(onTop: boolean): Promise;
+ function create(url: string, options?: WindowOptions): Promise;
+ function snapshot(path: string): Promise;
+ function setMainMenu(options: WindowMenu): Promise;
+}
+namespace events {
+ interface Response {
+ success: boolean;
+ message: string;
+ }
+ type Builtin = "ready" | "trayMenuItemClicked" | "windowClose" | "serverOffline" | "clientConnect" | "clientDisconnect" | "appClientConnect" | "appClientDisconnect" | "extClientConnect" | "extClientDisconnect" | "extensionReady" | "neuDev_reloadApp";
+ function on(event: string, handler: (ev: CustomEvent) => void): Promise;
+ function off(event: string, handler: (ev: CustomEvent) => void): Promise;
+ function dispatch(event: string, data?: any): Promise;
+ function broadcast(event: string, data?: any): Promise;
+}
+namespace extensions {
+ interface ExtensionStats {
+ loaded: string[];
+ connected: string[];
+ }
+ function dispatch(extensionId: string, event: string, data?: any): Promise;
+ function broadcast(event: string, data?: any): Promise;
+ function getStats(): Promise;
+}
+namespace updater {
+ interface Manifest {
+ applicationId: string;
+ version: string;
+ resourcesURL: string;
+ }
+ function checkForUpdates(url: string): Promise;
+ function install(): Promise;
+}
+namespace clipboard {
+ interface ClipboardImage {
+ width: number;
+ height: number;
+ bpp: number;
+ bpr: number;
+ redMask: number;
+ greenMask: number;
+ blueMask: number;
+ redShift: number;
+ greenShift: number;
+ blueShift: number;
+ data: ArrayBuffer;
+ }
+ // debug
+ enum LoggerType {
+ WARNING = "WARNING",
+ ERROR = "ERROR",
+ INFO = "INFO"
+ }
+ // os
+ enum Icon {
+ WARNING = "WARNING",
+ ERROR = "ERROR",
+ INFO = "INFO",
+ QUESTION = "QUESTION"
+ }
+ enum MessageBoxChoice {
+ OK = "OK",
+ OK_CANCEL = "OK_CANCEL",
+ YES_NO = "YES_NO",
+ YES_NO_CANCEL = "YES_NO_CANCEL",
+ RETRY_CANCEL = "RETRY_CANCEL",
+ ABORT_RETRY_IGNORE = "ABORT_RETRY_IGNORE"
+ }
+ //clipboard
+ enum ClipboardFormat {
+ unknown = "unknown",
+ text = "text",
+ image = "image"
+ }
+ // NL_GLOBALS
+ enum Mode {
+ window = "window",
+ browser = "browser",
+ cloud = "cloud",
+ chrome = "chrome"
+ }
+ enum OperatingSystem {
+ Linux = "Linux",
+ Windows = "Windows",
+ Darwin = "Darwin",
+ FreeBSD = "FreeBSD",
+ Unknown = "Unknown"
+ }
+ enum Architecture {
+ x64 = "x64",
+ arm = "arm",
+ itanium = "itanium",
+ ia32 = "ia32",
+ unknown = "unknown"
+ }
+ function getFormat(): Promise;
+ function readText(): Promise;
+ function readImage(format?: string): Promise;
+ function writeText(data: string): Promise;
+ function writeImage(image: ClipboardImage): Promise;
+ function readHTML(): Promise;
+ function writeHTML(data: string): Promise;
+ function clear(): Promise;
+}
+namespace resources {
+ interface Stats {
+ size: number;
+ isFile: boolean;
+ isDirectory: boolean;
+ }
+ function getFiles(): Promise;
+ function getStats(path: string): Promise;
+ function extractFile(path: string, destination: string): Promise;
+ function extractDirectory(path: string, destination: string): Promise;
+ function readFile(path: string): Promise;
+ function readBinaryFile(path: string): Promise;
+}
+namespace server {
+ function mount(path: string, target: string): Promise;
+ function unmount(path: string): Promise;
+ function getMounts(): Promise;
+}
+namespace custom {
+ function getMethods(): Promise;
+}
+interface InitOptions {
+ exportCustomMethods?: boolean;
+}
+function init(options?: InitOptions): void;
+type ErrorCode = "NE_FS_DIRCRER" | "NE_FS_RMDIRER" | "NE_FS_FILRDER" | "NE_FS_FILWRER" | "NE_FS_FILRMER" | "NE_FS_NOPATHE" | "NE_FS_COPYFER" | "NE_FS_MOVEFER" | "NE_OS_INVMSGA" | "NE_OS_INVKNPT" | "NE_ST_INVSTKY" | "NE_ST_STKEYWE" | "NE_RT_INVTOKN" | "NE_RT_NATPRME" | "NE_RT_APIPRME" | "NE_RT_NATRTER" | "NE_RT_NATNTIM" | "NE_CL_NSEROFF" | "NE_EX_EXTNOTC" | "NE_UP_CUPDMER" | "NE_UP_CUPDERR" | "NE_UP_UPDNOUF" | "NE_UP_UPDINER";
+interface Error {
+ code: ErrorCode;
+ message: string;
+}
+interface OpenActionOptions {
+ url: string;
+}
+interface RestartOptions {
+ args: string;
+}
+interface MemoryInfo {
+ physical: {
+ total: number;
+ available: number;
+ };
+ virtual: {
+ total: number;
+ available: number;
+ };
+}
+interface KernelInfo {
+ variant: string;
+ version: string;
+}
+interface OSInfo {
+ name: string;
+ description: string;
+ version: string;
+}
+interface CPUInfo {
+ vendor: string;
+ model: string;
+ frequency: number;
+ architecture: string;
+ logicalThreads: number;
+ physicalCores: number;
+ physicalUnits: number;
+}
+interface Display {
+ id: number;
+ resolution: Resolution;
+ dpi: number;
+ bpp: number;
+ refreshRate: number;
+}
+interface Resolution {
+ width: number;
+ height: number;
+}
+interface MousePosition {
+ x: number;
+ y: number;
+}
+interface ClipboardImage {
+ width: number;
+ height: number;
+ bpp: number;
+ bpr: number;
+ redMask: number;
+ greenMask: number;
+ blueMask: number;
+ redShift: number;
+ greenShift: number;
+ blueShift: number;
+ data: ArrayBuffer;
+}
+interface ExtensionStats {
+ loaded: string[];
+ connected: string[];
+}
+interface DirectoryEntry {
+ entry: string;
+ path: string;
+ type: string;
+}
+interface FileReaderOptions {
+ pos: number;
+ size: number;
+}
+interface DirectoryReaderOptions {
+ recursive: boolean;
+}
+interface OpenedFile {
+ id: number;
+ eof: boolean;
+ pos: number;
+ lastRead: number;
+}
+interface Stats {
+ size: number;
+ isFile: boolean;
+ isDirectory: boolean;
+ createdAt: number;
+ modifiedAt: number;
+}
+interface Watcher {
+ id: number;
+ path: string;
+}
+interface CopyOptions {
+ recursive: boolean;
+ overwrite: boolean;
+ skip: boolean;
+}
+interface PathParts {
+ rootName: string;
+ rootDirectory: string;
+ rootPath: string;
+ relativePath: string;
+ parentPath: string;
+ filename: string;
+ stem: string;
+ extension: string;
+}
+interface Permissions {
+ all: boolean;
+ ownerAll: boolean;
+ ownerRead: boolean;
+ ownerWrite: boolean;
+ ownerExec: boolean;
+ groupAll: boolean;
+ groupRead: boolean;
+ groupWrite: boolean;
+ groupExec: boolean;
+ othersAll: boolean;
+ othersRead: boolean;
+ othersWrite: boolean;
+ othersExec: boolean;
+}
+type PermissionsMode = "ADD" | "REPLACE" | "REMOVE";
+interface ExecCommandOptions {
+ stdIn?: string;
+ background?: boolean;
+ cwd?: string;
+}
+interface ExecCommandResult {
+ pid: number;
+ stdOut: string;
+ stdErr: string;
+ exitCode: number;
+}
+interface SpawnedProcess {
+ id: number;
+ pid: number;
+}
+interface SpawnedProcessOptions {
+ cwd?: string;
+ envs?: Record;
+}
+interface Envs {
+ [key: string]: string;
+}
+interface OpenDialogOptions {
+ multiSelections?: boolean;
+ filters?: Filter[];
+ defaultPath?: string;
+}
+interface FolderDialogOptions {
+ defaultPath?: string;
+}
+interface SaveDialogOptions {
+ forceOverwrite?: boolean;
+ filters?: Filter[];
+ defaultPath?: string;
+}
+interface Filter {
+ name: string;
+ extensions: string[];
+}
+interface TrayOptions {
+ icon: string;
+ menuItems: TrayMenuItem[];
+}
+interface TrayMenuItem {
+ id?: string;
+ text: string;
+ isDisabled?: boolean;
+ isChecked?: boolean;
+}
+type KnownPath = "config" | "data" | "cache" | "documents" | "pictures" | "music" | "video" | "downloads" | "savedGames1" | "savedGames2" | "temp";
+interface Manifest {
+ applicationId: string;
+ version: string;
+ resourcesURL: string;
+}
+interface WindowOptions extends WindowSizeOptions, WindowPosOptions {
+ title?: string;
+ icon?: string;
+ fullScreen?: boolean;
+ alwaysOnTop?: boolean;
+ enableInspector?: boolean;
+ borderless?: boolean;
+ maximize?: boolean;
+ hidden?: boolean;
+ maximizable?: boolean;
+ useSavedState?: boolean;
+ exitProcessOnClose?: boolean;
+ extendUserAgentWith?: string;
+ injectGlobals?: boolean;
+ injectClientLibrary?: boolean;
+ injectScript?: string;
+ processArgs?: string;
+}
+interface WindowSizeOptions {
+ width?: number;
+ height?: number;
+ minWidth?: number;
+ minHeight?: number;
+ maxWidth?: number;
+ maxHeight?: number;
+ resizable?: boolean;
+}
+interface WindowPosOptions {
+ x: number;
+ y: number;
+}
+interface WindowMenu extends Array {
+}
+interface WindowMenuItem {
+ id?: string;
+ text: string;
+ isDisabled?: boolean;
+ isChecked?: boolean;
+ menuItems?: WindowMenuItem[];
+}
+interface Response {
+ success: boolean;
+ message: string;
+}
+type Builtin = "ready" | "trayMenuItemClicked" | "windowClose" | "serverOffline" | "clientConnect" | "clientDisconnect" | "appClientConnect" | "appClientDisconnect" | "extClientConnect" | "extClientDisconnect" | "extensionReady" | "neuDev_reloadApp";
+
+}
+
+// debug
+enum LoggerType {
+ WARNING = 'WARNING',
+ ERROR = 'ERROR',
+ INFO = 'INFO'
+ }
+
+// os
+enum Icon {
+ WARNING = 'WARNING',
+ ERROR = 'ERROR',
+ INFO = 'INFO',
+ QUESTION = 'QUESTION'
+}
+
+enum MessageBoxChoice {
+ OK = 'OK',
+ OK_CANCEL = 'OK_CANCEL',
+ YES_NO = 'YES_NO',
+ YES_NO_CANCEL = 'YES_NO_CANCEL',
+ RETRY_CANCEL = 'RETRY_CANCEL',
+ ABORT_RETRY_IGNORE = 'ABORT_RETRY_IGNORE'
+}
+
+//clipboard
+enum ClipboardFormat {
+ unknown = 'unknown',
+ text = 'text',
+ image = 'image'
+}
+
+// NL_GLOBALS
+enum Mode {
+ window = 'window',
+ browser = 'browser',
+ cloud = 'cloud',
+ chrome = 'chrome'
+}
+
+enum OperatingSystem {
+ Linux = 'Linux',
+ Windows = 'Windows',
+ Darwin = 'Darwin',
+ FreeBSD = 'FreeBSD',
+ Unknown = 'Unknown'
+}
+
+enum Architecture {
+ x64 = 'x64',
+ arm = 'arm',
+ itanium = 'itanium',
+ ia32 = 'ia32',
+ unknown = 'unknown'
+}
+
+
+interface Response {
+ success: boolean;
+ message: string;
+ }
+
+ type Builtin =
+ 'ready' |
+ 'trayMenuItemClicked' |
+ 'windowClose' |
+ 'serverOffline' |
+ 'clientConnect' |
+ 'clientDisconnect' |
+ 'appClientConnect' |
+ 'appClientDisconnect' |
+ 'extClientConnect' |
+ 'extClientDisconnect' |
+ 'extensionReady' |
+ 'neuDev_reloadApp'
+
+
+// --- globals ---
+/** Mode of the application: window, browser, cloud, or chrome */
+declare const NL_MODE: Mode;
+/** Application port */
+declare const NL_PORT: number;
+/** Command-line arguments */
+declare const NL_ARGS: string[];
+/** Basic authentication token */
+declare const NL_TOKEN: string;
+/** Neutralinojs client version */
+declare const NL_CVERSION: string;
+/** Application identifier */
+declare const NL_APPID: string;
+/** Application version */
+declare const NL_APPVERSION: string;
+/** Application path */
+declare const NL_PATH: string;
+/** Application data path */
+declare const NL_DATAPATH: string;
+/** Returns true if extensions are enabled */
+declare const NL_EXTENABLED: boolean;
+/** Returns true if the client library is injected */
+declare const NL_GINJECTED: boolean;
+/** Returns true if globals are injected */
+declare const NL_CINJECTED: boolean;
+/** Operating system name: Linux, Windows, Darwin, FreeBSD, or Uknown */
+declare const NL_OS: OperatingSystem;
+/** CPU architecture: x64, arm, itanium, ia32, or unknown */
+declare const NL_ARCH: Architecture;
+/** Neutralinojs server version */
+declare const NL_VERSION: string;
+/** Current working directory */
+declare const NL_CWD: string;
+/** Identifier of the current process */
+declare const NL_PID: string;
+/** Source of application resources: bundle or directory */
+declare const NL_RESMODE: string;
+/** Release commit of the client library */
+declare const NL_CCOMMIT: string;
+/** An array of custom methods */
+declare const NL_CMETHODS: string[];
+
diff --git a/demo/app/resources/styles.css b/demo/app/resources/styles.css
new file mode 100644
index 0000000..5b0e4b9
--- /dev/null
+++ b/demo/app/resources/styles.css
@@ -0,0 +1,20 @@
+body {
+ background-color: white;
+}
+
+#neutralinoapp {
+ text-align: center;
+ -webkit-user-select: none;
+ user-select: none;
+ cursor: default;
+}
+
+#neutralinoapp h1{
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ font-size: 20px;
+}
+
+#neutralinoapp > div {
+ font-size: 16px;
+ font-weight: normal;
+}
diff --git a/go.mod b/go.mod
index 24fef97..db4746a 100644
--- a/go.mod
+++ b/go.mod
@@ -1,23 +1,30 @@
module apigo.cc/gojs/client
-go 1.18
+go 1.24
require (
- apigo.cc/gojs v0.0.3
- github.com/ssgo/tool v0.4.27
+ apigo.cc/gojs v0.0.25
+ github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327
+ github.com/chromedp/chromedp v0.14.0
+ github.com/ssgo/tool v0.4.29
+ github.com/ssgo/u v1.7.21
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6
)
require (
- github.com/dlclark/regexp2 v1.11.4 // indirect
- github.com/fsnotify/fsnotify v1.7.0 // indirect
+ github.com/chromedp/sysutil v1.1.0 // indirect
+ github.com/dlclark/regexp2 v1.11.5 // indirect
+ github.com/fsnotify/fsnotify v1.9.0 // indirect
+ github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
- github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
- github.com/ssgo/config v1.7.7 // indirect
- github.com/ssgo/log v1.7.7 // indirect
+ github.com/gobwas/httphead v0.1.0 // indirect
+ github.com/gobwas/pool v0.2.1 // indirect
+ github.com/gobwas/ws v1.4.0 // indirect
+ github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect
+ github.com/ssgo/config v1.7.9 // indirect
+ github.com/ssgo/log v1.7.9 // indirect
github.com/ssgo/standard v1.7.7 // indirect
- github.com/ssgo/u v1.7.9 // indirect
- golang.org/x/sys v0.26.0 // indirect
- golang.org/x/text v0.19.0 // indirect
+ golang.org/x/sys v0.34.0 // indirect
+ golang.org/x/text v0.27.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/tests/client.go b/tests/client.go
new file mode 100644
index 0000000..23c4652
--- /dev/null
+++ b/tests/client.go
@@ -0,0 +1,45 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "apigo.cc/gojs"
+ "apigo.cc/gojs/client"
+ "github.com/ssgo/u"
+)
+
+func main() {
+ gojs.ExportForDev()
+ testOK := false
+ client.Bind("setTestOK", func(testIsOK bool) {
+ testOK = testIsOK
+ })
+
+ scriptFile := "client.js"
+ if len(os.Args) > 1 {
+ scriptFile = os.Args[1]
+ }
+
+ exePath, _ := os.Executable()
+ appDir := ""
+ if strings.Contains(exePath, "Contents/MacOS") {
+ appDir = filepath.Join(filepath.Dir(filepath.Dir(exePath)), "Resources")
+ os.Chdir(appDir)
+ }
+ u.WriteFile("/tmp/aaa.txt", appDir)
+
+ r, err := gojs.RunFile(scriptFile)
+ if err != nil {
+ fmt.Println(u.Red(err.Error()))
+ }
+ gojs.WaitAll()
+
+ if !testOK {
+ fmt.Println(u.BRed("test failed"))
+ } else {
+ fmt.Println(u.BGreen("test OK"), r)
+ }
+}
diff --git a/tests/client.html b/tests/client.html
index 366c4c3..b3bc6b2 100644
--- a/tests/client.html
+++ b/tests/client.html
@@ -13,7 +13,8 @@
app.setSize(1200, 400, "fixed")
setTimeout(() => {
setTestOK(true)
- app.close()
+ document.querySelector('._dialogButton').click()
+ // app.close()
}, 1000)
}, 1000)
})
diff --git a/tests/client.js b/tests/client.js
index f4c8bcb..7762d43 100644
--- a/tests/client.js
+++ b/tests/client.js
@@ -5,4 +5,5 @@ client.open({
height: 600,
title: 'Hello',
url: 'client.html',
+ isDebug: false,
})
diff --git a/tests/client_test.go b/tests/client_test.go
deleted file mode 100644
index a09b880..0000000
--- a/tests/client_test.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package client_test
-
-import (
- "fmt"
- "testing"
-
- "apigo.cc/gojs"
- "apigo.cc/gojs/client"
-)
-
-func TestClient(t *testing.T) {
- gojs.ExportForDev()
- testOK := false
- client.Bind("setTestOK", func(testIsOK bool) {
- testOK = testIsOK
- })
-
- r, err := gojs.RunFile("client.js")
- if err != nil {
- t.Fatal(err)
- }
- gojs.WaitAll()
-
- if !testOK {
- t.Fatal("test failed")
- } else {
- fmt.Println("test OK", r)
- }
-}
diff --git a/tests/test_chromedp.sh b/tests/test_chromedp.sh
new file mode 100755
index 0000000..353e5dc
--- /dev/null
+++ b/tests/test_chromedp.sh
@@ -0,0 +1,2 @@
+export CGO_ENABLED=0
+go run .
diff --git a/tests/test_webview.sh b/tests/test_webview.sh
new file mode 100755
index 0000000..ae360a5
--- /dev/null
+++ b/tests/test_webview.sh
@@ -0,0 +1,2 @@
+export CGO_ENABLED=1
+go run .