glint

package module
v0.0.0-...-6515ceb Latest Latest
Warning

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

Go to latest
Published: Jul 22, 2021 License: MIT Imports: 18 Imported by: 9

README

go-glint Godoc

Glint is a component-based UI framework specifically targeted towards command-line interfaces. This allows you to create highly dynamic CLI interfaces using shared, easily testable components. Glint uses a Flexbox implementation to make it easy to lay out components in the CLI, including paddings, margins, and more.

API Status: Unstable. We're still actively working on the API and may change it in backwards incompatible ways. See the roadmap section in particular for work that may impact the API. In particular, we have integrated this library into Waypoint, and the experience of using this library in the real world will likely drive major changes.

Example

The example below shows a simple dynamic counter:

func main() {
	var counter uint32
	go func() {
		for {
			time.Sleep(100 * time.Millisecond)
			atomic.AddUint32(&counter, 1)
		}
	}()

	d := glint.New()
	d.Append(
		glint.Style(
			glint.TextFunc(func(rows, cols uint) string {
				return fmt.Sprintf("%d tests passed", atomic.LoadUint32(&counter))
			}),
			glint.Color("green"),
		),
	)
	d.Render(context.Background())
}

Output:

Example

Roadmap

Glint is still an early stage project and there is a lot that we want to improve on. This may introduce some backwards incompatibilities but we are trying to stabilize the API as quickly as possible.

  • Non-interactive interfaces. We want to add support for rendering to non-interactive interfaces and allowing components to provide custom behavior in these cases. For now, users of Glint should detect non-interactivity and avoid using Glint.

  • Windows PowerShell and Cmd. Glint works fine in ANSI-compatible terminals on Windows, but doesn't work with PowerShell and Cmd. We want to make this work.

  • Dirty tracking. Glint currently rerenders the entire frame on each tick. I'd like components to be able to report if there are changes (if they are "dirty") and need to be rerendered. We could then more efficiently recalculate layouts and rerender outputs.

  • User Input. Glint should be able to query for user input and render this within its existing set of components.

  • Expose styling to custom renderers. Currently the Style component is a special-case for the terminal renderer to render colors. I'd like to expose the styles in a way that other renderers could use it in some meaningful way.

Thanks

This library is heavily inspired by the Ink project. I saw this project and thought that having a central render loop along with a full layout engine was a fantastic idea. Most of my projects are in Go so I wanted to be able to realize these benefits with Go. Thank you!

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func MeasureTextNode

func MeasureTextNode(
	node *flex.Node,
	width float32,
	widthMode flex.MeasureMode,
	height float32,
	heightMode flex.MeasureMode,
) flex.Size

MeasureTextNode implements flex.MeasureFunc and returns the measurements for the given node only if the node represents a TextComponent. This is the MeasureFunc that is typically used for renderers since all component trees terminate in a text node.

The flex.Node must have Context set to TextNodeContext. After calling this, fields such as Text and Size will be populated on the node.

func TestRender

func TestRender(t testing.T, c Component) string

TestRender renders the component using the string renderer and returns the string. This is a test helper function for writing components.

func WithRenderer

func WithRenderer(ctx context.Context, r Renderer) context.Context

WithRenderer inserts the renderer into the context. This is done automatically by Document for components.

Types

type Component

type Component interface {
	// Body returns the body of this component. This can be another custom
	// component or a standard component such as Text.
	//
	// The context parameter is used to send parameters across multiple
	// components. It should not be used for timeouts; Body should aim to
	// not block ever since this can block the render loop.
	//
	// Components are highly encouraged to support finalization (see
	// ComponentFinalizer). Components can finalize early by wrapping
	// their body in a Finalize built-in component. Finalization allows
	// the renderer to highly optimize output.
	Body(context.Context) Component
}

Components are the individual items that are rendered within a document.

func Context

func Context(inner Component, kv ...interface{}) Component

Context is a component type that can be used to set data on the context given to Body calls for components that are children of this component.

func Finalize

func Finalize(c Component) Component

Finalize reutrns a component that will finalize the input component. See ComponentFinalizer for documentation on what finalization means.

func Fragment

func Fragment(c ...Component) Component

Fragment appends multiple components together. A fragment has no layout implications, it is as if the set of components were appended directly to the parent.

func Style

func Style(inner Component, opts ...StyleOption) Component

Style applies visual styles to this component and any children. This can be used to set a foreground color, for example, to a set of components.

type ComponentFinalizer

type ComponentFinalizer interface {
	Component

	// Finalize notifies the component that it will be finalized. This may
	// be called multiple times.
	Finalize()
}

ComponentFinalizer allows components to be notified they are going to be finalized. A finalized component may never be re-rendered again. The next call to Body should be considered the final call.

In a Document, if the component list has a set of finalized components at the front, the renderer will draw it once and only re-draw non-finalized components. For example, consider a document that is a set of text components followed by a progress bar. If the text components are static, then they will be written to the output once and only the progress bar will redraw.

Currently, Body may be called multiple times after Finalize. Implementers should return the same result after being finalized.

type ComponentMounter

type ComponentMounter interface {
	Component

	// Mount is called when the component is added to a render tree. The
	// context given to this is used to access data set by Glint and the
	// renderer in use.
	Mount(context.Context)

	// Unmount is called when the component is removed from a render tree.
	// This will be called under ANY scenario where the component is
	// removed from the render tree, including finalization.
	Unmount(context.Context)
}

ComponentMounter allows components to be notified when they are mounted and unmounted. A mounted component is one that is added to a render tree for the first time. A component is unmounted when it is removed from the render tree.

The callbacks here may be called multiple times under certain scenarios: (1) a component is used in multiple Document instances, (2) a component is unmounted and then remounted in the future.

A component mounted multiple times in the same render tree does NOT have the mount callbacks called multiple times.

A good use case for this interface is setting up and cleaning up resources.

type Document

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

Document is the primary structure for managing and drawing components.

A document represents a terminal window or session. The output can be set and components can be added, rendered, and drawn. All the methods on a Document are thread-safe unless otherwise documented. This allows you to draw, add components, replace components, etc. all while the render loop is active.

Currently, this can only render directly to an io.Writer that expects to be a terminal session. In the future, we'll further abstract the concept of a "renderer" so that rendering can be done to other mediums as well.

func New

func New() *Document

New returns a Document that will output to stdout.

func (*Document) Append

func (d *Document) Append(el ...Component)

Append appends components to the document.

func (*Document) Close

func (d *Document) Close() error

Close ensures that all elements are unmounted by finalizing all the output and then calling RenderFrame. Users of Document should ensure that Close is always called.

func (*Document) Pause

func (d *Document) Pause()

Pause will pause the renderer. This will case RenderFrame to do nothing until Resume is called. The use case for this is if you want to wait for input (stdin) or any other reason.

func (*Document) Render

func (d *Document) Render(ctx context.Context)

Render starts a render loop that continues to render until the context is cancelled. This will render at the configured refresh rate. If the refresh rate is changed, it will not affect an active render loop. You must cancel and restart the render loop.

func (*Document) RenderFrame

func (d *Document) RenderFrame()

RenderFrame will render a single frame and return.

If a manual size is not configured, this will recalcualte the window size on each call. This typically requires a syscall. This is a bit expensive but something we can optimize in the future if it ends up being a real source of FPS issues.

func (*Document) Resume

func (d *Document) Resume()

Resume undoes a Pause call. If not paused, this does nothing.

func (*Document) Set

func (d *Document) Set(els ...Component)

Set sets the components for the document. This will replace all previous components.

func (*Document) SetRefreshRate

func (d *Document) SetRefreshRate(dur time.Duration)

SetRefreshRate sets the rate at which output is rendered.

func (*Document) SetRenderer

func (d *Document) SetRenderer(r Renderer)

SetRenderer sets the renderer to use. If this isn't set then Render will do nothing and return immediately. Changes to this will have no impact on active render loops.

type LayoutComponent

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

LayoutComponent is a component used for layout settings. See Layout.

func Layout

func Layout(inner ...Component) *LayoutComponent

Layout is used to set layout properties for the child components. This can be used similarly to a "div" in HTML with "display: flex" set. This component follows the builder pattern for setting layout properties such as margins, paddings, etc.

func (*LayoutComponent) Body

Component implementation

func (*LayoutComponent) Layout

func (c *LayoutComponent) Layout() *layout.Builder

componentLayout internal implementation.

func (*LayoutComponent) MarginLeft

func (c *LayoutComponent) MarginLeft(x int) *LayoutComponent

MarginLeft sets the `margin-left` property.

func (*LayoutComponent) MarginRight

func (c *LayoutComponent) MarginRight(x int) *LayoutComponent

MarginRight sets the `margin-left` property.

func (*LayoutComponent) PaddingLeft

func (c *LayoutComponent) PaddingLeft(x int) *LayoutComponent

PaddingLeft sets the `margin-left` property.

func (*LayoutComponent) PaddingRight

func (c *LayoutComponent) PaddingRight(x int) *LayoutComponent

PaddingRight sets the `margin-left` property.

func (*LayoutComponent) Row

func (c *LayoutComponent) Row() *LayoutComponent

Row sets the `flex-direction: row` property.

type Renderer

type Renderer interface {
	// LayoutRoot returns the root node for the layout engine. This should
	// set any styling to restrict children such as width. If this returns nil
	// then rendering will do nothing.
	LayoutRoot() *flex.Node

	// RenderRoot is called to render the tree rooted at the given node.
	// This will always be called with the root node. In the future we plan
	// to support partial re-renders but this will be done via a separate call.
	//
	// The height of root is always greater than zero. RenderRoot won't be
	// called if the root has a zero height since this implies that nothing
	// has to be drawn.
	//
	// prev will be the previous root that was rendered. This can be used to
	// determine layout differences. This will be nil if this is the first
	// render. If the height of the previous node is zero then that means that
	// everything drawn was finalized.
	RenderRoot(root, prev *flex.Node)
}

Renderers are responsible for helping configure layout properties and ultimately drawing components.

Renderers may also optionally implement io.Closer. If a renderer implements io.Closer, the Close method will be called. After this is called. the Render methods will no longer be called again. This can be used to perform final cleanups.

func RendererFromContext

func RendererFromContext(ctx context.Context) Renderer

RendererFromContext returns the Renderer in the context or nil if no Renderer is found.

type StringRenderer

type StringRenderer struct {
	// Builder is the strings builder to write to. If this is nil then
	// it will be created on first render.
	Builder *strings.Builder

	// Width is a fixed width to set for the root node. If this isn't
	// set then a width of 80 is arbitrarily used.
	Width uint
}

StringRenderer renders output to a string builder. This will clear the builder on each frame render. The StringRenderer is primarily meant for testing components.

func (*StringRenderer) LayoutRoot

func (r *StringRenderer) LayoutRoot() *flex.Node

func (*StringRenderer) RenderRoot

func (r *StringRenderer) RenderRoot(root, prev *flex.Node)

type StyleOption

type StyleOption func(t *styleComponent)

StyleOption is an option that can be set when creating Text components.

func BGColor

func BGColor(name string) StyleOption

BGColor sets the background color by name. The supported colors are listed below.

black, red, green, yellow, blue, magenta, cyan, white, darkGray, lightRed, lightGreen, lightYellow, lightBlue, lightMagenta, lightCyan, lightWhite.

func BGColorHex

func BGColorHex(v string) StyleOption

BGColorHex sets the background color by hex code. The value can be in formats AABBCC, #AABBCC, 0xAABBCC.

func BGColorRGB

func BGColorRGB(r, g, b uint8) StyleOption

BGColorRGB sets the background color by RGB values.

func Bold

func Bold() StyleOption

Bold sets the text to bold.

func Color

func Color(name string) StyleOption

Color sets the color by name. The supported colors are listed below.

black, red, green, yellow, blue, magenta, cyan, white, darkGray, lightRed, lightGreen, lightYellow, lightBlue, lightMagenta, lightCyan, lightWhite.

func ColorHex

func ColorHex(v string) StyleOption

ColorHex sets the foreground color by hex code. The value can be in formats AABBCC, #AABBCC, 0xAABBCC.

func ColorRGB

func ColorRGB(r, g, b uint8) StyleOption

ColorRGB sets the foreground color by RGB values.

func Italic

func Italic() StyleOption

Italic sets the text to italic.

func Underline

func Underline() StyleOption

Underline sets the text to be underlined.

type TerminalRenderer

type TerminalRenderer struct {
	// Output is where to write to. This should be a TTY.
	Output io.Writer

	// Rows, Cols are the dimensions of the terminal. If these are not set
	// (zero), then we will auto-detect the size of the output if it is a TTY.
	// If the values are still zero, nothing will be rendered.
	Rows, Cols uint
}

TerminalRenderer renders output to a terminal. It expects the Output set to be a TTY. This will use ANSI escape codes to redraw.

func (*TerminalRenderer) Close

func (r *TerminalRenderer) Close() error

func (*TerminalRenderer) LayoutRoot

func (r *TerminalRenderer) LayoutRoot() *flex.Node

func (*TerminalRenderer) RenderRoot

func (r *TerminalRenderer) RenderRoot(root, prev *flex.Node)

type TextComponent

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

TextComponent is a Component that renders text.

func Text

func Text(v string) *TextComponent

Text creates a TextComponent for static text. The text here will be word wrapped automatically based on the width of the terminal.

func TextFunc

func TextFunc(f func(rows, cols uint) string) *TextComponent

TextFunc creates a TextComponent for text that is dependent on the size of the draw area.

func (*TextComponent) Body

func (*TextComponent) Render

func (el *TextComponent) Render(rows, cols uint) string

type TextNodeContext

type TextNodeContext struct {
	// C is the TextComponent represented.
	C *TextComponent

	// The context at the time layout was done
	Context context.Context

	// Text is the rendered text. This is populated after MeasureTextNode
	// is called. Note that this may not fit in the final layout calculations
	// since it is populated on measurement.
	Text string

	// Size is the measurement size returned. This can be used to determine
	// if the text above fits in the final size. Text is guaranteed to fit
	// in this size.
	Size flex.Size
}

TextNodeContext is the *flex.Node.Context set for all *TextComponent flex nodes.

func (*TextNodeContext) Component

func (c *TextNodeContext) Component() Component

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

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