fansiterm

package module
v0.0.0-...-2285f9f Latest Latest
Warning

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

Go to latest
Published: Mar 26, 2024 License: GPL-2.0 Imports: 18 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 backend 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 image.Image, draw.Image, and io.Writer. 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. Since this is meant to.

If you want to push your own graphics or other operations, you can draw directly to the Device 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 dependant. So

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 and Bold Font
  • Underline, Double Underline, Strikethrough
  • Custom Tile loading for alternate character set (shift-out character set, commonly used for line-drawing/pseudo graphics)
  • Font is 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 aganostic 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.

TODO

  • General Clean Up (Device struct is a bit of a mess)
  • Package documentation
  • Test on real hardware
  • 1-bit color/rendering support for very-very-constrained systems
  • More configurable font / better font configuration
    • Permit a very stripped down minimal font to be used or alternatively allow every option: Bold, Italic, Fraktur, etc.
  • Optimize drawing/rendering routines.
  • Standardize / settle upon an API
    • The main interaction will be via io.Write(), that won't change. But things like direct manipulation of the terminal (e.g. calling (*Device).MoveCursorAbs(x,y)) are still in flux.

Future

I want to keep a very stripped down, barebones 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 backend 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 and set to represent the characters '(' and ')' in the alternate chracter set (actived with the SHIFT-OUT byte, 0x0E, and deactived with SHIFT-IN byte, 0x0F).
  • Custom rounded-endcap tiles are used to surround 433 MHz and KHz, also via alternate chracter 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.
  • 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).

Documentation

Index

Constants

View Source
const (
	CursorBlock = iota
	CursorBeam
	CursorUnderscore
)

Variables

View Source
var (
	ColorBlack         = NewOpaqueColor(0, 0, 0)
	ColorBrightBlack   = NewOpaqueColor(85, 85, 85)
	ColorRed           = NewOpaqueColor(127, 0, 0)
	ColorBrightRed     = NewOpaqueColor(255, 0, 0)
	ColorGreen         = NewOpaqueColor(0, 170, 0)
	ColorBrightGreen   = NewOpaqueColor(85, 255, 85)
	ColorYellow        = NewOpaqueColor(170, 85, 0)
	ColorBrightYellow  = NewOpaqueColor(255, 255, 85)
	ColorBlue          = NewOpaqueColor(0, 0, 170)
	ColorBrightBlue    = NewOpaqueColor(85, 85, 255)
	ColorMagenta       = NewOpaqueColor(170, 0, 170)
	ColorBrightMagenta = NewOpaqueColor(255, 85, 255)
	ColorCyan          = NewOpaqueColor(0, 170, 170)
	ColorBrightCyan    = NewOpaqueColor(85, 255, 255)
	// Okay, I deviated from VGA colors here. VGA "white" is way too gray.
	ColorWhite = NewOpaqueColor(240, 240, 240)
	// ColorWhite       = NewOpaqueColor(170, 170, 170)
	ColorBrightWhite = NewOpaqueColor(255, 255, 255)
)
View Source
var AttrDefault = Attr{
	Fg: ColorWhite,
	Bg: ColorBlack,
}
View Source
var Colors256 = [256]Color{}/* 256 elements not displayed */

Colors256 defines the default set of 256 Colors

View Source
var ConfigDefault = Config{
	TabSize: 8,
}
View Source
var EmptyTile = image.NewAlpha(image.Rect(0, 0, 8, 16))
View Source
var ErrEscapeSequenceIncomplete = errors.New("escape sequence incomplete")

Functions

func FixedToImagePoint

func FixedToImagePoint(fp fixed.Point26_6) image.Point

func FixedToImageRect

func FixedToImageRect(fr fixed.Rectangle26_6) image.Rectangle

fixed.Rectangle26_6 to image.Image rectangle

func NewImageTranslate

func NewImageTranslate(offset image.Point, img draw.Image) *imageTranslate

NewImageTranslate returns an imageTranslate object which offsets all operations to img by offset.

Types

type Attr

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

type Color

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

Color both implements color.Color and image.Image. image.Image needs a color.Model, so for convenience's sake, Color also implements color.Model so it can simply have ColorModel() return itself. The main purpose of Color is so there is no need to instantiate an image.Unform everytime we need to draw something in a particular color.

func NewColor

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

func NewOpaqueColor

func NewOpaqueColor(r, g, b uint8) Color

func (Color) At

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

func (Color) Bounds

func (c Color) Bounds() image.Rectangle

func (Color) ColorModel

func (c Color) ColorModel() color.Model

func (Color) Convert

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

func (Color) RGBA

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

type Colorizer

type Colorizer func() color.RGBA

the tinygo.org/x/drivers/pixel package has a somewhat incompatible color interface with the color.Color interface. This type definition and it's associated function allows a pixel.Color's RGBA method to be cast so that it implements the color.Color interface. Example: pixelColor := pixel.NewColor[pixel.RGB888](127,127,127) drawImage.Set(xPos,yPos, Colorizer(pixelColor.RGBA))

func (Colorizer) RGBA

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

type Config

type Config struct {
	TabSize     int
	CursorStyle int
	CursorBlink bool
}

type Cursor

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

Cursor is used to track the cursor.

type Device

type Device struct {
	// BellFunc is called if it is non-null and the terminal would
	// display a bell character
	// TODO: Implement affirmative beep (default) and negative acknowledge beep
	// Negative acknowledge is produced when \a is sent while in SHIFT-OUT mode.
	// Affirmative: C-G (quarter notes?)
	// NAK: C♭ (whole note?)
	BellFunc func()

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

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

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

	// 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
	// contains filtered or unexported fields
}

func New

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

New returns an initialized *Device. If buf is nil, an internal buffer is used. Otherwise if you specify a hardware backed draw.Image, writes to Device will immediately be written to the backing hardware--whether this is instaneous or buffered is up to the device and the device driver.

func NewAtResolution

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

NewAtResolution is like New, but rather than specifying the columns and rows, you specify the desired resolution. The maximum rows and cols will be determined automatically and the terminal rendered in the center. TODO: allow offset to be manually specified

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) ColsRemaining

func (d *Device) ColsRemaining() int

ColsRemaining returns how many columns are remaining until EOL

func (*Device) HandleCSISequence

func (d *Device) HandleCSISequence(seq []rune)

func (*Device) HandleEscSequence

func (d *Device) HandleEscSequence(seq []rune)

HandleEscSequence handles escape sequences. This should be the whole complete sequence. Bounds are not checked so an incomplete sequence will cause a panic.

func (*Device) HandleOSCSequence

func (d *Device) HandleOSCSequence(seq []rune)

func (*Device) Image

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

func (*Device) MoveCursorAbs

func (d *Device) MoveCursorAbs(x, y int)

func (*Device) MoveCursorRel

func (d *Device) MoveCursorRel(x, y int)

func (*Device) RenderRunes

func (d *Device) RenderRunes(sym []rune)

RenderRunes does not do *any* interpretation of escape codes or control characters like \r or \n. It simply renders a slice of runes (as a string) at the cursor position. It is up to the caller of RenderRunes to ensure there's enough space for the runes on the buffer and to process any control sequences.

func (*Device) Scroll

func (d *Device) Scroll(amount int)

func (*Device) ScrollToCursor

func (d *Device) ScrollToCursor()

func (*Device) VisualBell

func (d *Device) VisualBell()

VisualBell inverts the screen for a quarter 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 with (*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.

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. Negative offset values are set to 0, values larger than d.rows * d.cols are set to d.rows*d.cols.

type Property

type Property int
const (
	PropertyWindowTitle Property = iota
)

type Render

type Render struct {
	draw.Image
	// contains filtered or unexported fields
}

type TileSet

type TileSet map[rune]image.Image

TileSet implements the golang.org/x/image/font.Face interface. It is a simple map of rune to image.Image. The images work best as an image.Alpha, that is, image data consisting solely of alpha channel. TODO: implement variable sized tiles, currently only 8x16 is supported

func NewTileSet

func NewTileSet() TileSet

func (TileSet) Close

func (ts TileSet) Close() error

func (TileSet) Glyph

func (ts TileSet) Glyph(dot fixed.Point26_6, r rune) (
	dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool)

func (TileSet) GlyphAdvance

func (ts TileSet) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool)

func (TileSet) GlyphBounds

func (ts TileSet) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool)

func (TileSet) Kern

func (ts TileSet) Kern(r0, r1 rune) fixed.Int26_6

func (TileSet) LoadTileFromFile

func (ts TileSet) LoadTileFromFile(r rune, file string)

func (TileSet) LoadTileFromReader

func (ts TileSet) LoadTileFromReader(r rune, rd io.Reader)

func (TileSet) Metrics

func (ts TileSet) Metrics() font.Metrics

Jump to

Keyboard shortcuts

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