fansiterm

package module
v0.0.0-...-7739168 Latest Latest
Warning

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

Go to latest
Published: Feb 11, 2026 License: GPL-2.0 Imports: 20 Imported by: 0

README

Fansiterm Screenshot

FANSITERM

Coverage Status Go ReportCard GoDoc

Fake (virtual) ANSI TERMinal.

Fansiterm is a golang package for implementing a partially compatible ANSI terminal, rendered to an image.Image (really, a golang.org/x/image/draw.Image). This is suitable for the graphical back-end of a virtual terminal emulator.

The intent is for implementing a terminal on micro controllers connected to graphical displays. This provides an easy way to make a TUI for the micro controller and take advantage of packages like github.com/charmbracelet/bubbletea or for making a simple dumb terminal-type device.

Overview

The (*fansiterm.Device) object implements io.Writer. (*fansiterm.Device).Render implements image.Draw. To push data (text) to the terminal, you simply call Write() against the Device object.

The text isn't buffered anywhere, if you need the text or want to implement more advanced features like scrolling, that's up to whatever is writing to (*fansiterm).Device. Incomplete escape sequences will be buffered and it's possible to "hang" the terminal by sending an incomplete sequence and then overloading the system memory. This is inline with how actual physical dumb terminals of yore worked.

If you want to push your own graphics or other operations, you can draw directly to the (*fansiterm.Device).Render object as well, as it implements draw.Image.

If Device is initialized with a nil image buffer, it allocates its own buffer. Otherwise, you can pass a draw.Image object (like what the driver for an OLED or TFT screen provides you) to it and any Write()s to the (*fansiterm.Device) will be immediately rendered to the backing screen. Whether the screen buffers image data and needs to be manually blitted is screen driver dependent.

For use with microcontrollers, you'll want to pass it the pseudo-buffer provided by the screen driver, as chances are your MCU does not have enough ram for a single frame buffer--in addition to the memory used for all the tiles and the rest of the program.

Features

  • Cursor styles: Block, Beam, Underscore
  • Bell is supported: a callback is provided for when the terminal receives a \a (bell character). So you could trigger a beep via a speaker and PWM or blink an LED or blink the backlight, etc.
  • Standard cursor manipulation supported.
  • Regular, Bold, and "italic" Font (italics are reasonably faked by rotating individual tiles)
  • Underline, Double Underline, Strike-through
  • Several "TileSets" come built-in: inconsolata, Fira Code Nerd Mono, x3270, julia mono, and fansi
  • Tool to generate additional tilesets from TTF fonts is included: look in tiles/ and tiles/gentileset/
  • Custom Tile loading for alternate character set (shift-out character set, commonly used for line-drawing/pseudo graphics)
  • Tiles are rendered using an 8-bit Alpha mask, allowing for clean blending and anti-aliased rendering of glyphs.
  • 4-bit (with extended codes for bright / high intensity) color; 256-Color; True Color (24 bit).

Non-Features

The main purpose of this package is for use on rather low-power microcontrollers, so some standard features for terminal emulators are not implemented.

  • Blinking text and blink cursors
    • this would require a some kind of timer-callback. As it is, fansiterm is only using CPU when bytes are being written to it.
  • Resizable Text
    • Right now, the pre-rendered inconsolata.Regular8x16 and inconsolata.Bold8x16 are used.
    • It's possible to use basicfont.Regular7x13, but you have to give up bold support.
  • Hardware acceleration. Fansiterm remains agnostic of what it's rendering to and thus can't take advantage of any double-buffers or hardware-cursors on its own. Backwards compatible PRs to improve hardware support / hardware acceleration are very much welcome.
    • Okay, I lied a little bit. By using the interfaces from github.com/sparques/gfx, like Filler, Scroller, and RegionScroller, we can get "hardware acceleration." It's not really hardware acceleration, but when the underlying device can use more efficient algorithms (for example, sending a specific "Fill" command to the display, or even just manipulating video memory directly) you can get much improved performance. Using the pure-Go software implementation of Scroll and Fill, it took an arbitrary amount of lines over two minutes to render. With the Scroller and Fill interfaces supported, that same amount of lines takes slightly more than 2 seconds. (And a "real" Terminal emulator takes about 0.03 seconds. Fansi don't mean fast.)

TODO

  • General Clean Up (Device struct is a bit of a mess) Always more to be done, but I'm relatively happy with things now.
  • Package documentation
    • Reviewing the package documentation now shows me I have far too much exported. A major to do is only have things exported if they actually need to be exported.
  • Test on real hardware
  • 1-bit color/rendering support for very-very-constrained systems
  • More configurable font / better font configuration Now using a purpose-built Tile system.
    • Better, user-oriented font config system: The means are in place, now just have to make it easy to use.
  • Optimize drawing/rendering routines This has been greatly improved.
    • Add in hardware accel / filling rectangle support (some hardware can fill rectangles more efficiently than the equivalent single-pixel-pushing)
  • Standardize / settle upon an API
    • Limit your interactions to the io.Write() interface.
  • Modify gentileset utility to only dump specific ranges--currently any of the "Nerd" fonts included (Fira, x3270, and julia) use too much RAM to actually load onto an RP2040.

Future

I want to keep a very stripped down, bare-bones version of fansiterm that will work on very resource constrained microcontrollers. However, I'm very open to having a more featureful v2 that is suitable for using as a back-end for something as full blown as desktop terminal emulator.

Screenshot

Fansiterm Screenshot

The screenshot demonstrates:

  • FANSITERM is colored using inverted VGA color ( SGR CSI34;7m ) and is also bold (SGR CSI1m).
  • The trademark character (™) is present in inconsolata.Regular8x16 and rendered correctly here.
  • On either end of FANSITERM are custom tiles, defined using 8x16 pixel PNGs, in the fansi TileSet (fansiterm/tiles/fansi) and set to represent the characters '(' and ')' in the alternate character set (activated with the SHIFT-OUT byte, 0x0E, and deactivated with SHIFT-IN byte, 0x0F).
  • Custom rounded-end-cap tiles are used to surround 433 MHz and KHz, also via alternate character set (and mapped to '{' and '}').
  • The distance between 'Freq:' and '443 MHz' and 'Bandwidth:' and '005 KHz' are managed via tab characters.
  • The gradient bar is implemented using 24-bit True Color and an on-the-fly generated gradient tile.
  • Finally, the cursor is a block style cursor. All cursor shapes are implemented by inverting the colors they land over top.
  • This is a 240x135 pixel. While 240 is evenly divisible by 8, 135 is not divisible by 16. The terminal is automatically centered. (It is a TODO item to add customizable offset).

htop Screenshot

Yes. Htop works.

htop Screenshot

See Also

I found out about this when nearly done with this project:

https://github.com/tinygo-org/tinyterm

Same basic idea--tinyterm is a (tiny)go implementation of a terminal. Tinyterm is meant to be a minimal implementation to aid in troubleshooting projects. Fansiterm is meant to be the main interface / visual subsystem.

Tinyterm is specifically for tinygo, but fansiterm will work anywhere regular go will.

Documentation

Overview

Package fansiterm provides a fully featured, graphics-capable virtual terminal implementation designed for environments with minimal or no operating support. Originally implemented to run on microcontrollers and works well with TinyGo.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ShowEsc if set to true (default false) prints to stdout escape sequences as received by fansiterm
	ShowEsc bool
	// ShowUnhandled if set to true (default false) prints to stdout escape sequencies that fansiterm does not actually handle.
	ShowUnhandled bool
)
View Source
var (
	PaletteANSI []Color
	Palette256  []Color
)

Ensure at build-time the Color implementation has implemented both color.Color and image.Image.

View Source
var (
	BlockCursor      = cursorRectFunc(blockRect)
	BeamCursor       = cursorRectFunc(beamRect)
	UnderscoreCursor = cursorRectFunc(underscoreRect)
)
View Source
var ConfigDefault = Config{
	TabSize:             8,
	StrikethroughHeight: 7,
	BoldColors:          true,
}

ConfigDefault provides the default configuration values for a Device.

View Source
var (
	LogOutput io.Writer
)

Functions

func DecodeImageData

func DecodeImageData(data []rune) (image.Image, error)

DecodeImageData accepts base64 encoded data and attempts to decode it as an image, returning the image.

Types

type Attr

type Attr struct {
	Bold            bool
	Underline       bool
	DoubleUnderline bool
	Strike          bool
	Blink           bool
	Reversed        bool
	Italic          bool
	Conceal         bool
	Fg              Color
	Bg              Color
}

Attr defines the attributes applied to rendered text.

type Color

type Color struct {
	RGB color.RGBA
}

func Color256

func Color256(n int) (c Color)

func ColorANSI

func ColorANSI(n int) (c Color)

func NewColor

func NewColor(r, g, b, a uint8) Color

NewColor returns a Color with the specified RGBA values.

func NewColorFromRGBA

func NewColorFromRGBA(c color.RGBA) Color

func NewOpaqueColor

func NewOpaqueColor(r, g, b uint8) Color

NewOpaqueColor returns a Color with full opacity (alpha = 255).

func (Color) At

func (c Color) At(int, int) color.Color

At implements image.Image by returning the embedded color value.

func (Color) Bounds

func (c Color) Bounds() image.Rectangle

Bounds implements image.Image. It returns an extremely large bounding rectangle to satisfy the image.Image interface when Color is used as an image.

func (Color) ColorModel

func (c Color) ColorModel() color.Model

ColorModel implements image.Image and color.Model.

func (Color) Convert

func (c Color) Convert(cin color.Color) color.Color

Convert implements color.Model.

func (Color) RGBA

func (c Color) RGBA() (r, g, b, a uint32)

type Colorizer

type Colorizer func() color.RGBA

Colorizer is an adapter that allows a pixel.Color's RGBA method to satisfy the color.Color interface used by the Go image packages.

Example usage:

pixelColor := pixel.NewColor[pixel.RGB888](127,127,127)
drawImage.Set(x, y, Colorizer(pixelColor.RGBA))

func (Colorizer) RGBA

func (c Colorizer) RGBA() (r, g, b, a uint32)

RGBA implements color.Color by invoking the wrapped function.

type Config

type Config struct {
	LocalEcho                bool
	TabSize                  int  // Number of spaces per tab.
	StrikethroughHeight      int  // Pixel height offset for strike-through.
	CursorStyle              int  // Default cursor style.
	BoldColors               bool // Whether bold colors are enabled.
	AltScreen                bool // Enable alternate screen buffer (expensive on MCUs).
	Wraparound               bool // Whether text wraps at the screen edge.
	CursorKeyApplicationMode bool // Enable application mode for cursor keys.
	MouseEvents              int  // 0, 1000, 10002, or 1003
	MouseSGR                 bool // if false, use \e[Mcbxbyb reporting; else use \e[<

	// Miscellaneous properties, like "Window Title"
	Properties map[Property]string
}

Config defines runtime settings for a Device.

func NewConfig

func NewConfig() Config

type Cursor

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

Cursor is used to track the cursor.

func (*Cursor) ColsRemaining

func (c *Cursor) ColsRemaining() int

ColsRemaining returns how many columns are remaining until EO

func (*Cursor) MoveAbs

func (c *Cursor) MoveAbs(x, y int)

func (*Cursor) MoveRel

func (c *Cursor) MoveRel(x, y int)

func (*Cursor) RestorePos

func (c *Cursor) RestorePos()

func (*Cursor) SavePos

func (c *Cursor) SavePos()

func (*Cursor) ToggleAltPos

func (c *Cursor) ToggleAltPos()

ToggleAltPos toggles between the main screen's position and the alt screen's position.

type Device

type Device struct {
	// BellFunc is called if it is non-null and the terminal would
	// display a bell character
	BellFunc func(id string)

	// Config specifies the runtime configurable features of fansiterm.
	Config Config

	// ConfigUpdate, if non-nil, is called when the config changes.
	ConfigUpdate func(conf Config)

	// Render collects together all the graphical rendering fields.
	Render Render

	// Output specifies the program attached to the terminal. This should be the
	// same interface that the input mechanism (whatever that may be) uses to write
	// to the program. On POSIX systems, this would be equivalent to Stdin.
	// Default is io.Discard. Setting to nil will cause Escape Sequences that
	// write a response to panic.
	Output io.Writer

	// UserResetFunc is called when fansiterm's Reset() method is called. This
	// is the same Reset triggered by an \x1bc escape sequence. This can be used
	// to reset a hardware display.
	UserResetFunc func()

	sync.Mutex
	// contains filtered or unexported fields
}

Device implements a virtual terminal. It satisfies the io.Writer interface and supports ANSI escape sequences, custom fonts, raster graphics, and more. It is thread-safe and optimized for embedded or minimal environments.

func New

func New(cols, rows int, buf draw.Image) *Device

New initializes a new terminal device with the specified dimensions and optional draw.Image buffer. If buf is nil, a default in-memory RGBA buffer is allocated. The terminal's character size is fixed.

func NewAtResolution

func NewAtResolution(x, y int, buf draw.Image) *Device

NewAtResolution returns a new Device sized to fit a resolution (x,y), centering the terminal.

func NewWithBuf

func NewWithBuf(buf draw.Image) *Device

NewWithBuf uses buf as its target. NewWithBuf() will panic if called against a nil buf. If using fansiterm with backing hardware, NewWithBuf is likely the way you want to instantiate fansiterm. If you have buf providing an interface to a 240x135 screen, using the default 8x16 tiles, you can have an 40x8 cell terminal, with 7 rows of pixels leftover. If you want to have those extra 7 rows above the rendered terminal, you can do so like this:

term := NewWithBuf(xform.SubImage(buf,image.Rect(0,0,240,128).Add(0,7)))

Note: you can skip the Add() and just define your rectangle as image.Rect(0,7,240,135), but I find supplying the actual dimensions and then adding an offset to be clearer.

func (*Device) BlinkCursor

func (d *Device) BlinkCursor()

func (*Device) Clear

func (d *Device) Clear(x1, y1, x2, y2 int)

Clear writes a block of current background color in a rectangular shape, specified in units of cells (rows and columns). So (*Device).Clear(0,0, (*Device).cols, (*Device).rows) would clear the whole screen.

func (*Device) GetReader

func (d *Device) GetReader() (rd io.Reader)

GetReader returns an io.Reader that fansiterm will use for output. This uses an io.Pipe under the hood. The write portion of the pipe displaces (*Device).Output. A new pipe is instantiated every time this is called and will displace the old pipe.

func (*Device) Image

func (d *Device) Image() image.Image

func (*Device) RenderRune

func (d *Device) RenderRune(sym rune) (width int)

RenderRune does not do *any* interpretation of escape codes or control characters like \r or \n. It simply renders a single rune at the cursor position. It is up to the caller of RenderRune to process any control sequences / handle non-printing characters.

func (*Device) Reset

func (d *Device) Reset()

func (*Device) Scroll

func (d *Device) Scroll(rowAmount int)

func (*Device) SetAttrDefault

func (d *Device) SetAttrDefault(attr Attr)

func (*Device) SetCursorStyle

func (d *Device) SetCursorStyle(style cursorRectFunc)

SetCursorStyle changes the shape of the cursor. Valid options are CursorBlock, CursorBeam, and CursorUnderscore. CursorBlock is the default.

func (*Device) Size

func (d *Device) Size() (int, int)

Size returns the size of the terminal in rows and columns.

func (*Device) Stop

func (d *Device) Stop()

func (*Device) UpdateAttr

func (d *Device) UpdateAttr()

func (*Device) UseBuf

func (d *Device) UseBuf(buf draw.Image)

func (*Device) VectorScrollCells

func (d *Device) VectorScrollCells(c1, r1, c2, r2, cn, rn int)

func (*Device) VisualBell

func (d *Device) VisualBell()

VisualBell inverts the screen for a tenth of a second.

func (*Device) Write

func (d *Device) Write(data []byte) (n int, err error)

Write implements io.Write and is the main way to interract with a (*fansiterm).Device. This is essentially writing to the "terminal." Writes are more or less unbuffered with the exception of escape sequences. If a partial escape sequence is written to Device, the beginning will be bufferred and prepended to the next write. Certain broken escape sequence can potentially block forever.

func (*Device) WriteAt

func (d *Device) WriteAt(p []byte, off int64) (n int, err error)

WriteAt works like calling the save cursor position escape sequence, then the absolute set cursor position escape sequence, writing to the terminal, and then finally restoring cursor position. The offset is just the i'th character on screen. Offset values are clamped: Negative offset values are set to 0, values larger than d.rows * d.cols are set to d.rows*d.cols-1.

type Logger

type Logger interface {
	Info(msg string, args ...any)
	Warn(msg string, args ...any)
	Error(msg string, args ...any)
}

type Property

type Property int
const (
	PropertyWindowTitle Property = iota
)

type RGBColor

type RGBColor struct {
	R, G, B uint8
}

func (RGBColor) RGBA

func (c RGBColor) RGBA() (r, g, b, a uint32)

type RGBImage

type RGBImage struct {
	Pix []uint8
	image.Rectangle
}

func NewRGBImage

func NewRGBImage(r image.Rectangle) *RGBImage

func (*RGBImage) At

func (p *RGBImage) At(x, y int) color.Color

func (*RGBImage) Bounds

func (p *RGBImage) Bounds() image.Rectangle

func (*RGBImage) ColorModel

func (p *RGBImage) ColorModel() color.Model

func (*RGBImage) Set

func (p *RGBImage) Set(x, y int, c color.Color)

type Render

type Render struct {
	draw.Image

	CharSet       tiles.Tiler
	AltCharSet    tiles.Tiler
	BoldCharSet   tiles.Tiler
	ItalicCharSet tiles.Tiler
	User          tiles.FullColorTileSet

	// DisplayFunc is called after a write to the terminal. This is for some displays that require a flush / blit / sync call.
	DisplayFunc func()
	// contains filtered or unexported fields
}

func (Render) Bounds

func (r Render) Bounds() image.Rectangle

Bounds returns the image.Rectangle that aligns with terminal cell boundaries

func (*Render) Fill

func (r *Render) Fill(region image.Rectangle, c color.Color)

func (*Render) RegionScroll

func (r *Render) RegionScroll(region image.Rectangle, pixAmt int)

func (*Render) Scroll

func (r *Render) Scroll(pixAmt int)

func (Render) Set

func (r Render) Set(x, y int, c color.Color)

func (*Render) VectorScroll

func (r *Render) VectorScroll(region image.Rectangle, vector image.Point)

Directories

Path Synopsis
fansi
fansi is a fansiterm/tiles.FontTileSet, implementing tiles.Tiler.
fansi is a fansiterm/tiles.FontTileSet, implementing tiles.Tiler.

Jump to

Keyboard shortcuts

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