Projects STRLCPY termdash Commits b8591308
🤬
Revision indexing in progress... (symbol navigation in revisions will be accurate after indexed)
  • ■ ■ ■ ■ ■ ■
    CHANGELOG.md
    skipped 6 lines
    7 7   
    8 8  ## [Unreleased]
    9 9   
     10 +## [0.7.2] - 25-Feb-2019
     11 + 
     12 +### Added
     13 + 
     14 +- Test coverage for data only packages.
     15 + 
     16 +### Changed
     17 + 
     18 +- Refactoring packages that contained a mix of public and internal identifiers.
     19 + 
     20 +#### Breaking API changes
     21 + 
     22 +The following packages were refactored, no impact is expected as the removed
     23 +identifiers shouldn't be used externally.
     24 + 
     25 +- Functions align.Text and align.Rectangle were moved to a new
     26 + internal/alignfor package.
     27 +- Types cell.Cell and cell.Buffer were moved into a new internal/canvas/buffer
     28 + package.
     29 + 
    10 30  ## [0.7.1] - 24-Feb-2019
    11 31   
    12 32  ### Fixed
    13 33   
    14 34  - Some of the packages that were moved into internal are required externally.
    15 35   This release makes them available again.
     36 + 
     37 +### Changed
    16 38   
    17 39  #### Breaking API changes
    18 40   
    skipped 166 lines
    185 207  - The Gauge widget.
    186 208  - The Text widget.
    187 209   
    188  -[Unreleased]: https://github.com/mum4k/termdash/compare/v0.7.1...devel
     210 +[Unreleased]: https://github.com/mum4k/termdash/compare/v0.7.2...devel
     211 +[0.7.2]: https://github.com/mum4k/termdash/compare/v0.7.1...v0.7.2
    189 212  [0.7.1]: https://github.com/mum4k/termdash/compare/v0.7.0...v0.7.1
    190 213  [0.7.0]: https://github.com/mum4k/termdash/compare/v0.6.1...v0.7.0
    191 214  [0.6.1]: https://github.com/mum4k/termdash/compare/v0.6.0...v0.6.1
    skipped 6 lines
  • ■ ■ ■ ■ ■ ■
    align/align.go
    skipped 14 lines
    15 15  // Package align defines constants representing types of alignment.
    16 16  package align
    17 17   
    18  -import (
    19  - "fmt"
    20  - "image"
    21  - "strings"
    22  - 
    23  - "github.com/mum4k/termdash/internal/runewidth"
    24  -)
    25  - 
    26 18  // Horizontal indicates the type of horizontal alignment.
    27 19  type Horizontal int
    28 20   
    skipped 48 lines
    77 69   VerticalBottom
    78 70  )
    79 71   
    80  -// hAlign aligns the given area in the rectangle horizontally.
    81  -func hAlign(rect image.Rectangle, ar image.Rectangle, h Horizontal) (image.Rectangle, error) {
    82  - gap := rect.Dx() - ar.Dx()
    83  - switch h {
    84  - case HorizontalRight:
    85  - // Use gap from above.
    86  - case HorizontalCenter:
    87  - gap /= 2
    88  - case HorizontalLeft:
    89  - gap = 0
    90  - default:
    91  - return image.ZR, fmt.Errorf("unsupported horizontal alignment %v", h)
    92  - }
    93  - 
    94  - return image.Rect(
    95  - rect.Min.X+gap,
    96  - ar.Min.Y,
    97  - rect.Min.X+gap+ar.Dx(),
    98  - ar.Max.Y,
    99  - ), nil
    100  -}
    101  - 
    102  -// vAlign aligns the given area in the rectangle vertically.
    103  -func vAlign(rect image.Rectangle, ar image.Rectangle, v Vertical) (image.Rectangle, error) {
    104  - gap := rect.Dy() - ar.Dy()
    105  - switch v {
    106  - case VerticalBottom:
    107  - // Use gap from above.
    108  - case VerticalMiddle:
    109  - gap /= 2
    110  - case VerticalTop:
    111  - gap = 0
    112  - default:
    113  - return image.ZR, fmt.Errorf("unsupported vertical alignment %v", v)
    114  - }
    115  - 
    116  - return image.Rect(
    117  - ar.Min.X,
    118  - rect.Min.Y+gap,
    119  - ar.Max.X,
    120  - rect.Min.Y+gap+ar.Dy(),
    121  - ), nil
    122  -}
    123  - 
    124  -// Rectangle aligns the area within the rectangle returning the
    125  -// aligned area. The area must fall within the rectangle.
    126  -func Rectangle(rect image.Rectangle, ar image.Rectangle, h Horizontal, v Vertical) (image.Rectangle, error) {
    127  - if !ar.In(rect) {
    128  - return image.ZR, fmt.Errorf("cannot align area %v inside rectangle %v, the area falls outside of the rectangle", ar, rect)
    129  - }
    130  - 
    131  - aligned, err := hAlign(rect, ar, h)
    132  - if err != nil {
    133  - return image.ZR, err
    134  - }
    135  - aligned, err = vAlign(rect, aligned, v)
    136  - if err != nil {
    137  - return image.ZR, err
    138  - }
    139  - return aligned, nil
    140  -}
    141  - 
    142  -// Text aligns the text within the given rectangle, returns the start point for the text.
    143  -// For the purposes of the alignment this assumes that text will be trimmed if
    144  -// it overruns the rectangle.
    145  -// This only supports a single line of text, the text must not contain newlines.
    146  -func Text(rect image.Rectangle, text string, h Horizontal, v Vertical) (image.Point, error) {
    147  - if strings.ContainsRune(text, '\n') {
    148  - return image.ZP, fmt.Errorf("the provided text contains a newline character: %q", text)
    149  - }
    150  - 
    151  - cells := runewidth.StringWidth(text)
    152  - var textLen int
    153  - if cells < rect.Dx() {
    154  - textLen = cells
    155  - } else {
    156  - textLen = rect.Dx()
    157  - }
    158  - 
    159  - textRect := image.Rect(
    160  - rect.Min.X,
    161  - rect.Min.Y,
    162  - // For the purposes of aligning the text, assume that it will be
    163  - // trimmed to the available space.
    164  - rect.Min.X+textLen,
    165  - rect.Min.Y+1,
    166  - )
    167  - 
    168  - aligned, err := Rectangle(rect, textRect, h, v)
    169  - if err != nil {
    170  - return image.ZP, err
    171  - }
    172  - return image.Point{aligned.Min.X, aligned.Min.Y}, nil
    173  -}
    174  - 
  • ■ ■ ■ ■ ■
    align/align_test.go
    1  -// Copyright 2018 Google Inc.
     1 +// Copyright 2019 Google Inc.
    2 2  //
    3 3  // Licensed under the Apache License, Version 2.0 (the "License");
    4 4  // you may not use this file except in compliance with the License.
    skipped 9 lines
    14 14   
    15 15  package align
    16 16   
    17  -import (
    18  - "image"
    19  - "testing"
     17 +import "testing"
    20 18   
    21  - "github.com/kylelemons/godebug/pretty"
    22  -)
    23  - 
    24  -func TestRectangle(t *testing.T) {
     19 +func TestHorizontal(t *testing.T) {
    25 20   tests := []struct {
    26  - desc string
    27  - rect image.Rectangle
    28  - area image.Rectangle
    29  - hAlign Horizontal
    30  - vAlign Vertical
    31  - want image.Rectangle
    32  - wantErr bool
     21 + desc string
     22 + align Horizontal
     23 + want string
    33 24   }{
    34 25   {
    35  - desc: "area falls outside of the rectangle",
    36  - rect: image.Rect(0, 0, 1, 1),
    37  - area: image.Rect(1, 1, 2, 2),
    38  - hAlign: HorizontalLeft,
    39  - vAlign: VerticalTop,
    40  - wantErr: true,
    41  - },
    42  - {
    43  - desc: "unsupported horizontal alignment",
    44  - rect: image.Rect(0, 0, 2, 2),
    45  - area: image.Rect(0, 0, 1, 1),
    46  - hAlign: Horizontal(-1),
    47  - vAlign: VerticalTop,
    48  - wantErr: true,
    49  - },
    50  - {
    51  - desc: "unsupported vertical alignment",
    52  - rect: image.Rect(0, 0, 2, 2),
    53  - area: image.Rect(0, 0, 1, 1),
    54  - hAlign: HorizontalLeft,
    55  - vAlign: Vertical(-1),
    56  - wantErr: true,
    57  - },
    58  - {
    59  - desc: "nothing to align if the rectangles are equal",
    60  - rect: image.Rect(0, 0, 2, 2),
    61  - area: image.Rect(0, 0, 2, 2),
    62  - hAlign: HorizontalLeft,
    63  - vAlign: VerticalTop,
    64  - want: image.Rect(0, 0, 2, 2),
    65  - },
    66  - {
    67  - desc: "aligns top and left, area is zero based",
    68  - rect: image.Rect(0, 0, 3, 3),
    69  - area: image.Rect(0, 0, 1, 1),
    70  - hAlign: HorizontalLeft,
    71  - vAlign: VerticalTop,
    72  - want: image.Rect(0, 0, 1, 1),
    73  - },
    74  - {
    75  - desc: "aligns top and center, area is zero based",
    76  - rect: image.Rect(0, 0, 3, 3),
    77  - area: image.Rect(0, 0, 1, 1),
    78  - hAlign: HorizontalCenter,
    79  - vAlign: VerticalTop,
    80  - want: image.Rect(1, 0, 2, 1),
    81  - },
    82  - {
    83  - desc: "aligns top and right, area is zero based",
    84  - rect: image.Rect(0, 0, 3, 3),
    85  - area: image.Rect(0, 0, 1, 1),
    86  - hAlign: HorizontalRight,
    87  - vAlign: VerticalTop,
    88  - want: image.Rect(2, 0, 3, 1),
    89  - },
    90  - {
    91  - desc: "aligns middle and left, area is zero based",
    92  - rect: image.Rect(0, 0, 3, 3),
    93  - area: image.Rect(0, 0, 1, 1),
    94  - hAlign: HorizontalLeft,
    95  - vAlign: VerticalMiddle,
    96  - want: image.Rect(0, 1, 1, 2),
    97  - },
    98  - {
    99  - desc: "aligns middle and center, area is zero based",
    100  - rect: image.Rect(0, 0, 3, 3),
    101  - area: image.Rect(0, 0, 1, 1),
    102  - hAlign: HorizontalCenter,
    103  - vAlign: VerticalMiddle,
    104  - want: image.Rect(1, 1, 2, 2),
    105  - },
    106  - {
    107  - desc: "aligns middle and right, area is zero based",
    108  - rect: image.Rect(0, 0, 3, 3),
    109  - area: image.Rect(0, 0, 1, 1),
    110  - hAlign: HorizontalRight,
    111  - vAlign: VerticalMiddle,
    112  - want: image.Rect(2, 1, 3, 2),
    113  - },
    114  - {
    115  - desc: "aligns bottom and left, area is zero based",
    116  - rect: image.Rect(0, 0, 3, 3),
    117  - area: image.Rect(0, 0, 1, 1),
    118  - hAlign: HorizontalLeft,
    119  - vAlign: VerticalBottom,
    120  - want: image.Rect(0, 2, 1, 3),
    121  - },
    122  - {
    123  - desc: "aligns bottom and center, area is zero based",
    124  - rect: image.Rect(0, 0, 3, 3),
    125  - area: image.Rect(0, 0, 1, 1),
    126  - hAlign: HorizontalCenter,
    127  - vAlign: VerticalBottom,
    128  - want: image.Rect(1, 2, 2, 3),
    129  - },
    130  - {
    131  - desc: "aligns bottom and right, area is zero based",
    132  - rect: image.Rect(0, 0, 3, 3),
    133  - area: image.Rect(0, 0, 1, 1),
    134  - hAlign: HorizontalRight,
    135  - vAlign: VerticalBottom,
    136  - want: image.Rect(2, 2, 3, 3),
    137  - },
    138  - {
    139  - desc: "aligns top and left, area isn't zero based",
    140  - rect: image.Rect(0, 0, 3, 3),
    141  - area: image.Rect(0, 0, 1, 1),
    142  - hAlign: HorizontalLeft,
    143  - vAlign: VerticalTop,
    144  - want: image.Rect(0, 0, 1, 1),
     26 + desc: "unknown",
     27 + align: Horizontal(-1),
     28 + want: "HorizontalUnknown",
    145 29   },
    146 30   {
    147  - desc: "aligns top and center, area isn't zero based",
    148  - rect: image.Rect(0, 0, 3, 3),
    149  - area: image.Rect(1, 1, 2, 2),
    150  - hAlign: HorizontalCenter,
    151  - vAlign: VerticalTop,
    152  - want: image.Rect(1, 0, 2, 1),
     31 + desc: "left",
     32 + align: HorizontalLeft,
     33 + want: "HorizontalLeft",
    153 34   },
    154 35   {
    155  - desc: "aligns top and right, area isn't zero based",
    156  - rect: image.Rect(0, 0, 3, 3),
    157  - area: image.Rect(1, 1, 2, 2),
    158  - hAlign: HorizontalRight,
    159  - vAlign: VerticalTop,
    160  - want: image.Rect(2, 0, 3, 1),
     36 + desc: "center",
     37 + align: HorizontalCenter,
     38 + want: "HorizontalCenter",
    161 39   },
    162 40   {
    163  - desc: "aligns middle and left, area isn't zero based",
    164  - rect: image.Rect(0, 0, 3, 3),
    165  - area: image.Rect(1, 1, 2, 2),
    166  - hAlign: HorizontalLeft,
    167  - vAlign: VerticalMiddle,
    168  - want: image.Rect(0, 1, 1, 2),
    169  - },
    170  - {
    171  - desc: "aligns middle and center, area isn't zero based",
    172  - rect: image.Rect(0, 0, 3, 3),
    173  - area: image.Rect(1, 1, 2, 2),
    174  - hAlign: HorizontalCenter,
    175  - vAlign: VerticalMiddle,
    176  - want: image.Rect(1, 1, 2, 2),
    177  - },
    178  - {
    179  - desc: "aligns middle and right, area isn't zero based",
    180  - rect: image.Rect(0, 0, 3, 3),
    181  - area: image.Rect(1, 1, 2, 2),
    182  - hAlign: HorizontalRight,
    183  - vAlign: VerticalMiddle,
    184  - want: image.Rect(2, 1, 3, 2),
    185  - },
    186  - {
    187  - desc: "aligns bottom and left, area isn't zero based",
    188  - rect: image.Rect(0, 0, 3, 3),
    189  - area: image.Rect(1, 1, 2, 2),
    190  - hAlign: HorizontalLeft,
    191  - vAlign: VerticalBottom,
    192  - want: image.Rect(0, 2, 1, 3),
    193  - },
    194  - {
    195  - desc: "aligns bottom and center, area isn't zero based",
    196  - rect: image.Rect(0, 0, 3, 3),
    197  - area: image.Rect(1, 1, 2, 2),
    198  - hAlign: HorizontalCenter,
    199  - vAlign: VerticalBottom,
    200  - want: image.Rect(1, 2, 2, 3),
    201  - },
    202  - {
    203  - desc: "aligns bottom and right, area isn't zero based",
    204  - rect: image.Rect(0, 0, 3, 3),
    205  - area: image.Rect(1, 1, 2, 2),
    206  - hAlign: HorizontalRight,
    207  - vAlign: VerticalBottom,
    208  - want: image.Rect(2, 2, 3, 3),
     41 + desc: "right",
     42 + align: HorizontalRight,
     43 + want: "HorizontalRight",
    209 44   },
    210 45   }
    211 46   
    212 47   for _, tc := range tests {
    213 48   t.Run(tc.desc, func(t *testing.T) {
    214  - got, err := Rectangle(tc.rect, tc.area, tc.hAlign, tc.vAlign)
    215  - if (err != nil) != tc.wantErr {
    216  - t.Errorf("Rectangle => unexpected error: %v, wantErr: %v", err, tc.wantErr)
    217  - }
    218  - if err != nil {
    219  - return
    220  - }
    221  - 
    222  - if diff := pretty.Compare(tc.want, got); diff != "" {
    223  - t.Errorf("Rectangle => unexpected diff (-want, +got):\n%s", diff)
     49 + if got := tc.align.String(); got != tc.want {
     50 + t.Errorf("String => %q, want %q", got, tc.want)
    224 51   }
    225 52   })
    226 53   }
    227 54  }
    228 55   
    229  -func TestText(t *testing.T) {
     56 +func TestVertical(t *testing.T) {
    230 57   tests := []struct {
    231  - desc string
    232  - rect image.Rectangle
    233  - text string
    234  - hAlign Horizontal
    235  - vAlign Vertical
    236  - want image.Point
    237  - wantErr bool
     58 + desc string
     59 + align Vertical
     60 + want string
    238 61   }{
    239 62   {
    240  - desc: "fails when text contains newline",
    241  - rect: image.Rect(0, 0, 3, 3),
    242  - text: "a\nb",
    243  - wantErr: true,
     63 + desc: "unknown",
     64 + align: Vertical(-1),
     65 + want: "VerticalUnknown",
    244 66   },
    245 67   {
    246  - desc: "aligns text top and left",
    247  - rect: image.Rect(1, 1, 4, 4),
    248  - text: "a",
    249  - hAlign: HorizontalLeft,
    250  - vAlign: VerticalTop,
    251  - want: image.Point{1, 1},
     68 + desc: "top",
     69 + align: VerticalTop,
     70 + want: "VerticalTop",
    252 71   },
    253 72   {
    254  - desc: "aligns text top and center",
    255  - rect: image.Rect(1, 1, 4, 4),
    256  - text: "a",
    257  - hAlign: HorizontalCenter,
    258  - vAlign: VerticalTop,
    259  - want: image.Point{2, 1},
     73 + desc: "middle",
     74 + align: VerticalMiddle,
     75 + want: "VerticalMiddle",
    260 76   },
    261 77   {
    262  - desc: "aligns text top and right",
    263  - rect: image.Rect(1, 1, 4, 4),
    264  - text: "a",
    265  - hAlign: HorizontalRight,
    266  - vAlign: VerticalTop,
    267  - want: image.Point{3, 1},
    268  - },
    269  - {
    270  - desc: "aligns text middle and left",
    271  - rect: image.Rect(1, 1, 4, 4),
    272  - text: "a",
    273  - hAlign: HorizontalLeft,
    274  - vAlign: VerticalMiddle,
    275  - want: image.Point{1, 2},
    276  - },
    277  - {
    278  - desc: "aligns half-width text rune middle and center",
    279  - rect: image.Rect(1, 1, 4, 4),
    280  - text: "a",
    281  - hAlign: HorizontalCenter,
    282  - vAlign: VerticalMiddle,
    283  - want: image.Point{2, 2},
    284  - },
    285  - {
    286  - desc: "aligns full-width text rune middle and center",
    287  - rect: image.Rect(1, 1, 4, 4),
    288  - text: "界",
    289  - hAlign: HorizontalCenter,
    290  - vAlign: VerticalMiddle,
    291  - want: image.Point{1, 2},
    292  - },
    293  - {
    294  - desc: "aligns text middle and right",
    295  - rect: image.Rect(1, 1, 4, 4),
    296  - text: "a",
    297  - hAlign: HorizontalRight,
    298  - vAlign: VerticalMiddle,
    299  - want: image.Point{3, 2},
    300  - },
    301  - {
    302  - desc: "aligns text bottom and left",
    303  - rect: image.Rect(1, 1, 4, 4),
    304  - text: "a",
    305  - hAlign: HorizontalLeft,
    306  - vAlign: VerticalBottom,
    307  - want: image.Point{1, 3},
    308  - },
    309  - {
    310  - desc: "aligns text bottom and center",
    311  - rect: image.Rect(1, 1, 4, 4),
    312  - text: "a",
    313  - hAlign: HorizontalCenter,
    314  - vAlign: VerticalBottom,
    315  - want: image.Point{2, 3},
    316  - },
    317  - {
    318  - desc: "aligns text bottom and right",
    319  - rect: image.Rect(1, 1, 4, 4),
    320  - text: "a",
    321  - hAlign: HorizontalRight,
    322  - vAlign: VerticalBottom,
    323  - want: image.Point{3, 3},
    324  - },
    325  - {
    326  - desc: "aligns text that is too long, assumes trimming",
    327  - rect: image.Rect(1, 1, 4, 4),
    328  - text: "abcd",
    329  - hAlign: HorizontalCenter,
    330  - vAlign: VerticalTop,
    331  - want: image.Point{1, 1},
     78 + desc: "bottom",
     79 + align: VerticalBottom,
     80 + want: "VerticalBottom",
    332 81   },
    333 82   }
    334 83   
    335 84   for _, tc := range tests {
    336 85   t.Run(tc.desc, func(t *testing.T) {
    337  - got, err := Text(tc.rect, tc.text, tc.hAlign, tc.vAlign)
    338  - if (err != nil) != tc.wantErr {
    339  - t.Errorf("Text => unexpected error: %v, wantErr: %v", err, tc.wantErr)
    340  - }
    341  - if err != nil {
    342  - return
    343  - }
    344  - 
    345  - if diff := pretty.Compare(tc.want, got); diff != "" {
    346  - t.Errorf("Text => unexpected diff (-want, +got):\n%s", diff)
     86 + if got := tc.align.String(); got != tc.want {
     87 + t.Errorf("String => %q, want %q", got, tc.want)
    347 88   }
    348 89   })
    349 90   }
    skipped 2 lines
  • ■ ■ ■ ■ ■
    cell/cell.go
    skipped 11 lines
    12 12  // See the License for the specific language governing permissions and
    13 13  // limitations under the License.
    14 14   
    15  -/*
    16  -Package cell implements cell options and attributes.
    17  - 
    18  -A cell is the smallest point on the terminal.
    19  -*/
     15 +// Package cell implements cell options and attributes.
    20 16  package cell
    21 17   
    22  -import (
    23  - "fmt"
    24  - "image"
    25  - 
    26  - "github.com/mum4k/termdash/internal/area"
    27  - "github.com/mum4k/termdash/internal/runewidth"
    28  -)
    29  - 
    30 18  // Option is used to provide options for cells on a 2-D terminal.
    31 19  type Option interface {
    32  - // set sets the provided option.
    33  - set(*Options)
     20 + // Set sets the provided option.
     21 + Set(*Options)
    34 22  }
    35 23   
    36 24  // Options stores the provided options.
    skipped 2 lines
    39 27   BgColor Color
    40 28  }
    41 29   
    42  -// set allows existing options to be passed as an option.
    43  -func (o *Options) set(other *Options) {
     30 +// Set allows existing options to be passed as an option.
     31 +func (o *Options) Set(other *Options) {
    44 32   *other = *o
    45 33  }
    46 34   
    skipped 1 lines
    48 36  func NewOptions(opts ...Option) *Options {
    49 37   o := &Options{}
    50 38   for _, opt := range opts {
    51  - opt.set(o)
     39 + opt.Set(o)
    52 40   }
    53 41   return o
    54 42  }
    55 43   
    56  -// Cell represents a single cell on the terminal.
    57  -type Cell struct {
    58  - // Rune is the rune stored in the cell.
    59  - Rune rune
    60  - 
    61  - // Opts are the cell options.
    62  - Opts *Options
    63  -}
    64  - 
    65  -// Copy returns a copy the cell.
    66  -func (c *Cell) Copy() *Cell {
    67  - return &Cell{
    68  - Rune: c.Rune,
    69  - Opts: NewOptions(c.Opts),
    70  - }
    71  -}
    72  - 
    73  -// New returns a new cell.
    74  -func New(r rune, opts ...Option) *Cell {
    75  - return &Cell{
    76  - Rune: r,
    77  - Opts: NewOptions(opts...),
    78  - }
    79  -}
    80  - 
    81  -// Apply applies the provided options to the cell.
    82  -func (c *Cell) Apply(opts ...Option) {
    83  - for _, opt := range opts {
    84  - opt.set(c.Opts)
    85  - }
    86  -}
    87  - 
    88  -// Buffer is a 2-D buffer of cells.
    89  -// The axes increase right and down.
    90  -// Uninitialized buffer is invalid, use NewBuffer to create an instance.
    91  -// Don't set cells directly, use the SetCell method instead which safely
    92  -// handles limits and wide unicode characters.
    93  -type Buffer [][]*Cell
    94  - 
    95  -// SetCell sets the rune of the specified cell in the buffer. Returns the
    96  -// number of cells the rune occupies, wide runes can occupy multiple cells when
    97  -// printed on the terminal. See http://www.unicode.org/reports/tr11/.
    98  -// Use the options to specify which attributes to modify, if an attribute
    99  -// option isn't specified, the attribute retains its previous value.
    100  -func (b Buffer) SetCell(p image.Point, r rune, opts ...Option) (int, error) {
    101  - partial, err := b.IsPartial(p)
    102  - if err != nil {
    103  - return -1, err
    104  - }
    105  - if partial {
    106  - 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)
    107  - }
    108  - 
    109  - remW, err := b.RemWidth(p)
    110  - if err != nil {
    111  - return -1, err
    112  - }
    113  - rw := runewidth.RuneWidth(r)
    114  - if rw > remW {
    115  - 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)
    116  - }
    117  - 
    118  - cell := b[p.X][p.Y]
    119  - cell.Rune = r
    120  - cell.Apply(opts...)
    121  - return rw, nil
    122  -}
    123  - 
    124  -// IsPartial returns true if the cell at the specified point holds a part of a
    125  -// full width rune from a previous cell. See
    126  -// http://www.unicode.org/reports/tr11/.
    127  -func (b Buffer) IsPartial(p image.Point) (bool, error) {
    128  - size := b.Size()
    129  - ar, err := area.FromSize(size)
    130  - if err != nil {
    131  - return false, err
    132  - }
    133  - 
    134  - if !p.In(ar) {
    135  - return false, fmt.Errorf("point %v falls outside of the area %v occupied by the buffer", p, ar)
    136  - }
    137  - 
    138  - if p.X == 0 && p.Y == 0 {
    139  - return false, nil
    140  - }
    141  - 
    142  - prevP := image.Point{p.X - 1, p.Y}
    143  - if prevP.X < 0 {
    144  - prevP = image.Point{size.X - 1, p.Y - 1}
    145  - }
    146  - 
    147  - prevR := b[prevP.X][prevP.Y].Rune
    148  - switch rw := runewidth.RuneWidth(prevR); rw {
    149  - case 0, 1:
    150  - return false, nil
    151  - case 2:
    152  - return true, nil
    153  - default:
    154  - return false, fmt.Errorf("buffer cell %v contains rune %q which has an unsupported rune with %d", prevP, prevR, rw)
    155  - }
    156  -}
    157  - 
    158  -// RemWidth returns the remaining width (horizontal row of cells) available
    159  -// from and inclusive of the specified point.
    160  -func (b Buffer) RemWidth(p image.Point) (int, error) {
    161  - size := b.Size()
    162  - ar, err := area.FromSize(size)
    163  - if err != nil {
    164  - return -1, err
    165  - }
    166  - 
    167  - if !p.In(ar) {
    168  - return -1, fmt.Errorf("point %v falls outside of the area %v occupied by the buffer", p, ar)
    169  - }
    170  - return size.X - p.X, nil
    171  -}
    172  - 
    173  -// NewBuffer returns a new Buffer of the provided size.
    174  -func NewBuffer(size image.Point) (Buffer, error) {
    175  - if size.X <= 0 {
    176  - return nil, fmt.Errorf("invalid buffer width (size.X): %d, must be a positive number", size.X)
    177  - }
    178  - if size.Y <= 0 {
    179  - return nil, fmt.Errorf("invalid buffer height (size.Y): %d, must be a positive number", size.Y)
    180  - }
    181  - 
    182  - b := make([][]*Cell, size.X)
    183  - for col := range b {
    184  - b[col] = make([]*Cell, size.Y)
    185  - for row := range b[col] {
    186  - b[col][row] = New(0)
    187  - }
    188  - }
    189  - return b, nil
    190  -}
    191  - 
    192  -// Size returns the size of the buffer.
    193  -func (b Buffer) Size() image.Point {
    194  - return image.Point{
    195  - len(b),
    196  - len(b[0]),
    197  - }
    198  -}
    199  - 
    200 44  // option implements Option.
    201 45  type option func(*Options)
    202 46   
    203  -// set implements Option.set.
    204  -func (co option) set(opts *Options) {
     47 +// Set implements Option.set.
     48 +func (co option) Set(opts *Options) {
    205 49   co(opts)
    206 50  }
    207 51   
    skipped 14 lines
  • ■ ■ ■ ■ ■
    cell/cell_test.go
    skipped 14 lines
    15 15  package cell
    16 16   
    17 17  import (
    18  - "image"
    19 18   "testing"
    20 19   
    21 20   "github.com/kylelemons/godebug/pretty"
    skipped 38 lines
    60 59   BgColor: ColorMagenta,
    61 60   },
    62 61   },
    63  - }
    64  - 
    65  - for _, tc := range tests {
    66  - t.Run(tc.desc, func(t *testing.T) {
    67  - got := NewOptions(tc.opts...)
    68  - if diff := pretty.Compare(tc.want, got); diff != "" {
    69  - t.Errorf("NewOptions => unexpected diff (-want, +got):\n%s", diff)
    70  - }
    71  - })
    72  - }
    73  -}
    74  - 
    75  -func TestNew(t *testing.T) {
    76  - tests := []struct {
    77  - desc string
    78  - r rune
    79  - opts []Option
    80  - want Cell
    81  - }{
    82 62   {
    83  - desc: "creates empty cell with default options",
    84  - want: Cell{
    85  - Opts: &Options{},
    86  - },
    87  - },
    88  - {
    89  - desc: "cell with the specified rune",
    90  - r: 'X',
    91  - want: Cell{
    92  - Rune: 'X',
    93  - Opts: &Options{},
    94  - },
    95  - },
    96  - {
    97  - desc: "cell with options",
    98  - r: 'X',
     63 + desc: "setting options by passing the options struct",
    99 64   opts: []Option{
    100  - FgColor(ColorCyan),
    101  - BgColor(ColorMagenta),
    102  - },
    103  - want: Cell{
    104  - Rune: 'X',
    105  - Opts: &Options{
     65 + &Options{
    106 66   FgColor: ColorCyan,
    107 67   BgColor: ColorMagenta,
    108 68   },
    109 69   },
    110  - },
    111  - {
    112  - desc: "passing full Options overwrites existing",
    113  - r: 'X',
    114  - opts: []Option{
    115  - &Options{
    116  - FgColor: ColorBlack,
    117  - BgColor: ColorBlue,
    118  - },
    119  - },
    120  - want: Cell{
    121  - Rune: 'X',
    122  - Opts: &Options{
    123  - FgColor: ColorBlack,
    124  - BgColor: ColorBlue,
    125  - },
    126  - },
    127  - },
    128  - }
    129  - 
    130  - for _, tc := range tests {
    131  - t.Run(tc.desc, func(t *testing.T) {
    132  - got := New(tc.r, tc.opts...)
    133  - if diff := pretty.Compare(tc.want, got); diff != "" {
    134  - t.Errorf("New => unexpected diff (-want, +got):\n%s", diff)
    135  - }
    136  - })
    137  - }
    138  -}
    139  - 
    140  -func TestCellApply(t *testing.T) {
    141  - tests := []struct {
    142  - desc string
    143  - cell *Cell
    144  - opts []Option
    145  - want *Cell
    146  - }{
    147  - {
    148  - desc: "no options provided",
    149  - cell: New(0),
    150  - want: New(0),
    151  - },
    152  - {
    153  - desc: "no change in options",
    154  - cell: New(0, FgColor(ColorCyan)),
    155  - opts: []Option{
    156  - FgColor(ColorCyan),
    157  - },
    158  - want: New(0, FgColor(ColorCyan)),
    159  - },
    160  - {
    161  - desc: "retains previous values",
    162  - cell: New(0, FgColor(ColorCyan)),
    163  - opts: []Option{
    164  - BgColor(ColorBlack),
    165  - },
    166  - want: New(
    167  - 0,
    168  - FgColor(ColorCyan),
    169  - BgColor(ColorBlack),
    170  - ),
    171  - },
    172  - }
    173  - 
    174  - for _, tc := range tests {
    175  - t.Run(tc.desc, func(t *testing.T) {
    176  - got := tc.cell
    177  - got.Apply(tc.opts...)
    178  - if diff := pretty.Compare(tc.want, got); diff != "" {
    179  - t.Errorf("Apply => unexpected diff (-want, +got):\n%s", diff)
    180  - }
    181  - })
    182  - }
    183  -}
    184  - 
    185  -func TestNewBuffer(t *testing.T) {
    186  - tests := []struct {
    187  - desc string
    188  - size image.Point
    189  - want Buffer
    190  - wantErr bool
    191  - }{
    192  - {
    193  - desc: "zero buffer is invalid",
    194  - wantErr: true,
    195  - },
    196  - {
    197  - desc: "width cannot be negative",
    198  - size: image.Point{-1, 1},
    199  - wantErr: true,
    200  - },
    201  - {
    202  - desc: "height cannot be negative",
    203  - size: image.Point{1, -1},
    204  - wantErr: true,
    205  - },
    206  - {
    207  - desc: "creates single cell buffer",
    208  - size: image.Point{1, 1},
    209  - want: Buffer{
    210  - {
    211  - New(0),
    212  - },
    213  - },
    214  - },
    215  - {
    216  - desc: "creates the buffer",
    217  - size: image.Point{2, 3},
    218  - want: Buffer{
    219  - {
    220  - New(0),
    221  - New(0),
    222  - New(0),
    223  - },
    224  - {
    225  - New(0),
    226  - New(0),
    227  - New(0),
    228  - },
    229  - },
    230  - },
    231  - }
    232  - 
    233  - for _, tc := range tests {
    234  - t.Run(tc.desc, func(t *testing.T) {
    235  - got, err := NewBuffer(tc.size)
    236  - if (err != nil) != tc.wantErr {
    237  - t.Errorf("NewBuffer => unexpected error: %v, wantErr: %v", err, tc.wantErr)
    238  - }
    239  - if err != nil {
    240  - return
    241  - }
    242  - 
    243  - if diff := pretty.Compare(tc.want, got); diff != "" {
    244  - t.Errorf("NewBuffer => unexpected diff (-want, +got):\n%s", diff)
    245  - }
    246  - })
    247  - }
    248  -}
    249  - 
    250  -func TestBufferSize(t *testing.T) {
    251  - sizes := []image.Point{
    252  - {1, 1},
    253  - {2, 3},
    254  - }
    255  - 
    256  - for _, size := range sizes {
    257  - t.Run("", func(t *testing.T) {
    258  - b, err := NewBuffer(size)
    259  - if err != nil {
    260  - t.Fatalf("NewBuffer => unexpected error: %v", err)
    261  - }
    262  - 
    263  - got := b.Size()
    264  - if diff := pretty.Compare(size, got); diff != "" {
    265  - t.Errorf("Size => unexpected diff (-want, +got):\n%s", diff)
    266  - }
    267  - })
    268  - }
    269  -}
    270  - 
    271  -// mustNewBuffer returns a new Buffer or panics.
    272  -func mustNewBuffer(size image.Point) Buffer {
    273  - b, err := NewBuffer(size)
    274  - if err != nil {
    275  - panic(err)
    276  - }
    277  - return b
    278  -}
    279  - 
    280  -func TestSetCell(t *testing.T) {
    281  - size := image.Point{3, 3}
    282  - tests := []struct {
    283  - desc string
    284  - buffer Buffer
    285  - point image.Point
    286  - r rune
    287  - opts []Option
    288  - wantCells int
    289  - want Buffer
    290  - wantErr bool
    291  - }{
    292  - {
    293  - desc: "point falls before the buffer",
    294  - buffer: mustNewBuffer(size),
    295  - point: image.Point{-1, -1},
    296  - r: 'A',
    297  - wantErr: true,
    298  - },
    299  - {
    300  - desc: "point falls after the buffer",
    301  - buffer: mustNewBuffer(size),
    302  - point: image.Point{3, 3},
    303  - r: 'A',
    304  - wantErr: true,
    305  - },
    306  - {
    307  - desc: "point falls on cell with partial rune",
    308  - buffer: func() Buffer {
    309  - b := mustNewBuffer(size)
    310  - b[0][0].Rune = '世'
    311  - return b
    312  - }(),
    313  - point: image.Point{1, 0},
    314  - r: 'A',
    315  - wantErr: true,
    316  - },
    317  - {
    318  - desc: "point falls on cell with full-width rune and overwrites with half-width rune",
    319  - buffer: func() Buffer {
    320  - b := mustNewBuffer(size)
    321  - b[0][0].Rune = '世'
    322  - return b
    323  - }(),
    324  - point: image.Point{0, 0},
    325  - r: 'A',
    326  - wantCells: 1,
    327  - want: func() Buffer {
    328  - b := mustNewBuffer(size)
    329  - b[0][0].Rune = 'A'
    330  - return b
    331  - }(),
    332  - },
    333  - {
    334  - desc: "point falls on cell with full-width rune and overwrites with full-width rune",
    335  - buffer: func() Buffer {
    336  - b := mustNewBuffer(size)
    337  - b[0][0].Rune = '世'
    338  - return b
    339  - }(),
    340  - point: image.Point{0, 0},
    341  - r: '界',
    342  - wantCells: 2,
    343  - want: func() Buffer {
    344  - b := mustNewBuffer(size)
    345  - b[0][0].Rune = '界'
    346  - return b
    347  - }(),
    348  - },
    349  - {
    350  - desc: "not enough space for a wide rune on the line",
    351  - buffer: mustNewBuffer(image.Point{3, 3}),
    352  - point: image.Point{2, 0},
    353  - r: '界',
    354  - wantErr: true,
    355  - },
    356  - {
    357  - desc: "sets half-width rune in a cell",
    358  - buffer: mustNewBuffer(image.Point{3, 3}),
    359  - point: image.Point{1, 1},
    360  - r: 'A',
    361  - wantCells: 1,
    362  - want: func() Buffer {
    363  - b := mustNewBuffer(size)
    364  - b[1][1].Rune = 'A'
    365  - return b
    366  - }(),
    367  - },
    368  - {
    369  - desc: "sets full-width rune in a cell",
    370  - buffer: mustNewBuffer(image.Point{3, 3}),
    371  - point: image.Point{1, 2},
    372  - r: '界',
    373  - wantCells: 2,
    374  - want: func() Buffer {
    375  - b := mustNewBuffer(size)
    376  - b[1][2].Rune = '界'
    377  - return b
    378  - }(),
    379  - },
    380  - {
    381  - desc: "sets cell options",
    382  - buffer: mustNewBuffer(image.Point{3, 3}),
    383  - point: image.Point{1, 2},
    384  - r: 'A',
    385  - opts: []Option{
    386  - FgColor(ColorRed),
    387  - BgColor(ColorBlue),
     70 + want: &Options{
     71 + FgColor: ColorCyan,
     72 + BgColor: ColorMagenta,
    388 73   },
    389  - wantCells: 1,
    390  - want: func() Buffer {
    391  - b := mustNewBuffer(size)
    392  - cell := b[1][2]
    393  - cell.Rune = 'A'
    394  - cell.Opts = NewOptions(FgColor(ColorRed), BgColor(ColorBlue))
    395  - return b
    396  - }(),
    397  - },
    398  - {
    399  - desc: "overwrites only provided options",
    400  - buffer: func() Buffer {
    401  - b := mustNewBuffer(size)
    402  - cell := b[1][2]
    403  - cell.Opts = NewOptions(BgColor(ColorBlue))
    404  - return b
    405  - }(),
    406  - point: image.Point{1, 2},
    407  - r: 'A',
    408  - opts: []Option{
    409  - FgColor(ColorRed),
    410  - },
    411  - wantCells: 1,
    412  - want: func() Buffer {
    413  - b := mustNewBuffer(size)
    414  - cell := b[1][2]
    415  - cell.Rune = 'A'
    416  - cell.Opts = NewOptions(FgColor(ColorRed), BgColor(ColorBlue))
    417  - return b
    418  - }(),
    419 74   },
    420 75   }
    421 76   
    422 77   for _, tc := range tests {
    423 78   t.Run(tc.desc, func(t *testing.T) {
    424  - gotCells, err := tc.buffer.SetCell(tc.point, tc.r, tc.opts...)
    425  - if (err != nil) != tc.wantErr {
    426  - t.Errorf("SetCell => unexpected error: %v, wantErr: %v", err, tc.wantErr)
    427  - }
    428  - if err != nil {
    429  - return
    430  - }
    431  - 
    432  - if gotCells != tc.wantCells {
    433  - t.Errorf("SetCell => unexpected cell count, got %d, want %d", gotCells, tc.wantCells)
    434  - }
    435  - 
    436  - got := tc.buffer
     79 + got := NewOptions(tc.opts...)
    437 80   if diff := pretty.Compare(tc.want, got); diff != "" {
    438  - t.Errorf("SetCell=> unexpected buffer, diff (-want, +got):\n%s", diff)
    439  - }
    440  - })
    441  - }
    442  -}
    443  - 
    444  -func TestIsPartial(t *testing.T) {
    445  - tests := []struct {
    446  - desc string
    447  - buffer Buffer
    448  - point image.Point
    449  - want bool
    450  - wantErr bool
    451  - }{
    452  - {
    453  - desc: "point falls before the buffer",
    454  - buffer: mustNewBuffer(image.Point{1, 1}),
    455  - point: image.Point{-1, -1},
    456  - wantErr: true,
    457  - },
    458  - {
    459  - desc: "point falls after the buffer",
    460  - buffer: mustNewBuffer(image.Point{1, 1}),
    461  - point: image.Point{1, 1},
    462  - wantErr: true,
    463  - },
    464  - {
    465  - desc: "the first cell cannot be partial",
    466  - buffer: mustNewBuffer(image.Point{1, 1}),
    467  - point: image.Point{0, 0},
    468  - want: false,
    469  - },
    470  - {
    471  - desc: "previous cell on the same line contains no rune",
    472  - buffer: mustNewBuffer(image.Point{3, 3}),
    473  - point: image.Point{1, 0},
    474  - want: false,
    475  - },
    476  - {
    477  - desc: "previous cell on the same line contains half-width rune",
    478  - buffer: func() Buffer {
    479  - b := mustNewBuffer(image.Point{3, 3})
    480  - b[0][0].Rune = 'A'
    481  - return b
    482  - }(),
    483  - point: image.Point{1, 0},
    484  - want: false,
    485  - },
    486  - {
    487  - desc: "previous cell on the same line contains full-width rune",
    488  - buffer: func() Buffer {
    489  - b := mustNewBuffer(image.Point{3, 3})
    490  - b[0][0].Rune = '世'
    491  - return b
    492  - }(),
    493  - point: image.Point{1, 0},
    494  - want: true,
    495  - },
    496  - {
    497  - desc: "previous cell on previous line contains no rune",
    498  - buffer: mustNewBuffer(image.Point{3, 3}),
    499  - point: image.Point{0, 1},
    500  - want: false,
    501  - },
    502  - {
    503  - desc: "previous cell on previous line contains half-width rune",
    504  - buffer: func() Buffer {
    505  - b := mustNewBuffer(image.Point{3, 3})
    506  - b[2][0].Rune = 'A'
    507  - return b
    508  - }(),
    509  - point: image.Point{0, 1},
    510  - want: false,
    511  - },
    512  - {
    513  - desc: "previous cell on previous line contains full-width rune",
    514  - buffer: func() Buffer {
    515  - b := mustNewBuffer(image.Point{3, 3})
    516  - b[2][0].Rune = '世'
    517  - return b
    518  - }(),
    519  - point: image.Point{0, 1},
    520  - want: true,
    521  - },
    522  - }
    523  - 
    524  - for _, tc := range tests {
    525  - t.Run(tc.desc, func(t *testing.T) {
    526  - got, err := tc.buffer.IsPartial(tc.point)
    527  - if (err != nil) != tc.wantErr {
    528  - t.Errorf("IsPartial => unexpected error: %v, wantErr: %v", err, tc.wantErr)
    529  - }
    530  - if err != nil {
    531  - return
    532  - }
    533  - 
    534  - if got != tc.want {
    535  - t.Errorf("IsPartial => got %v, want %v", got, tc.want)
    536  - }
    537  - })
    538  - }
    539  -}
    540  - 
    541  -func TestRemWidth(t *testing.T) {
    542  - tests := []struct {
    543  - desc string
    544  - size image.Point
    545  - point image.Point
    546  - want int
    547  - wantErr bool
    548  - }{
    549  - {
    550  - desc: "point falls before the buffer",
    551  - size: image.Point{1, 1},
    552  - point: image.Point{-1, -1},
    553  - wantErr: true,
    554  - },
    555  - {
    556  - desc: "point falls after the buffer",
    557  - size: image.Point{1, 1},
    558  - point: image.Point{1, 1},
    559  - wantErr: true,
    560  - },
    561  - {
    562  - desc: "remaining width from the first cell on the line",
    563  - size: image.Point{3, 3},
    564  - point: image.Point{0, 1},
    565  - want: 3,
    566  - },
    567  - {
    568  - desc: "remaining width from the last cell on the line",
    569  - size: image.Point{3, 3},
    570  - point: image.Point{2, 2},
    571  - want: 1,
    572  - },
    573  - }
    574  - 
    575  - for _, tc := range tests {
    576  - t.Run(tc.desc, func(t *testing.T) {
    577  - b, err := NewBuffer(tc.size)
    578  - if err != nil {
    579  - t.Fatalf("NewBuffer => unexpected error: %v", err)
    580  - }
    581  - got, err := b.RemWidth(tc.point)
    582  - if (err != nil) != tc.wantErr {
    583  - t.Errorf("RemWidth => unexpected error: %v, wantErr: %v", err, tc.wantErr)
    584  - }
    585  - if err != nil {
    586  - return
    587  - }
    588  - if got != tc.want {
    589  - t.Errorf("RemWidth => got %d, want %d", got, tc.want)
     81 + t.Errorf("NewOptions => unexpected diff (-want, +got):\n%s", diff)
    590 82   }
    591 83   })
    592 84   }
    skipped 2 lines
  • ■ ■ ■ ■ ■
    cell/color_test.go
    skipped 13 lines
    14 14   
    15 15  package cell
    16 16   
    17  -import "testing"
     17 +import (
     18 + "fmt"
     19 + "testing"
     20 +)
    18 21   
    19 22  func TestColorNumber(t *testing.T) {
    20 23   tests := []struct {
    skipped 25 lines
    46 49   
    47 50   for _, tc := range tests {
    48 51   t.Run(tc.desc, func(t *testing.T) {
     52 + t.Logf(fmt.Sprintf("color: %v", tc.want))
    49 53   got := ColorNumber(tc.number)
    50 54   if got != tc.want {
    51 55   t.Errorf("ColorNumber(%v) => %v, want %v", tc.number, got, tc.want)
    skipped 153 lines
  • ■ ■ ■ ■ ■ ■
    container/container.go
    skipped 25 lines
    26 26   "image"
    27 27   "sync"
    28 28   
    29  - "github.com/mum4k/termdash/align"
     29 + "github.com/mum4k/termdash/internal/alignfor"
    30 30   "github.com/mum4k/termdash/internal/area"
    31 31   "github.com/mum4k/termdash/internal/event"
    32 32   "github.com/mum4k/termdash/internal/widgetapi"
    skipped 109 lines
    142 142   if wOpts.Ratio.X > 0 && wOpts.Ratio.Y > 0 {
    143 143   adjusted = area.WithRatio(adjusted, wOpts.Ratio)
    144 144   }
    145  - adjusted, err := align.Rectangle(c.usable(), adjusted, c.opts.hAlign, c.opts.vAlign)
     145 + adjusted, err := alignfor.Rectangle(c.usable(), adjusted, c.opts.hAlign, c.opts.vAlign)
    146 146   if err != nil {
    147 147   return image.ZR, err
    148 148   }
    skipped 158 lines
  • ■ ■ ■ ■ ■ ■
    internal/alignfor/align.go
     1 +// Copyright 2018 Google Inc.
     2 +//
     3 +// Licensed under the Apache License, Version 2.0 (the "License");
     4 +// you may not use this file except in compliance with the License.
     5 +// You may obtain a copy of the License at
     6 +//
     7 +// http://www.apache.org/licenses/LICENSE-2.0
     8 +//
     9 +// Unless required by applicable law or agreed to in writing, software
     10 +// distributed under the License is distributed on an "AS IS" BASIS,
     11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 +// See the License for the specific language governing permissions and
     13 +// limitations under the License.
     14 + 
     15 +// Package alignfor provides functions that align elements.
     16 +package alignfor
     17 + 
     18 +import (
     19 + "fmt"
     20 + "image"
     21 + "strings"
     22 + 
     23 + "github.com/mum4k/termdash/align"
     24 + "github.com/mum4k/termdash/internal/runewidth"
     25 +)
     26 + 
     27 +// hAlign aligns the given area in the rectangle horizontally.
     28 +func hAlign(rect image.Rectangle, ar image.Rectangle, h align.Horizontal) (image.Rectangle, error) {
     29 + gap := rect.Dx() - ar.Dx()
     30 + switch h {
     31 + case align.HorizontalRight:
     32 + // Use gap from above.
     33 + case align.HorizontalCenter:
     34 + gap /= 2
     35 + case align.HorizontalLeft:
     36 + gap = 0
     37 + default:
     38 + return image.ZR, fmt.Errorf("unsupported horizontal alignment %v", h)
     39 + }
     40 + 
     41 + return image.Rect(
     42 + rect.Min.X+gap,
     43 + ar.Min.Y,
     44 + rect.Min.X+gap+ar.Dx(),
     45 + ar.Max.Y,
     46 + ), nil
     47 +}
     48 + 
     49 +// vAlign aligns the given area in the rectangle vertically.
     50 +func vAlign(rect image.Rectangle, ar image.Rectangle, v align.Vertical) (image.Rectangle, error) {
     51 + gap := rect.Dy() - ar.Dy()
     52 + switch v {
     53 + case align.VerticalBottom:
     54 + // Use gap from above.
     55 + case align.VerticalMiddle:
     56 + gap /= 2
     57 + case align.VerticalTop:
     58 + gap = 0
     59 + default:
     60 + return image.ZR, fmt.Errorf("unsupported vertical alignment %v", v)
     61 + }
     62 + 
     63 + return image.Rect(
     64 + ar.Min.X,
     65 + rect.Min.Y+gap,
     66 + ar.Max.X,
     67 + rect.Min.Y+gap+ar.Dy(),
     68 + ), nil
     69 +}
     70 + 
     71 +// Rectangle aligns the area within the rectangle returning the
     72 +// aligned area. The area must fall within the rectangle.
     73 +func Rectangle(rect image.Rectangle, ar image.Rectangle, h align.Horizontal, v align.Vertical) (image.Rectangle, error) {
     74 + if !ar.In(rect) {
     75 + return image.ZR, fmt.Errorf("cannot align area %v inside rectangle %v, the area falls outside of the rectangle", ar, rect)
     76 + }
     77 + 
     78 + aligned, err := hAlign(rect, ar, h)
     79 + if err != nil {
     80 + return image.ZR, err
     81 + }
     82 + aligned, err = vAlign(rect, aligned, v)
     83 + if err != nil {
     84 + return image.ZR, err
     85 + }
     86 + return aligned, nil
     87 +}
     88 + 
     89 +// Text aligns the text within the given rectangle, returns the start point for the text.
     90 +// For the purposes of the alignment this assumes that text will be trimmed if
     91 +// it overruns the rectangle.
     92 +// This only supports a single line of text, the text must not contain newlines.
     93 +func Text(rect image.Rectangle, text string, h align.Horizontal, v align.Vertical) (image.Point, error) {
     94 + if strings.ContainsRune(text, '\n') {
     95 + return image.ZP, fmt.Errorf("the provided text contains a newline character: %q", text)
     96 + }
     97 + 
     98 + cells := runewidth.StringWidth(text)
     99 + var textLen int
     100 + if cells < rect.Dx() {
     101 + textLen = cells
     102 + } else {
     103 + textLen = rect.Dx()
     104 + }
     105 + 
     106 + textRect := image.Rect(
     107 + rect.Min.X,
     108 + rect.Min.Y,
     109 + // For the purposes of aligning the text, assume that it will be
     110 + // trimmed to the available space.
     111 + rect.Min.X+textLen,
     112 + rect.Min.Y+1,
     113 + )
     114 + 
     115 + aligned, err := Rectangle(rect, textRect, h, v)
     116 + if err != nil {
     117 + return image.ZP, err
     118 + }
     119 + return image.Point{aligned.Min.X, aligned.Min.Y}, nil
     120 +}
     121 + 
  • ■ ■ ■ ■ ■ ■
    internal/alignfor/align_test.go
     1 +// Copyright 2018 Google Inc.
     2 +//
     3 +// Licensed under the Apache License, Version 2.0 (the "License");
     4 +// you may not use this file except in compliance with the License.
     5 +// You may obtain a copy of the License at
     6 +//
     7 +// http://www.apache.org/licenses/LICENSE-2.0
     8 +//
     9 +// Unless required by applicable law or agreed to in writing, software
     10 +// distributed under the License is distributed on an "AS IS" BASIS,
     11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 +// See the License for the specific language governing permissions and
     13 +// limitations under the License.
     14 + 
     15 +package alignfor
     16 + 
     17 +import (
     18 + "image"
     19 + "testing"
     20 + 
     21 + "github.com/kylelemons/godebug/pretty"
     22 + "github.com/mum4k/termdash/align"
     23 +)
     24 + 
     25 +func TestRectangle(t *testing.T) {
     26 + tests := []struct {
     27 + desc string
     28 + rect image.Rectangle
     29 + area image.Rectangle
     30 + hAlign align.Horizontal
     31 + vAlign align.Vertical
     32 + want image.Rectangle
     33 + wantErr bool
     34 + }{
     35 + {
     36 + desc: "area falls outside of the rectangle",
     37 + rect: image.Rect(0, 0, 1, 1),
     38 + area: image.Rect(1, 1, 2, 2),
     39 + hAlign: align.HorizontalLeft,
     40 + vAlign: align.VerticalTop,
     41 + wantErr: true,
     42 + },
     43 + {
     44 + desc: "unsupported horizontal alignment",
     45 + rect: image.Rect(0, 0, 2, 2),
     46 + area: image.Rect(0, 0, 1, 1),
     47 + hAlign: align.Horizontal(-1),
     48 + vAlign: align.VerticalTop,
     49 + wantErr: true,
     50 + },
     51 + {
     52 + desc: "unsupported vertical alignment",
     53 + rect: image.Rect(0, 0, 2, 2),
     54 + area: image.Rect(0, 0, 1, 1),
     55 + hAlign: align.HorizontalLeft,
     56 + vAlign: align.Vertical(-1),
     57 + wantErr: true,
     58 + },
     59 + {
     60 + desc: "nothing to align if the rectangles are equal",
     61 + rect: image.Rect(0, 0, 2, 2),
     62 + area: image.Rect(0, 0, 2, 2),
     63 + hAlign: align.HorizontalLeft,
     64 + vAlign: align.VerticalTop,
     65 + want: image.Rect(0, 0, 2, 2),
     66 + },
     67 + {
     68 + desc: "aligns top and left, area is zero based",
     69 + rect: image.Rect(0, 0, 3, 3),
     70 + area: image.Rect(0, 0, 1, 1),
     71 + hAlign: align.HorizontalLeft,
     72 + vAlign: align.VerticalTop,
     73 + want: image.Rect(0, 0, 1, 1),
     74 + },
     75 + {
     76 + desc: "aligns top and center, area is zero based",
     77 + rect: image.Rect(0, 0, 3, 3),
     78 + area: image.Rect(0, 0, 1, 1),
     79 + hAlign: align.HorizontalCenter,
     80 + vAlign: align.VerticalTop,
     81 + want: image.Rect(1, 0, 2, 1),
     82 + },
     83 + {
     84 + desc: "aligns top and right, area is zero based",
     85 + rect: image.Rect(0, 0, 3, 3),
     86 + area: image.Rect(0, 0, 1, 1),
     87 + hAlign: align.HorizontalRight,
     88 + vAlign: align.VerticalTop,
     89 + want: image.Rect(2, 0, 3, 1),
     90 + },
     91 + {
     92 + desc: "aligns middle and left, area is zero based",
     93 + rect: image.Rect(0, 0, 3, 3),
     94 + area: image.Rect(0, 0, 1, 1),
     95 + hAlign: align.HorizontalLeft,
     96 + vAlign: align.VerticalMiddle,
     97 + want: image.Rect(0, 1, 1, 2),
     98 + },
     99 + {
     100 + desc: "aligns middle and center, area is zero based",
     101 + rect: image.Rect(0, 0, 3, 3),
     102 + area: image.Rect(0, 0, 1, 1),
     103 + hAlign: align.HorizontalCenter,
     104 + vAlign: align.VerticalMiddle,
     105 + want: image.Rect(1, 1, 2, 2),
     106 + },
     107 + {
     108 + desc: "aligns middle and right, area is zero based",
     109 + rect: image.Rect(0, 0, 3, 3),
     110 + area: image.Rect(0, 0, 1, 1),
     111 + hAlign: align.HorizontalRight,
     112 + vAlign: align.VerticalMiddle,
     113 + want: image.Rect(2, 1, 3, 2),
     114 + },
     115 + {
     116 + desc: "aligns bottom and left, area is zero based",
     117 + rect: image.Rect(0, 0, 3, 3),
     118 + area: image.Rect(0, 0, 1, 1),
     119 + hAlign: align.HorizontalLeft,
     120 + vAlign: align.VerticalBottom,
     121 + want: image.Rect(0, 2, 1, 3),
     122 + },
     123 + {
     124 + desc: "aligns bottom and center, area is zero based",
     125 + rect: image.Rect(0, 0, 3, 3),
     126 + area: image.Rect(0, 0, 1, 1),
     127 + hAlign: align.HorizontalCenter,
     128 + vAlign: align.VerticalBottom,
     129 + want: image.Rect(1, 2, 2, 3),
     130 + },
     131 + {
     132 + desc: "aligns bottom and right, area is zero based",
     133 + rect: image.Rect(0, 0, 3, 3),
     134 + area: image.Rect(0, 0, 1, 1),
     135 + hAlign: align.HorizontalRight,
     136 + vAlign: align.VerticalBottom,
     137 + want: image.Rect(2, 2, 3, 3),
     138 + },
     139 + {
     140 + desc: "aligns top and left, area isn't zero based",
     141 + rect: image.Rect(0, 0, 3, 3),
     142 + area: image.Rect(0, 0, 1, 1),
     143 + hAlign: align.HorizontalLeft,
     144 + vAlign: align.VerticalTop,
     145 + want: image.Rect(0, 0, 1, 1),
     146 + },
     147 + {
     148 + desc: "aligns top and center, area isn't zero based",
     149 + rect: image.Rect(0, 0, 3, 3),
     150 + area: image.Rect(1, 1, 2, 2),
     151 + hAlign: align.HorizontalCenter,
     152 + vAlign: align.VerticalTop,
     153 + want: image.Rect(1, 0, 2, 1),
     154 + },
     155 + {
     156 + desc: "aligns top and right, area isn't zero based",
     157 + rect: image.Rect(0, 0, 3, 3),
     158 + area: image.Rect(1, 1, 2, 2),
     159 + hAlign: align.HorizontalRight,
     160 + vAlign: align.VerticalTop,
     161 + want: image.Rect(2, 0, 3, 1),
     162 + },
     163 + {
     164 + desc: "aligns middle and left, area isn't zero based",
     165 + rect: image.Rect(0, 0, 3, 3),
     166 + area: image.Rect(1, 1, 2, 2),
     167 + hAlign: align.HorizontalLeft,
     168 + vAlign: align.VerticalMiddle,
     169 + want: image.Rect(0, 1, 1, 2),
     170 + },
     171 + {
     172 + desc: "aligns middle and center, area isn't zero based",
     173 + rect: image.Rect(0, 0, 3, 3),
     174 + area: image.Rect(1, 1, 2, 2),
     175 + hAlign: align.HorizontalCenter,
     176 + vAlign: align.VerticalMiddle,
     177 + want: image.Rect(1, 1, 2, 2),
     178 + },
     179 + {
     180 + desc: "aligns middle and right, area isn't zero based",
     181 + rect: image.Rect(0, 0, 3, 3),
     182 + area: image.Rect(1, 1, 2, 2),
     183 + hAlign: align.HorizontalRight,
     184 + vAlign: align.VerticalMiddle,
     185 + want: image.Rect(2, 1, 3, 2),
     186 + },
     187 + {
     188 + desc: "aligns bottom and left, area isn't zero based",
     189 + rect: image.Rect(0, 0, 3, 3),
     190 + area: image.Rect(1, 1, 2, 2),
     191 + hAlign: align.HorizontalLeft,
     192 + vAlign: align.VerticalBottom,
     193 + want: image.Rect(0, 2, 1, 3),
     194 + },
     195 + {
     196 + desc: "aligns bottom and center, area isn't zero based",
     197 + rect: image.Rect(0, 0, 3, 3),
     198 + area: image.Rect(1, 1, 2, 2),
     199 + hAlign: align.HorizontalCenter,
     200 + vAlign: align.VerticalBottom,
     201 + want: image.Rect(1, 2, 2, 3),
     202 + },
     203 + {
     204 + desc: "aligns bottom and right, area isn't zero based",
     205 + rect: image.Rect(0, 0, 3, 3),
     206 + area: image.Rect(1, 1, 2, 2),
     207 + hAlign: align.HorizontalRight,
     208 + vAlign: align.VerticalBottom,
     209 + want: image.Rect(2, 2, 3, 3),
     210 + },
     211 + }
     212 + 
     213 + for _, tc := range tests {
     214 + t.Run(tc.desc, func(t *testing.T) {
     215 + got, err := Rectangle(tc.rect, tc.area, tc.hAlign, tc.vAlign)
     216 + if (err != nil) != tc.wantErr {
     217 + t.Errorf("Rectangle => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     218 + }
     219 + if err != nil {
     220 + return
     221 + }
     222 + 
     223 + if diff := pretty.Compare(tc.want, got); diff != "" {
     224 + t.Errorf("Rectangle => unexpected diff (-want, +got):\n%s", diff)
     225 + }
     226 + })
     227 + }
     228 +}
     229 + 
     230 +func TestText(t *testing.T) {
     231 + tests := []struct {
     232 + desc string
     233 + rect image.Rectangle
     234 + text string
     235 + hAlign align.Horizontal
     236 + vAlign align.Vertical
     237 + want image.Point
     238 + wantErr bool
     239 + }{
     240 + {
     241 + desc: "fails when text contains newline",
     242 + rect: image.Rect(0, 0, 3, 3),
     243 + text: "a\nb",
     244 + wantErr: true,
     245 + },
     246 + {
     247 + desc: "aligns text top and left",
     248 + rect: image.Rect(1, 1, 4, 4),
     249 + text: "a",
     250 + hAlign: align.HorizontalLeft,
     251 + vAlign: align.VerticalTop,
     252 + want: image.Point{1, 1},
     253 + },
     254 + {
     255 + desc: "aligns text top and center",
     256 + rect: image.Rect(1, 1, 4, 4),
     257 + text: "a",
     258 + hAlign: align.HorizontalCenter,
     259 + vAlign: align.VerticalTop,
     260 + want: image.Point{2, 1},
     261 + },
     262 + {
     263 + desc: "aligns text top and right",
     264 + rect: image.Rect(1, 1, 4, 4),
     265 + text: "a",
     266 + hAlign: align.HorizontalRight,
     267 + vAlign: align.VerticalTop,
     268 + want: image.Point{3, 1},
     269 + },
     270 + {
     271 + desc: "aligns text middle and left",
     272 + rect: image.Rect(1, 1, 4, 4),
     273 + text: "a",
     274 + hAlign: align.HorizontalLeft,
     275 + vAlign: align.VerticalMiddle,
     276 + want: image.Point{1, 2},
     277 + },
     278 + {
     279 + desc: "aligns half-width text rune middle and center",
     280 + rect: image.Rect(1, 1, 4, 4),
     281 + text: "a",
     282 + hAlign: align.HorizontalCenter,
     283 + vAlign: align.VerticalMiddle,
     284 + want: image.Point{2, 2},
     285 + },
     286 + {
     287 + desc: "aligns full-width text rune middle and center",
     288 + rect: image.Rect(1, 1, 4, 4),
     289 + text: "界",
     290 + hAlign: align.HorizontalCenter,
     291 + vAlign: align.VerticalMiddle,
     292 + want: image.Point{1, 2},
     293 + },
     294 + {
     295 + desc: "aligns text middle and right",
     296 + rect: image.Rect(1, 1, 4, 4),
     297 + text: "a",
     298 + hAlign: align.HorizontalRight,
     299 + vAlign: align.VerticalMiddle,
     300 + want: image.Point{3, 2},
     301 + },
     302 + {
     303 + desc: "aligns text bottom and left",
     304 + rect: image.Rect(1, 1, 4, 4),
     305 + text: "a",
     306 + hAlign: align.HorizontalLeft,
     307 + vAlign: align.VerticalBottom,
     308 + want: image.Point{1, 3},
     309 + },
     310 + {
     311 + desc: "aligns text bottom and center",
     312 + rect: image.Rect(1, 1, 4, 4),
     313 + text: "a",
     314 + hAlign: align.HorizontalCenter,
     315 + vAlign: align.VerticalBottom,
     316 + want: image.Point{2, 3},
     317 + },
     318 + {
     319 + desc: "aligns text bottom and right",
     320 + rect: image.Rect(1, 1, 4, 4),
     321 + text: "a",
     322 + hAlign: align.HorizontalRight,
     323 + vAlign: align.VerticalBottom,
     324 + want: image.Point{3, 3},
     325 + },
     326 + {
     327 + desc: "aligns text that is too long, assumes trimming",
     328 + rect: image.Rect(1, 1, 4, 4),
     329 + text: "abcd",
     330 + hAlign: align.HorizontalCenter,
     331 + vAlign: align.VerticalTop,
     332 + want: image.Point{1, 1},
     333 + },
     334 + }
     335 + 
     336 + for _, tc := range tests {
     337 + t.Run(tc.desc, func(t *testing.T) {
     338 + got, err := Text(tc.rect, tc.text, tc.hAlign, tc.vAlign)
     339 + if (err != nil) != tc.wantErr {
     340 + t.Errorf("Text => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     341 + }
     342 + if err != nil {
     343 + return
     344 + }
     345 + 
     346 + if diff := pretty.Compare(tc.want, got); diff != "" {
     347 + t.Errorf("Text => unexpected diff (-want, +got):\n%s", diff)
     348 + }
     349 + })
     350 + }
     351 +}
     352 + 
  • ■ ■ ■ ■ ■ ■
    internal/canvas/buffer/buffer.go
     1 +// Copyright 2018 Google Inc.
     2 +//
     3 +// Licensed under the Apache License, Version 2.0 (the "License");
     4 +// you may not use this file except in compliance with the License.
     5 +// You may obtain a copy of the License at
     6 +//
     7 +// http://www.apache.org/licenses/LICENSE-2.0
     8 +//
     9 +// Unless required by applicable law or agreed to in writing, software
     10 +// distributed under the License is distributed on an "AS IS" BASIS,
     11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 +// See the License for the specific language governing permissions and
     13 +// limitations under the License.
     14 + 
     15 +// Package buffer implements a 2-D buffer of cells.
     16 +package buffer
     17 + 
     18 +import (
     19 + "fmt"
     20 + "image"
     21 + 
     22 + "github.com/mum4k/termdash/cell"
     23 + "github.com/mum4k/termdash/internal/area"
     24 + "github.com/mum4k/termdash/internal/runewidth"
     25 +)
     26 + 
     27 +// Cell represents a single cell on the terminal.
     28 +type Cell struct {
     29 + // Rune is the rune stored in the cell.
     30 + Rune rune
     31 + 
     32 + // Opts are the cell options.
     33 + Opts *cell.Options
     34 +}
     35 + 
     36 +// NewCell returns a new cell.
     37 +func NewCell(r rune, opts ...cell.Option) *Cell {
     38 + return &Cell{
     39 + Rune: r,
     40 + Opts: cell.NewOptions(opts...),
     41 + }
     42 +}
     43 + 
     44 +// Copy returns a copy the cell.
     45 +func (c *Cell) Copy() *Cell {
     46 + return &Cell{
     47 + Rune: c.Rune,
     48 + Opts: cell.NewOptions(c.Opts),
     49 + }
     50 +}
     51 + 
     52 +// Apply applies the provided options to the cell.
     53 +func (c *Cell) Apply(opts ...cell.Option) {
     54 + for _, opt := range opts {
     55 + opt.Set(c.Opts)
     56 + }
     57 +}
     58 + 
     59 +// Buffer is a 2-D buffer of cells.
     60 +// The axes increase right and down.
     61 +// Uninitialized buffer is invalid, use New to create an instance.
     62 +// Don't set cells directly, use the SetCell method instead which safely
     63 +// handles limits and wide unicode characters.
     64 +type Buffer [][]*Cell
     65 + 
     66 +// New returns a new Buffer of the provided size.
     67 +func New(size image.Point) (Buffer, error) {
     68 + if size.X <= 0 {
     69 + return nil, fmt.Errorf("invalid buffer width (size.X): %d, must be a positive number", size.X)
     70 + }
     71 + if size.Y <= 0 {
     72 + return nil, fmt.Errorf("invalid buffer height (size.Y): %d, must be a positive number", size.Y)
     73 + }
     74 + 
     75 + b := make([][]*Cell, size.X)
     76 + for col := range b {
     77 + b[col] = make([]*Cell, size.Y)
     78 + for row := range b[col] {
     79 + b[col][row] = NewCell(0)
     80 + }
     81 + }
     82 + return b, nil
     83 +}
     84 + 
     85 +// SetCell sets the rune of the specified cell in the buffer. Returns the
     86 +// number of cells the rune occupies, wide runes can occupy multiple cells when
     87 +// printed on the terminal. See http://www.unicode.org/reports/tr11/.
     88 +// Use the options to specify which attributes to modify, if an attribute
     89 +// option isn't specified, the attribute retains its previous value.
     90 +func (b Buffer) SetCell(p image.Point, r rune, opts ...cell.Option) (int, error) {
     91 + partial, err := b.IsPartial(p)
     92 + if err != nil {
     93 + return -1, err
     94 + }
     95 + if partial {
     96 + 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)
     97 + }
     98 + 
     99 + remW, err := b.RemWidth(p)
     100 + if err != nil {
     101 + return -1, err
     102 + }
     103 + rw := runewidth.RuneWidth(r)
     104 + if rw > remW {
     105 + 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)
     106 + }
     107 + 
     108 + c := b[p.X][p.Y]
     109 + c.Rune = r
     110 + c.Apply(opts...)
     111 + return rw, nil
     112 +}
     113 + 
     114 +// IsPartial returns true if the cell at the specified point holds a part of a
     115 +// full width rune from a previous cell. See
     116 +// http://www.unicode.org/reports/tr11/.
     117 +func (b Buffer) IsPartial(p image.Point) (bool, error) {
     118 + size := b.Size()
     119 + ar, err := area.FromSize(size)
     120 + if err != nil {
     121 + return false, err
     122 + }
     123 + 
     124 + if !p.In(ar) {
     125 + return false, fmt.Errorf("point %v falls outside of the area %v occupied by the buffer", p, ar)
     126 + }
     127 + 
     128 + if p.X == 0 && p.Y == 0 {
     129 + return false, nil
     130 + }
     131 + 
     132 + prevP := image.Point{p.X - 1, p.Y}
     133 + if prevP.X < 0 {
     134 + prevP = image.Point{size.X - 1, p.Y - 1}
     135 + }
     136 + 
     137 + prevR := b[prevP.X][prevP.Y].Rune
     138 + switch rw := runewidth.RuneWidth(prevR); rw {
     139 + case 0, 1:
     140 + return false, nil
     141 + case 2:
     142 + return true, nil
     143 + default:
     144 + return false, fmt.Errorf("buffer cell %v contains rune %q which has an unsupported rune with %d", prevP, prevR, rw)
     145 + }
     146 +}
     147 + 
     148 +// RemWidth returns the remaining width (horizontal row of cells) available
     149 +// from and inclusive of the specified point.
     150 +func (b Buffer) RemWidth(p image.Point) (int, error) {
     151 + size := b.Size()
     152 + ar, err := area.FromSize(size)
     153 + if err != nil {
     154 + return -1, err
     155 + }
     156 + 
     157 + if !p.In(ar) {
     158 + return -1, fmt.Errorf("point %v falls outside of the area %v occupied by the buffer", p, ar)
     159 + }
     160 + return size.X - p.X, nil
     161 +}
     162 + 
     163 +// Size returns the size of the buffer.
     164 +func (b Buffer) Size() image.Point {
     165 + return image.Point{
     166 + len(b),
     167 + len(b[0]),
     168 + }
     169 +}
     170 + 
  • ■ ■ ■ ■ ■ ■
    internal/canvas/buffer/buffer_test.go
     1 +// Copyright 2018 Google Inc.
     2 +//
     3 +// Licensed under the Apache License, Version 2.0 (the "License");
     4 +// you may not use this file except in compliance with the License.
     5 +// You may obtain a copy of the License at
     6 +//
     7 +// http://www.apache.org/licenses/LICENSE-2.0
     8 +//
     9 +// Unless required by applicable law or agreed to in writing, software
     10 +// distributed under the License is distributed on an "AS IS" BASIS,
     11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 +// See the License for the specific language governing permissions and
     13 +// limitations under the License.
     14 + 
     15 +package buffer
     16 + 
     17 +import (
     18 + "image"
     19 + "testing"
     20 + 
     21 + "github.com/kylelemons/godebug/pretty"
     22 + "github.com/mum4k/termdash/cell"
     23 +)
     24 + 
     25 +func TestNewCell(t *testing.T) {
     26 + tests := []struct {
     27 + desc string
     28 + r rune
     29 + opts []cell.Option
     30 + want Cell
     31 + }{
     32 + {
     33 + desc: "creates empty cell with default options",
     34 + want: Cell{
     35 + Opts: &cell.Options{},
     36 + },
     37 + },
     38 + {
     39 + desc: "cell with the specified rune",
     40 + r: 'X',
     41 + want: Cell{
     42 + Rune: 'X',
     43 + Opts: &cell.Options{},
     44 + },
     45 + },
     46 + {
     47 + desc: "cell with options",
     48 + r: 'X',
     49 + opts: []cell.Option{
     50 + cell.FgColor(cell.ColorCyan),
     51 + cell.BgColor(cell.ColorMagenta),
     52 + },
     53 + want: Cell{
     54 + Rune: 'X',
     55 + Opts: &cell.Options{
     56 + FgColor: cell.ColorCyan,
     57 + BgColor: cell.ColorMagenta,
     58 + },
     59 + },
     60 + },
     61 + {
     62 + desc: "passing full cell.Options overwrites existing",
     63 + r: 'X',
     64 + opts: []cell.Option{
     65 + &cell.Options{
     66 + FgColor: cell.ColorBlack,
     67 + BgColor: cell.ColorBlue,
     68 + },
     69 + },
     70 + want: Cell{
     71 + Rune: 'X',
     72 + Opts: &cell.Options{
     73 + FgColor: cell.ColorBlack,
     74 + BgColor: cell.ColorBlue,
     75 + },
     76 + },
     77 + },
     78 + }
     79 + 
     80 + for _, tc := range tests {
     81 + t.Run(tc.desc, func(t *testing.T) {
     82 + got := NewCell(tc.r, tc.opts...)
     83 + if diff := pretty.Compare(tc.want, got); diff != "" {
     84 + t.Errorf("New => unexpected diff (-want, +got):\n%s", diff)
     85 + }
     86 + })
     87 + }
     88 +}
     89 + 
     90 +func TestCellCopy(t *testing.T) {
     91 + tests := []struct {
     92 + desc string
     93 + cell *Cell
     94 + want *Cell
     95 + }{
     96 + {
     97 + desc: "copies empty cell",
     98 + cell: NewCell(0),
     99 + want: NewCell(0),
     100 + },
     101 + {
     102 + desc: "copies cell with a rune",
     103 + cell: NewCell(33),
     104 + want: NewCell(33),
     105 + },
     106 + {
     107 + desc: "copies cell with rune and options",
     108 + cell: NewCell(42, cell.FgColor(cell.ColorCyan), cell.BgColor(cell.ColorBlack)),
     109 + want: NewCell(
     110 + 42,
     111 + cell.FgColor(cell.ColorCyan),
     112 + cell.BgColor(cell.ColorBlack),
     113 + ),
     114 + },
     115 + }
     116 + 
     117 + for _, tc := range tests {
     118 + t.Run(tc.desc, func(t *testing.T) {
     119 + got := tc.cell.Copy()
     120 + if diff := pretty.Compare(tc.want, got); diff != "" {
     121 + t.Errorf("Copy => unexpected diff (-want, +got):\n%s", diff)
     122 + }
     123 + })
     124 + }
     125 +}
     126 + 
     127 +func TestCellApply(t *testing.T) {
     128 + tests := []struct {
     129 + desc string
     130 + cell *Cell
     131 + opts []cell.Option
     132 + want *Cell
     133 + }{
     134 + {
     135 + desc: "no options provided",
     136 + cell: NewCell(0),
     137 + want: NewCell(0),
     138 + },
     139 + {
     140 + desc: "no change in options",
     141 + cell: NewCell(0, cell.FgColor(cell.ColorCyan)),
     142 + opts: []cell.Option{
     143 + cell.FgColor(cell.ColorCyan),
     144 + },
     145 + want: NewCell(0, cell.FgColor(cell.ColorCyan)),
     146 + },
     147 + {
     148 + desc: "retains previous values",
     149 + cell: NewCell(0, cell.FgColor(cell.ColorCyan)),
     150 + opts: []cell.Option{
     151 + cell.BgColor(cell.ColorBlack),
     152 + },
     153 + want: NewCell(
     154 + 0,
     155 + cell.FgColor(cell.ColorCyan),
     156 + cell.BgColor(cell.ColorBlack),
     157 + ),
     158 + },
     159 + }
     160 + 
     161 + for _, tc := range tests {
     162 + t.Run(tc.desc, func(t *testing.T) {
     163 + got := tc.cell
     164 + got.Apply(tc.opts...)
     165 + if diff := pretty.Compare(tc.want, got); diff != "" {
     166 + t.Errorf("Apply => unexpected diff (-want, +got):\n%s", diff)
     167 + }
     168 + })
     169 + }
     170 +}
     171 + 
     172 +func TestNew(t *testing.T) {
     173 + tests := []struct {
     174 + desc string
     175 + size image.Point
     176 + want Buffer
     177 + wantErr bool
     178 + }{
     179 + {
     180 + desc: "zero buffer is invalid",
     181 + wantErr: true,
     182 + },
     183 + {
     184 + desc: "width cannot be negative",
     185 + size: image.Point{-1, 1},
     186 + wantErr: true,
     187 + },
     188 + {
     189 + desc: "height cannot be negative",
     190 + size: image.Point{1, -1},
     191 + wantErr: true,
     192 + },
     193 + {
     194 + desc: "creates single cell buffer",
     195 + size: image.Point{1, 1},
     196 + want: Buffer{
     197 + {
     198 + NewCell(0),
     199 + },
     200 + },
     201 + },
     202 + {
     203 + desc: "creates the buffer",
     204 + size: image.Point{2, 3},
     205 + want: Buffer{
     206 + {
     207 + NewCell(0),
     208 + NewCell(0),
     209 + NewCell(0),
     210 + },
     211 + {
     212 + NewCell(0),
     213 + NewCell(0),
     214 + NewCell(0),
     215 + },
     216 + },
     217 + },
     218 + }
     219 + 
     220 + for _, tc := range tests {
     221 + t.Run(tc.desc, func(t *testing.T) {
     222 + got, err := New(tc.size)
     223 + if (err != nil) != tc.wantErr {
     224 + t.Errorf("New => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     225 + }
     226 + if err != nil {
     227 + return
     228 + }
     229 + 
     230 + if diff := pretty.Compare(tc.want, got); diff != "" {
     231 + t.Errorf("New => unexpected diff (-want, +got):\n%s", diff)
     232 + }
     233 + })
     234 + }
     235 +}
     236 + 
     237 +func TestBufferSize(t *testing.T) {
     238 + sizes := []image.Point{
     239 + {1, 1},
     240 + {2, 3},
     241 + }
     242 + 
     243 + for _, size := range sizes {
     244 + t.Run("", func(t *testing.T) {
     245 + b, err := New(size)
     246 + if err != nil {
     247 + t.Fatalf("New => unexpected error: %v", err)
     248 + }
     249 + 
     250 + got := b.Size()
     251 + if diff := pretty.Compare(size, got); diff != "" {
     252 + t.Errorf("Size => unexpected diff (-want, +got):\n%s", diff)
     253 + }
     254 + })
     255 + }
     256 +}
     257 + 
     258 +// mustNew returns a new Buffer or panics.
     259 +func mustNew(size image.Point) Buffer {
     260 + b, err := New(size)
     261 + if err != nil {
     262 + panic(err)
     263 + }
     264 + return b
     265 +}
     266 + 
     267 +func TestSetCell(t *testing.T) {
     268 + size := image.Point{3, 3}
     269 + tests := []struct {
     270 + desc string
     271 + buffer Buffer
     272 + point image.Point
     273 + r rune
     274 + opts []cell.Option
     275 + wantCells int
     276 + want Buffer
     277 + wantErr bool
     278 + }{
     279 + {
     280 + desc: "point falls before the buffer",
     281 + buffer: mustNew(size),
     282 + point: image.Point{-1, -1},
     283 + r: 'A',
     284 + wantErr: true,
     285 + },
     286 + {
     287 + desc: "point falls after the buffer",
     288 + buffer: mustNew(size),
     289 + point: image.Point{3, 3},
     290 + r: 'A',
     291 + wantErr: true,
     292 + },
     293 + {
     294 + desc: "point falls on cell with partial rune",
     295 + buffer: func() Buffer {
     296 + b := mustNew(size)
     297 + b[0][0].Rune = '世'
     298 + return b
     299 + }(),
     300 + point: image.Point{1, 0},
     301 + r: 'A',
     302 + wantErr: true,
     303 + },
     304 + {
     305 + desc: "point falls on cell with full-width rune and overwrites with half-width rune",
     306 + buffer: func() Buffer {
     307 + b := mustNew(size)
     308 + b[0][0].Rune = '世'
     309 + return b
     310 + }(),
     311 + point: image.Point{0, 0},
     312 + r: 'A',
     313 + wantCells: 1,
     314 + want: func() Buffer {
     315 + b := mustNew(size)
     316 + b[0][0].Rune = 'A'
     317 + return b
     318 + }(),
     319 + },
     320 + {
     321 + desc: "point falls on cell with full-width rune and overwrites with full-width rune",
     322 + buffer: func() Buffer {
     323 + b := mustNew(size)
     324 + b[0][0].Rune = '世'
     325 + return b
     326 + }(),
     327 + point: image.Point{0, 0},
     328 + r: '界',
     329 + wantCells: 2,
     330 + want: func() Buffer {
     331 + b := mustNew(size)
     332 + b[0][0].Rune = '界'
     333 + return b
     334 + }(),
     335 + },
     336 + {
     337 + desc: "not enough space for a wide rune on the line",
     338 + buffer: mustNew(image.Point{3, 3}),
     339 + point: image.Point{2, 0},
     340 + r: '界',
     341 + wantErr: true,
     342 + },
     343 + {
     344 + desc: "sets half-width rune in a cell",
     345 + buffer: mustNew(image.Point{3, 3}),
     346 + point: image.Point{1, 1},
     347 + r: 'A',
     348 + wantCells: 1,
     349 + want: func() Buffer {
     350 + b := mustNew(size)
     351 + b[1][1].Rune = 'A'
     352 + return b
     353 + }(),
     354 + },
     355 + {
     356 + desc: "sets full-width rune in a cell",
     357 + buffer: mustNew(image.Point{3, 3}),
     358 + point: image.Point{1, 2},
     359 + r: '界',
     360 + wantCells: 2,
     361 + want: func() Buffer {
     362 + b := mustNew(size)
     363 + b[1][2].Rune = '界'
     364 + return b
     365 + }(),
     366 + },
     367 + {
     368 + desc: "sets cell options",
     369 + buffer: mustNew(image.Point{3, 3}),
     370 + point: image.Point{1, 2},
     371 + r: 'A',
     372 + opts: []cell.Option{
     373 + cell.FgColor(cell.ColorRed),
     374 + cell.BgColor(cell.ColorBlue),
     375 + },
     376 + wantCells: 1,
     377 + want: func() Buffer {
     378 + b := mustNew(size)
     379 + c := b[1][2]
     380 + c.Rune = 'A'
     381 + c.Opts = cell.NewOptions(cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
     382 + return b
     383 + }(),
     384 + },
     385 + {
     386 + desc: "overwrites only provided options",
     387 + buffer: func() Buffer {
     388 + b := mustNew(size)
     389 + c := b[1][2]
     390 + c.Opts = cell.NewOptions(cell.BgColor(cell.ColorBlue))
     391 + return b
     392 + }(),
     393 + point: image.Point{1, 2},
     394 + r: 'A',
     395 + opts: []cell.Option{
     396 + cell.FgColor(cell.ColorRed),
     397 + },
     398 + wantCells: 1,
     399 + want: func() Buffer {
     400 + b := mustNew(size)
     401 + c := b[1][2]
     402 + c.Rune = 'A'
     403 + c.Opts = cell.NewOptions(cell.FgColor(cell.ColorRed), cell.BgColor(cell.ColorBlue))
     404 + return b
     405 + }(),
     406 + },
     407 + }
     408 + 
     409 + for _, tc := range tests {
     410 + t.Run(tc.desc, func(t *testing.T) {
     411 + gotCells, err := tc.buffer.SetCell(tc.point, tc.r, tc.opts...)
     412 + if (err != nil) != tc.wantErr {
     413 + t.Errorf("SetCell => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     414 + }
     415 + if err != nil {
     416 + return
     417 + }
     418 + 
     419 + if gotCells != tc.wantCells {
     420 + t.Errorf("SetCell => unexpected cell count, got %d, want %d", gotCells, tc.wantCells)
     421 + }
     422 + 
     423 + got := tc.buffer
     424 + if diff := pretty.Compare(tc.want, got); diff != "" {
     425 + t.Errorf("SetCell=> unexpected buffer, diff (-want, +got):\n%s", diff)
     426 + }
     427 + })
     428 + }
     429 +}
     430 + 
     431 +func TestIsPartial(t *testing.T) {
     432 + tests := []struct {
     433 + desc string
     434 + buffer Buffer
     435 + point image.Point
     436 + want bool
     437 + wantErr bool
     438 + }{
     439 + {
     440 + desc: "point falls before the buffer",
     441 + buffer: mustNew(image.Point{1, 1}),
     442 + point: image.Point{-1, -1},
     443 + wantErr: true,
     444 + },
     445 + {
     446 + desc: "point falls after the buffer",
     447 + buffer: mustNew(image.Point{1, 1}),
     448 + point: image.Point{1, 1},
     449 + wantErr: true,
     450 + },
     451 + {
     452 + desc: "the first cell cannot be partial",
     453 + buffer: mustNew(image.Point{1, 1}),
     454 + point: image.Point{0, 0},
     455 + want: false,
     456 + },
     457 + {
     458 + desc: "previous cell on the same line contains no rune",
     459 + buffer: mustNew(image.Point{3, 3}),
     460 + point: image.Point{1, 0},
     461 + want: false,
     462 + },
     463 + {
     464 + desc: "previous cell on the same line contains half-width rune",
     465 + buffer: func() Buffer {
     466 + b := mustNew(image.Point{3, 3})
     467 + b[0][0].Rune = 'A'
     468 + return b
     469 + }(),
     470 + point: image.Point{1, 0},
     471 + want: false,
     472 + },
     473 + {
     474 + desc: "previous cell on the same line contains full-width rune",
     475 + buffer: func() Buffer {
     476 + b := mustNew(image.Point{3, 3})
     477 + b[0][0].Rune = '世'
     478 + return b
     479 + }(),
     480 + point: image.Point{1, 0},
     481 + want: true,
     482 + },
     483 + {
     484 + desc: "previous cell on previous line contains no rune",
     485 + buffer: mustNew(image.Point{3, 3}),
     486 + point: image.Point{0, 1},
     487 + want: false,
     488 + },
     489 + {
     490 + desc: "previous cell on previous line contains half-width rune",
     491 + buffer: func() Buffer {
     492 + b := mustNew(image.Point{3, 3})
     493 + b[2][0].Rune = 'A'
     494 + return b
     495 + }(),
     496 + point: image.Point{0, 1},
     497 + want: false,
     498 + },
     499 + {
     500 + desc: "previous cell on previous line contains full-width rune",
     501 + buffer: func() Buffer {
     502 + b := mustNew(image.Point{3, 3})
     503 + b[2][0].Rune = '世'
     504 + return b
     505 + }(),
     506 + point: image.Point{0, 1},
     507 + want: true,
     508 + },
     509 + }
     510 + 
     511 + for _, tc := range tests {
     512 + t.Run(tc.desc, func(t *testing.T) {
     513 + got, err := tc.buffer.IsPartial(tc.point)
     514 + if (err != nil) != tc.wantErr {
     515 + t.Errorf("IsPartial => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     516 + }
     517 + if err != nil {
     518 + return
     519 + }
     520 + 
     521 + if got != tc.want {
     522 + t.Errorf("IsPartial => got %v, want %v", got, tc.want)
     523 + }
     524 + })
     525 + }
     526 +}
     527 + 
     528 +func TestRemWidth(t *testing.T) {
     529 + tests := []struct {
     530 + desc string
     531 + size image.Point
     532 + point image.Point
     533 + want int
     534 + wantErr bool
     535 + }{
     536 + {
     537 + desc: "point falls before the buffer",
     538 + size: image.Point{1, 1},
     539 + point: image.Point{-1, -1},
     540 + wantErr: true,
     541 + },
     542 + {
     543 + desc: "point falls after the buffer",
     544 + size: image.Point{1, 1},
     545 + point: image.Point{1, 1},
     546 + wantErr: true,
     547 + },
     548 + {
     549 + desc: "remaining width from the first cell on the line",
     550 + size: image.Point{3, 3},
     551 + point: image.Point{0, 1},
     552 + want: 3,
     553 + },
     554 + {
     555 + desc: "remaining width from the last cell on the line",
     556 + size: image.Point{3, 3},
     557 + point: image.Point{2, 2},
     558 + want: 1,
     559 + },
     560 + }
     561 + 
     562 + for _, tc := range tests {
     563 + t.Run(tc.desc, func(t *testing.T) {
     564 + b, err := New(tc.size)
     565 + if err != nil {
     566 + t.Fatalf("New => unexpected error: %v", err)
     567 + }
     568 + got, err := b.RemWidth(tc.point)
     569 + if (err != nil) != tc.wantErr {
     570 + t.Errorf("RemWidth => unexpected error: %v, wantErr: %v", err, tc.wantErr)
     571 + }
     572 + if err != nil {
     573 + return
     574 + }
     575 + if got != tc.want {
     576 + t.Errorf("RemWidth => got %d, want %d", got, tc.want)
     577 + }
     578 + })
     579 + }
     580 +}
     581 + 
  • ■ ■ ■ ■ ■ ■
    internal/canvas/canvas.go
    skipped 20 lines
    21 21   
    22 22   "github.com/mum4k/termdash/cell"
    23 23   "github.com/mum4k/termdash/internal/area"
     24 + "github.com/mum4k/termdash/internal/canvas/buffer"
    24 25   "github.com/mum4k/termdash/internal/runewidth"
    25 26   "github.com/mum4k/termdash/terminal/terminalapi"
    26 27  )
    skipped 6 lines
    33 34   area image.Rectangle
    34 35   
    35 36   // buffer is where the drawing happens.
    36  - buffer cell.Buffer
     37 + buffer buffer.Buffer
    37 38  }
    38 39   
    39 40  // New returns a new Canvas with a buffer for the provided area.
    skipped 2 lines
    42 43   return nil, fmt.Errorf("area cannot start or end on the negative axis, got: %+v", ar)
    43 44   }
    44 45   
    45  - b, err := cell.NewBuffer(area.Size(ar))
     46 + b, err := buffer.New(area.Size(ar))
    46 47   if err != nil {
    47 48   return nil, err
    48 49   }
    skipped 16 lines
    65 66   
    66 67  // Clear clears all the content on the canvas.
    67 68  func (c *Canvas) Clear() error {
    68  - b, err := cell.NewBuffer(c.Size())
     69 + b, err := buffer.New(c.Size())
    69 70   if err != nil {
    70 71   return err
    71 72   }
    skipped 11 lines
    83 84  }
    84 85   
    85 86  // Cell returns a copy of the specified cell.
    86  -func (c *Canvas) Cell(p image.Point) (*cell.Cell, error) {
     87 +func (c *Canvas) Cell(p image.Point) (*buffer.Cell, error) {
    87 88   ar, err := area.FromSize(c.Size())
    88 89   if err != nil {
    89 90   return nil, err
    skipped 158 lines
  • ■ ■ ■ ■ ■ ■
    internal/canvas/canvas_test.go
    skipped 20 lines
    21 21   "github.com/kylelemons/godebug/pretty"
    22 22   "github.com/mum4k/termdash/cell"
    23 23   "github.com/mum4k/termdash/internal/area"
     24 + "github.com/mum4k/termdash/internal/canvas/buffer"
    24 25   "github.com/mum4k/termdash/internal/faketerm"
    25 26  )
    26 27   
    skipped 520 lines
    547 548   point image.Point
    548 549   r rune
    549 550   opts []cell.Option
    550  - want cell.Buffer // Expected back buffer in the fake terminal.
     551 + want buffer.Buffer // Expected back buffer in the fake terminal.
    551 552   wantCells int
    552 553   wantSetCellErr bool
    553 554   wantApplyErr bool
    skipped 12 lines
    566 567   point: image.Point{0, 0},
    567 568   r: 'X',
    568 569   wantCells: 1,
    569  - want: cell.Buffer{
     570 + want: buffer.Buffer{
    570 571   {
    571  - cell.New(0),
    572  - cell.New(0),
    573  - cell.New(0),
     572 + buffer.NewCell(0),
     573 + buffer.NewCell(0),
     574 + buffer.NewCell(0),
    574 575   },
    575 576   {
    576  - cell.New(0),
    577  - cell.New('X'),
    578  - cell.New(0),
     577 + buffer.NewCell(0),
     578 + buffer.NewCell('X'),
     579 + buffer.NewCell(0),
    579 580   },
    580 581   {
    581  - cell.New(0),
    582  - cell.New(0),
    583  - cell.New(0),
     582 + buffer.NewCell(0),
     583 + buffer.NewCell(0),
     584 + buffer.NewCell(0),
    584 585   },
    585 586   },
    586 587   },
    skipped 4 lines
    591 592   point: image.Point{0, 0},
    592 593   r: '界',
    593 594   wantCells: 2,
    594  - want: cell.Buffer{
     595 + want: buffer.Buffer{
    595 596   {
    596  - cell.New(0),
    597  - cell.New(0),
    598  - cell.New(0),
     597 + buffer.NewCell(0),
     598 + buffer.NewCell(0),
     599 + buffer.NewCell(0),
    599 600   },
    600 601   {
    601  - cell.New(0),
    602  - cell.New('界'),
    603  - cell.New(0),
     602 + buffer.NewCell(0),
     603 + buffer.NewCell('界'),
     604 + buffer.NewCell(0),
    604 605   },
    605 606   {
    606  - cell.New(0),
    607  - cell.New(0),
    608  - cell.New(0),
     607 + buffer.NewCell(0),
     608 + buffer.NewCell(0),
     609 + buffer.NewCell(0),
    609 610   },
    610 611   },
    611 612   },
    skipped 12 lines
    624 625   point: image.Point{1, 0},
    625 626   r: 'X',
    626 627   wantCells: 1,
    627  - want: cell.Buffer{
     628 + want: buffer.Buffer{
    628 629   {
    629  - cell.New(0),
    630  - cell.New(0),
    631  - cell.New(0),
     630 + buffer.NewCell(0),
     631 + buffer.NewCell(0),
     632 + buffer.NewCell(0),
    632 633   },
    633 634   {
    634  - cell.New(0),
    635  - cell.New(0),
    636  - cell.New(0),
     635 + buffer.NewCell(0),
     636 + buffer.NewCell(0),
     637 + buffer.NewCell(0),
    637 638   },
    638 639   {
    639  - cell.New(0),
    640  - cell.New('X'),
    641  - cell.New(0),
     640 + buffer.NewCell(0),
     641 + buffer.NewCell('X'),
     642 + buffer.NewCell(0),
    642 643   },
    643 644   },
    644 645   },
    skipped 4 lines
    649 650   point: image.Point{0, 1},
    650 651   r: 'X',
    651 652   wantCells: 1,
    652  - want: cell.Buffer{
     653 + want: buffer.Buffer{
    653 654   {
    654  - cell.New(0),
    655  - cell.New(0),
    656  - cell.New(0),
     655 + buffer.NewCell(0),
     656 + buffer.NewCell(0),
     657 + buffer.NewCell(0),
    657 658   },
    658 659   {
    659  - cell.New(0),
    660  - cell.New(0),
    661  - cell.New('X'),
     660 + buffer.NewCell(0),
     661 + buffer.NewCell(0),
     662 + buffer.NewCell('X'),
    662 663   },
    663 664   {
    664  - cell.New(0),
    665  - cell.New(0),
    666  - cell.New(0),
     665 + buffer.NewCell(0),
     666 + buffer.NewCell(0),
     667 + buffer.NewCell(0),
    667 668   },
    668 669   },
    669 670   },
    skipped 4 lines
    674 675   point: image.Point{1, 1},
    675 676   r: 'Z',
    676 677   wantCells: 1,
    677  - want: cell.Buffer{
     678 + want: buffer.Buffer{
    678 679   {
    679  - cell.New(0),
    680  - cell.New(0),
    681  - cell.New(0),
     680 + buffer.NewCell(0),
     681 + buffer.NewCell(0),
     682 + buffer.NewCell(0),
    682 683   },
    683 684   {
    684  - cell.New(0),
    685  - cell.New(0),
    686  - cell.New(0),
     685 + buffer.NewCell(0),
     686 + buffer.NewCell(0),
     687 + buffer.NewCell(0),
    687 688   },
    688 689   {
    689  - cell.New(0),
    690  - cell.New(0),
    691  - cell.New('Z'),
     690 + buffer.NewCell(0),
     691 + buffer.NewCell(0),
     692 + buffer.NewCell('Z'),
    692 693   },
    693 694   },
    694 695   },
    skipped 7 lines
    702 703   cell.BgColor(cell.ColorRed),
    703 704   },
    704 705   wantCells: 1,
    705  - want: cell.Buffer{
     706 + want: buffer.Buffer{
    706 707   {
    707  - cell.New(0),
    708  - cell.New(0),
    709  - cell.New(0),
     708 + buffer.NewCell(0),
     709 + buffer.NewCell(0),
     710 + buffer.NewCell(0),
    710 711   },
    711 712   {
    712  - cell.New(0),
    713  - cell.New(0),
    714  - cell.New(0),
     713 + buffer.NewCell(0),
     714 + buffer.NewCell(0),
     715 + buffer.NewCell(0),
    715 716   },
    716 717   {
    717  - cell.New(0),
    718  - cell.New(0),
    719  - cell.New('A', cell.BgColor(cell.ColorRed)),
     718 + buffer.NewCell(0),
     719 + buffer.NewCell(0),
     720 + buffer.NewCell('A', cell.BgColor(cell.ColorRed)),
    720 721   },
    721 722   },
    722 723   },
    skipped 4 lines
    727 728   point: image.Point{0, 0},
    728 729   r: 'A',
    729 730   wantCells: 1,
    730  - want: cell.Buffer{
     731 + want: buffer.Buffer{
    731 732   {
    732  - cell.New('A'),
     733 + buffer.NewCell('A'),
    733 734   },
    734 735   },
    735 736   },
    skipped 70 lines
    806 807   t.Fatalf("Apply => unexpected error: %v", err)
    807 808   }
    808 809   
    809  - want := cell.Buffer{
     810 + want := buffer.Buffer{
    810 811   {
    811  - cell.New('A'),
    812  - cell.New(0),
    813  - cell.New(0),
     812 + buffer.NewCell('A'),
     813 + buffer.NewCell(0),
     814 + buffer.NewCell(0),
    814 815   },
    815 816   {
    816  - cell.New(0),
    817  - cell.New('X'),
    818  - cell.New(0),
     817 + buffer.NewCell(0),
     818 + buffer.NewCell('X'),
     819 + buffer.NewCell(0),
    819 820   },
    820 821   {
    821  - cell.New(0),
    822  - cell.New(0),
    823  - cell.New(0),
     822 + buffer.NewCell(0),
     823 + buffer.NewCell(0),
     824 + buffer.NewCell(0),
    824 825   },
    825 826   }
    826 827   got := ft.BackBuffer()
    skipped 10 lines
    837 838   t.Fatalf("Apply => unexpected error: %v", err)
    838 839   }
    839 840   
    840  - want = cell.Buffer{
     841 + want = buffer.Buffer{
    841 842   {
    842  - cell.New('A'),
    843  - cell.New(0),
    844  - cell.New(0),
     843 + buffer.NewCell('A'),
     844 + buffer.NewCell(0),
     845 + buffer.NewCell(0),
    845 846   },
    846 847   {
    847  - cell.New(0),
    848  - cell.New(0),
    849  - cell.New(0),
     848 + buffer.NewCell(0),
     849 + buffer.NewCell(0),
     850 + buffer.NewCell(0),
    850 851   },
    851 852   {
    852  - cell.New(0),
    853  - cell.New(0),
    854  - cell.New(0),
     853 + buffer.NewCell(0),
     854 + buffer.NewCell(0),
     855 + buffer.NewCell(0),
    855 856   },
    856 857   }
    857 858   
    skipped 31 lines
    889 890   t.Fatalf("Apply => unexpected error: %v", err)
    890 891   }
    891 892   
    892  - want, err := cell.NewBuffer(area.Size(ar))
     893 + want, err := buffer.New(area.Size(ar))
    893 894   if err != nil {
    894  - t.Fatalf("NewBuffer => unexpected error: %v", err)
     895 + t.Fatalf("buffer.New => unexpected error: %v", err)
    895 896   }
    896 897   want[fullP.X][fullP.Y].Rune = '界'
    897 898   want[partP.X][partP.Y].Rune = 'A'
    skipped 9 lines
    907 908   desc string
    908 909   cvs func() (*Canvas, error)
    909 910   point image.Point
    910  - want *cell.Cell
     911 + want *buffer.Cell
    911 912   wantErr bool
    912 913   }{
    913 914   {
    skipped 25 lines
    939 940   return cvs, nil
    940 941   },
    941 942   point: image.Point{1, 1},
    942  - want: &cell.Cell{
     943 + want: &buffer.Cell{
    943 944   Rune: 'A',
    944 945   Opts: cell.NewOptions(
    945 946   cell.FgColor(cell.ColorRed),
    skipped 211 lines
  • ■ ■ ■ ■ ■
    internal/canvas/testcanvas/testcanvas.go
    skipped 20 lines
    21 21   
    22 22   "github.com/mum4k/termdash/cell"
    23 23   "github.com/mum4k/termdash/internal/canvas"
     24 + "github.com/mum4k/termdash/internal/canvas/buffer"
    24 25   "github.com/mum4k/termdash/internal/faketerm"
    25 26  )
    26 27   
    skipped 32 lines
    59 60  }
    60 61   
    61 62  // MustCell returns the cell or panics.
    62  -func MustCell(c *canvas.Canvas, p image.Point) *cell.Cell {
     63 +func MustCell(c *canvas.Canvas, p image.Point) *buffer.Cell {
    63 64   cell, err := c.Cell(p)
    64 65   if err != nil {
    65 66   panic(fmt.Sprintf("canvas.Cell => unexpected error: %v", err))
    skipped 12 lines
  • ■ ■ ■ ■ ■
    internal/draw/border.go
    skipped 21 lines
    22 22   
    23 23   "github.com/mum4k/termdash/align"
    24 24   "github.com/mum4k/termdash/cell"
     25 + "github.com/mum4k/termdash/internal/alignfor"
    25 26   "github.com/mum4k/termdash/internal/canvas"
    26 27   "github.com/mum4k/termdash/linestyle"
    27 28  )
    skipped 92 lines
    120 121   border.Max.X-1, // One space for the top right corner char.
    121 122   border.Min.Y+1,
    122 123   )
    123  - start, err := align.Text(available, opt.title, opt.titleHAlign, align.VerticalTop)
     124 + start, err := alignfor.Text(available, opt.title, opt.titleHAlign, align.VerticalTop)
    124 125   if err != nil {
    125 126   return err
    126 127   }
    skipped 56 lines
  • ■ ■ ■ ■ ■ ■
    internal/faketerm/faketerm.go
    skipped 23 lines
    24 24   "sync"
    25 25   
    26 26   "github.com/mum4k/termdash/cell"
     27 + "github.com/mum4k/termdash/internal/canvas/buffer"
    27 28   "github.com/mum4k/termdash/internal/event/eventqueue"
    28 29   "github.com/mum4k/termdash/terminal/terminalapi"
    29 30  )
    skipped 25 lines
    55 56  // This implementation is thread-safe.
    56 57  type Terminal struct {
    57 58   // buffer holds the terminal cells.
    58  - buffer cell.Buffer
     59 + buffer buffer.Buffer
    59 60   
    60 61   // events is a queue of input events.
    61 62   events *eventqueue.Unbound
    skipped 4 lines
    66 67   
    67 68  // New returns a new fake Terminal.
    68 69  func New(size image.Point, opts ...Option) (*Terminal, error) {
    69  - b, err := cell.NewBuffer(size)
     70 + b, err := buffer.New(size)
    70 71   if err != nil {
    71 72   return nil, err
    72 73   }
    skipped 22 lines
    95 96   t.mu.Lock()
    96 97   defer t.mu.Unlock()
    97 98   
    98  - b, err := cell.NewBuffer(size)
     99 + b, err := buffer.New(size)
    99 100   if err != nil {
    100 101   return err
    101 102   }
    skipped 3 lines
    105 106  }
    106 107   
    107 108  // BackBuffer returns the back buffer of the fake terminal.
    108  -func (t *Terminal) BackBuffer() cell.Buffer {
     109 +func (t *Terminal) BackBuffer() buffer.Buffer {
    109 110   t.mu.Lock()
    110 111   defer t.mu.Unlock()
    111 112   
    skipped 43 lines
    155 156   t.mu.Lock()
    156 157   defer t.mu.Unlock()
    157 158   
    158  - b, err := cell.NewBuffer(t.buffer.Size())
     159 + b, err := buffer.New(t.buffer.Size())
    159 160   if err != nil {
    160 161   return err
    161 162   }
    skipped 50 lines
  • ■ ■ ■ ■ ■ ■
    keyboard/keyboard_test.go
     1 +// Copyright 2019 Google Inc.
     2 +//
     3 +// Licensed under the Apache License, Version 2.0 (the "License");
     4 +// you may not use this file except in compliance with the License.
     5 +// You may obtain a copy of the License at
     6 +//
     7 +// http://www.apache.org/licenses/LICENSE-2.0
     8 +//
     9 +// Unless required by applicable law or agreed to in writing, software
     10 +// distributed under the License is distributed on an "AS IS" BASIS,
     11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 +// See the License for the specific language governing permissions and
     13 +// limitations under the License.
     14 + 
     15 +package keyboard
     16 + 
     17 +import "testing"
     18 + 
     19 +func TestString(t *testing.T) {
     20 + tests := []struct {
     21 + desc string
     22 + key Key
     23 + want string
     24 + }{
     25 + {
     26 + desc: "unknown",
     27 + key: Key(-1000),
     28 + want: "KeyUnknown",
     29 + },
     30 + {
     31 + desc: "defined value",
     32 + key: KeyEnter,
     33 + want: "KeyEnter",
     34 + },
     35 + {
     36 + desc: "standard key",
     37 + key: 'a',
     38 + want: "a",
     39 + },
     40 + }
     41 + 
     42 + for _, tc := range tests {
     43 + t.Run(tc.desc, func(t *testing.T) {
     44 + if got := tc.key.String(); got != tc.want {
     45 + t.Errorf("String => %q, want %q", got, tc.want)
     46 + }
     47 + })
     48 + }
     49 +}
     50 + 
  • ■ ■ ■ ■ ■ ■
    mouse/mouse_test.go
     1 +// Copyright 2019 Google Inc.
     2 +//
     3 +// Licensed under the Apache License, Version 2.0 (the "License");
     4 +// you may not use this file except in compliance with the License.
     5 +// You may obtain a copy of the License at
     6 +//
     7 +// http://www.apache.org/licenses/LICENSE-2.0
     8 +//
     9 +// Unless required by applicable law or agreed to in writing, software
     10 +// distributed under the License is distributed on an "AS IS" BASIS,
     11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 +// See the License for the specific language governing permissions and
     13 +// limitations under the License.
     14 + 
     15 +package mouse
     16 + 
     17 +import "testing"
     18 + 
     19 +func TestString(t *testing.T) {
     20 + tests := []struct {
     21 + desc string
     22 + button Button
     23 + want string
     24 + }{
     25 + {
     26 + desc: "unknown",
     27 + button: Button(-1000),
     28 + want: "ButtonUnknown",
     29 + },
     30 + {
     31 + desc: "defined value",
     32 + button: ButtonLeft,
     33 + want: "ButtonLeft",
     34 + },
     35 + }
     36 + 
     37 + for _, tc := range tests {
     38 + t.Run(tc.desc, func(t *testing.T) {
     39 + if got := tc.button.String(); got != tc.want {
     40 + t.Errorf("String => %q, want %q", got, tc.want)
     41 + }
     42 + })
     43 + }
     44 +}
     45 + 
  • ■ ■ ■ ■ ■
    widgets/barchart/barchart.go
    skipped 23 lines
    24 24   
    25 25   "github.com/mum4k/termdash/align"
    26 26   "github.com/mum4k/termdash/cell"
     27 + "github.com/mum4k/termdash/internal/alignfor"
    27 28   "github.com/mum4k/termdash/internal/area"
    28 29   "github.com/mum4k/termdash/internal/canvas"
    29 30   "github.com/mum4k/termdash/internal/draw"
    skipped 109 lines
    139 140   barCol = image.Rect(r.Min.X, cvs.Area().Min.Y, r.Max.X, cvs.Area().Max.Y)
    140 141   }
    141 142   
    142  - start, err := align.Text(barCol, text, align.HorizontalCenter, align.VerticalBottom)
     143 + start, err := alignfor.Text(barCol, text, align.HorizontalCenter, align.VerticalBottom)
    143 144   if err != nil {
    144 145   return err
    145 146   }
    skipped 167 lines
  • ■ ■ ■ ■ ■
    widgets/button/button.go
    skipped 23 lines
    24 24   
    25 25   "github.com/mum4k/termdash/align"
    26 26   "github.com/mum4k/termdash/cell"
     27 + "github.com/mum4k/termdash/internal/alignfor"
    27 28   "github.com/mum4k/termdash/internal/button"
    28 29   "github.com/mum4k/termdash/internal/canvas"
    29 30   "github.com/mum4k/termdash/internal/draw"
    skipped 112 lines
    142 143   }
    143 144   
    144 145   textAr := image.Rect(buttonAr.Min.X+1, buttonAr.Min.Y, buttonAr.Dx()-1, buttonAr.Max.Y)
    145  - start, err := align.Text(textAr, b.text, align.HorizontalCenter, align.VerticalMiddle)
     146 + start, err := alignfor.Text(textAr, b.text, align.HorizontalCenter, align.VerticalMiddle)
    146 147   if err != nil {
    147 148   return err
    148 149   }
    skipped 59 lines
  • ■ ■ ■ ■ ■
    widgets/donut/donut.go
    skipped 22 lines
    23 23   "sync"
    24 24   
    25 25   "github.com/mum4k/termdash/align"
     26 + "github.com/mum4k/termdash/internal/alignfor"
    26 27   "github.com/mum4k/termdash/internal/canvas"
    27 28   "github.com/mum4k/termdash/internal/canvas/braille"
    28 29   "github.com/mum4k/termdash/internal/draw"
    skipped 144 lines
    173 174   }
    174 175   
    175 176   ar := image.Rect(first.X, first.Y, first.X+cells+2, first.Y+1)
    176  - start, err := align.Text(ar, t, align.HorizontalCenter, align.VerticalMiddle)
     177 + start, err := alignfor.Text(ar, t, align.HorizontalCenter, align.VerticalMiddle)
    177 178   if err != nil {
    178  - return fmt.Errorf("align.Text => %v", err)
     179 + return fmt.Errorf("alignfor.Text => %v", err)
    179 180   }
    180 181   if err := draw.Text(cvs, t, start, draw.TextMaxX(start.X+needCells), draw.TextCellOpts(d.opts.textCellOpts...)); err != nil {
    181 182   return fmt.Errorf("draw.Text => %v", err)
    skipped 73 lines
  • ■ ■ ■ ■ ■ ■
    widgets/gauge/gauge.go
    skipped 21 lines
    22 22   "image"
    23 23   "sync"
    24 24   
    25  - "github.com/mum4k/termdash/align"
    26 25   "github.com/mum4k/termdash/cell"
     26 + "github.com/mum4k/termdash/internal/alignfor"
    27 27   "github.com/mum4k/termdash/internal/area"
    28 28   "github.com/mum4k/termdash/internal/canvas"
    29 29   "github.com/mum4k/termdash/internal/draw"
    skipped 170 lines
    200 200   return err
    201 201   }
    202 202   
    203  - cur, err := align.Text(ar, trimmed, g.opts.hTextAlign, g.opts.vTextAlign)
     203 + cur, err := alignfor.Text(ar, trimmed, g.opts.hTextAlign, g.opts.vTextAlign)
    204 204   if err != nil {
    205 205   return err
    206 206   }
    skipped 131 lines
  • ■ ■ ■ ■ ■
    widgets/linechart/internal/axes/label.go
    skipped 20 lines
    21 21   "image"
    22 22   
    23 23   "github.com/mum4k/termdash/align"
     24 + "github.com/mum4k/termdash/internal/alignfor"
    24 25  )
    25 26   
    26 27  // LabelOrientation represents the orientation of text labels.
    skipped 88 lines
    115 116   }
    116 117   
    117 118   ar := rowLabelArea(y, labelWidth)
    118  - pos, err := align.Text(ar, v.Text(), align.HorizontalRight, align.VerticalMiddle)
     119 + pos, err := alignfor.Text(ar, v.Text(), align.HorizontalRight, align.VerticalMiddle)
    119 120   if err != nil {
    120 121   return nil, fmt.Errorf("unable to align the label value: %v", err)
    121 122   }
    skipped 145 lines
  • ■ ■ ■ ■ ■ ■
    widgets/segmentdisplay/segmentdisplay.go
    skipped 22 lines
    23 23   "image"
    24 24   "sync"
    25 25   
    26  - "github.com/mum4k/termdash/align"
     26 + "github.com/mum4k/termdash/internal/alignfor"
    27 27   "github.com/mum4k/termdash/internal/attrrange"
    28 28   "github.com/mum4k/termdash/internal/canvas"
    29 29   "github.com/mum4k/termdash/internal/segdisp/sixteen"
    skipped 158 lines
    188 188   }
    189 189   
    190 190   text := sd.buff.String()
    191  - aligned, err := align.Rectangle(cvs.Area(), segAr.needArea(), sd.opts.hAlign, sd.opts.vAlign)
     191 + aligned, err := alignfor.Rectangle(cvs.Area(), segAr.needArea(), sd.opts.hAlign, sd.opts.vAlign)
    192 192   if err != nil {
    193  - return fmt.Errorf("align.Rectangle => %v", err)
     193 + return fmt.Errorf("alignfor.Rectangle => %v", err)
    194 194   }
    195 195   
    196 196   optRange, err := sd.wOptsTracker.ForPosition(0) // Text options for the current byte.
    skipped 69 lines
Please wait...
Page is in error, reload to recover