window

package module
v0.30.0 Latest Latest
Warning

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

Go to latest
Published: Aug 13, 2026 License: BSD-3-Clause Imports: 18 Imported by: 0

README

go-widgets/window

A pure-Go, CGO-free windowing backend for the go-widgets toolkit, with five interchangeable backends behind one Open/Run API — X11, Wayland, macOS Cocoa/AppKit, Windows Win32/GDI and wasmbox (the wasmdesk/wasmbox browser compositor). Open auto-selects per environment: a real X11/Wayland window on Linux, a real NSWindow on macOS, a real Win32 window on Windows, and — when built for js/wasm — a wasmbox external client. One go-widgets application runs unchanged natively AND inside wasmdesk.

The macOS backend reaches AppKit through the fleet's shared purego Objective-C bridge go-macos/objc — no cgo; the Windows backend reaches Win32/GDI through the process' own user32/gdi32/kernel32 DLLs via syscall.NewLazyDLL and a syscall.NewCallback WNDPROC — no cgo — so both link with CGO_ENABLED=0.

It implements the X11 core protocol (v11.0) from scratch over the unix socket — no Xlib, no XCB, no cgo — the same sovereign transport + wire-codec approach used by godbus/dbus/v5. It opens a real window on Linux, blits the toolkit's RGBA framebuffer into it via the core-protocol PutImage, and routes X input into toolkit.Event.

┌──────────────────────────────────────────────────────────────┐
│  go-widgets/toolkit widget tree  (Button, Label, VBox, …)      │
├──────────────────────────────────────────────────────────────┤
│  window.Window   layout → painter.PixelPainter → RGBA buffer   │
│                  X events → toolkit.Event → root.OnEvent       │
├──────────────────────────────────────────────────────────────┤
│  internal/x11    sovereign X11 core protocol (from scratch)    │
│    · wire codec (both byte orders)  · setup handshake          │
│    · MIT-MAGIC-COOKIE-1 Xauthority  · keycode→keysym mapping   │
│    · request/reply/error/event demux                           │
│    · PutImage (RGBA→visual pixel packing, max-request tiling)  │
│    · MIT-SHM 1.2 fast path (shm fd over SCM_RIGHTS, ShmPutImage)│
├──────────────────────────────────────────────────────────────┤
│  unix socket  /tmp/.X11-unix/X<n>   →  X server                │
└──────────────────────────────────────────────────────────────┘

Usage

package main

import (
	"github.com/go-widgets/toolkit"
	"github.com/go-widgets/window"
)

func main() {
	w, err := window.Open(window.Config{Title: "Demo", Width: 480, Height: 320})
	if err != nil {
		panic(err)
	}
	defer w.Close()

	box := toolkit.NewVBox()
	box.Append(toolkit.NewLabel("Hello from a pure-Go X11 window"))
	box.Append(toolkit.NewButton("Click me", func() { /* ... */ }))

	w.Run(box) // drives layout/draw/present + dispatches input until closed
}

Run the bundled example: go run ./cmd/windowdemo.

Backends

Open returns a Backend (Run/Close/Size/String); the application is backend-agnostic. The environment selects the implementation:

GOOS/env Backend Transport
Linux, $WAYLAND_DISPLAY set Wayland (internal/wayland) xdg-shell over the compositor unix socket
Linux, else $DISPLAY X11 (internal/x11) X11 core protocol over the unix socket (+ MIT-SHM)
macOS (darwin) Cocoa/AppKit (internal/cocoa) NSWindow + NSView via go-macos/objc (purego), NSBitmapImageRep present
Windows (windows) Win32/GDI (internal/win32) top-level HWND via user32/gdi32 syscalls + NewCallback WNDPROC, StretchDIBits BGRA present
js/wasm wasmbox (internal/wasmbox) wasmbox client protocol over a MessagePort + a SharedArrayBuffer surface
other (BSD, …) stub → ErrUnsupported
macOS Cocoa/AppKit backend (darwin)

On macOS Open creates a real NSWindow with a flipped content NSView, presents the toolkit's RGBA framebuffer by wrapping it in an NSBitmapImageRep drawn in -drawRect:, and decodes native NSEvent mouse/scroll/key input into toolkit.Event. It honours the opt-in DamageRenderer (only damaged rectangles are invalidated via -setNeedsDisplayInRect: and re-blitted). Everything runs through go-macos/objc over puregono cgo. The OS-independent NSEventtoolkit.Event mapping, flipped-view coordinate maths and damage→dirty-rect conversion live in a sovereign, 100%-covered codec (internal/cocoa/mapping.go); the darwin-only AppKit glue (internal/cocoa/cocoa_darwin.go) is proven live on-device by the darwin (cocoa) CI lane (open a window, render it, assert sampled pixels, synthesise a click + key and assert the dispatched event + the button counter).

Windows Win32/GDI backend (windows)

On Windows Open declares Per-Monitor-V2 DPI awareness, registers a window class and creates a real titled, resizable top-level HWND, presents the toolkit's RGBA framebuffer by packing it BGRA into a top-down 32bpp DIB and blitting it with StretchDIBits on WM_PAINT, and decodes native WM_* mouse/wheel/key messages into toolkit.Event. It honours the opt-in DamageRenderer (only damaged rectangles are re-packed and InvalidateRect'd, so WM_PAINT's update region blits just those). To stay readable on HiDPI it renders the toolkit at logical size and lets the OS up-sample to the physical client area (scale = GetDpiForWindow/96), rather than rendering at device pixels and presenting into a smaller area. The whole path reaches Win32 through the process' own user32/gdi32/kernel32 DLLs via syscall.NewLazyDLL and a syscall.NewCallback WNDPROC — no cgo. The OS-independent WM_*toolkit.Event mapping, RGBA→BGRA DIB packing, DPI/size maths and damage→InvalidateRect conversion live in a sovereign, 100%-covered codec (internal/win32/mapping.go); the windows-only Win32 glue (internal/win32/win32_windows.go) is proven live on-device on a Windows 11 arm64 QEMU VM — a real Win32 window rendering a VBox+Label+Button (capture), with three injected WM_LBUTTONDOWN/UP messages driving the button's counter 0 → 3 end to end through the WNDPROC (after).

wasmbox client backend (js/wasm)

On js/wasm the environment is the wasmdesk/wasmbox browser compositor, so instead of dialling a display server the backend runs as an external client of the compositor: it allocates the surface SharedArrayBuffer, posts hello over its per-client MessagePort, awaits welcome, paints the widget tree into the SAB and posts commit — whole-surface, or (when the root implements DamageRenderer, e.g. toolkit/scene.HostRoot) just the damaged rectangles. Incoming input messages map to toolkit.Event exactly as the X11/Wayland backends do. The wire protocol (wasmbox docs/protocol.md) is implemented in a sovereign, transport-agnostic codec (internal/wasmbox/protocol.go, unit-tested to 100% on every GOOS); the syscall/js glue (client_js.go) only carries the live JS handles. The wasmbox repository is not modified — this is purely a client-side backend plus a worker shim.

Build the client and run it inside a compositor:

clients/gowidgets/build.sh          # → clients/gowidgets/{gowidgets.wasm,wasm_exec.js}
# a wasmbox compositor spawns it via:
#   wasmboxSpawnExternal("<origin>/clients/gowidgets/worker.js")

The live browser proof (headless Chromium via Playwright, served by wasmbox's own COOP/COEP cmd/serve) lives in test/, in two tiers:

  • Real desktop (test/probe-wasmbox-real.mjs) — drives the actual wasmdesk/wasmbox Ruby compositor (compositor/*.rb on the pure-Go rbgo interpreter, baked into wasmbox.wasm). It boots the real desktop, spawns this client with the documented globalThis.wasmboxSpawnExternal("clients/gowidgets/worker.js") hook (a real external Worker + wasm instance over the step-C.1 MessagePort + SAB), reads the compositor's own composited pixels (__wasmboxReadRegion) to assert the VBox+Label+Button rendered at the window's live focused rect, and injects a real page.mouse.click that the compositor routes to the focused window — asserting the counter goes 0→1 (input → toolkit.Event through the real input routing). Captured: test/wasmbox-live-proof-real-desktop-2026-08-09.png (the go-widgets window composited on the rbgo desktop, reading "Clicks: 1"). The wasmbox repo is unmodified; the client is served same-origin via a symlink overlay — see test/README-real-desktop.md.
  • Deterministic floor (test/probe-wasmbox.mjs) — the same assertions against test/harness.html, a protocol-faithful compositor stand-in, so the wire + SAB + input round-trip are exercised even without building the ~80 MB Ruby compositor. Captured: test/wasmbox-live-proof-2026-08-09.png.

Public API

  • window.Open(cfg Config) (*Window, error) — dial $DISPLAY, authenticate, create and map the window. Linux only; returns window.ErrUnsupported elsewhere so cross-builds stay green.
  • (*Window).Run(root toolkit.Widget) error — the host loop: initial layout/draw/present, then translate X events (Expose, KeyPress/Release, ButtonPress/Release, MotionNotify, ConfigureNotify, ClientMessage) into toolkit.Event and dispatch them, re-laying-out on resize.
  • (*Window).Close() error, (*Window).Size() (int, int).

Design notes

  • Sovereign protocol. internal/x11 speaks the wire format byte-for-byte and is transport-agnostic (io.ReadWriteCloser), so the full request/reply/event machine is tested in-process against a scripted fake server — 100 % statement coverage on the codec, Xauthority parser and keysym mapping, both byte orders, error branches included.
  • Present. The toolkit's painter.PixelPainter renders into the backing RGBA buffer; the backend converts to the screen visual's pixel layout (channel masks + image byte order) and tiles PutImage under the server's maximum request length. A presentRect damage-region path is ready for when a scene damage list becomes available (toolkit exposes none today, so a full-surface present follows input).
  • Wayland is a separate future backend, intentionally out of scope here.

Verification

  • Unit tests run on amd64, arm64, and under qemu on riscv64, loong64, ppc64le, s390x — the big-endian wire path exercised on real big-endian (s390x) models, all strictly CGO=0.
  • A live X11 proof (-tags=integration, WINDOW_X11_INTEGRATION=1) runs under Xvfb: it opens a window, presents a known four-quadrant pattern, captures it with import, asserts the sampled pixels, then synthesises a click and a key with xdotool and asserts the dispatched toolkit.Event.

License

BSD-3-Clause. Copyright (c) the go-widgets/window authors.

Documentation

Overview

Package window is a pure-Go (CGO-free, no Xlib/XCB) X11 windowing backend for the go-widgets toolkit. It opens a real window on an X11 server, blits the toolkit's RGBA framebuffer into it via the core protocol's PutImage, and routes X input events into toolkit.Event, so a go-widgets widget tree runs on a Linux desktop exactly as it does in the browser/wasm host.

The X11 protocol itself is implemented from scratch in the internal/x11 package over a raw byte stream, mirroring the sovereign transport+codec approach of a pure-Go wire library such as github.com/godbus/dbus/v5.

Open dials the server named by $DISPLAY; it is implemented on Linux and returns ErrUnsupported elsewhere, so cross-builds stay green. The windowing logic (framebuffer, present, event translation, run loop) is platform-independent and driven through the transport-agnostic internal/x11 connection.

Index

Constants

View Source
const NativeScale = -1.0

NativeScale asks for a framebuffer at the display's own resolution rather than one pixel per logical point. See Config.RenderScale for when that is the right thing to ask for -- it is a narrower case than it sounds.

Variables

View Source
var ErrUnsupported = errors.New("window: no native windowing backend for this platform")

ErrUnsupported is returned by Open on platforms with no windowing backend — everything that is neither Linux (X11/Wayland) nor macOS (Cocoa/AppKit) nor Windows (Win32/GDI) nor the js/wasm wasmbox environment.

Functions

func VisibleScreenSize added in v0.27.0

func VisibleScreenSize() (w, h int, ok bool)

VisibleScreenSize returns the usable area of the primary screen in LOGICAL points (see screen_darwin.go for the full contract). On every non-macOS platform it reports ok=false for now: the X11, Wayland, Windows and js/wasm backends have no screen-size query yet, so a caller must fall back to its own default — or leave Config.Width/Height ≤ 0 and let the backend choose a readable size.

Types

type Appearance added in v0.14.0

type Appearance struct {
	// Dark is the effective dark/light mode.
	Dark bool
	// Accent is the user's accent colour, meaningful only when HasAccent is
	// set. A system too old to have the notion, or one where the user made no
	// choice, reports HasAccent false rather than a made-up colour.
	Accent    color.RGBA
	HasAccent bool
}

Appearance is the host UI's look: what the user has told their system they want everything to look like.

A go-widgets app picks its own theme, which is right for an app with a designed identity and wrong for one that should feel native. An app that wants to belong on the desktop it is running on needs to know that the user chose dark mode and picked purple as their accent, and no amount of theming inside the toolkit can discover that: it is a platform fact.

type AppearanceReader added in v0.14.0

type AppearanceReader interface {
	Appearance() Appearance
	SystemFontTTF() ([]byte, error)
}

AppearanceReader is an optional Backend capability: reading the host look.

Appearance is cheap enough to poll -- a handful of platform queries, no allocation -- so a back-end need not push changes and an app can simply ask each frame and act when the answer differs. That keeps the seam a plain question instead of a callback with a lifetime.

SystemFontTTF is separate precisely because it is NOT cheap: the macOS system face is tens of megabytes on disk, so it is asked for once at startup, not on every poll. It returns the raw sfnt bytes, ready for toolkit.NewTrueTypeFont, and an error when the platform has no such file to offer.

Implemented by macOS (Cocoa), Windows (the registry's personalisation keys) and both Linux back-ends, which read the same XDG desktop portal — the desktop look is not a property of the display server carrying the pixels.

type Backend

type Backend interface {
	// Run binds root, performs the initial layout+present, then dispatches
	// server/compositor events into the widget tree until the window closes.
	Run(root toolkit.Widget) error
	// Close releases the window and its connection.
	Close() error
	// Size returns the current client size in pixels.
	Size() (int, int)
	// String identifies the window for debugging.
	String() string
}

Backend is an open, backend-specific window bound to a go-widgets scene. The X11 (*Window), Wayland, macOS Cocoa, Windows Win32 and wasmbox backends all satisfy it, so Open can return whichever the environment selects and a go-widgets application is backend-agnostic: it just calls Run, Size, String and Close.

func Open

func Open(cfg Config) (Backend, error)

Open connects to the running display server and returns a window ready for Run. It auto-selects the backend: Wayland when $WAYLAND_DISPLAY is set (the modern default on contemporary Linux desktops), otherwise the X11 backend driven by $DISPLAY. Both are sovereign, pure-Go, CGO-free implementations of their wire protocols.

type Clipboard added in v0.13.0

type Clipboard interface {
	// ClipboardText returns the pasteboard's plain-text contents, or "" when it
	// holds no text (an image, a file promise, or nothing at all).
	ClipboardText() string
	// SetClipboardText replaces the pasteboard's contents with text.
	SetClipboardText(text string)
}

Clipboard is an optional Backend capability: the host OS text clipboard.

Copy and paste inside a go-widgets app already work through the toolkit's own in-process clipboard. What that cannot do is carry text ACROSS applications — paste a URL from a browser into a text field, or copy an article's title out to somewhere else — because the OS pasteboard is a platform facility and the toolkit is deliberately platform-free.

A back-end that can reach the pasteboard implements this. Its method set is deliberately identical to toolkit.Clipboard, so an app installs it in one line and every text widget's copy/cut/paste starts going through the real OS pasteboard:

w, err := window.Open(cfg)
...
if c, ok := w.(window.Clipboard); ok {
	toolkit.SetClipboard(c)
}

That line is the app's to write, not Open's: reaching into a package-level toolkit setting is a decision an app makes, not a side effect a constructor should have. A back-end that cannot reach a pasteboard simply does not implement this, the assertion fails, and the toolkit's in-process clipboard stays in place — copy/paste still works within the app.

Implemented by every back-end that has a pasteboard to reach: macOS through NSPasteboard, Windows through the Win32 clipboard, X11 through selection ownership and Wayland through wl_data_device.

type Config

type Config struct {
	// Title is the WM_NAME shown in the title bar.
	Title string
	// Instance and Class populate WM_CLASS (window-manager grouping). When
	// empty they default to Title (Instance) and Title (Class).
	Instance string
	Class    string
	// Width and Height are the initial client size in LOGICAL points (the unit
	// the toolkit lays out and the user reads in — not device pixels). A value ≤ 0
	// asks the backend for a readable default: the macOS (Cocoa) backend derives
	// it from the main screen's visible frame; the X11/Wayland backends use their
	// standard 640×480. A desktop shell should pass its own point size here.
	Width  int
	Height int
	// Display overrides $DISPLAY (e.g. ":0"). Empty uses the environment.
	Display string
	// Theme overrides the toolkit theme used to paint the background and
	// widgets. Nil uses toolkit.DefaultDark.
	Theme *toolkit.Theme
	// RenderScale is how many framebuffer pixels the back-end allocates per
	// logical point.
	//
	// Zero, the default, is one pixel per point. The UI is laid out and painted
	// at a readable size and the compositor up-samples it to a HiDPI display,
	// which is slightly soft but correct for every widget tree: the toolkit lays
	// out in the same units it paints in, so a framebuffer twice as wide would
	// give a window full of widgets at half the size.
	//
	// [NativeScale] follows the display's backing factor, giving a framebuffer at
	// the panel's true resolution. It is CORRECT ONLY FOR A ROOT THAT RENDERS ITS
	// OWN PIXELS at the size it is given -- a [toolkit.Surface] over an
	// application's own scene -- because such a root is told the render-pixel
	// size and composes for it, so nothing is laid out in the wrong unit. Passing
	// it with an ordinary widget tree is not an error the back-end can detect; it
	// simply makes everything half-size on a 2x display.
	//
	// Any other positive value is used as-is.
	//
	// Honoured today by the macOS (Cocoa) back-end.
	RenderScale float64
}

Config parametrises a window.

type DamageRenderer added in v0.4.0

type DamageRenderer interface {
	// RenderDamaged paints this frame into p (the framebuffer painter, whose
	// clip seam confines each rectangle's repaint to the damage) and returns
	// the rectangles it repainted, in surface pixel coordinates. An empty
	// result means nothing changed this frame, so the backend presents nothing.
	// The returned slice need only stay valid until the backend has presented
	// it (which it does immediately, before the next frame).
	RenderDamaged(p painter.Painter, th *toolkit.Theme) []toolkit.Rect
}

DamageRenderer is the OPT-IN capability a root handed to Run may implement to drive incremental (damage-region) present instead of full-surface present.

A plain toolkit.Widget root keeps the full-surface path: every frame the whole framebuffer is repainted and blitted (correct, simple, unchanged). A root that ALSO implements DamageRenderer lets Run repaint and blit ONLY the rectangles that actually changed: Run draws the frame through RenderDamaged, takes the returned damage, and packs+presents just its (coalesced) union via the backend's small-rect present path (X11 MIT-SHM ShmPutImage over a framebuffer-mirroring segment, or a wl_shm sub-rect DamageBuffer). The very first frame, a resize and an X11 Expose still present the full surface — a resize because the framebuffer is reallocated, an Expose because the server discarded the window's contents — after which Run resumes incremental present.

github.com/go-widgets/toolkit/scene provides the reference implementation (scene.HostRoot), which is pixel-identical to a full repaint by construction; the interface is declared here, structurally, so the backend needs no import of the scene layer.

type Repainter added in v0.17.0

type Repainter interface {
	Repaint()
}

Repainter is an optional Backend capability: ask for a repaint from ANY goroutine.

This package repaints when something happens — an event, a resize, a [scene.HostRoot] invalidation — which covers an interface that only changes because the user did something. It does not cover an application whose content arrives on its own: a feed reader with a fetch in flight, a log viewer, a clock. Such an application draws its first frame and, with the window idle, would show it for as long as it runs.

Repaint is safe to call from any goroutine and returns immediately; the back-end marshals the work to whatever thread its platform demands. Calling it more often than the display refreshes is not an error, just wasted frames.

Implemented today by the macOS (Cocoa) back-end.

type Scaler added in v0.16.0

type Scaler interface {
	RenderScale() float64
}

Scaler is an optional Backend capability: how many framebuffer pixels the back-end is allocating per logical point.

It is the answer to Config.RenderScale, which may have been NativeScale -- "whatever the panel is" -- and so is not something the caller can compute from what it passed in. A self-rendering root needs it to tell its own renderer what a point is worth: Backend.Size reports FRAMEBUFFER pixels, and dividing by this gives the logical size the user actually sees.

A back-end that does not implement it renders one pixel per point.

type Window

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

Window is an open X11 window bound to a go-widgets scene. It owns the backing RGBA framebuffer, presents it to the server and drives the toolkit widget tree from X input events.

func (*Window) Appearance added in v0.22.0

func (w *Window) Appearance() Appearance

Appearance reports the desktop's colour scheme and accent colour. Implements the AppearanceReader capability.

func (*Window) ClipboardText added in v0.21.0

func (w *Window) ClipboardText() string

ClipboardText asks the current owner for the selection and waits for it.

Implements the Clipboard capability.

func (*Window) Close

func (w *Window) Close() error

Close closes the window's connection to the server.

func (*Window) Run

func (w *Window) Run(root toolkit.Widget) error

Run binds root to the window, performs the initial layout+draw+present, then dispatches server events into the toolkit until the window is closed (WM_DELETE_WINDOW) or the connection ends. It is the real-window analogue of the wasm compositor host loop.

func (*Window) SetClipboardText added in v0.21.0

func (w *Window) SetClipboardText(text string)

SetClipboardText claims the CLIPBOARD selection and remembers the text, which is all copying is here. The text is handed out later, one requestor at a time, by answerSelectionRequest.

Implements the Clipboard capability.

func (*Window) Size

func (w *Window) Size() (int, int)

Size returns the current client size in pixels.

func (*Window) String

func (w *Window) String() string

String identifies the window for debugging.

func (*Window) SystemFontTTF added in v0.22.0

func (w *Window) SystemFontTTF() ([]byte, error)

SystemFontTTF reports that there is no font file to hand over. Implements the AppearanceReader capability.

Directories

Path Synopsis
cmd
gowidgetsclient command
windowdemo command
Command windowdemo opens a real native window showing a few go-widgets widgets, driven by the pure-Go github.com/go-widgets/window backend — an X11 or Wayland window on Linux, an NSWindow on macOS, a Win32 window on Windows.
Command windowdemo opens a real native window showing a few go-widgets widgets, driven by the pure-Go github.com/go-widgets/window backend — an X11 or Wayland window on Linux, an NSWindow on macOS, a Win32 window on Windows.
internal
atspi
Package atspi is the Linux accessibility bridge: it publishes the widget tree on the AT-SPI bus, where Orca and every other Linux screen reader read it.
Package atspi is the Linux accessibility bridge: it publishes the widget tree on the AT-SPI bus, where Orca and every other Linux screen reader read it.
cocoa
Package cocoa is the pure-Go (CGO-free, via purego) macOS AppKit windowing backend for the go-widgets toolkit.
Package cocoa is the pure-Go (CGO-free, via purego) macOS AppKit windowing backend for the go-widgets toolkit.
dnd
Package dnd is the backend-agnostic drag-and-drop state machine that every native windowing backend shares.
Package dnd is the backend-agnostic drag-and-drop state machine that every native windowing backend shares.
wasmbox
Package wasmbox implements the client half of the wasmdesk/wasmbox external-client wire protocol, so a go-widgets application can run as a client of the browser compositor exactly as it runs on X11 or Wayland.
Package wasmbox implements the client half of the wasmdesk/wasmbox external-client wire protocol, so a go-widgets application can run as a client of the browser compositor exactly as it runs on X11 or Wayland.
wayland
Package wayland is a from-scratch, pure-Go (CGO-free, zero non-stdlib dependency) implementation of the Wayland wire protocol, spoken directly over a UNIX-domain stream socket.
Package wayland is a from-scratch, pure-Go (CGO-free, zero non-stdlib dependency) implementation of the Wayland wire protocol, spoken directly over a UNIX-domain stream socket.
win32
Package win32 is the pure-Go (CGO-free) Windows Win32/GDI windowing backend for the go-widgets toolkit.
Package win32 is the pure-Go (CGO-free) Windows Win32/GDI windowing backend for the go-widgets toolkit.
x11
Package x11 is a from-scratch, pure-Go (CGO-free, zero non-stdlib dependency) implementation of the X Window System core protocol, version 11.0, spoken directly over a byte stream (a unix-domain socket in practice).
Package x11 is a from-scratch, pure-Go (CGO-free, zero non-stdlib dependency) implementation of the X Window System core protocol, version 11.0, spoken directly over a byte stream (a unix-domain socket in practice).

Jump to

Keyboard shortcuts

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