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
go get github.com/stubbedev/lys-ui@latestA 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:
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.
| Package | Contents |
|---|---|
component | the component framework: components, controllers, factories, workers, commands |
component/componenttest | a loop that runs components in tests without a compositor |
ui | typed view builders, one per widget |
app | the session, windows and layer surfaces, the loop, dialogs, popovers, desktop services |
widget | the widget tree, theme, CSS, actions, bindings |
widget/css | libadwaita's style classes and named colors as constants |
anim | tweens, easing, springs and timelines |
appearance | the desktop's color scheme, accent and contrast preferences |
gtksettings | GTK 4's settings.ini, read with GTK's file order |
icontheme | freedesktop icon theme lookup for programs that draw icons without widgets |
i18n | gettext catalogs (.mo, .po) |
highlight | syntax highlighters for the code view |
media | video and sound playback |
pdf | PDF writing |
transfer | the 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.
The gallery
just galleryopens every component in one window. Ctrl+T toggles the palette, Ctrl+K opens the command palette, and Escape quits. Widgets shows each one.