Documentation
¶
Overview ¶
Package tuohi is a pure-Go foundation for web-based desktop applications: it embeds the platform View (WKWebView on macOS, WebKitGTK on Linux, WebView2 on Windows) behind a single Go API - windowing and app windows, drag regions, custom URL schemes, bindings and events, opening URLs and native file dialogs - all cgo-free.
The package is the window. The desktop services live in subpackages that do not import it: tuohi/instance (single instance), tuohi/autostart (launch at login), tuohi/clipboard, tuohi/notify and tuohi/tray. A service that needs the UI thread, such as the tray, is set up in App.Start.
The main thread ¶
Call App.Show and App.Wait from main, or from functions main calls, and run everything else in goroutines. macOS requires it: AppKit accepts UI work only on the process's main thread, and the package keeps the main goroutine there from its init on. Off the main thread, Show and Wait return ErrNotMainThread unless a run loop is already running on the main thread to hand the work to. Linux and the BSDs require it too: WebKitGTK 2.54 aborts the process when it is first used off the main thread, so the package keeps the main goroutine there as well, and a first Show elsewhere returns ErrNotMainThread. Windows has no such rule, but a program written this way runs on all of them. A program that serves its interface itself, over loopback HTTP, starts the server in a goroutine and opens the window on main:
func main() {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
log.Fatal(err)
}
go http.Serve(ln, handler)
app := &tuohi.App{Exit: true}
view := &tuohi.View{URL: "http://" + ln.Addr().String() + "/"}
if err := app.Show(view); err != nil {
log.Fatal(err)
}
if err := app.Wait(); err != nil {
log.Fatal(err)
}
}
Other View methods, such as Eval, Navigate and Close, are safe from any goroutine; they are marshalled to the UI thread.
Source layout: app*.go holds the application scope - the App type (configuration + runtime context), its app-scope methods (Show, Wait, Open/Reveal, Backend) and the per-OS app internals (app icon, Open/Reveal); view*.go holds the view/window API surface (the define-first View struct and its methods, geometry and State, scheme types, App.Show glue, the View Dialog method, drag regions); bind.go (registry model + value conversion), bind_gen.go (the generated JS) and bind_evt.go (the events bridge) hold the binding and events machinery; engine.go holds the engine interface every platform satisfies and the code they share; and lib_darwin.go, lib_unix.go and lib_windows.go are the per-platform engine layer, which talks to WKWebView, WebKitGTK and WebView2 and the platform APIs.
Platform code lives in *_unix.go / *_windows.go / *_darwin.go files; a capability that a platform cannot provide returns an Err* sentinel or is a documented best-effort no-op rather than failing at compile time.
Index ¶
- Variables
- type App
- type CloseRequest
- type Permission
- type State
- type View
- func (v *View) Close()
- func (v *View) Dialog(opts dialog.Options) ([]string, error)
- func (v *View) Emit(name string, data ...any) error
- func (v *View) Eval(js string)
- func (v *View) Focus(raise bool)
- func (v *View) Hide()
- func (v *View) Maximize()
- func (v *View) Minimize()
- func (v *View) Navigate(url string)
- func (v *View) Off(name string)
- func (v *View) On(name string, handler func(args ...json.RawMessage)) (cancel func())
- func (v *View) RequestClose()
- func (v *View) SetTitle(title string)
- func (v *View) Show()
- func (v *View) Unmaximize()
- func (v *View) Unminimize()
- func (v *View) Window(f func(wnd unsafe.Pointer))
- func (v *View) WindowState() WindowState
- type WindowState
Constants ¶
This section is empty.
Variables ¶
var ErrNotMainThread = errors.New("tuohi: the UI runs on the main thread; call App.Show and App.Wait from main")
ErrNotMainThread is returned by App.Show and App.Wait called off the process's main thread when nothing there can take the work. On macOS that is when no run loop is running on the main thread: AppKit runs only there. On Linux and the BSDs it is a first Show, before any window exists: WebKitGTK 2.54 aborts the process when it is first used off the main thread. Call Show and Wait from main and run anything else, such as an HTTP server, in a goroutine (see the package doc). It is never returned on Windows.
var ErrScheme = errors.New("tuohi: refused URL scheme")
ErrScheme is returned by Open when the URL's scheme is not in the allow-list.
Functions ¶
This section is empty.
Types ¶
type App ¶
type App struct {
// Debug turns the platform web inspector / developer tools on for every
// window of this app (the app-wide default for View.Debug): set it once
// for "every window is debuggable". A view's own View.Debug ORs over it,
// and so does the TUOHI_DEBUG=1 environment variable when AllowEnvDebug
// is set.
//
// Like every App field it is committed when the app scope opens (later
// edits have no effect) and it is read exactly once per view, at window
// creation. Backend mapping - WebView2 AreDevToolsEnabled (Windows),
// WebKitGTK enable-developer-extras (Linux), WKPreferences
// developerExtrasEnabled (macOS).
Debug bool
// AllowEnvDebug lets whoever launches the program turn the developer
// tools on for every view by setting the TUOHI_DEBUG=1 environment
// variable. It is off by default, so a release build opens its web
// inspector only when the application itself sets Debug or View.Debug:
// the environment belongs to the person launching the program, and the
// inspector reaches every page and binding. Set it in a development
// build, or when debugging an installed build is worth that. It is
// committed when the app scope opens, like every App field.
AllowEnvDebug bool
// Events names the JavaScript global the tuohi events bridge installs on
// every page of this app: window.<Events> with on/off/emit (see
// View.On/Off/Emit). Empty (the default) uses the name "events". The name
// is fixed when each view is created.
Events string
// Name is the application name, used where the OS asks for one: as the
// title of a window that has no other (see View.Title), and under GTK3 on
// Wayland as the name of the desktop entry DesktopEntry installs.
Name string
// Icon is a PNG image for the running application, applied on a
// best-effort basis wherever the platform supports it at runtime (macOS
// Dock, Linux GTK3/GTK4 window icons, ...). It is applied once, when the
// app scope opens and before the first window exists. When Icon is unset
// the embedded tuohi mark is used instead (unexported; tuohi applies it
// itself), so a tuohi application always has a process face unless it
// brings its own. Unlike the per-window page icon it sets the face of the PROCESS; a
// stable, runtime process icon is intentionally a best-effort feature
// because it is hard to keep identical across all platforms. An unset icon
// is silently ignored; so are environments that cannot take a runtime icon
// (Windows reads the icon from the executable's own resources). Under an
// X11 window manager the icon is pushed per window; under Wayland, where
// the protocol has no per-window icons, GTK4 (>= 4.20) sends pixels via
// the xdg-toplevel-icon protocol. A GTK3 window on Wayland shows the icon
// only through a desktop entry: see DesktopEntry. A PNG that cannot be
// decoded is never fatal.
Icon []byte
// DesktopEntry lets tuohi install a hidden desktop entry and a themed
// copy of Icon under the user's data directory (XDG_DATA_HOME, by default
// ~/.local/share), named after Name, so that a GTK3 window on Wayland
// shows the icon. Wayland has no per-window icons, and the compositor
// finds a window's icon through the desktop entry that matches its
// app_id; GTK3, unlike GTK4, cannot send the pixels itself.
//
// It is false by default, and then opening a window writes nothing under
// the user's home directory. It has no effect anywhere but GTK3 on
// Wayland. An entry of that name that tuohi did not write, in the user's
// data directory or a system one (XDG_DATA_DIRS), such as one an
// installed package ships, is left alone and used as it is; once such an
// entry appears, the one tuohi wrote is removed so that it is the one used.
// The files stay after the application exits.
DesktopEntry bool
// Bind holds the application's declarative bindings: every entry is bound
// onto each view App.Show creates, so one entry here covers all windows.
// Keys and values follow the rules [View.Bind] documents: a dotted key
// names a nested page variable, and the value's kind decides whether it
// becomes a function, an accessor property or a frozen constant.
//
// What differs from View.Bind: a nil entry here binds nothing, and a view
// overrides an app-wide name - or unbinds it with a nil entry - through
// its own View.Bind map. The entries are applied to every view in
// alphabetical key order, before the view's own (see App.Show). Like every
// App field, Bind is committed when the app scope opens (later edits have
// no effect) and is read once per shown view, at window creation.
Bind map[string]any
// FS is the filesystem the application serves to its views - the app's
// content: HTML, CSS, scripts and anything else the page loads. When it is
// set (it is read once, when the app scope opens, like every App setting;
// later edits have no effect), every view shown by App.Show is served
// from it. The consumer never picks a serving mechanism: navigate the view
// to the uniform "app://" origin -
//
// w.Navigate("app://app/index.html")
//
// - and tuohi serves the file at that path in the filesystem on every
// platform. A URL with no host, such as a bare "app://" or
// "app://#route", is read as the root of the "app" host,
// "app://app/#route", so the page keeps its bridge. The serving is scheme-first on Windows and Linux: WebView2's
// https vhost for the custom "app" scheme (whose responses carry the
// isolation headers), the registered custom scheme on Linux (WebKitGTK
// cannot attach the headers to scheme responses, so a scheme-served Linux
// page is not crossOriginIsolated - SharedArrayBuffer still works through
// the JSC_useSharedArrayBuffer option). macOS always serves over a
// per-view loopback http://localhost server (WKWebView cannot make a
// custom scheme a secure context, and a long-standing WebKit bug keeps
// SharedArrayBuffer off plain WKWebView pages); App.HTTP opts Linux and
// Windows into that same loopback origin. SharedArrayBuffer is available
// on every platform. A path without a file answers "not found". A nil FS serves
// no content - the window shows whatever the consumer navigates it to
// itself.
//
// What FS holds is readable by the views it is served to, not private to
// the process. The loopback server answers only its own view (see HTTP),
// but another process running as the same user can read FS anyway, from
// the binary or from memory; keep secrets out of it.
FS fs.FS
// HTTP serves the app's content over a per-view loopback http://localhost
// server instead of the platform's custom "app" scheme. On Linux and
// Windows it is an opt-in fallback - the native scheme serves them (see
// the FS doc), so only HTTP-opted windows load their app:// content from
// the loopback origin. Each such view has a server of its own, listening
// on 127.0.0.1 only, from the view's creation until it is destroyed. The
// server answers only requests that name its own origin and carry its
// per-server token, which the view's first navigation turns into a
// cookie, so neither a web page reaching the port by DNS rebinding nor
// another user's process gets anything from it. macOS always serves over the loopback origin
// (WKWebView cannot make a custom scheme a secure context and cannot
// provide SharedArrayBuffer on plain pages - a long-standing WebKit
// bug). Either
// way the consumer still navigates to the uniform "app://" origin, so
// HTTP is a purely internal serving choice. Every served response -
// loopback and vhost alike - carries the cross-origin-isolation headers
// (COOP/COEP + CORP), so every app page is cross-origin isolated and can
// use SharedArrayBuffer. Ignored when FS is nil.
HTTP bool
// Exit ends the application process when its last window closes: with
// Exit true, Wait returns as soon as the last window created with
// App.Show is gone. The default (false) keeps the process alive after the
// windows close - menu-bar/tray/background applications - and Wait then
// returns only when App.Quit is called.
Exit bool
// Start, when set, runs on the UI thread when Wait starts, before its
// loop dispatches any event. It is where a service that needs the UI
// thread is set up, such as a tray icon:
//
// app.Start = func() error { return tray.Set(cfg) }
//
// and tear it down after Wait returns (tray.Remove). An error from
// Start ends Wait, which returns it.
//
// Like every App field it is committed when the app scope opens (later
// edits have no effect).
Start func() error
// OpenExternal replaces the system browser/mail opener for App.Open and
// URLs a view hands outside after applying its navigation policy. Nil
// uses the platform opener. Set it before the first App method, like
// other app settings; later assignments do not affect the scope.
//
// App.Open calls it synchronously and returns its error. Navigation
// handoffs call it on a goroutine and log failures; calls may overlap,
// so the function must be safe for concurrent use. It may call View
// methods. Disallowed URL schemes never reach it, and a configured hook
// completely replaces the system call, including when it returns an
// error. Consumer tests can record handoffs without starting a browser.
OpenExternal func(url string) error
// contains filtered or unexported fields
}
App configures a tuohi application and carries its runtime scope.
It is the single application-scoped object: the exported fields hold the application settings (content filesystem, icon, the Start hook, ...) and unexported fields hold the state of the scope (committed settings and the one-time platform initialization). It is conceptually similar to how http.Server holds configuration and context together.
The exported settings are read once - when the first App method is called - and are then committed: later edits to the fields do not affect the running application. That first call also performs the one-time platform initialization (ensureInit) before the requested action runs, so the call that opens the scope should come from the goroutine that will own the UI (the main goroutine).
The app-scoped operations are methods on *App (Show, Wait, Quit, Open, Reveal, Backend); App.Bind is a declarative map instead of a method - see its field doc. This is a deliberate design choice: the App scope is never hidden from the consumer. The desktop services - single instance, autostart, clipboard, notifications and the tray - are not part of App; they live in their own subpackages (see the package doc).
func (*App) Backend ¶
Backend reports which web-engine backend the App scope uses for its views, after the one-time platform initialization has run:
- "webkitgtk-6.0" or "webkit2gtk-4.1" on Linux - the stack that was actually loaded, honoring the TUOHI_BACKEND environment variable (see README "Linux shared libraries");
- "WKWebView" on macOS and "WebView2" on Windows, whose single built-in backend ignores the variable.
Like every App method it opens the scope first, so the returned name always matches the loaded backend rather than the requested one. It returns an empty string when the platform could not be initialized.
func (*App) Open ¶
Open opens rawurl with the user's default handler: the browser or the mail client. Only http, https, and mailto URLs are allowed. Anything else, including file: and a bare hostname or path with no scheme, returns ErrScheme. To show a local file, use Reveal.
file: is refused because the platform opener runs what it is given: a file: URL for an executable, a .app, a .lnk, or a .desktop file launches it through ShellExecute, NSWorkspace, or xdg-open. Open is also where a view's external links are sent, so its argument may come from a page.
func (*App) Quit ¶
func (a *App) Quit()
Quit asks a running application to terminate: Wait returns and the process may finish. It is safe to call from any goroutine (the UI loop is woken). Calling Quit before Wait is harmless - Wait then returns immediately.
func (*App) Reveal ¶
Reveal opens the platform file manager with path's location shown: Finder selects the file on macOS, Explorer selects it on Windows, and on Linux the containing folder is opened (selecting the file itself is file-manager specific and not portable). The path must exist.
func (*App) Show ¶
Show presents a configured View: the first time a View is shown, App.Show creates its window and web view (opens/commits the App scope, runs the one-time platform initialization) and registers the View with the App, which manages it from then on. Calling Show again on the SAME View while its window is alive brings it back instead of recreating it: the window is un-minimized, shown and focused (Focus(true)). After View.Close the View is unregistered and reset, so the same View can be Show'n again later.
The View's exported fields (Debug/FirstMouse, URL, Ready, the geometry fields Left/Top/Width/Height/State and the Bind map) are read once, exactly at the first Show; keep the *View afterwards - it is the handle to the shown window.
Show is safe to call from any goroutine. The window is always created on the UI thread: the main thread on macOS, Linux and the BSDs, and on Windows the thread of the goroutine whose Show first created a window, which Show pins to it. On macOS, Linux and the BSDs the first window must be created on the main thread, and a first Show elsewhere returns ErrNotMainThread. A call from any other goroutine hands the whole creation to the UI thread and waits for it, so the View is live when Show returns. That needs a loop running there (App.Wait, or on macOS one another owner runs); with none, Show returns an error and creates nothing. Revealing a View that is already shown is queued and never waits.
A Show of a View whose window is still being created by another Show cannot wait for it. That happens when the creation runs a loop that runs the second Show (WebView2 does while it installs scripts). It returns an error saying so, and the first Show finishes the window.
Every binding is applied while the window is created, deterministically: the app-wide App.Bind entries first, then the view's own View.Bind entries, each map iterated in alphabetical key order, so the outcome never depends on Go's map iteration order. Each entry is ONE name - a function value becomes a callable JS function, any other JSON-encodable value a frozen JS constant - and once the page's binding batch is installed the whole bound namespace is frozen (see makeBinding). A view entry overrides the same app name; a nil view entry unbinds it. The events bridge is installed before the page loads, so View.On/Off/Emit work immediately.
func (*App) Wait ¶
Wait blocks until the application exits. It is the app-level run loop: it opens the app scope once (committing the settings and performing the one-time platform initialization) and then runs the platform UI loop until the application should exit:
- App.Quit was called, or
- the last window created with App.Show was closed and App.Exit is true.
With Exit false (the default) the process keeps running after its windows are gone - menu-bar/tray/background applications - and only App.Quit (or the process being killed) ends it.
Create all windows with App.Show and keep every UI call on the goroutine that calls Wait (the main goroutine). Every app, even one with a single window, runs its loop through Wait: View has no Run method of its own.
type CloseRequest ¶ added in v0.2.0
type CloseRequest struct {
// contains filtered or unexported fields
}
CloseRequest is one native or View.RequestClose request. Its methods are safe from any goroutine. Only the first Allow or Cancel wins. If the callback returns without a decision or Defer, closing is allowed. Defer keeps the existing window alive until a later decision; repeated requests coalesce. View.Close and App.Quit invalidate the request without waiting for a decision.
func (*CloseRequest) Allow ¶ added in v0.2.0
func (r *CloseRequest) Allow() bool
Allow accepts the close, marshaling a deferred decision to the UI thread.
func (*CloseRequest) Cancel ¶ added in v0.2.0
func (r *CloseRequest) Cancel() bool
Cancel keeps the existing window and renderer alive. A hide-to-tray handler can Cancel then call View.Hide without rebuilding its page.
func (*CloseRequest) Defer ¶ added in v0.2.0
func (r *CloseRequest) Defer() bool
Defer keeps the request pending after CloseRequested returns. The consumer must eventually Allow or Cancel, unless teardown/quit invalidates it first.
func (*CloseRequest) Done ¶ added in v0.2.0
func (r *CloseRequest) Done() <-chan struct{}
Done closes when decided or invalidated. It carries no decision; a stale request's Allow, Cancel and Defer return false and cannot affect a new view.
type Permission ¶
type Permission int
Permission names something a page may use only when the application allows it for the view, in View.Permissions. Everything else a page can ask the web engine for, such as geolocation or notifications, is always denied.
const ( // PermissionCamera lets a trusted page open the camera with // navigator.mediaDevices.getUserMedia. PermissionCamera Permission = iota + 1 // PermissionMicrophone lets a trusted page open the microphone with // navigator.mediaDevices.getUserMedia. PermissionMicrophone // PermissionClipboard lets a trusted page read the clipboard from // script, with navigator.clipboard.readText or read, without the user // confirming each read. WebKitGTK allows a read only on a user gesture. // WKWebView never reads silently: it shows the system's Paste button, // and a read the user confirms there is the user's own paste, like // Command-V, whatever the view lists. So on macOS this permission changes // nothing, and an empty list does not stop a paste the user confirms. // Copying on a click needs no permission on any engine. PermissionClipboard )
func (Permission) String ¶
func (p Permission) String() string
String returns the permission's name.
type State ¶
type State int
State values configure window sizing and resizing at creation time.
const ( // StateNone lets the window size freely; zero Width/Height pick the // backend default. StateNone State = iota // StateMin makes Width and Height the minimum bounds. StateMin // StateMax makes Width and Height the maximum bounds. StateMax // StateFixed prevents the user from resizing the window. StateFixed )
type View ¶
type View struct {
// Debug turns the platform web inspector / developer tools on for this
// window. App.Show ORs it with the app-wide App.Debug - a true on either
// side opens the tools, and so does the TUOHI_DEBUG=1 environment
// variable when the App sets AllowEnvDebug.
//
// Backend mapping - WebView2 AreDevToolsEnabled (Windows), WebKitGTK
// enable-developer-extras (Linux), WKPreferences developerExtrasEnabled
// (macOS).
Debug bool
// FirstMouse makes a click on an INACTIVE window reach the page instead
// of only bringing the window forward.
//
// macOS only; ignored elsewhere, where a click on an inactive window
// already reaches the content. AppKit's default is the opposite of what
// most web UIs want: the first click is swallowed as activation, so a
// user who clicks a button in a window that lost focus has to click twice
// - and the first click looks broken. Turn this on for control panels,
// dashboards, players and anything else the user clicks in passing.
//
// It is OPT-IN because the default protects destructive interfaces: in a
// drawing tool, an editor, or any window with a delete button, a click
// that merely raises the window must NOT also press what happens to be
// under the cursor. Leave it off when a stray first click could destroy
// something.
FirstMouse bool
// URL is the page the window loads first. App.Show navigates the view
// here once the window exists (Navigate). The URL may be the loopback
// address of a server the program runs itself, such as
// http://127.0.0.1:8080/, a uniform "app://" URL served by App.FS, an
// https:// URL, or a data: URI. Empty (the default) starts a blank window
// and no navigation happens - and because Ready fires on a completed
// load, it will not fire until a later Navigate completes.
URL string
// Origins lists origins, besides the ones the view is navigated to from
// Go, whose pages may call the view's bindings and use its events, such
// as "https://auth.example.com". Every URL given to Navigate, including
// URL, is trusted already. A page on any other origin, reached through a
// link, a redirect, or a frame, cannot reach Go. An about:blank page is
// never trusted, because any page can create one.
//
// These are also the only pages the view shows. When a page leads the
// whole view elsewhere, by a link, a redirect, a script, or a new window,
// the view stays where it was and an http, https, or mailto URL opens in
// the system browser or mail client instead; data:, blob:, file:, and
// other schemes are dropped. about:blank is shown, and frames are left
// alone. A new window (target=_blank, window.open) never opens: one to a
// trusted origin loads in this view instead, one to about:blank is
// dropped, and any other follows the rule above. So a URL that
// redirects to a sign-in page on another origin
// needs that origin listed here, or the view stays blank while the
// browser opens it. A redirect to another scheme or host, such as http
// to https, or 127.0.0.1 to localhost, counts as another origin.
//
// Origins controls top-level documents and bridge access, not all network
// traffic. On Windows, WebView2 may send a GET before a cancelled
// NavigationStarting completes; on Linux, response-time decisions can
// also follow a request. Redirects and meta refresh can therefore contact
// an untrusted destination even though its page is never shown. Frames
// and subresources can also contact other origins. See
// docs/navigation-security.md for the platform limits.
Origins []string
// Permissions lists what a page in this view may use that the web engine
// would otherwise ask the user for: the camera, the microphone, and
// script reads of the clipboard (see Permission). Only a page on an
// origin the view trusts (see Origins) receives one, and only if it is
// listed here. Everything else a page asks for, such as geolocation or
// notifications, is denied, and no engine shows a prompt of its own.
// Empty, the default, denies everything. Pointer lock is not a
// permission: every engine allows it on a user gesture. Element fullscreen
// is also outside this list: tuohi enables it across engines where their
// fullscreen API is available, subject to the engine's user-gesture rules.
//
// A frame on another origin can ask only when the trusted page delegates
// the feature to it with the iframe's allow attribute. WebView2 and
// WKWebView then decide by the frame's own origin and deny it. WebKitGTK
// does not say which frame asks, so on Linux such a frame asks as the
// trusted page: delegating a listed permission to a frame grants it
// there. Delegate only to frames you would trust with the permission.
//
// On macOS, a program that lists the camera or the microphone also needs
// NSCameraUsageDescription or NSMicrophoneUsageDescription in its
// Info.plist.
Permissions []Permission
// Title is the window's title. It names the window in its title bar,
// the taskbar or Dock, the window switcher, and accessibility tools, so
// it matters for a frameless window too. SetTitle changes it after Show.
//
// When Title is empty the window follows the page instead: it takes the
// document.title of the page it shows, and App.Name while that page has
// none. Only a page on a trusted origin (see Origins) can set it, so a
// page the view does not trust, such as about:blank, leaves the title
// as it was. A window embedded from the host's own takes only a
// non-empty Title; the page and App.Name leave it alone.
Title string
// Ready, when non-nil, is called exactly once, on the UI thread, the
// first time a page finishes loading after Show (the initial Navigate to URL
// or the first later navigation). It is the "the window is fully up"
// callback: bindings and the events bridge are live by then.
//
// Pair it with Eval to run JavaScript on init: Ready fires only after the
// first page load completed, so the DOM and the page's own scripts are in
// place and a single Eval reaches them reliably. Injecting script before
// the document exists is not reliable across the three engines, so tuohi
// has no declarative JS/CSS injection API - call Eval from Ready to run
// code when the page comes up.
Ready func()
// CloseRequested intercepts user/keyboard close and RequestClose. It runs
// on the native UI thread and must return promptly. Nil allows closing.
// Use Cancel then Hide to preserve this view, or Defer and decide later
// from a worker. View.Close and App.Quit bypass this policy. Configure
// before Show; changing the field later has no effect on the live window.
CloseRequested func(*CloseRequest)
// WindowChanged receives an initial native state after engine publication,
// subsequent visibility/activation changes, and a terminal Closed state
// on destruction. It runs on the UI thread without internal locks and
// must return promptly. Calls may be nested if the callback changes native
// state (for example Hide). State is updated before each call. Configure
// before Show. Occlusion and workspace visibility are not reported.
WindowChanged func(WindowState)
// Bind holds this view's declarative bindings: every entry is bound onto
// the web view when App.Show runs. A key is a DOTTED path - dots
// separate nested variables on the page, so a value bound at
// "app.someAPI.call" appears as window.app.someAPI.call. What a value
// becomes is decided by its kind alone:
//
// - a function becomes a JS function the page calls. Its arity decides
// whether it ALSO works as a variable: a zero-argument function is a
// callable GETTER - call it (`window.name()`), or read it as a value
// (`await window.name`, which calls it with no arguments); a
// one-argument function is a callable SETTER - call it
// (`window.name(v)`), or ASSIGN to it (`window.name = v`, which runs
// it with the assigned value; the assignment expression yields that
// value, so await the CALL form for the result);
// - a length-2 array of two functions ([2]any{getter, setter}) becomes
// a readable AND writable property: reading it runs the getter over
// the bridge (`const v = await window.name`), assigning to it runs
// the setter (`window.name = v`);
// - any other value - a bool, a number, a string, or any JSON-encodable
// value such as a struct, map or slice - becomes an immutable JS
// constant bound wholesale under that name.
//
// The page's calls are dispatched to Go in the order it makes them, so a
// read issued after a write observes the write
// (`window.count = 1; await window.count`).
//
// No part of a Go type is ever bound separately: structs and maps are
// never walked. The namespace the bindings of a page are installed into
// is frozen once the batch finishes, so the page cannot mutate the
// functions, constants or accessor objects it was given.
//
// A name set here overrides the same name in the app-wide App.Bind map;
// a nil entry under a name UNBINDS that name again, removing an app-wide
// binding this view does not want. Entries are applied in alphabetical
// key order, after the app-wide entries (see App.Show). A nil entry in
// App.Bind itself binds nothing.
Bind map[string]any
// Frame creates the window with the OS frame - the title bar and system
// buttons - and an opaque background. The default (false) is a frameless
// window: NO OS decoration of any kind and a fully transparent background,
// so the desktop shows through everywhere the page does not paint. The
// page is then responsible for the window chrome and marks the movable
// pieces with the "-app-region" CSS attribute ("drag" / "no-drag");
// see the package documentation. Resizing still works from the window
// edges unless State is StateFixed.
//
// Only meaningful for windows tuohi owns; an embedded window keeps its
// host's frame and background.
Frame bool
// Left/Top optionally place the window on the screen, in pixels. They are
// best effort, because not every window system lets a client pick its
// position: supported on Windows and on the GTK3/X11 stack; ignored on
// GTK4 and on Wayland (the compositor places windows). macOS treats the
// coordinates as AppKit screen coordinates (origin at the bottom-left).
// Both zero mean "let the platform decide".
Left, Top int
// Width/Height set the initial window size in pixels. Both zero mean the
// backend default (640x480), matching the behavior of a window that was
// never sized. With State StateMin/StateMax they are the respective
// minimum/maximum bounds instead.
Width, Height int
// State is the initial resize state (StateNone/StateMin/StateMax/
// StateFixed): StateFixed makes the window non-resizable (and disables the
// frameless edge resize), StateMin/StateMax turn Width/Height into bounds.
//
// Every geometry field above is applied once, when the window is created -
// see the note on the View type about window control.
State State
// contains filtered or unexported fields
}
View describes one window and its embedded web view. It is tuohi's define-first window object, the same pattern App uses for the application: the exported fields are the configuration of a window that does not exist yet, and App.Show(view) turns it into a live window. Configure a View, hand it to App.Show, and keep the pointer - after Show the same View is the handle to its window (Navigate, On/Off/Emit, Show/Hide, ...), so there is no separate window object to track.
Show reads the fields exactly once, when the window is created; later edits have no effect on the running window (matching how App commits its own settings). Re-showing the same View while its window is alive brings the window back (see App.Show); after View.Close the fields are read again, so the View can be reconfigured and shown anew. Before Show the imperative methods below have no engine behind them and either return a clear error/zero value or panic with "View is not shown".
func (*View) Close ¶
func (v *View) Close()
Close tears the view down for good: it terminates the view's run loop (if one is running) and destroys the native window and web view, then UNREGISTERS the View from the App that showed it and resets its internal state (engine handle, App reference) so the same View can be App.Show'n again later. It is idempotent and safe to call from any goroutine, concurrently too: exactly one call tears the window down. It is a no-op before the View was ever shown - closing an unshown View is not an error. Off the UI thread the teardown is queued and Close does not wait for it.
func (*View) Dialog ¶
Dialog presents a native, application-modal file panel chosen by opts.Type (open, multi-open, save or directory) and built on the github.com/terva-sh/tuohi/dialog package. Unlike the other View methods it BLOCKS the calling goroutine until the user dismisses the dialog. Call it from a Bind callback - which runs on a background goroutine - or any other goroutine; on the UI thread it runs the panel in place. Off the UI thread it needs a running loop (App.Wait): when none is running, or the loop stops before the panel opens, it returns an error and no panel is shown.
A cancelled dialog - and a dialog that could not be presented (no backend, no display) - returns an empty result and a nil error, per the dialog package contract.
func (*View) Emit ¶
Emit publishes an event to every listener on both sides. Each value in data becomes one argument delivered to the handlers (Go handlers receive it as raw JSON, JS handlers as a decoded value). It is safe to call from any goroutine; the JS-side listeners are notified on the UI thread. The page receives the event only while it is a trusted one (see View.Origins): a page on any other origin, and about:blank, receives nothing. Emit returns an error only if a value in data cannot be JSON-encoded, in which case nothing is published.
func (*View) Eval ¶
Eval evaluates arbitrary JavaScript asynchronously; the result is ignored. It is the supported way to run code on init: call it from the View.Ready callback, once the first page load completed, so the DOM and the page's own scripts are in place. Injecting script before the document exists is not reliable across the three engines, so tuohi exposes no declarative JS/CSS injection API.
func (*View) Focus ¶
Focus moves keyboard focus into the web content - so typing, and a screen reader's cursor, land inside the page - and, when raise is true, FIRST brings the window to the front and gives the application focus (the case a program that took focus away from itself needs: it launched a window that activates, finished a job that raised something else). Use Focus(true) sparingly - stealing focus from someone typing in another application is worse than the extra click it saves.
func (*View) Hide ¶
func (v *View) Hide()
Hide removes the window from the screen AND from the taskbar / window list - the classic "hide to tray" behavior: the process keeps running and the window stays alive until Show brings it back. On macOS a fullscreen window exits fullscreen before hiding; Show cancels that pending hide. Safe to call from any goroutine.
func (*View) Maximize ¶
func (v *View) Maximize()
Maximize enlarges the window to fill the available screen area. On macOS it performs the native zoom, which is a TOGGLE: calling Maximize on an already-zoomed window restores its previous size. Safe to call from any goroutine.
func (*View) Minimize ¶
func (v *View) Minimize()
Minimize shrinks the window to the taskbar / Dock (on macOS it is miniaturized into the Dock). Show restores it. Safe to call from any goroutine.
func (*View) Navigate ¶
Navigate loads the given URL in the view. The URL may be an "app://" URL served by App.FS, an https:// URL, a properly encoded data URI, or any other URL the platform engine accepts. An "app://" URL with no host loads from the "app" host: "app://#route" is "app://app/#route". Examples:
v.Navigate("https://github.com/terva-sh/tuohi")
v.Navigate("app://app/index.html")
v.Navigate("data:text/html,%3Ch1%3EHello%3C%2Fh1%3E")
func (*View) Off ¶
Off removes every Go handler subscribed to the named event. It does not affect the page's own JS listeners.
func (*View) On ¶
func (v *View) On(name string, handler func(args ...json.RawMessage)) (cancel func())
On subscribes handler to the named event and returns a function that cancels just this subscription. The handler receives the event's arguments, each as the raw JSON the emitter sent, to unmarshal into whatever type it expects. Handlers for a JS-originated event run on the binding goroutine; handlers for a Go-originated event run on the goroutine that called Emit. Re-enter the UI thread with Dispatch if a handler touches the window. Before Show the returned cancel is a no-op.
func (*View) RequestClose ¶ added in v0.2.0
func (v *View) RequestClose()
RequestClose asks to close through CloseRequested, for example from custom window chrome. It is safe from any goroutine and queues onto the UI thread. It does nothing before Show or after Close. Close tears down unconditionally.
func (*View) SetTitle ¶
SetTitle sets the window's title, as View.Title does at Show. An empty title hands the window back to the page: it follows the page's document.title again, or App.Name while the page has none. Safe to call from any goroutine.
func (*View) Show ¶
func (v *View) Show()
Show makes the window visible again and brings it to the front, putting it back into the taskbar / window list after Hide, or restoring it after Minimize. Safe to call from any goroutine (the backends marshal to the UI thread).
func (*View) Unmaximize ¶
func (v *View) Unmaximize()
Unmaximize restores a maximized window to its previous normal size (the inverse of Maximize). It is a no-op when the window is not maximized. Safe to call from any goroutine.
func (*View) Unminimize ¶
func (v *View) Unminimize()
Unminimize restores a minimized window to its normal on-screen state (the inverse of Minimize). It is a no-op when the window is not minimized. Safe to call from any goroutine.
func (*View) Window ¶
Window marshals f to the UI thread and calls it with the view's native window handle (a GtkWindow* / NSWindow* / HWND). Use it instead of touching the platform from a background goroutine: it is the re-enter-the-UI-thread entry point (the former Dispatch) with the handle handed to you. f runs on the UI thread; keep it short. Before Show it panics with "View is not shown".
func (*View) WindowState ¶ added in v0.2.0
func (v *View) WindowState() WindowState
WindowState returns the last delivered native snapshot, safe from any goroutine. An unshown or programmatically closed View returns Closed=true.
type WindowState ¶ added in v0.2.0
WindowState describes the native top-level window, independent of page focus. Visible means mapped/shown and not miniaturized where the native toolkit exposes that state. It does not imply that pixels are unobscured, on the current workspace, or visible to the user. Closed is terminal for this window lifetime. Close resets the View before creating a new lifetime.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package autostart registers an application to launch when the user logs in, cgo-free.
|
Package autostart registers an application to launch when the user logs in, cgo-free. |
|
Package clipboard reads and writes the system clipboard's text, and reads its image.
|
Package clipboard reads and writes the system clipboard's text, and reads its image. |
|
Package dialog shows the operating system's native open, save and choose-directory panels, cgo-free.
|
Package dialog shows the operating system's native open, save and choose-directory panels, cgo-free. |
|
Package instance keeps one process of an application running per user and hands the command line of every later launch to it, cgo-free.
|
Package instance keeps one process of an application running per user and hands the command line of every later launch to it, cgo-free. |
|
internal
|
|
|
desktopentry
Package desktopentry writes and reads values of a freedesktop.org Desktop Entry file, for the two places tuohi writes one: the GTK3 Wayland identity in the root package and the XDG backend of tuohi/autostart.
|
Package desktopentry writes and reads values of a freedesktop.org Desktop Entry file, for the two places tuohi writes one: the GTK3 Wayland identity in the root package and the XDG backend of tuohi/autostart. |
|
toolkit
Package toolkit hands what the root package knows about the native toolkit it has started to the desktop-service packages, which must not import the root.
|
Package toolkit hands what the root package knows about the native toolkit it has started to the desktop-service packages, which must not import the root. |
|
Package notify displays OS-level desktop notifications without cgo or bundled libraries.
|
Package notify displays OS-level desktop notifications without cgo or bundled libraries. |
|
Package tray puts an icon with a menu in the system tray / menu bar, with no cgo and no bundled libraries.
|
Package tray puts an icon with a menu in the system tray / menu bar, with no cgo and no bundled libraries. |
|
Package updater verifies, stages and installs explicitly selected desktop updates.
|
Package updater verifies, stages and installs explicitly selected desktop updates. |