Skip to content

Library Usage

Gaurav Gosain edited this page Oct 15, 2026 · 2 revisions

Embed TUIOS in your own Bubble Tea program with the pkg/tuios Go package, or serve it over SSH or in the browser.

The pkg/tuios package exposes the window manager as a Bubble Tea model. You can run it on its own, wrap it in your own model, or serve it over SSH or the browser.

go get github.com/Gaurav-Gosain/tuios/pkg/tuios

Warning

The API is pre-1.0 and can change between releases. Model is a type alias for the internal app.OS type, so its exported fields and methods change with it. Pin a version in your go.mod.

Parts of TUIOS are also Go libraries of their own. (Added in v0.10.0)

Module What it does
tuios.dev/vt The terminal emulator of TUIOS. Needs Go 1.26
tuios.dev/swatch Reads the colour theme of a desktop, such as Omarchy's, and follows it. Reads kitty, ghostty, alacritty and wezterm colour files
tuios.dev/progstatus The parser and encoder of the Program Status Protocol

The pages are at tuios.dev/docs/libraries.

Run it

tuios.New returns a *tuios.Model, which implements tea.Model. You build the program yourself, so you keep control of program options and error handling. There is no tuios.Run.

package main

import (
	"log"

	tea "charm.land/bubbletea/v2"
	"github.com/Gaurav-Gosain/tuios/pkg/tuios"
)

func main() {
	model := tuios.New()

	p := tea.NewProgram(model, tuios.ProgramOptions()...)
	if _, err := p.Run(); err != nil {
		log.Fatal(err)
	}
}

Options

New takes functional options:

model := tuios.New(
	tuios.WithTheme("dracula"),
	tuios.WithShowKeys(true),
	tuios.WithAnimations(false),
	tuios.WithWorkspaces(4),
	tuios.WithBorderStyle("thick"),
	tuios.WithScrollbackLines(50000),
)

TUIOS reads the user's config file first and then applies your options on top, the same way the tuios binary applies its command-line flags. An option left at its zero value keeps whatever the config file says.

Option Type If not set Notes
WithTheme string config file A theme name, such as dracula or nord.
WithShowKeys bool config file Shows the showkeys overlay.
WithAnimations bool config file false turns animations off. true cannot turn them on if the config turned them off.
WithASCIIOnly bool config file ASCII instead of Nerd Font glyphs.
WithWorkspaces int 9 Clamped to 1 to 9.
WithBorderStyle string config file See border_style.
WithDockbarPosition string config file bottom, top or hidden.
WithHideWindowButtons bool config file Hides minimize, maximize and close.
WithWindowButtonStyle string config file pill or dots (macOS traffic lights). The built-in default is dots.
WithWindowButtonPosition string config file left or right. The built-in default is left.
WithScrollbackLines int 10000 Clamped to 100 to 1,000,000. Always applied, so it replaces the config's scrollback_lines.
WithSize int, int detected Initial width and height in cells.
WithSSHMode bool false Marks the model as an SSH client. See Serve over SSH.
WithUserConfig *config.UserConfig loaded from disk Use this config instead of reading the user's file.

Out-of-range numbers are clamped, not rejected. WithWorkspaces(50) gives 9 and WithScrollbackLines(10) gives 100. DefaultOptions() returns the starting values as an Options struct.

Use your own config

tuios.Config exposes three config functions, so you do not need to import an internal package:

cfg, err := tuios.Config.LoadUserConfig()
if err != nil {
	cfg = tuios.Config.DefaultConfig()
}
cfg.Keybindings.LeaderKey = "ctrl+a"

model := tuios.New(tuios.WithUserConfig(cfg))

tuios.Config.GetConfigPath() returns the path of the config file and an error.

Wrap it in your own model

Delegate to the TUIOS model and handle your own messages first. In Bubble Tea v2, View returns a tea.View.

type app struct {
	wm *tuios.Model
}

func (a *app) Init() tea.Cmd {
	return a.wm.Init()
}

func (a *app) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	if _, ok := msg.(pingMsg); ok {
		// Handle your own message and stop here.
		return a, nil
	}
	updated, cmd := a.wm.Update(msg)
	a.wm = updated.(*tuios.Model)
	return a, cmd
}

func (a *app) View() tea.View {
	return a.wm.View()
}

To check the mode, read the Mode field. The package exports WindowManagementMode, which TUIOS starts in, and TerminalMode:

if model.Mode == tuios.TerminalMode {
	// keys go to the focused pane
}

Program options

ProgramOptions() returns the Bubble Tea options every TUIOS client runs with:

  • the frame rate cap from the config (max_fps)
  • no signal handler, so your program owns the process signals
  • tea.WithFilter(tuios.FilterMouseMotion)

Pass them through instead of writing your own, so you get later additions.

FilterMouseMotion drops mouse motion that nothing on screen reacts to. Motion still passes when something needs it, for example during a drag or resize, over a link, the dock or the sidebar, and when the focused pane runs an application that tracks the mouse. It is exported for callers that build their own option list.

Warning

The last filter wins

Bubble Tea keeps only one tea.WithFilter. SSH and web serving libraries add their own filter, so put their options first and tuios.ProgramOptions() last. Otherwise the TUIOS filter is replaced and every mouse motion event reaches the model.

Size from a PTY

NewForPTY takes the initial size from anything with Width() and Height() methods:

type PTY interface {
	Width() int
	Height() int
}

func NewForPTY(pty PTY, opts ...Option) *Model

The interface needs methods. A struct with Width and Height fields, such as sip.Pty, does not satisfy it. For those, use New with WithSize, as the recipes below do.

Serve in the browser

sip serves a Bubble Tea program to the browser. It is the library behind tuios-web. Use ServeWithProgram and build the program yourself:

package main

import (
	"context"
	"log"

	tea "charm.land/bubbletea/v2"
	"github.com/Gaurav-Gosain/sip"
	"github.com/Gaurav-Gosain/tuios/pkg/tuios"
)

func main() {
	server := sip.NewServer(sip.DefaultConfig())

	err := server.ServeWithProgram(context.Background(), func(sess sip.Session) *tea.Program {
		pty := sess.Pty()
		model := tuios.New(
			tuios.WithSize(pty.Width, pty.Height),
			tuios.WithTheme("dracula"),
		)
		// sip's options first, then tuios's, so the tuios filter wins.
		opts := append(sip.MakeOptions(sess), tuios.ProgramOptions()...)
		return tea.NewProgram(model, opts...)
	})
	if err != nil {
		log.Fatal(err)
	}
}

Do not use sip's Serve here. It appends its own options after the ones you return, so its filter replaces the TUIOS one. A model served this way counts as a local client: there is no option that marks it as a browser.

Serve over SSH

With Wish, use MiddlewareWithProgramHandler for the same reason: bubbletea.Middleware appends its options after yours.

package main

import (
	"log"

	tea "charm.land/bubbletea/v2"
	"charm.land/ssh"
	"charm.land/wish/v2"
	"charm.land/wish/v2/bubbletea"
	"github.com/Gaurav-Gosain/tuios/pkg/tuios"
)

func main() {
	s, err := wish.NewServer(
		wish.WithAddress(":2222"),
		wish.WithHostKeyPath(".ssh/tuios_ed25519"),
		wish.WithMiddleware(
			bubbletea.MiddlewareWithProgramHandler(func(sess ssh.Session) *tea.Program {
				pty, _, _ := sess.Pty()
				model := tuios.New(
					tuios.WithSize(pty.Window.Width, pty.Window.Height),
					tuios.WithSSHMode(true),
				)
				// wish's options first, then tuios's, so the tuios filter wins.
				opts := append(bubbletea.MakeOptions(sess), tuios.ProgramOptions()...)
				return tea.NewProgram(model, opts...)
			}),
		),
	)
	if err != nil {
		log.Fatal(err)
	}
	log.Fatal(s.ListenAndServe())
}

WithSSHMode(true) makes the model an SSH client. The settings page does not write the server's config file, and desktop actions do not run on the server. It does not give the model the SSH session, a graphics channel or the client terminal's capabilities, so images are not forwarded to the client.

Limitations

  • No daemon. Sessions, detach and reattach, and the control protocol are internal and only reachable through the tuios binary. pkg/tuios gives you an in-process window manager.
  • No SSH or web helper. tuios ssh and tuios-web are built from internal packages. Use the recipes above to serve the model yourself.
  • Some settings are process-wide. Theme, border style, dockbar position, window buttons, ASCII mode, scrollback and animations are written to package-level state in the config package. Two models in one process share them, and the last one built wins. This matters when you serve several sessions from one process.
  • No Go examples in the repository. The examples/ directory holds .tape scripts and a dock config, not Go programs. The recipes on this page all compile against current main.

Related

Clone this wiki locally