// Copyright 2018 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /* Package cell implements cell options and attributes. A cell is the smallest point on the terminal. */ package cell import ( "fmt" "image" runewidth "github.com/mattn/go-runewidth" "github.com/mum4k/termdash/area" ) // Option is used to provide options for cells on a 2-D terminal. type Option interface { // set sets the provided option. set(*Options) } // Options stores the provided options. type Options struct { FgColor Color BgColor Color } // set allows existing options to be passed as an option. func (o *Options) set(other *Options) { *other = *o } // NewOptions returns a new Options instance after applying the provided options. func NewOptions(opts ...Option) *Options { o := &Options{} for _, opt := range opts { opt.set(o) } return o } // Cell represents a single cell on the terminal. type Cell struct { // Rune is the rune stored in the cell. Rune rune // Opts are the cell options. Opts *Options } // Copy returns a copy the cell. func (c *Cell) Copy() *Cell { return &Cell{ Rune: c.Rune, Opts: NewOptions(c.Opts), } } // New returns a new cell. func New(r rune, opts ...Option) *Cell { return &Cell{ Rune: r, Opts: NewOptions(opts...), } } // Apply applies the provided options to the cell. func (c *Cell) Apply(opts ...Option) { for _, opt := range opts { opt.set(c.Opts) } } // Buffer is a 2-D buffer of cells. // The axes increase right and down. // Uninitialized buffer is invalid, use NewBuffer to create an instance. // Don't set cells directly, use the SetCell method instead which safely // handles limits and wide unicode characters. type Buffer [][]*Cell // SetCell sets the rune of the specified cell in the buffer. Returns the // number of cells the rune occupies, wide runes can occupy multiple cells when // printed on the terminal. See http://www.unicode.org/reports/tr11/. // Use the options to specify which attributes to modify, if an attribute // option isn't specified, the attribute retains its previous value. func (b Buffer) SetCell(p image.Point, r rune, opts ...Option) (int, error) { partial, err := b.IsPartial(p) if err != nil { return -1, err } if partial { return -1, fmt.Errorf("cannot set rune %q at point %v, it is a partial cell occupied by a wide rune in the previous cell", r, p) } remW, err := b.RemWidth(p) if err != nil { return -1, err } rw := runewidth.RuneWidth(r) if rw > remW { return -1, fmt.Errorf("cannot set rune %q of width %d at point %v, only have %d remaining cells at this line", r, rw, p, remW) } cell := b[p.X][p.Y] cell.Rune = r cell.Apply(opts...) return rw, nil } // IsPartial returns true if the cell at the specified point holds a part of a // full width rune from a previous cell. See // http://www.unicode.org/reports/tr11/. func (b Buffer) IsPartial(p image.Point) (bool, error) { size := b.Size() ar, err := area.FromSize(size) if err != nil { return false, err } if !p.In(ar) { return false, fmt.Errorf("point %v falls outside of the area %v occupied by the buffer", p, ar) } if p.X == 0 && p.Y == 0 { return false, nil } prevP := image.Point{p.X - 1, p.Y} if prevP.X < 0 { prevP = image.Point{size.X - 1, p.Y - 1} } prevR := b[prevP.X][prevP.Y].Rune switch rw := runewidth.RuneWidth(prevR); rw { case 0, 1: return false, nil case 2: return true, nil default: return false, fmt.Errorf("buffer cell %v contains rune %q which has an unsupported rune with %d", prevP, prevR, rw) } } // RemWidth returns the remaining width (horizontal row of cells) available // from and inclusive of the specified point. func (b Buffer) RemWidth(p image.Point) (int, error) { size := b.Size() ar, err := area.FromSize(size) if err != nil { return -1, err } if !p.In(ar) { return -1, fmt.Errorf("point %v falls outside of the area %v occupied by the buffer", p, ar) } return size.X - p.X, nil } // NewBuffer returns a new Buffer of the provided size. func NewBuffer(size image.Point) (Buffer, error) { if size.X <= 0 { return nil, fmt.Errorf("invalid buffer width (size.X): %d, must be a positive number", size.X) } if size.Y <= 0 { return nil, fmt.Errorf("invalid buffer height (size.Y): %d, must be a positive number", size.Y) } b := make([][]*Cell, size.X) for col := range b { b[col] = make([]*Cell, size.Y) for row := range b[col] { b[col][row] = New(0) } } return b, nil } // Size returns the size of the buffer. func (b Buffer) Size() image.Point { return image.Point{ len(b), len(b[0]), } } // option implements Option. type option func(*Options) // set implements Option.set. func (co option) set(opts *Options) { co(opts) } // FgColor sets the foreground color of the cell. func FgColor(color Color) Option { return option(func(co *Options) { co.FgColor = color }) } // BgColor sets the background color of the cell. func BgColor(color Color) Option { return option(func(co *Options) { co.BgColor = color }) }