Library Usage

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

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.

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.

OptionTypeIf not setNotes
WithThemestringconfig fileA theme name, such as dracula or nord.
WithShowKeysboolconfig fileShows the showkeys overlay.
WithAnimationsboolconfig filefalse turns animations off. true cannot turn them on if the config turned them off.
WithASCIIOnlyboolconfig fileASCII instead of Nerd Font glyphs.
WithWorkspacesint9Clamped to 1 to 9.
WithBorderStylestringconfig fileSee border_style.
WithDockbarPositionstringconfig filebottom, top or hidden.
WithHideWindowButtonsboolconfig fileHides minimize, maximize and close.
WithWindowButtonStylestringconfig filepill or dots (macOS traffic lights). The built-in default is dots.
WithWindowButtonPositionstringconfig fileleft or right. The built-in default is left.
WithScrollbackLinesint10000Clamped to 100 to 1,000,000. Always applied, so it replaces the config's scrollback_lines.
WithSizeint, intdetectedInitial width and height in cells.
WithSSHModeboolfalseMarks the model as an SSH client. See Serve over SSH.
WithUserConfig*config.UserConfigloaded from diskUse 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.

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.

On this page