desktop

package
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 13, 2026 License: MIT Imports: 13 Imported by: 0

README

dark/desktop

Native desktop window for dark applications, powered by glaze WebView.

Wraps your http.Handler in a native window with Go↔JS function bindings and a bidirectional event system — inspired by Wails and Tauri. All dark features (SSR, Islands, htmx, sessions) work unmodified in desktop mode.

Import this package only when building desktop apps — it pulls in the glaze WebView native library dependency.

Quick Start

package main

import (
    "runtime"

    "github.com/i2y/dark"
    "github.com/i2y/dark/desktop"
)

func init() { runtime.LockOSThread() }

func main() {
    app, _ := dark.New(
        dark.WithLayout("layouts/default.tsx"),
        dark.WithTemplateDir("views"),
    )
    defer app.Close()

    app.Get("/", dark.Route{
        Component: "pages/index.tsx",
        Loader:    indexLoader,
    })

    // Simple: one-liner convenience function
    desktop.Run(app.MustHandler(), desktop.WithTitle("My App"), desktop.WithSize(1024, 768))
}

Full API

For Go↔JS bindings and events, use desktop.New + dsk.Run:

func init() { runtime.LockOSThread() }

func main() {
    app, _ := dark.New(/* ... */)
    defer app.Close()
    // ... register routes, islands, middleware ...

    dsk := desktop.New(app.MustHandler(),
        desktop.WithTitle("My App"),
        desktop.WithSize(1280, 800),
        desktop.WithMinSize(640, 480),
        desktop.WithDebug(true),
        desktop.WithOnReady(func(url string) { fmt.Println("Running at", url) }),
    )

    // Expose Go functions to JavaScript (appear as globals, return Promises)
    dsk.Bind("readFile", func(path string) (string, error) {
        data, err := os.ReadFile(path)
        return string(data), err
    })

    // Bind all exported methods of a struct
    dsk.BindMethods("api", myService) // → window.api_get_user(), api_list_items(), etc.

    // Listen for events from frontend
    dsk.On("save", func(data any) {
        fmt.Println("save requested:", data)
    })

    // Wait for WebView to be ready before emitting events
    go func() {
        <-dsk.Ready()
        dsk.Emit("notify", map[string]any{"message": "Hello from Go!"})
    }()

    dsk.Run() // blocks until window closed
}

Go↔JS Bindings

Bind

Expose a Go function as a global JavaScript function. The function appears as window.<name>(...) and returns a Promise.

dsk.Bind("greet", func(name string) string {
    return "Hello, " + name
})
const msg = await greet("World"); // "Hello, World"

Functions may accept JSON-serializable arguments and return nothing, a value, an error, or (value, error).

BindMethods

Expose all exported methods of a struct. Method names are converted to snake_case.

type UserService struct { /* ... */ }
func (s *UserService) GetByID(id int) (*User, error) { /* ... */ }
func (s *UserService) ListAll() []User { /* ... */ }

dsk.BindMethods("users", &UserService{})
const user = await users_get_by_id(42);
const all  = await users_list_all();

Events

Bidirectional event system between Go and JavaScript.

Go side
// Listen for events from frontend
dsk.On("save", func(data any) {
    fmt.Println("save:", data)
})

// Send events to frontend (safe from any goroutine)
dsk.Emit("notify", map[string]any{"message": "Done!"})
JavaScript side

The window.dark API is auto-injected into every page:

// Listen for events from Go
dark.on("notify", (data) => {
    console.log(data.message); // "Done!"
});

// Unsubscribe
dark.off("notify");          // remove all listeners
dark.off("notify", handler); // remove specific listener

// Send events to Go
dark.emit("save", { draft: true });

App Lifecycle

Ready() returns a channel that closes once the WebView and JS bridge are initialized. Use it to safely start background work:

go func() {
    <-dsk.Ready()
    dsk.Emit("started", nil)
    dsk.SetTitle("Ready!")
}()
dsk.Run()

Native Features

Built-in support for file dialogs, clipboard, and OS notifications — available from JavaScript via window.dark.* with no setup required.

File Dialogs

Native open/save dialogs powered by zenity (pure Go, no CGo).

// Open a file (returns path or "" if cancelled)
const path = await dark.openFile({ title: "Open", filters: ["*.txt", "*.md"] });

// Open multiple files
const paths = await dark.openFiles({ title: "Select files" });

// Save file dialog
const savePath = await dark.saveFile({ title: "Save as", filename: "doc.txt" });

// Pick a folder
const dir = await dark.pickFolder({ title: "Select folder" });

Options: title (dialog title), filename (initial filename), filters (glob patterns).

Clipboard

Read and write the system clipboard via atotto/clipboard.

const text = await dark.readClipboard();
await dark.writeClipboard("copied text");
OS Notifications

Native desktop notifications via beeep.

await dark.notify("Title", "Message body");

Links to external URLs automatically open in the system's default browser instead of navigating inside the webview. Any <a href="https://..."> pointing outside the internal server is intercepted.

// Programmatic usage
await dark.openExternal("https://example.com");

Only http and https URLs are supported. Other schemes (file://, javascript:, etc.) are ignored for security.

Window Control

From JavaScript
dark.setTitle("New Title");
dark.close();
From Go

These methods are safe to call from any goroutine:

dsk.SetTitle("Updated")
dsk.SetSize(800, 600)
dsk.Eval("console.log('hello')")
dsk.Terminate()

Window Options

desktop.WithTitle("App")           // window title (default: "App")
desktop.WithSize(1024, 768)        // initial dimensions (default: 1024x768)
desktop.WithMinSize(400, 300)      // minimum window size
desktop.WithMaxSize(1920, 1080)    // maximum window size
desktop.WithFixedSize()            // non-resizable window
desktop.WithDebug(true)            // enable browser DevTools
desktop.WithAddr("127.0.0.1:0")   // HTTP listen address (default: random port)
desktop.WithOnReady(func(url string) { ... })

Threading

The WebView requires the main OS thread. Your main package must include:

func init() { runtime.LockOSThread() }

Run() calls runtime.LockOSThread() internally as well. All App methods (SetTitle, SetSize, Eval, Emit, Terminate) are safe to call from any goroutine — they use Dispatch internally to post work to the UI thread.

Example

See _examples/desktop-demo/ for a full example combining Islands, htmx, sessions, form validation, Go↔JS bindings, desktop events, file dialogs, clipboard, and notifications.

Documentation

Overview

Package desktop provides a native desktop window for dark applications. Import this package only when building desktop apps — it pulls in the WebView native library dependency.

Simple usage with convenience Run:

func init() { runtime.LockOSThread() }

func main() {
    app, _ := dark.New(...)
    desktop.Run(app.MustHandler(), desktop.WithTitle("My App"))
}

Full usage with Go↔JS bridge and events:

func init() { runtime.LockOSThread() }

func main() {
    app, _ := dark.New(...)
    dsk := desktop.New(app.MustHandler(),
        desktop.WithTitle("My App"),
        desktop.WithSize(1280, 800),
        desktop.WithMinSize(640, 480),
        desktop.WithDebug(true),
    )
    dsk.Bind("greet", func(name string) string {
        return "Hello, " + name
    })
    dsk.On("save", func(data any) {
        fmt.Println("save:", data)
    })
    dsk.Run()
}

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Run

func Run(handler http.Handler, opts ...Option) error

Run is a convenience function for simple desktop apps that don't need Go↔JS bindings or events. For the full API, use New instead.

Types

type App

type App struct {
	// contains filtered or unexported fields
}

App wraps an http.Handler in a native desktop window with optional Go↔JS bindings and a bidirectional event system.

func New

func New(handler http.Handler, opts ...Option) *App

New creates a desktop App. Call Bind, BindMethods, and On to configure the Go↔JS bridge, then call Run to open the window.

func (*App) Bind

func (a *App) Bind(name string, fn any) error

Bind exposes a Go function as a global JavaScript function. The function appears as window.<name>(...) and returns a Promise.

The function may accept JSON-serializable arguments and return:

  • nothing
  • a value
  • an error
  • (value, error)

Call Bind before Run to register bindings. Calling after Run starts applies the binding immediately (thread-safe).

func (*App) BindMethods

func (a *App) BindMethods(prefix string, obj any) error

BindMethods exposes all exported methods of obj as global JavaScript functions named window.<prefix>_<method_name>(). Method names are converted to snake_case (e.g., GetUser → get_user).

Methods must follow the same signature rules as Bind.

func (*App) Emit

func (a *App) Emit(event string, data any)

Emit sends a named event with data to the frontend. Listeners registered via window.dark.on(event, callback) will be invoked. Safe to call from any goroutine.

func (*App) Eval

func (a *App) Eval(js string)

Eval executes JavaScript in the WebView. Safe to call from any goroutine.

func (*App) On

func (a *App) On(event string, fn func(data any))

On registers a handler for events emitted from the frontend via window.dark.emit(event, data). Safe to call before or during Run.

func (*App) Ready

func (a *App) Ready() <-chan struct{}

Ready returns a channel that is closed once the WebView and JS bridge are initialized. Use this to safely wait before calling Emit or other methods from background goroutines:

go func() {
    <-dsk.Ready()
    dsk.Emit("started", nil)
}()

func (*App) Run

func (a *App) Run() error

Run starts the internal HTTP server, opens a native WebView window, and blocks until the window is closed.

The caller must pin the main goroutine to the main OS thread:

func init() { runtime.LockOSThread() }

func (*App) SetSize

func (a *App) SetSize(w, h int)

SetSize changes the window dimensions at runtime. Safe to call from any goroutine.

func (*App) SetTitle

func (a *App) SetTitle(title string)

SetTitle changes the window title at runtime. Safe to call from any goroutine.

func (*App) Terminate

func (a *App) Terminate()

Terminate closes the window and stops the event loop. Safe to call from any goroutine.

type Option

type Option func(*config)

Option configures the desktop App.

func WithAddr

func WithAddr(addr string) Option

WithAddr sets the listen address for the internal HTTP server. Defaults to "127.0.0.1:0" (random port).

func WithDebug

func WithDebug(debug bool) Option

WithDebug enables browser developer tools in the WebView.

func WithFixedSize

func WithFixedSize() Option

WithFixedSize makes the window non-resizable.

func WithMaxSize

func WithMaxSize(w, h int) Option

WithMaxSize sets the maximum window dimensions.

func WithMinSize

func WithMinSize(w, h int) Option

WithMinSize sets the minimum window dimensions.

func WithOnReady

func WithOnReady(fn func(url string)) Option

WithOnReady registers a callback invoked with the local URL once the HTTP server is listening.

func WithSize

func WithSize(w, h int) Option

WithSize sets the initial window dimensions. Defaults to 1024×768.

func WithTitle

func WithTitle(title string) Option

WithTitle sets the window title. Defaults to "App".

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL