matcha

package module
v0.0.0-...-0c776f6 Latest Latest
Warning

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

Go to latest
Published: Aug 22, 2025 License: MIT Imports: 14 Imported by: 0

README


Matcha

Matcha is an experimental terminal UI (TUI) framework written in Go. It’s inspired by React and Flutter — focusing on components, state, and composition instead of the traditional Elm-style architecture.

⚠️ This is an early project. Expect breaking changes, incomplete features, and lots of iteration. Contributions, ideas, and feedback are welcome!


Features (current + planned)

  • ✅ Component-based API (similar to React/Flutter)
  • ✅ Local component state with UseState
  • ✅ Built-in components: Row, Column, Text, Button
  • ✅ Style support with Lip Gloss
  • ✅ Event handling with tcell
  • 🚧 Layout improvements (flex, sizing)
  • 🚧 More interactive components (input fields, lists, etc.)
  • 🚧 Continuous rendering / animations

Example

Below is a simple counter app built with Matcha:

package main

import (
	"fmt"

	"github.com/cchirag/matcha"
	"github.com/charmbracelet/lipgloss"
	"github.com/gdamore/tcell/v2"
)

var (
	buttonStyle = lipgloss.NewStyle().
		Padding(1, 2).
		Foreground(lipgloss.Color("#FFFFFF")).
		Background(lipgloss.Color("#44624a"))

	columnStyle = lipgloss.NewStyle().
		Background(lipgloss.Color("#FFFFFF"))

	rowStyle = lipgloss.NewStyle().
		Background(lipgloss.Color("#ffffff")).
		Padding(1)

	textStyle = lipgloss.NewStyle().
		Foreground(lipgloss.Color("#000000")).
		Background(lipgloss.Color("#FFFFFF"))
)

type app struct{}

func (a *app) Render(ctx *matcha.Context) matcha.Component {
	count, setCount := matcha.UseState(ctx, 0)

	return matcha.Row([]matcha.Component{
		matcha.Text(fmt.Sprintf("Count: %d", count), textStyle),
		matcha.Column([]matcha.Component{
			matcha.Button("Increment", func(event *tcell.EventMouse) bool {
				if event.Buttons() == tcell.Button1 {
					setCount(func(i int) int { return i + 1 })
					return true
				}
				return false
			}, buttonStyle),
			matcha.Button("Decrement", func(event *tcell.EventMouse) bool {
				if event.Buttons() == tcell.Button1 {
					setCount(func(i int) int { return i - 1 })
					return true
				}
				return false
			}, buttonStyle),
		}, 2, columnStyle),
	}, 2, rowStyle)
}

func main() {
	app := matcha.NewApp(&app{})
	if err := app.Render(); err != nil {
		fmt.Println(err.Error())
	}
}

Run it:

go run example/example.go

Why Matcha?

Most Go TUI frameworks (like Bubble Tea) follow an Elm-style model/update/view loop. Matcha explores a different approach:

  • Components are composable (like React)
  • Each component manages its own state
  • You can pass props to children and render trees declaratively

This makes Matcha feel familiar to developers coming from UI frameworks like Flutter or React, but in the terminal.


Getting Started

go get github.com/cchirag/matcha

Then start building by defining a root component with a Render method.


Contributing

This project is experimental and under heavy iteration. If you have ideas, bug reports, or feature suggestions:

  • Open an issue
  • Start a discussion
  • Or submit a PR

Let’s shape this together.


License

MIT


Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Conditional

func Conditional[T any](test bool, happy, sad T) T

func Map

func Map[T any, K any](slice []T, iterator func(index int, element T) K) []K

func UseAtomSetter

func UseAtomSetter[T any](ctx *Context, atom *Atom[T]) func(func(T) T)

UseAtomSetter binds only a setter function for updating the Atom's value.

The setter applies the same update/notify/unsubscribe cycle as UseAtomState, but does not return the current value.

func UseAtomState

func UseAtomState[T any](ctx *Context, atom *Atom[T]) (T, func(func(T) T))

UseAtomState binds an Atom's value to a component's context and returns:

  1. The current value
  2. A setter function for updating the value

When called:

  • A subscription is registered for this context, with the current Atom version embedded in the subscriber ID.
  • On state change, the UI is re-rendered and stale subscriptions from previous versions are automatically cleaned up.

Thread-safe.

func UseAtomValue

func UseAtomValue[T any](ctx *Context, atom *Atom[T]) T

UseAtomValue binds an Atom's value to a component's context and returns only the current value.

The component will re-render when the Atom's value changes. Subscription uses the current Atom version to ensure proper cleanup.

func UseCallback

func UseCallback[T any](ctx *Context, fn func() T, deps []any) func() T

func UseEffect

func UseEffect(ctx *Context, effect func() func(), deps []any)

func UseEvent

func UseEvent(ctx *Context, handler func(event tcell.Event) bool)

UseEvent registers an event handler for the component associated with this Context.

The `handler` function should return true if the event is handled and should not bubble further up the tree, or false if it should continue bubbling to parent components.

Handlers are keyed by the component's ID (`ctx.id`) and are stored in the global event manager. Thread-safe.

func UseFocus

func UseFocus(ctx *Context, id string) (isFocused bool, setIsFocused func(id string), blur func())

UseFocus registers a focusable element for the current component and returns focus helpers.

Parameters:

  • ctx: The component's rendering context, which holds a globally stable componentID for focus tracking.
  • id: A per-element focusable ID unique within the component.

Returns:

  • isFocused: true if the current component is focused.
  • setIsFocused: function to set focus to the component that owns the given focusableID.
  • blur: function to clear focus from all components.

Behavior:

  • Only one component can have focus at a time.
  • The mapping from focusable IDs to component IDs enables fast lookup during event bubbling.
  • This function does not trigger a rerender on focus changes — that should be handled by the caller if needed.

Example:

isFocused, setFocus, blur := UseFocus(ctx, "input1")
if isFocused {
    // render input as highlighted
}
setFocus("input2") // move focus to another registered element
blur()             // clear focus completely

func UseMemo

func UseMemo[T any](ctx *Context, fn func() T, deps []any) T

UseMemo memoizes the result of a computation between renders. It accepts a function `fn` that computes a value of type `T`, and a list of dependencies `deps`.

On the first call, UseMemo executes `fn`, stores the result along with the dependencies, and returns the computed value. On subsequent calls, if the dependencies are deeply equal to the previously stored dependencies, the cached value is returned without re-executing `fn`. If the dependencies differ, `fn` is executed again, and the new value is cached.

Usage example:

count := UseMemo(ctx, func() int {
	return expensiveCalculation()
}, []any{dep1, dep2})

func UseState

func UseState[T any](ctx *Context, initial T) (T, func(func(T) T))

UseState is a state management hook inspired by React's useState.

It associates a persistent value with the current component and provides a setter function to update it. State is identified by the position of the hook call within a component's Render function, so the order of hooks must remain stable across renders.

Example usage:

count, setCount := UseState(ctx, 0)

Button("Increment", func() {
    setCount(func(prev int) int { return prev + 1 })
})

Parameters:

ctx    - the component's render context
initial - the initial value to use if no prior state exists

Returns:

value  - the current state value
setter - a function that updates state. It accepts a function that
         transforms the previous state into a new state.

Types

type App

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

func NewApp

func NewApp(component Component) *App

func (*App) Render

func (a *App) Render() error

type Atom

type Atom[T any] struct {
	ID    string
	Value T
	// contains filtered or unexported fields
}

Atom is a reactive state container that holds a value of type T and notifies its subscribers whenever the value changes.

Concurrency:

  • All reads/writes to the Atom's value and subscriber list are protected by an internal RWMutex.
  • Callbacks are invoked outside of the lock to avoid deadlocks and blocking other operations.
  • Each update increments a `version` counter; this version is embedded in subscriber IDs to ensure stale subscribers from previous UI builds are never removed prematurely.

Subscribers are stored in a map keyed by subscriber ID.

type Component

type Component interface {
	Render(ctx *Context) Component
}

func Button

func Button(text string, handler func(event *tcell.EventMouse) bool, style lipgloss.Style) Component

func Column

func Column(children []Component, gap int, style lipgloss.Style) Component

func Row

func Row(children []Component, gap int, style lipgloss.Style) Component

func Text

func Text(content string, style lipgloss.Style, key ...string) Component

type Context

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

func (*Context) GetDimensions

func (c *Context) GetDimensions() *dimensions

func (*Context) Quit

func (c *Context) Quit()

type HasKey

type HasKey interface {
	Key() string
}

type Ref

type Ref[T any] struct {
	Current T
}

Ref is a container type that holds a mutable value.

The value is stored in the Current field and can be freely read or modified. Unlike state, updating Current does not cause the component to re-render.

Refs are useful for persisting values across renders or for accessing values inside closures without capturing stale data.

func UseRef

func UseRef[T any](ctx *Context, initial T) *Ref[T]

UseRef creates and returns a new reference object that persists across component re-renders.

The initial value is only applied on the first render. Subsequent renders return the same Ref. Updating ref.Current will not trigger a re-render.

Typical use cases include:

  • Storing values that should persist without causing re-renders
  • Holding onto resources like timers, connections, or UI handles
  • Accessing mutable values inside goroutines or event handlers

Example:

func Render(ctx *matcha.Context) matcha.Component {
    countRef := matcha.UseRef(ctx, 0)

    // Increment without causing a re-render
    countRef.Current++

    return matcha.Text(fmt.Sprintf("Count is %d", countRef.Current))
}

type Subscriber

type Subscriber[T any] struct {
	// contains filtered or unexported fields
}

Subscriber represents a callback function that is triggered whenever the Atom's value changes.

Each subscriber is identified by a unique ID so that it can be individually added or removed.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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