Files
Duc-Tam Nguyen 5b7f7d9f31 Add an optional native-window viewer behind the webview tag
A packed binary opened the system browser, so it felt like a tab, not
an app. Build with -tags webview (cgo) and the viewer instead opens the
site in its own window backed by the OS WebView: WKWebView on macOS,
WebView2 on Windows, WebKitGTK on Linux.

The viewer package picks an implementation at build time. The default
file opens the browser and keeps the build pure Go, so CGO_ENABLED=0 and
the release pipeline are untouched. The webview file links the platform
WebView and runs its event loop on the main goroutine, which main now
pins with LockOSThread before anything else, since macOS requires UI on
the initial thread. Both kage open and the embedded viewer serve over
HTTP in a goroutine and hand the URL to the viewer, then tear the server
down when the window closes or Ctrl-C cancels.

The window title comes from the archive's M/Title. OpenInBrowser moves
out of pack into the viewer package, its only caller.
2026-06-14 21:07:53 +07:00

54 lines
1.5 KiB
Go

//go:build webview
package viewer
import (
"context"
"runtime"
webview "github.com/webview/webview_go"
)
// Native is true in the webview build: Show opens a real window backed by the
// operating system's WebView (WKWebView on macOS, WebView2 on Windows,
// WebKitGTK on Linux), so a packed kage feels like a standalone app.
//
// This build needs cgo and links the platform WebView, so it is opt-in
// (-tags webview) and kept out of the default CGO_ENABLED=0 release pipeline.
const Native = true
// LockMainThread pins the calling goroutine to its OS thread. main calls it
// first thing, while the main goroutine is still on the process's initial
// thread, because the macOS WebView must be driven from that thread.
func LockMainThread() { runtime.LockOSThread() }
// Show opens a native window pointed at o.URL and runs the UI event loop on the
// calling (main) goroutine, blocking until the window is closed. A cancelled
// context terminates the loop too, so Ctrl-C still shuts the viewer down. The
// o.Browser flag is ignored: the whole point of this build is the native window.
func Show(ctx context.Context, o Options) error {
w := webview.New(false)
defer w.Destroy()
title := o.Title
if title == "" {
title = "kage"
}
w.SetTitle(title)
w.SetSize(1024, 768, webview.HintNone)
w.Navigate(o.URL)
done := make(chan struct{})
go func() {
select {
case <-ctx.Done():
w.Dispatch(func() { w.Terminate() })
case <-done:
}
}()
w.Run()
close(done)
return nil
}