Skip to content

Getting started ​

lys-ui is a pure-Go (no cgo) widget toolkit for Wayland. It runs on any compositor with xdg-shell; sway and Hyprland are its CI gates.

Install ​

sh
go get github.com/stubbedev/lys-ui@latest

A first app ​

A counter: a component with typed messages, its view built with the ui builders. This is component/example_test.go, kept compiling as an example test:

go
package component_test

import (
	"errors"
	"log"
	"strconv"

	"github.com/stubbedev/lys-ui/app"
	"github.com/stubbedev/lys-ui/component"
	"github.com/stubbedev/lys-ui/ui"
	"github.com/stubbedev/lys-ui/widget"
)

type Msg int

const (
	Increment Msg = iota
	Decrement
)

type Counter struct {
	env ui.Env
	n   int
}

func (c *Counter) Init(cx *component.Context[Msg, int]) widget.Widget {
	return ui.Mount(cx, c.env, ui.Row(
		ui.Button(ui.Label("-"), 8, 6).OnClick(func() { cx.Input(Decrement) }),
		ui.Expand(ui.Label("").WatchText(func() string { return strconv.Itoa(c.n) })),
		ui.Button(ui.Label("+"), 8, 6).OnClick(func() { cx.Input(Increment) }),
	).Spacing(6))
}

func (c *Counter) Update(cx *component.Context[Msg, int], msg Msg) {
	switch msg {
	case Increment:
		c.n++
	case Decrement:
		c.n--
	}
	cx.Output(c.n)
}

func ExampleWindow() {
	sess, err := app.Connect()
	if err != nil {
		log.Fatal(err)
	}
	defer sess.Close()
	face, err := app.Font("sans", 15)
	if err != nil {
		log.Fatal(err)
	}
	application := app.NewApplication(sess)
	if _, _, err := component.Window(application, app.WindowConfig{
		Title: "counter", AppID: "dev.example.counter",
	}, &Counter{env: ui.Env{Face: face, Size: 15}}); err != nil {
		log.Fatal(err)
	}
	if err := application.Run(); err != nil && !errors.Is(err, app.ErrClosed) {
		log.Fatal(err)
	}
}

func ExampleRun() {
	err := component.Run(app.WindowConfig{Title: "counter", AppID: "dev.example.counter"},
		func(a *app.Application) (*Counter, error) {
			face, err := app.Font("sans-serif", 15)
			if err != nil {
				return nil, err
			}
			if err := a.AddAccel("Escape", widget.NewAction("quit", a.Quit)); err != nil {
				return nil, err
			}
			return &Counter{env: ui.Env{Face: face, Size: 15}}, nil
		})
	if err != nil {
		log.Fatal(err)
	}
}

component.Run opens the window and runs the loop. Components walks through components, the typed builders, factories, workers and actions. app.NewApplication and NewWindow are the lower-level path for apps that want the widget tree without components (ExampleApplication in app/example_test.go).

Packages ​

Apps import the public packages; everything under internal/ is implementation.

PackageContents
componentthe component framework: components, controllers, factories, workers, commands
component/componenttesta loop that runs components in tests without a compositor
uityped view builders, one per widget
appthe session, windows and layer surfaces, the loop, dialogs, popovers, desktop services
widgetthe widget tree, theme, CSS, actions, bindings
widget/csslibadwaita's style classes and named colors as constants
animtweens, easing, springs and timelines
appearancethe desktop's color scheme, accent and contrast preferences
gtksettingsGTK 4's settings.ini, read with GTK's file order
iconthemefreedesktop icon theme lookup for programs that draw icons without widgets
i18ngettext catalogs (.mo, .po)
highlightsyntax highlighters for the code view
mediavideo and sound playback
pdfPDF writing
transferthe shared shape of clipboard and drag-and-drop content

The canvas widgets paint on (render), the GPU renderer (gpu), screen capture (capture), virtual input (vinput), the Wayland protocol bindings (wlr), dmabufs, MP4/Matroska muxing and the Opus encoder live in lys-lib, which lys-ui imports.

The demos show the packages at work.

sh
just gallery

opens every component in one window. Ctrl+T toggles the palette, Ctrl+K opens the command palette, and Escape quits. Widgets shows each one.

Released under the MIT License.